<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Aidan Lister &#187; Programming</title>
	<atom:link href="http://aidanlister.com/category/programming/feed/" rel="self" type="application/rss+xml" />
	<link>http://aidanlister.com</link>
	<description>Code is poetry</description>
	<lastBuildDate>Tue, 24 Jan 2012 05:15:31 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.3</generator>
		<item>
		<title>Drupal meets geolocation with Quova</title>
		<link>http://aidanlister.com/2011/09/drupal-meets-geolocation-with-quova/</link>
		<comments>http://aidanlister.com/2011/09/drupal-meets-geolocation-with-quova/#comments</comments>
		<pubDate>Wed, 07 Sep 2011 23:54:12 +0000</pubDate>
		<dc:creator>Aidan Lister</dc:creator>
				<category><![CDATA[Programming]]></category>

		<guid isPermaLink="false">http://aidanlister.com/?p=507</guid>
		<description><![CDATA[<a href="http://developer.quova.com">Quova</a> - the worlds largest geolocation provider - has sponsored the development of a suite of <a href="http://drupal.org">Drupal</a> modules to bring geolocation to Drupal.

With their <a href="http://drupal.org/project/quova">suite of modules</a> site developers can now easily create locale aware, geo-targeted compelling experiences in Drupal.]]></description>
			<content:encoded><![CDATA[<p><a href="http://developer.quova.com">Quova</a> &#8211; the worlds largest geolocation provider &#8211; has sponsored the development of a suite of <a href="http://drupal.org">Drupal</a> modules to bring geolocation to Drupal.</p>
<p>With their <a href="http://drupal.org/project/quova">suite of modules</a> site developers can now easily create locale aware, geo-targeted compelling experiences in Drupal.</p>
<p>The suite of modules for both Drupal 6 and 7 includes:</p>
<ul>
<li><em>Quova Core</em> &#8211; Provides an API to interact with the Quova IP Info service, and helpers for location based customizable content.</li>
<li><em>Quova Context</em> &#8211; Built in context support allows you to use country and line speed as a context argument. This allows you to easily display localised blocks, display country dependent blocks, or swap out bandwidth sensitive content.</li>
<li><em>Quova Locale</em> &#8211; Provides automatic language switching based on the users country and region.</li>
<li><em>Quova Weather</em> &#8211; Integrates with the Weather module to provide a block showing weather at the users current location.</li>
<li><em>Rules</em> &#8211; Built in rules support allows you to use country as a rules condition allowing location based site customizations or geofencing. Combined with <a href="http://drupal.org/project/commerce">Drupal Commerce</a>, this allows you to enable and disable payment processors depending on the users location.</li>
<li><em>Views / Location</em> &#8211; Built in helpers to allow you to interface easily with Views and the Location module. For example this can be used to automatically display branches closest to a potential customer.</li>
<li><em>Ad</em> &#8211; Allows geotargeting of advertising using Quova and the Ad module.</li>
</ul>
<p>For more information, visit the <a href="http://drupal.org/project/quova">project page</a> or check out the <a href="http://drupal.org/node/1263112">documentation</a>.</p>
]]></content:encoded>
			<wfw:commentRss>http://aidanlister.com/2011/09/drupal-meets-geolocation-with-quova/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Better integration with Facebook for Django</title>
		<link>http://aidanlister.com/2010/11/better-integration-with-facebook-for-django/</link>
		<comments>http://aidanlister.com/2010/11/better-integration-with-facebook-for-django/#comments</comments>
		<pubDate>Thu, 18 Nov 2010 16:39:30 +0000</pubDate>
		<dc:creator>Aidan Lister</dc:creator>
				<category><![CDATA[Programming]]></category>

		<guid isPermaLink="false">http://aidanlister.com/?p=464</guid>
		<description><![CDATA[I have released <a href="https://github.com/aidanlister/django-facebook">django-facebook</a> which offers total and seamless integration with Facebook for your social apps.]]></description>
			<content:encoded><![CDATA[<p>I&#8217;ve really been enjoying working with <a href="http://djangoproject.com/">Django</a> for the last few months, it&#8217;s an incredibly powerful and elegant framework and I sorely wish there was an equivalent in PHP.</p>
<p>This said, I was sadly unimpressed with the social landscape support. Facebook&#8217;s own <a href="">Python SDK</a> has a slew of bugs and is not actively maintained, I recommend people use <a href="https://github.com/dziegler/python-sdk">David Ziegler&#8217;s fork</a>.</p>
<p>For integration with Django, there was <a href="https://github.com/tschellenbach/Django-facebook">Django-facebook</a> which had good intentions but doesn&#8217;t quite cut the mustard, <a href="https://github.com/sciyoshi/pyfacebook/">Pyfacebook</a> which is no longer actively maintained and does not work with the new Graph API, and <a href="https://github.com/flashingpumpkin/django-socialregistration">Social registration</a> which was the best of the bunch, but still offered poor integration with the django authentication system.</p>
<p>To this end, I have released <a href="https://github.com/aidanlister/django-facebook">django-facebook</a> which offers total and seamless integration with Facebook for your social apps.</p>
<p>It has a bunch of nifty tools including template tags, a nice middleware, an authentication backend and some <a href="https://github.com/aidanlister/django-facebook/blob/master/README.md">actual documentation</a>!</p>
<p>Spread the word, and watch this space.</p>
]]></content:encoded>
			<wfw:commentRss>http://aidanlister.com/2010/11/better-integration-with-facebook-for-django/feed/</wfw:commentRss>
		<slash:comments>7</slash:comments>
		</item>
		<item>
		<title>Executing a callback for all files in a directory</title>
		<link>http://aidanlister.com/2009/06/executing-a-callback-for-all-files-in-a-directory/</link>
		<comments>http://aidanlister.com/2009/06/executing-a-callback-for-all-files-in-a-directory/#comments</comments>
		<pubDate>Tue, 16 Jun 2009 23:22:56 +0000</pubDate>
		<dc:creator>Aidan Lister</dc:creator>
				<category><![CDATA[Programming]]></category>
		<category><![CDATA[Functions]]></category>
		<category><![CDATA[PHP]]></category>

		<guid isPermaLink="false">http://aidanlister.com/?p=381</guid>
		<description><![CDATA[<code>directory_walk</code> allows running an arbitrary callback on all files in a deep directory structure.]]></description>
			<content:encoded><![CDATA[<p>A common question when dealing with deep directory structures concerns how a function can be applied recursively to all the files in the target directory, regardless of the depth. This function, <code>directory_walk</code>, builds an internal stack (uses no recursion) and iteratively applies the user supplied callback providing a fast and flexible approach.</p>
<pre class="brush: php; title: ; notranslate">
/**
 * Allows running a callback on all files in a deep directory structure
 *
 * @author Aidan Lister &lt;aidan@php.net&gt;
 * @param string $dirname The directory to walk
 * @param callable $callable The callable to execute on all files found
 * @param mixed $arg{n} Extra parameters to be passed to the callable
 * @return mixed The return value of the last callable run
 */
function directory_walk($dirname, $callable)
{
    $ignore = array('.', '..', '.DS_Store');
    $args = func_get_args();
    array_shift($args);

    // Sanity check
    if (!file_exists($dirname)) {
        return false;
    }

    // Create and iterate stack
    $stack = array($dirname);
    while ($entry = array_pop($stack)) {
        if (is_link($entry)) continue;

        // Run the action
        if (is_file($entry)) {
          $ret = call_user_func_array($callable, array($entry) + $args);
          continue;
        }

        // Add the directory into the stack
        $dh = opendir($entry);
        while (false !== $child = readdir($dh)) {
            if (in_array($child, $ignore)) continue;
            $child = $entry . DIRECTORY_SEPARATOR . $child;
            $stack[] = $child;
        }
        closedir($dh);
    }

    return $ret;
}
</pre>
<p>So, for some examples. We&#8217;ll start simple and simply print the directory:</p>
<pre class="brush: php; title: ; notranslate">
$func = create_function('$file', 'echo &quot;found $file&quot;;');
directory_walk('target-dir', $func);
</pre>
<p>Which for me outputs:</p>
<pre class="brush: plain; title: ; notranslate">
found target-dir/foo
found target-dir/bar
found target-dir/baz
found target-dir/baz/ding
found target-dir/baz/dong
found target-dir/baz/dong/witch
</pre>
<p>What if we wanted to add a .txt extension to all of these files? We could write:</p>
<pre class="brush: php; title: ; notranslate">
function my_rename($entry, $extension) {
    if (is_file($entry)) {
        rename($entry, $entry . $extension);
    }
}
directory_walk('target-dir', 'my_rename', '.txt');
</pre>
<p>Another example might be deleting all the .svn or CVS folders in a directory. We could write:</p>
<pre class="brush: php; title: ; notranslate">
function delsvn($file) {
  $ext = substr($file, strlen($file)-4,strlen($file));
  if ($ext === '.svn') rmdirr($ext);
}
directory_walk('test', 'delsvn');
</pre>
<p>Note that the above example used the <a href="http://aidanlister.com/2004/04/recursively-deleting-a-folder-in-php/">recursive delete function</a> also.</p>
<p>Happy stacking.</p>
]]></content:encoded>
			<wfw:commentRss>http://aidanlister.com/2009/06/executing-a-callback-for-all-files-in-a-directory/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Implementing row level access control in CakePHP</title>
		<link>http://aidanlister.com/2009/05/implementing-row-level-access-control-in-cakephp/</link>
		<comments>http://aidanlister.com/2009/05/implementing-row-level-access-control-in-cakephp/#comments</comments>
		<pubDate>Sat, 02 May 2009 10:01:34 +0000</pubDate>
		<dc:creator>Aidan Lister</dc:creator>
				<category><![CDATA[Programming]]></category>
		<category><![CDATA[CakePHP]]></category>

		<guid isPermaLink="false">http://aidanlister.com/?p=327</guid>
		<description><![CDATA[The ACL Component in CakePHP is very powerful and can be used to solve a wide variety of access control problems. In this tutorial, we provide a step-by-step guide for implementing row level access control to a model.]]></description>
			<content:encoded><![CDATA[<p>The ACL Component in CakePHP is very powerful and can be used to solve a wide variety of access control problems. In this tutorial, we provide a step-by-step guide for implementing row level access control to a model. We will assume at least a basic understanding of ACL and Auth in CakePHP.</p>
<p>The example I am using is an editing platform. At the top of the tree are <code>Volumes</code>, each of which contain many <code>Papers</code>. <code>Authors</code> have access to papers, while <code>Editors</code> have access to volumes and all the papers there-in. Because we have two trees, it&#8217;s important to design our ACL tree well or it quickly becomes unmanageable.  We&#8217;ll call the top of our tree <code>Papers</code>, but this choice is arbitrary.</p>
<p>We create the following ACO hierarchy: Papers/<code>Volume</code>/<code>Paper</code></p>
<p>This tree structure allows you to provide editors access to a volume, which<br />
automatically gives access to the papers inside. This is the beauty of<br />
ACLs.</p>
<p>To create the ACO tree for an existing dataset, we can develop our own cake shell tool. I will assume you have already created the ARO tree for your users and groups.</p>
<pre class="brush: php; title: ; notranslate">
class AcltoolShell extends Shell
{
    /**
     * Build a complete ACO tree for two linked models
     *
     * Usage: $ cake acltool aco_models
     */
    function aco_models()
    {
        $this-&gt;out('Starting models sync');
        $Paper  = ClassRegistry::init('Paper');
        $Volume = ClassRegistry::init('Volume');

        // Create the root node
        $root_alias = 'papers';
        $this-&gt;Aco-&gt;create();
        $this-&gt;Aco-&gt;save(array('parent_id' =&gt; null, 'model' =&gt; null, 'alias' =&gt; $root_alias));
        $aco_root = $this-&gt;Aco-&gt;id;

        // Iterate all the volumes
        $volumes = $Volume-&gt;findAll();
        foreach ($volumes as $volume) {
            // Create a node for the volume
            $this-&gt;out(sprintf('Created Aco node: %s/%s', $root_alias, $volume['Volume']['number']));
            $this-&gt;Aco-&gt;create();
            $row = array('parent_id' =&gt; $aco_root, 'foreign_key' =&gt; $volume['Volume']['id'], 'model' =&gt; 'Volume', 'alias' =&gt; $volume['Volume']['number']);
            $this-&gt;Aco-&gt;save($row);
            $parent_id = $this-&gt;Aco-&gt;id;

            // Iterate all the papers
            $papers = $Paper-&gt;find('all', array('conditions' =&gt; array('volume_id' =&gt; $volume['Volume']['id']), 'recursive' =&gt; -1));
            foreach ($papers as $paper) {
                // Create a node for the paper
                $this-&gt;out(sprintf('Created Aco node: %s/%s/%s', $root_alias, $volume['Volume']['number'], $paper['Paper']['slug']));
                $this-&gt;Acl-&gt;Aco-&gt;create();
                $row = array('parent_id' =&gt; $parent_id, 'foreign_key' =&gt; $paper['Paper']['id'], 'model' =&gt; 'Paper', 'alias' =&gt; $paper['Paper']['slug']);
                $this-&gt;Acl-&gt;Aco-&gt;save($row);
            }
        }
    }
}
</pre>
<p>Once our ACO tree is built, we need to give our users permissions. Again we will use a cake shell tool.</p>
<pre class="brush: php; title: ; notranslate">
    /**
     * Grant user access to two related models
     *
     * Usage: $ cake acltool vol_perms
     */
    function vol_perms()
    {
        // Row level access for volumes
        $this-&gt;out('Creating row-level permissions for volumes');
        $Volume = ClassRegistry::init('Volume');
        $volumes = $Volume-&gt;findAll();
        foreach ($volumes as $vol) {
            $this-&gt;out(sprintf('- Entering volume number %s', $vol['Volume']['number']));
            $Volume-&gt;id = $vol['Volume']['id'];
            foreach ($vol['User'] as $user) {
                $this-&gt;out(sprintf('-- Granting access to %s', $user['name']));
                $User-&gt;id = $user['id'];
                $this-&gt;Acl-&gt;allow($User, $Volume);
            }
        }
    }
}

?&gt;
</pre>
<p>We need to inform our models about our chosen ACO structure. We do this for Papers and Volumes in the same way we would for Users and Groups.</p>
<pre class="brush: php; title: ; notranslate">
models/volume.php
class Volume extends AppModel
{
    /**
     * Describe our ACO tree
     */
    function parentNode()
    {
        return null;
    }
}
</pre>
<pre class="brush: php; title: ; notranslate">
models/paper.php
class Paper extends AppModel
{
    /**
     * Describe our ACO tree
     */
    function parentNode()
    {
        if (!$this-&gt;id &amp;&amp; empty($this-&gt;data)) {
            return null;
        }
        $data = $this-&gt;data;
        if (empty($this-&gt;data)) {
            $data = $this-&gt;read();
        }
        if (empty($data['Paper']['volume_id'])) {
            return null;
        } else {
            return array('Volume' =&gt; array('id' =&gt; $data['Paper']['volume_id']));
        }
    }
}
</pre>
<p>Unfortunately Auth does not provide a way to automatically handle model access. We use <code>beforeFilter</code> to implement access control manually in the necessary controllers.</p>
<p>First we check that they&#8217;re not an admin, then we apply our Acl check. This relies on the fact that a) access is blocked to users by the &#8216;controllers&#8217; Aco tree and b) access is granted to editors/volumes to this controller by the &#8216;controllers&#8217; Aco tree. Both of these constraints are enforced by Auth (with $this->Auth->authorize = &#8216;actions&#8217;).</p>
<pre class="brush: php; title: ; notranslate">
class PapersController extends AppController
{
    /**
     * Row level access checking
     */
    function beforeFilter()
    {
        $methods = array('admin_edit', 'admin_view', 'admin_delete');
        if (isset($this-&gt;params['pass'][0]) &amp;&amp; in_array($this-&gt;params['pass'][0], $methods)) {
            $aco = $this-&gt;Acl-&gt;Aco-&gt;findByModelAndForeignKey('Paper', $this-&gt;params['pass'][0]);
            $aro = $this-&gt;Acl-&gt;Aro-&gt;findByModelAndForeignKey('User', $this-&gt;Auth-&gt;user('id'));
            if (!$this-&gt;Acl-&gt;check($aro['Aro'], $aco['Aco'])) {
                $this-&gt;Session-&gt;setFlash($this-&gt;Auth-&gt;authError);
                $this-&gt;redirect(array('su' =&gt; true, 'controller' =&gt; 'papers', 'action' =&gt; 'index'));
            }
        }
    }
}
</pre>
<p>The only thing remaining is displaying a list of papers and volumes<br />
that a user has access to, instead of all the papers/volumes. This is quite difficult for large trees because it&#8217;s simply not what ACL&#8217;s are designed for. There&#8217;s <a href="https://trac.cakephp.org/ticket/6153">work being done</a> in Cake 1.3.x.x to build an access cache which would solve this problem, but in the mean time:</p>
<pre class="brush: php; title: ; notranslate">
class PapersController extends AppController
{
    /**
     * Display a list of papers for which the user has access to view
     */
    function admin_index()
    {
        $papers = array();
        $user_id = $this-&gt;Auth-&gt;user('id');
        $nodes = $this-&gt;Acl-&gt;Aro-&gt;findByForeignKeyAndModel($user_id, 'User');
        foreach ($nodes['Aco'] as $node) {
            if ($node['model'] === 'Paper') {
                $papers[] = $node['foreign_key'];
            }

            // Get children from volumes
            if ($node['model'] === 'Volume') {
                $children = $this-&gt;Acl-&gt;Aco-&gt;children($node['id']);
                foreach ($children as $child) {
                    $papers[] = $child['Aco']['foreign_key'];
                }
            }
        }
        $conditions = array('Paper.id' =&gt; $papers);

        if ($this-&gt;Auth-&gt;user('group_id') == 1) {
            $conditions = null;
        }

        $this-&gt;set('papers', $this-&gt;paginate($conditions));
    }
}
</pre>
<p>The same applies to the volumes controller, but a little simpler as<br />
you don&#8217;t need the hierarchy.</p>
<pre class="brush: php; title: ; notranslate">
class VolumesController extends AppController
{
    /**
     *
     */
    function admin_index()
    {
        $volumes = array();
        $user_id = $this-&gt;Auth-&gt;user('id');
        $nodes = $this-&gt;Acl-&gt;Aro-&gt;findByForeignKeyAndModel($user_id, 'User');
        foreach ($nodes['Aco'] as $node) {
            if ($node['model'] === 'Volume') {
                $volumes[] = $node['foreign_key'];
            }
        }
        $conditions = array('Volume.id' =&gt; $volumes);

        if ($this-&gt;Auth-&gt;user('group_id') == 1) {
            $conditions = null;
        }

        $this-&gt;set('volumes', $this-&gt;paginate($conditions));
    }
}
</pre>
<p>And that&#8217;s it, we&#8217;ve implemented complete row-level access control in CakePHP. Cake&#8217;s AclComponent is incredibly flexible and very powerful, as you can see you can design quite clean and fine-grained permissions quickly and efficiently.</p>
<p>I hope this tutorial helps other developers out there with similar problems, and as always let me know if you have comments or suggestions.</p>
]]></content:encoded>
			<wfw:commentRss>http://aidanlister.com/2009/05/implementing-row-level-access-control-in-cakephp/feed/</wfw:commentRss>
		<slash:comments>4</slash:comments>
		</item>
		<item>
		<title>Creating a community in five minutes with CakePHP</title>
		<link>http://aidanlister.com/2009/05/creating-a-community-in-five-minutes-with-cakephp/</link>
		<comments>http://aidanlister.com/2009/05/creating-a-community-in-five-minutes-with-cakephp/#comments</comments>
		<pubDate>Fri, 01 May 2009 11:22:44 +0000</pubDate>
		<dc:creator>Aidan Lister</dc:creator>
				<category><![CDATA[Programming]]></category>
		<category><![CDATA[CakePHP]]></category>
		<category><![CDATA[PHP]]></category>

		<guid isPermaLink="false">http://aidanlister.com/?p=302</guid>
		<description><![CDATA[CakePHP's automatic hashing makes things a lot harder than they need to be, and simple tasks (e.g. a registration page) become annoyingly difficult.

Here, we build a complete community based website in five minutes using Cake best practices, with the following features:
<ul>
    <li>Account registration</li>
    <li>Login and logout</li>
    <li>Account management page</li>
    <li>Password retrieval</li>
</ul>]]></description>
			<content:encoded><![CDATA[<p>CakePHP&#8217;s automatic hashing makes things a lot harder than they need to be, and simple tasks (e.g. a registration page) become annoyingly difficult.</p>
<p><strong>Update</strong>: Now compatible with CakePHP 1.3.x. You can also <a href="http://github.com/aidanlister/cakecommunity">download the source code directly</a> from GitHub.</p>
<p>Here, we build a complete community based website in five minutes using Cake best practices, with the following features: account registration, login and logout, account management page, and password retrieval.</p>
<p>We will take the following steps:</p>
<ol>
<li>Bake a new Cake website</li>
<li>Build an account registration page</li>
<li>Create a login page, and optionally add a <code>lastlogin</code> field</li>
<li>Add change password functionality to our account page</li>
<li>Finish with the password retrieval feature</li>
</ol>
<p>At any time you can <a href='http://aidanlister.com/wp-content/uploads/2009/05/building-a-community-in-cakephp.zip'>download the source code</a> used in this tutorial. Let&#8217;s get started!</p>
<h3>Step 1. Bake a new Cake website</h3>
<p>I&#8217;m going to assume that you have a working <a href="http://book.cakephp.org/view/108/The-CakePHP-Console">cake shell installation</a>, so change to your DocumentRoot and execute:<br />
<code>$ cake bake community</code></p>
<p>This will create a <code>community</code> folder with all the necessary files for your new website. If you visit http://example.com/community, you should see the &#8220;all green&#8221; indicating you have a working cake installation.</p>
<p>Next, we&#8217;ll create the <code>users</code> table by executing the following SQL:</p>
<pre class="brush: sql; title: ; notranslate">
CREATE TABLE `users` (
  `id` int(11) NOT NULL auto_increment,
  `created` datetime,
  `modified` datetime,
  `lastlogin` datetime,
  `name` varchar(100),
  `email` varchar(200),
  `username` varchar(50),
  `password` varchar(42),
  PRIMARY KEY (`id`),
  UNIQUE KEY `email` (`email`)
)
</pre>
<p>That&#8217;s it for the setting up &#8230; Let&#8217;s move on.</p>
<h3>Step 2. Build an account registration page</h3>
<p>We&#8217;ll get straight into it by creating the users controller, model and the register view.</p>
<p>Because of CakePHP&#8217;s automatic hashing, we have to do all our validation work on the <code>password_confirm</code> field, as the <code>password</code> field contains only the hash of the users password.</p>
<pre class="brush: php; title: ; notranslate">
models/user.php:
&lt;?php
class User extends AppModel
{
    /**
     * Standard validation behaviour
     */
    var $validate = array(
        'name' =&gt; array(
            'length' =&gt; array(
                'rule'      =&gt; array('minLength', 5),
                'message'   =&gt; 'Please enter your full name (more than 5 chars)',
                'required'  =&gt; true,
            ),
        ),
        'username' =&gt; array(
            'length' =&gt; array(
                'rule'      =&gt; array('minLength', 5),
                'message'   =&gt; 'Must be more than 5 characters',
                'required'  =&gt; true,
            ),
            'alphanum' =&gt; array(
                'rule'      =&gt; 'alphanumeric',
                'message'   =&gt; 'May only contain letters and numbers',
            ),
            'unique' =&gt; array(
                'rule'      =&gt; 'isUnique',
                'message'   =&gt; 'Already taken',
            ),
        ),
        'email' =&gt; array(
            'email' =&gt; array(
                'rule'      =&gt; 'email',
                'message'   =&gt; 'Must be a valid email address',
            ),
            'unique' =&gt; array(
                'rule'      =&gt; 'isUnique',
                'message'   =&gt; 'Already taken',
            ),
        ),
        'password' =&gt; array(
            'empty' =&gt; array(
                'rule'      =&gt; 'notEmpty',
                'message'   =&gt; 'Must not be blank',
                'required'  =&gt; true,
            ),
        ),
        'password_confirm' =&gt; array(
            'compare'    =&gt; array(
                'rule'      =&gt; array('password_match', 'password', true),
                'message'   =&gt; 'The password you entered does not match',
                'required'  =&gt; true,
            ),
            'length' =&gt; array(
                'rule'      =&gt; array('between', 6, 20),
                'message'   =&gt; 'Use between 6 and 20 characters',
            ),
            'empty' =&gt; array(
                'rule'      =&gt; 'notEmpty',
                'message'   =&gt; 'Must not be blank',
            ),
        ),
    );

    /**
     * Ensure two password fields match
     *
     * @param   array   data provided by the controller
     * @param   string  the original (already hashed) password fieldname
     * @param   bool    whether the password field has been hashed,
     *                  only hashed when a username input is present
     */
    function password_match($data, $password_field, $hashed = true)
    {
        $password         = $this-&gt;data[$this-&gt;alias][$password_field];
        $keys             = array_keys($data);
        $password_confirm = $hashed ?
              Security::hash($data[$keys[0]], null, true) :
              $data[$keys[0]];
        return $password === $password_confirm;
    }
}
</pre>
<p>Next we create our view at <code>views/users/register.ctp</code>. Because we&#8217;ve done all our validation on the <code>password_confirm</code> field, the error messages (password too short, etc.) will appear under <code>password_confirm</code>. This isn&#8217;t ideal from the user interface perspective; luckily we can just swap the input labels and noone is the wiser.</p>
<pre class="brush: php; title: ; notranslate">
views/users/register.ctp:
&lt;?php
echo $this-&gt;Form-&gt;create(array('action' =&gt; 'register'));
echo $this-&gt;Form-&gt;input('name');
echo $this-&gt;Form-&gt;input('email');
echo $this-&gt;Form-&gt;input('username');
echo $this-&gt;Form-&gt;input('password_confirm', array('label' =&gt; 'Password', 'type' =&gt; 'password'));
echo $this-&gt;Form-&gt;input('password', array('label' =&gt; 'Password Confirm'));
echo $this-&gt;Form-&gt;end('Register');
</pre>
<p>Finally we create our users controller at <code>controllers/users_controller.php</code>. We define a <code>beforeFilter</code> method to make our registration page publicly accessible, our <code>register</code> action, and a blank <code>login</code> action.</p>
<pre class="brush: php; title: ; notranslate">
controllers/users_controller.php:
&lt;?php
class UsersController extends AppController
{
    var $components = array('Auth');

    /**
     * Runs automatically before the controller action is called
     */
    function beforeFilter()
    {
        $this-&gt;Auth-&gt;allow('register');
        parent::beforeFilter();
    }

    /**
     * Registration page for new users
     */
    function register()
    {
        if (!empty($this-&gt;data)) {
            $this-&gt;User-&gt;create();
            if ($this-&gt;User-&gt;save($this-&gt;data)) {
                $this-&gt;Session-&gt;setFlash(__('Your account has been created.', true));
                $this-&gt;redirect('/');
            } else {
                $this-&gt;Session-&gt;setFlash(__('Your account could not be created.', true));
            }
        }
    }

    /**
     * Account login page
     */
    function login()
    {

    }

    /**
     * Log a user out
     */
    function logout()
    {
       return $this-&gt;redirect($this-&gt;Auth-&gt;logout());
    }
}
</pre>
<p>The next issue with Auth arises when the user has entered a valid password, but has failed to validate the entire form &#8211; for example, if they picked a username that is in use and must be re-entered. As the password has already been replaced by the hashed password, the form will be redisplayed with the hashed password in the <code>password</code> input. If the user attempts to resubmit the form without retyping their password, form validation will fail on the <code>password_confirm</code> field. Even worse, if you&#8217;re not using a <code>password_confirm</code> field, the user will simply be unable to log in to their newly created account.</p>
<p>Thus, we need some hackery to make sure encrypted passwords are not sent back to the user. We can do this in <code>AppController</code>, by writing a <code>beforeRender</code> method.</p>
<pre class="brush: php; title: ; notranslate">
app_controller.php:
class AppController extends Controller
{
    /**
     * Before Render
     */
    function beforeRender()
    {
        unset($this-&gt;data['User']['password']);
        unset($this-&gt;data['User']['password_confirm']);
    }
}
</pre>
<p>And that&#8217;s it, we have a complete working registration page. We can view this completed page by visiting <code>/users/register</code> and create our first account.</p>
<h3>Step 3. Create a login page</h3>
<p>Next up is our login page. We need to modify the default layout which lives at <code>views/layouts/default.ctp</code>. We replace the <code>content</code> div which allows <code>Auth</code> to display messages to the user.</p>
<pre class="brush: php; title: ; notranslate">
views/layouts/default.ctp:
&lt;div id=&quot;content&quot;&gt;
    &lt;?php echo $this-&gt;Session-&gt;flash(); ?&gt;
    &lt;?php echo $this-&gt;Session-&gt;flash('auth'); ?&gt;
    &lt;?php echo $content_for_layout; ?&gt;
&lt;/div&gt;
</pre>
<p>Next we create a very simple login page:</p>
<pre class="brush: php; title: ; notranslate">
views/users/login.ctp:
&lt;?php
echo $this-&gt;Form-&gt;create(array('action' =&gt; 'login'));
echo $this-&gt;Form-&gt;input('username');
echo $this-&gt;Form-&gt;input('password');
echo $this-&gt;Form-&gt;end('Login');
</pre>
<p>And that&#8217;s it &#8211; it doesn&#8217;t get much simpler. We can view our new login page at <code>/users/login</code>.</p>
<p>If we wanted to track the time the user logged in last, we can do some extra work:</p>
<p>Adding to the <code>AppController</code>, we set <code>Auth->autoRedirect</code>:</p>
<pre class="brush: php; title: ; notranslate">
app_controller.php:
class AppController extends Controller
{
    ...

    /**
     * Before Filter
     */
    function beforeFilter()
    {
        $this-&gt;Auth-&gt;autoRedirect = false;
    }
}
</pre>
<p>Then we modify our login controller to do the necessary work:</p>
<pre class="brush: php; title: ; notranslate">
controllers/users_controller.php:
class UsersController extends AppController
{
    ...

    /**
     * Ran directly after the Auth component has executed
     */
    function login()
    {
        // Check for a successful login
        if (!empty($this-&gt;data) &amp;&amp; $id = $this-&gt;Auth-&gt;user('id')) {

            // Set the lastlogin time
            $fields = array('lastlogin' =&gt; date('Y-m-d H:i:s'), 'modified' =&gt; false);
            $this-&gt;User-&gt;id = $id;
            $this-&gt;User-&gt;save($fields, false, array('lastlogin'));

            // Redirect the user
            $url = array('controller' =&gt; 'users', 'action' =&gt; 'account');
            if ($this-&gt;Session-&gt;check('Auth.redirect')) {
                $url = $this-&gt;Session-&gt;read('Auth.redirect');
            }
            $this-&gt;redirect($url);
        }
    }

}
</pre>
<p>Not a lot of additional work, and it will come in handy if you need to prune inactive accounts. Note we can&#8217;t this part of the step yet, as it&#8217;s expecting an <code>/users/account</code> page to exist (which we create next).</p>
<h3>Step 4. Add change password functionality</h3>
<p>Cake&#8217;s form validation functionality is somewhat limited, in that it only allows you to define validation rules at the model level. Often, you will want to validate data at a per-form level instead. To achieve this, we can dynamically modify the validation rules before form validation takes place. We dive into the user model again:</p>
<p>Note: If we simply added the <code>password_old</code> rule to our validation behaviour, other forms that didn&#8217;t include <code>password_old</code> would not validate (because we have set <code>required = true</code>). If we removed <code>required = true</code>, a POST request can easily be forged bypassing this security check.</p>
<pre class="brush: php; title: ; notranslate">
models/user.php:
class User extends AppModel
{
    ...

    /**
     * Extra form dependent validation rules
     */
    var $validateChangePassword = array(
        '_import' =&gt; array('password', 'password_confirm'),
        'password_old' =&gt; array(
            'correct' =&gt; array(
                'rule'      =&gt; 'password_old',
                'message'   =&gt; 'Does not match',
                'required'  =&gt; true,
            ),
            'empty' =&gt; array(
                'rule'      =&gt; 'notEmpty',
                'message'   =&gt; 'Must not be blank',
            ),
        ),
    );

    /**
     * Dynamically adjust our validation behaviour
     *
     * Look for an _import key in new ruleset, and import
     * those rules from the base validation ruleset
     *
     * @param   string  array key of the validation ruleset to use
     */
    function useValidationRules($key)
    {
        $variable = 'validate' . $key;
        $rules = $this-&gt;$variable;

        if (isset($rules['_import'])) {
            foreach ($rules['_import'] as $key) {
                $rules[$key] = $this-&gt;validate[$key];
            }
            unset($rules['_import']);
        }

        $this-&gt;validate = $rules;
    }

    /**
     * Ensure password matches the user session
     *
     * @param   array   data provided by the controller
     */
    function password_old($data)
    {
        $password = $this-&gt;field('password',
            array('User.id' =&gt; $this-&gt;id));
        return $password ===
            Security::hash($data['password_old'], null, true);
    }

    ...
}
</pre>
<p>Our change password logic now becomes very simple. We validate, then update the password and redirect the user.</p>
<p>Note: In this case Cake decides not to automatically hash the users password field, so we must perform the hashing manually. This happens because Cake will only hash the password if both the <code>username</code> and <code>password</code> key are set.</p>
<pre class="brush: php; title: ; notranslate">
controllers/users_controller.php:
class UsersController extends AppController
{
    /**
     * Account details page (change password)
     */
    function account()
    {
        // Set User's ID in model which is needed for validation
        $this-&gt;User-&gt;id = $this-&gt;Auth-&gt;user('id');

        // Load the user (avoid populating $this-&gt;data)
        $current_user = $this-&gt;User-&gt;findById($this-&gt;User-&gt;id);
        $this-&gt;set('current_user', $current_user);

        $this-&gt;User-&gt;useValidationRules('ChangePassword');
        $this-&gt;User-&gt;validate['password_confirm']['compare']['rule'] =
            array('password_match', 'password', false);

        $this-&gt;User-&gt;set($this-&gt;data);
        if (!empty($this-&gt;data) &amp;&amp; $this-&gt;User-&gt;validates()) {
            $password = $this-&gt;Auth-&gt;password($this-&gt;data['User']['password']);
            $this-&gt;User-&gt;saveField('password', $password);

            $this-&gt;Session-&gt;setFlash('Your password has been updated');
            $this-&gt;redirect(array('action' =&gt; 'account'));
        }
    }

    ...
}
</pre>
<p>We create a template for the account page as follows:</p>
<pre class="brush: php; title: ; notranslate">
views/users/account.ctp:
&lt;h2&gt;Account Page&lt;/h2&gt;
&lt;h3&gt;Change your password&lt;/h3&gt;
&lt;p&gt;You are &lt;?php echo $current_user['User']['name']; ?&gt; who last logged in &lt;?php echo $current_user['User']['lastlogin']; ?&gt;.&lt;/p&gt;

&lt;?php
echo $this-&gt;Form-&gt;create(array('action' =&gt; 'account'));
echo $this-&gt;Form-&gt;input('password_old',     array('label' =&gt; 'Old password', 'type' =&gt; 'password', 'autocomplete' =&gt; 'off'));
echo $this-&gt;Form-&gt;input('password_confirm', array('label' =&gt; 'New password', 'type' =&gt; 'password', 'autocomplete' =&gt; 'off'));
echo $this-&gt;Form-&gt;input('password',         array('label' =&gt; 'Re-enter new password', 'type' =&gt; 'password', 'autocomplete' =&gt; 'off'));
echo $this-&gt;Form-&gt;end('Update Password');
?&gt;
</pre>
<p>The user is now able to change their password. We can test this by logging in at <code>/users/login</code> which will take us to our new account page.</p>
<h3>Step 5. Create a password retrieval page</h3>
<p>In our final step, we allow the user to retrieve a forgotten password. We do this by emailing the user a token. The user clicks back to our website with the token, and gets emailed a replacement password.</p>
<p>We create a <code>models/token.php</code> to handle the token generation, information storage and retrieval.</p>
<pre class="brush: php; title: ; notranslate">
models/token.php:
class Token extends AppModel
{
    /**
     * Create a new ticket by providing the data to be stored in the ticket.
     */
    function generate($data = null)
    {
        $data = array(
          'token' =&gt; substr(md5(uniqid(rand(), 1)), 0, 10),
          'data'  =&gt; serialize($data),
        );

        if ($this-&gt;save($data)) {
            return $data['token'];
        }

        return false;
    }

    /**
     * Return the value stored or false if the ticket can not be found.
     */
    function get($token)
    {
        $this-&gt;garbage();
        $token = $this-&gt;findByToken($token);
        if ($token) {
          $this-&gt;delete($token['Token']['id']);
          return unserialize($token['Token']['data']);
        }

        return false;
    }

    /**
     * Remove old tickets
     */
    function garbage()
    {
        return $this-&gt;deleteAll(array('created &lt; INTERVAL -1 DAY + NOW()'));
    }
}
</pre>
<p>We&#8217;ll also create the necessary database table.</p>
<pre class="brush: sql; title: ; notranslate">
CREATE TABLE `tokens` (
  `id` int(11) unsigned NOT NULL auto_increment,
  `created` datetime default NULL,
  `modified` datetime default NULL,
  `token` varchar(32) default NULL,
  `data` text,
  PRIMARY KEY  (`id`),
  UNIQUE KEY `token` (`token`)
)
</pre>
<p>We next update the users controller to make the <code>recover</code> and <code>verify</code> actions public, then we define our <code>recover</code> and <code>verify</code> actions:</p>
<pre class="brush: php; title: ; notranslate">
controllers/users_controller.php:
&lt;?php
class UsersController extends AppController
{
    var $components = array('Auth', 'Email');

    ...

    /**
     * Runs automatically before the controller action is called
     */
    function beforeFilter()
    {
        $this-&gt;Auth-&gt;allow('register', 'recover', 'verify');
        parent::beforeFilter();
    }

    /**
     * Allows the user to email themselves a password redemption token
     */
    function recover()
    {
        if ($this-&gt;Auth-&gt;user()) {
            $this-&gt;redirect(array('controller' =&gt; 'users', 'action' =&gt; 'account'));
        }

        if (!empty($this-&gt;data['User']['email'])) {
            $Token = ClassRegistry::init('Token');
            $user = $this-&gt;User-&gt;findByEmail($this-&gt;data['User']['email']);

            if ($user === false) {
                $this-&gt;Session-&gt;setFlash('No matching user found');
                return false;
            }

            $token = $Token-&gt;generate(array('User' =&gt; $user['User']));
            $this-&gt;Session-&gt;setFlash('An email has been sent to your account, please follow the instructions in this email.');
            $this-&gt;Email-&gt;to = $user['User']['email'];
            $this-&gt;Email-&gt;subject = 'Password Recovery';
            $this-&gt;Email-&gt;from = 'Support &lt;support@example.com&gt;';
            $this-&gt;Email-&gt;template = 'recover';
            $this-&gt;set('user', $user);
            $this-&gt;set('token', $token);
            $this-&gt;Email-&gt;send();
        }
    }

    /**
     * Accepts a valid token and resets the users password
     */
    function verify($token_str = null)
    {
        if ($this-&gt;Auth-&gt;user()) {
            $this-&gt;redirect(array('controller' =&gt; 'users', 'action' =&gt; 'account'));
        }

        $Token = ClassRegistry::init('Token');

        $res = $Token-&gt;get($token_str);
        if ($res) {
            // Update the users password
            $password = $this-&gt;User-&gt;generatePassword();
            $this-&gt;User-&gt;id = $res['User']['id'];
            $this-&gt;User-&gt;saveField('password', $this-&gt;Auth-&gt;password($password));
            $this-&gt;set('success', true);

            // Send email with new password
            $this-&gt;Email-&gt;to = $res['User']['email'];
            $this-&gt;Email-&gt;subject = 'Password Changed';
            $this-&gt;Email-&gt;from = 'Support &lt;support@example.com&gt;';
            $this-&gt;Email-&gt;template = 'verify';
            $this-&gt;set('user', $res);
            $this-&gt;set('password', $password);
            $this-&gt;Email-&gt;send();
        }
    }

}
</pre>
<p>Because we are generating new passwords for users, we need a function to do this. Alternative approaches, like <a href="http://pear.php.net/package/Text_Password">PEAR&#8217;s Text/Password</a> can be substituted if you like.</p>
<pre class="brush: php; title: ; notranslate">
models/user.php:
class User extends AppModel
{
    ....

    /**
     * Generate a random pronounceable password
     */
    function generatePassword($length = 10) {
        // Seed
        srand((double) microtime()*1000000);

        $vowels = array('a', 'e', 'i', 'o', 'u');
        $cons = array('b', 'c', 'd', 'g', 'h', 'j', 'k', 'l', 'm', 'n',
            'p', 'r', 's', 't', 'u', 'v', 'w', 'tr',
            'cr', 'br', 'fr', 'th', 'dr', 'ch', 'ph',
            'wr', 'st', 'sp', 'sw', 'pr', 'sl', 'cl');

        $num_vowels = count($vowels);
        $num_cons = count($cons);

        $password = '';
        for ($i = 0; $i &lt; $length; $i++){
            $password .= $cons[rand(0, $num_cons - 1)] . $vowels[rand(0, $num_vowels - 1)];
        }

        return substr($password, 0, $length);
    }
</pre>
<p>We define two email templates for recovery and verification respectively:</p>
<pre class="brush: php; title: ; notranslate">
views/elements/email/text/recover.ctp:
Dear &lt;?php echo $user['User']['name']; ?&gt;,

Someone is attempting to reset your password.

Your username for this account is: &lt;?php echo $user['User']['username']; ?&gt;

If you wish to continue, you may reset your password by
following this link:

    &lt;?php echo Router::url(array('controller' =&gt; 'users', 'action' =&gt; 'verify', $token), true); ?&gt;

If you did not initiate this action, please contact
support. You can log in to change your password
at this address:

    &lt;?php echo Router::url(array('controller' =&gt; 'users', 'action' =&gt; 'login'), true); ?&gt;

Thanks,
Support
</pre>
<pre class="brush: php; title: ; notranslate">
views/elements/email/text/verify.ctp:
Dear &lt;?php echo $user['User']['name']; ?&gt;,

Your password has been reset, please use the following
details to log into our site.

    Username: &lt;?php echo $user['User']['username']; ?&gt;

    Password: &lt;?php echo $password; ?&gt;

Please change your password to something more memorable.
You can log in to change your password at this address:

    &lt;?php echo Router::url(array('controller' =&gt; 'users', 'action' =&gt; 'login'), true); ?&gt;

Thanks,
Support
</pre>
<p>And finally we define our two templates for the recovery and verification views:</p>
<pre class="brush: php; title: ; notranslate">
views/users/recover.ctp:
&lt;h2&gt;Recover Password&lt;/h2&gt;

&lt;?php
echo $this-&gt;Form-&gt;create('User', array('action' =&gt; 'recover'));
echo $this-&gt;Form-&gt;input('email');
echo $this-&gt;Form-&gt;end('Recover');
?&gt;
</pre>
<pre class="brush: php; title: ; notranslate">
views/users/verify.ctp:
&lt;h2&gt;Recover Password&lt;/h2&gt;

&lt;?php if (isset($success)): ?&gt;
    &lt;div class=&quot;message&quot;&gt;Access verified. Your new password has been emailed to you.&lt;/div&gt;
    &lt;p&gt;A new password has been generated for your account and mailed to you. After you've logged in, you should change your password to something memorable via the account information page.&lt;/p&gt;
&lt;?php else: ?&gt;
    &lt;div class=&quot;warning&quot;&gt;Invalid token. This page has expired, or the link was not copied from your email client correctly.&lt;/div&gt;
    &lt;p&gt;Make sure you have copied the entire link correctly, pasting it together if the link was split over two lines. If you're copying the link correctly and still can't get access, please contact us.&lt;/p&gt;
&lt;?php endif; ?&gt;
</pre>
<p>And that&#8217;s it &#8230; we&#8217;ve developed a complete community orientated website for CakePHP in less time it takes than to make a cup of tea (if you&#8217;re very slow at making tea).</p>
<p>On a side note: you may have noticed that CakePHP&#8217;s automatic hashing makes things a lot harder than they should be. In my opinion, automatic hashing directly goes against Cake&#8217;s mantra of &#8220;If it&#8217;s easy to do in Cake, then it doesn&#8217;t belong in core&#8221;, and I hope the core development team rethink in the idea in Cake 1.3.x.x. Although forcing developers to automatically hash their users password is a nice idea, the current implementation a major stumbling block for new bakers. We shouldn&#8217;t need a <a href="http://book.cakephp.org/view/565/Troubleshooting-Auth-Problems ">section in the manual</a> about common problems the user will encounter trying to do simple tasks. Instead, we should fix the cause of the common problems. It is, however, a testament to Cake&#8217;s flexibility that we are able to work around most of these issues quite elegantly. It&#8217;s also worth noting that there are several ways to <a href="http://teknoid.wordpress.com/2008/10/08/demystifying-auth-features-in-cakephp-12/">override this behavior</a>.</p>
<p>I hope you&#8217;ve enjoyed this tutorial, please feel free to leave comments, suggestions or improvements.</p>
]]></content:encoded>
			<wfw:commentRss>http://aidanlister.com/2009/05/creating-a-community-in-five-minutes-with-cakephp/feed/</wfw:commentRss>
		<slash:comments>30</slash:comments>
		</item>
		<item>
		<title>Better Error Handling with CakePHP</title>
		<link>http://aidanlister.com/2009/04/better-error-handling-with-cakephp/</link>
		<comments>http://aidanlister.com/2009/04/better-error-handling-with-cakephp/#comments</comments>
		<pubDate>Mon, 06 Apr 2009 07:10:49 +0000</pubDate>
		<dc:creator>Aidan Lister</dc:creator>
				<category><![CDATA[Programming]]></category>
		<category><![CDATA[CakePHP]]></category>
		<category><![CDATA[PHP]]></category>

		<guid isPermaLink="false">http://aidanlister.com/?p=5</guid>
		<description><![CDATA[CakePHP is a wonderful framework, but it really drops the ball when it comes to practical error management. In production environments (DEBUG = 0), only 404 or 500 errors are displayed to the user, and no errors are written to the log files.

To solve these two problems we override php's error handler to enable production error logging, and cake's error handler to allow forward facing error pages.]]></description>
			<content:encoded><![CDATA[<p>CakePHP is a wonderful framework, but it really drops the ball when it comes to practical error management. In production environments (DEBUG = 0), only 404 or 500 errors are displayed to the user, and no errors are written to the log files.</p>
<p>This means runtime errors (e.g. an unexpected divide-by-zero) are not logged, and service errors (e.g. when a paypal checkout fails) are not explained to the user.</p>
<p>To solve these two problems we override php&#8217;s error handler to enable production error logging, and cake&#8217;s error handler to allow forward facing error pages.</p>
<p>Part 1: To enable production error logging, we can override cake&#8217;s production error handling code by conditionally setting DISABLE_DEFAULT_ERROR_HANDLING. This code only kicks in when cake is in production and logs all notices and warnings in the cake log file.</p>
<pre class="brush: php; title: ; notranslate">
app/config/bootstrap.php
&lt;?php
/**
 * Handle logging errors in production mode
 */
if (Configure::read() === 0) {
    // Disable the default handling and include logger
    define('DISABLE_DEFAULT_ERROR_HANDLING', 1);
    uses('cake_log');
    error_reporting(E_ALL);

    /**
     * A function to directly log errors
     *
     * @param $errno The error number
     * @param $errstr The error description
     * @param $errfile The file where the error occured
     * @param $errline The line of the file where the error occured
     * @return bool Success
     */
    function productionError($errno, $errstr, $errfile, $errline) {
        // Ignore E_STRICT and suppressed errors
        if ($errno === 2048 || error_reporting() === 0) {
            return;
        }

        // What type of error
        $level = LOG_DEBUG;
        switch ($errno) {
            case E_PARSE:
            case E_ERROR:
            case E_CORE_ERROR:
            case E_COMPILE_ERROR:
            case E_USER_ERROR:
                $error = 'Fatal Error';
                $level = LOG_ERROR;
            break;
            case E_WARNING:
            case E_USER_WARNING:
            case E_COMPILE_WARNING:
            case E_RECOVERABLE_ERROR:
                $error = 'Warning';
                $level = LOG_WARNING;
            break;
            case E_NOTICE:
            case E_USER_NOTICE:
                $error = 'Notice';
                $level = LOG_NOTICE;
            break;
            default:
                return false;
            break;
        }

        // Log
        CakeLog::write($level, sprintf('%s (%d): %s in [%s, line %d]',
            $error, $errno, $errstr, $errfile, $errline));

        // Die if fatal
        if ($level === LOG_ERROR) {
            die();
        }

        return true;
    }

    // Use the above handling
    set_error_handler('productionError');
}
?&gt;
</pre>
<p>Part 2: To enable forward facing user error pages, we can define an AppError class. This code allows you to set up custom error handlers, and decide how you want to handle each &#8211; be it publicly displayed, logged or mailed to the site managers.</p>
<pre class="brush: php; title: ; notranslate">
app/app_error.php:
&lt;?php
class AppError extends ErrorHandler
{
    /**
     * List of errors which are displayed, even in production mode
     */
    var $displayErrors = array('logic', 'paypal', 'payflow');

    /**
     * List of errors which, when occur, information is emailed to
     * the site administrator
     */
    var $emailErrors   = array();

    /**
     * List of errors which, when occur, will result in log entries
     */
    var $logErrors     = array('logic', 'system');

    /**
     * A string containing people to be notified in the event of an
     * error. The string must be acceptable input for the mail function
     */
    var $siteManager   = 'aidan@php.net';

    /**
     * Override the default cakeError error handling behaviour
     *
     * By setting the debug switch, the page will be publically visisble
     * Alternatively, or in conjunction with, we can log and notify the
     * site owner
     */
    function __construct($method, $messages)
    {
        if (in_array($method, $this-&gt;displayErrors)) {
            Configure::write('debug', 1);
        }

        if (in_array($method, $this-&gt;logErrors)) {
            $bt = debug_backtrace();
            $errfile = $bt[1]['file'];
            $errline = $bt[1]['line'];
            $parameters = str_replace(&quot;\n&quot;, '',
                print_r($messages, true));
            $error = sprintf('%s: Called with parameters (%s) in [%s, line %d]',
                $method, $parameters, $errfile, $errline);
            CakeLog::write(LOG_ERROR, $error);
        }

        if (in_array($method, $this-&gt;emailErrors)) {
            $this-&gt;_notify($method, $messages);
        }

        // Handle as normal
        parent::__construct($method, $messages);
    }

    /**
     * Send out email when an error occurs
     */
    function _notify($method, $messages)
    {
        $subject  = '[CakePHP] Site Error';
        $headers  = 'From: cakephp@' . $_SERVER['HTTP_HOST'] . &quot;\r\n&quot;;
        $headers .= 'Reply-To: cakephp@' . $_SERVER['HTTP_HOST'];
        $message  = 'An occurred error at ' . $_SERVER['HTTP_HOST'] . &quot;.\n\n&quot;;
        foreach ($messages as $key =&gt; $value) {
            $message .= sprintf(&quot;    %s: %s\n&quot;, $key, $value);
        }

        mail($this-&gt;siteManager, $subject, $message, $headers);
    }

    /**
     * A fatal logic error occured
     *
     * @param $params Expects keys [message]
     */
    function logic($params)
    {
        $this-&gt;controller-&gt;set('message', $params['message']);
        $this-&gt;_outputMessage('logic');
    }
}
?&gt;
</pre>
<p>Both of these solutions are independent of each other, and provide a way to practically manage error handling in your CakePHP deployment.</p>
<p>I have also opened a <a href="https://trac.cakephp.org/ticket/6165">RFC</a> to get this behavior included in cake&#8217;s core.</p>
]]></content:encoded>
			<wfw:commentRss>http://aidanlister.com/2009/04/better-error-handling-with-cakephp/feed/</wfw:commentRss>
		<slash:comments>6</slash:comments>
		</item>
	</channel>
</rss>

