Archive for June, 2009

Math Captcha Component

Posted in CakePHP on June 30th, 2009 by Jamie – 7 Comments

I don’t like CAPTCHAs. I don’t know anyone who does. But most forms need some sort of protection against spam, especially where heavyweight spam detection services (e.g. Akismet) aren’t suitable. The downfalls of CAPTCHAs are many – hard to read, annoying, impossible for those with vision difficulties – and the benefits are slim. So, a few months ago I wrote a little function (and I do mean little – like 10 lines of code) to generate a random math question to ask the user in plain text instead of a CAPTCHA. The idea was that a bot wouldn’t be able to answer it, since it requires some human logic. And you know what? It worked – spam went way down on our websites.

So, fast forward a few months and now I’m writing a CakePHP CMS, and I’ve decided to turn my ‘math captcha’ function into a Component. So here it is: my Math Captcha Component. This component generates a random equation and registers the answer as a session variable. The programmer can then check the form submitter’s answer against that registered answer using the validation function provided in the component.

Updated – January 19/2010: Download the Component from Github

Usage is really simple – I’ll run through putting it into an equally simple contact form, which looks a lot like the one done by Jonathan Snook. The Contact model looks like this (app/models/contact.php):

<?php
class Contact extends AppModel {
var $name = 'Contact';
var $useTable = false;

var $_schema = array(
'name'		=>array('type' => 'string', 'length' => 100),
'email'		=>array('type' => 'string', 'length' => 255),
'comments'	=>array('type' => 'text')
);

var $validate = array(
'name' => array(
'rule'=>array('minLength', 1),
'message'=>'Please enter a name so the Geek know what to call you!' ),
'email' => array(
'rule'=>'email',
'message'=>'Please enter an email address so the Geek knows how to reach you.' ),
'details' => array(
'rule'=>array('minLength', 1),
'message'=> 'Don\'t forget to enter some comments.' )
);
}
?>

No DB; manual schema; just a placeholder, really.

The Contact controller is set up like this (app/controllers/contact_controller.php):

<?php
class ContactController extends AppController {
var $name = 'Contact';
var $uses = 'Contact';
var $components = array('RequestHandler', 'Email', 'Session', 'MathCaptcha');

function index() {
if ($this->RequestHandler->isPost()) {
$this->Contact->set($this->data);
if ($this->MathCaptcha->validates($this->data['Contact']['security_code'])) {
if ($this->Contact->validates()) {
$this->Email->to = Configure::read('SiteSettings.email_form_address');
$this->Email->subject = 'Contact from message from ' . $this->data['Contact']['name'];
$this->Email->from = $this->data['Contact']['email'];

$this->Email->send($this->data['Contact']['comments']);
}
} else {
$this->Session->setFlash(__('Please enter the correct answer to the math question.', true));
}
}

$this->set('mathCaptcha', $this->MathCaptcha->generateEquation());
}
}
?>

So, we’ve added MathCaptcha to our list of components. There are various configuration options which you can set when adding MathCaptcha to the $components array – the config array (with defaults) looks like this:

private $__defaults = array(
'operand' =&amp;amp;amp;gt; '+',
'minNumber' =&amp;amp;amp;gt; 1,
'maxNumber' =&amp;amp;amp;gt; 5,
'numberOfVariables' =&amp;amp;amp;gt; 2
);

In the index() method, you can see the usage: if we’ve got a POST request, we call the component’s validates() method and pass to it the relevant data from the form – the user’s answer to the question. If it validates then we continue with the rest of the data validation, otherwise we give an error message. You’ll notice that the generateEquation() method is called regardless; we want a new question generated each time the page loads.

Finally, we just need one line in the view to grab the ’security_code’. Here’s the entire contact form (app/views/contact/index.ctp):

<?php
echo $form->create('Contact', array('url' => $this->here));
echo $form->input('name');
echo $form->input('email');
echo $form->input('comments');
echo $form->input('security_code', array('label' => 'Please Enter the Sum of ' . $mathCaptcha));
echo $form->end(array('name' => 'Send', 'class' => 'input_btn'));
?>

I’ve called the form field ’security_code’, but you can call it whatever you want.

And that’s it! A plain text math ‘captcha’ in almost no time.

Useful CakePHP Tutorial Roundup for June 26, 2009

Posted in CakePHP on June 26th, 2009 by Jamie – 1 Comment

I read a lot about CakePHP almost every day at work, since I’m writing a CMS that will become the standard for most of our new sites. I’m constantly coming across useful tips, code, and tutorials that save me a lot of time. So I thought it might be useful to others to gather up some tutorials every Friday and just get them out there. Some are new; some are old chestnuts. I’ll try to post at least 5 each week.

Here’s what I found useful this week:

  • Better Error Handling with CakePHP – Aidan Lister (April 6, 2009)
    I’ve been planning a more useful error handling system, and Aidan’s method is probably the one I’ll employ now. He tackles the “all or nothing” approach to error handling that CakePHP uses by default: every error in development mode, no error in production mode. Aidan creates a de facto ‘0.5′ debug mode, allowing errors to be logged (and administrators to be notified) while still displaying user-friendly messages.
  • Creating a community in five minutes with CakePHP – Aidan Lister (May 1, 2009)
    Yup, Aidan again. He’s good. This article is a good rundown of how to implement a simple, yet robust, user login system. Although I’ve already written my login system, I was able to take a few tidbits from Aidan’s article and strengthen my own implementation. I highly recommend this article for anyone who thinks that authenticating users with CakePHP’s Auth system is a bit intimidating (and it is, since it’s so poorly documented). Aidan runs through confirming passwords, setting last login dates, and a password retrieval system, among other things.
  • CakePHP Auth Component – Users, Groups & Permissions Revisited – Studio Canaria (June 6, 2008)
    An interesting and effective implementation of CakePHP’s Auth component that stays away from ACL, which, while powerful, is far too complex for the needs of many (most?) small-to-mid size websites. Peter’s solution allows for a good degree of flexibility – you can set permissions for individual actions as well as entire controllers – without the sharp learning curve that comes with ACL.
  • CakePHP Sequence Behavior – Neil Crookes (February 9, 2009)
    Like so many other features, I was researching the best way to handle re-ordering of records via a CMS (e.g. so a user can change the order of photos in a gallery) when I chanced upon Neil’s handy little model behavior. His implementation is pretty smart in that it only executes database queries when it absolutely needs to, rather than just updating every record in a group when any order is changed. Throw in some JQuery AJAX drag and drop that Neil also provides – I’ve been a Prototype/Scriptaculous guy for a long time, but this is easy as pie! – and you’ve got a great re-ordering system in no time.
  • Super Awesome Advanced CakePHP Tips – Matt Curry (May 13, 2009)
    A free e-book with pages upon pages of useful CakePHP tidbits. Matt doesn’t mince words and gets right to the nitty gritty, which, if you’re a developer on a timeline like I am, is a good thing. The book covers a wide array of subjects and has more than a few “why didn’t I think of that?!?” moments – merging add and edit functions is a prime example. It looks like he’s added to the book since I downloaded it, which means that it’s in continued development, which is also great.

Simple Site Maintenance Mode with CakePHP 1.2

Posted in CakePHP on June 24th, 2009 by Jamie – 3 Comments

The ability to put a website into offline or ‘maintenance’ mode seems like a pretty common request these days – it certainly was one of the more important things in the list for me when I was beginning my CMS. Thanks to the beforeFilter() method in CakePHP’s AppController, we can add the ability to turn a website off and on to visitors easily; here’s how I did it.

Adding a ‘maintenance mode’ (or “Site Online/Offline” feature as I call it in my CMS) is actually pretty simple, and just involves a few steps. I have the offline/online toggle as a switch in the admin area of my CMS, but for the sake of simplicity I’ll just make it a hard-coded variable here (putting the option in your CMS should be pretty simple). Here’s what we’ll do:

  • Add the Necessary Variables to the Core
  • Check the Status in AppController

I told you it was easy! So…

Add the Necessary Variables to the Core

As I mentioned, I set the status of my sites in the admin area of my CMS. The status belongs to the settings table, which, as the name might suggest, stores a bunch of different site settings. But for the sake of simplicity we’ll just set what we need in /app/config/core.php.

We need two variables, ’site_status’ and ’site_offline_url’. So, in /app/config/core.php, add this:

Configure::write('SiteSettings.site_status', 'Offline');
Configure::write('SiteSettings.site_offline_url', '/coming-soon');

They’re both pretty straightforward: set ’site_status’ to either ‘Offline’ or ‘Online’ as you see fit, and set ’site_offline_url’ to the URL to which you want to redirect visitors.

Check the Status in AppController

Now that we’ve got our variables, we just need to add a simple bit of code in AppController::beforeFilter() which will check the status of the site and redirect if necessary. I’ll do two versions: one dead simple, and one a little more complex that includes logic to allow for a login system.

The simple version:

function beforeFilter() {
    ...
    if (Configure::read('SiteSettings.site_status') == 'Offline'
        && $this->here != Configure::read('SiteSettings.site_offline_url')) {
        $this->redirect(Configure::read('SiteSettings.site_offline_url'));
    }
}

The slightly-more-complex-but-still-pretty-simple version:

function beforeFilter() {
    ...
    if (Configure::read('SiteSettings.site_status') == 'Offline'
        && $this->here != Configure::read('SiteSettings.site_offline_url')) {
        if ($this->action != 'login' && $this->action != 'logout'
        && !in_array('*', (array)$this->Session->read('Permissions'))) {
            $this->redirect(Configure::read('SiteSettings.site_offline_url'));
        }
    }
}

Of course you may have to change the code a bit depending on how you deal with Permissions. In this example, I’m using a permission system inspired by studiocanaria.com’s non-ACL Auth component implementation, but adding ACL support (which I found to be just a little too heavyweight for my needs) should be a straightforward task. Basically, if the user isn’t trying to login and isn’t an admin, they get redirected to the site offline page.

And that’s it. Your visitors will now be redirected to a predefined URL if your site is in offline ‘maintenance’ mode. Lots of room for extension with this simple code. Some ideas off the cuff:

  • Add the variables to your admin area
  • Use a different layout for the site offline URL
  • Put a contact form on the page
  • etc.

A more robust implementation would probably be best as a component, rather than just a couple of lines of code in AppController::beforeFilter(). But I would suggest that most small-to-medium sites can probably get by just fine with this. A component would most certainly be “cakier”, and I may rewrite this as a component eventually. But for now, here you go. ;)

Attachable Behavior for Dynamic HABTM Relationships in CakePHP 1.2

Posted in CakePHP on June 19th, 2009 by Jamie – Be the first to comment

It’s not normally a pain to add new HABTM relationships in CakePHP: just edit two the two model files, throw in some almost-stock code, and bob’s yer uncle. But I found myself in a sticky situation: I’m writing (almost done!) a general-purpose CMS, the core of which will be used by multiple sites. The ‘base’ of the CMS, such as the pages controller, the user auth system, and the media attachment system, are mostly contained within the core – only the view files are installed on a site-by-site basis. Since my Attachment model (the model that deals with attachments such as images, videos and documents to other models) is part of the core, I couldn’t just edit the HABTM array in the Attachment class every time I wanted to add a new association (since all relationships should be reciprocal).

So, the solution I came up with is a behavior that just binds the HABTM associations on the fly, with the only configuration being the addition of Attachable to a model’s $actsAs array. Is this the best way to do things? I’m not sure. But it’s a solution that works for me, so I thought I’d share it with everyone else. It’s a bit limited in that only you need to know one of the model names in advance – which is exactly what my CMS needs – but it would be easy enough to modify it so that it’s completely dynamic. If there are enough requests for such a version I’ll do one up and post it.

I’ll briefly run over its implementation here, but I’ve also provided a more permanent home:
http://jamienay.com/code/attachable-behavior

The Behavior

/app/models/behaviors/attachable.php

<?php
/**
 * Attachable Behavior class file.
 *
 * Allow a model to be associated another model via an on-the-fly HABTM binding
 *
 * @filesource
 * @author			Jamie Nay
 * @copyright       Jamie Nay
 * @license			http://www.opensource.org/licenses/mit-license.php The MIT License
 * @link            http://jamienay.com/code/attachable-behavior
 */
class AttachableBehavior extends ModelBehavior {

    /**
	 * Behavior settings
	 *
	 * @access public
	 * @var array
	 */
	public $settings = array();

	/**
	 * Default values for settings.
	 *
	 * - attachmentClass: the class to which the Attachable models will be attaching.
     * - joinTable: the HABTM join table used to, well, join the tables.
     * - attachmentTable: the table with which attachmentClass is associated.
     * - attachmentForeignKey: the foreign key for attachmentClass in joinTable.
     * - foreignKey: the foreign key field for the Attachable model in joinTable.
     * - modelField: the field that stores the name of the Attachable model in joinTable.
     * - And then the rest of the HABTM config options, with the except of condition, which is hard-coded in conjunction with modelField.
	 *
	 * @access private
	 * @var array
	 */
    private $defaults = array(
        'attachmentClass' => 'Attachment',
    	'joinTable' => 'attachments_models',
    	'attachmentTable' => 'attachments',
    	'attachmentForeignKey' => 'attachment_id',
    	'foreignKey' => 'foreign_id',
    	'modelField' => 'model',
    	'unique' => true,
    	'fields' => '',
    	'order' => '',
    	'limit' => '',
    	'offset' => '',
    	'finderQuery' => '',
    	'deleteQuery' => '',
    	'insertQuery' => ''
    );    

    /**
     * Configuration method.
     *
     * @access public
     * @param object $Model
     * @param array $config
     */

    public function setup(&$Model, $config = null) {
		if (is_array($config)) {
			$this->settings[$Model->alias] = array_merge($this->defaults, $config);
		} else {
			$this->settings[$Model->alias] = $this->defaults;
		}
	}

	/**
	 * Attach our HABTM relationships to both the Attachment model and the
	 * model we're dealing with.
	 *
	 * @access public
	 * @param object $model
	 * @param array $query
	 *
	 */
	function beforeFind(&$model, $query) {
	    $class = $model->alias;
  	    $model->bindModel(array('hasAndBelongsToMany' => array(
  	        $this->settings[$class]['attachmentClass'] => array(
  	            'className' => $this->settings[$class]['attachmentClass'],
  	            'joinTable' => $this->settings[$class]['joinTable'],
  	            'foreignKey' => $this->settings[$class]['foreignKey'],
  	            'associationForeignKey' => $this->settings[$class]['attachmentForeignKey'],
  	            'conditions' => $this->settings[$class]['modelField']." = '".$class."'",
  	            'unique' => $this->settings[$class]['unique'],
  	            'fields' => $this->settings[$class]['fields'],
  	            'order' => $this->settings[$class]['order'],
  	            'limit' => $this->settings[$class]['limit'],
  	            'offset' => $this->settings[$class]['offset'],
  	            'finderQuery' => $this->settings[$class]['finderQuery'],
  	            'deleteQuery' => $this->settings[$class]['deleteQuery'],
  	            'insertQuery' => $this->settings[$class]['insertQuery']
  	        )
  	    )));
  	    $model->{$this->settings[$class]['attachmentClass']}->bindModel(array('hasAndBelongsToMany' => array(
  	        $class => array(
  	            'className' => $class,
  	            'joinTable' => $this->settings[$class]['joinTable'],
  	            'foreignKey' => $this->settings[$class]['attachmentForeignKey'],
  	            'associationForeignKey' => $this->settings[$class]['foreignKey'],
  	        	'conditions' => array($this->settings[$class]['modelField'] => $class),
  	            'unique' => $this->settings[$class]['unique'],
  	            'fields' => $this->settings[$class]['fields'],
  	            'order' => $this->settings[$class]['order'],
  	            'limit' => $this->settings[$class]['limit'],
  	            'offset' => $this->settings[$class]['offset'],
  	            'finderQuery' => $this->settings[$class]['finderQuery'],
  	            'deleteQuery' => $this->settings[$class]['deleteQuery'],
  	            'insertQuery' => $this->settings[$class]['insertQuery']
  	        )
  	    )));
	}

}
?>

SQL

The table you use as the join table should look something like this (the default table):

CREATE TABLE IF NOT EXISTS `attachments_models` (
  `id` int(11) NOT NULL auto_increment,
  `created` datetime default NULL,
  `modified` datetime default NULL,
  `attachment_id` int(11) NOT NULL,
  `foreign_id` int(11) NOT NULL,
  `model` varchar(75) NOT NULL,
  `rank` int(11) NOT NULL default '1',
  PRIMARY KEY  (`id`)
) ENGINE=InnoDB ;

Usage

To use the Attachable behavior, simply add it to the $actsAs array for a model. For example:

class PressRelease extends AppModel {
 var $name = 'PressRelease';
 var $actsAs = array('Attachable');
}

And that’s it. You’ll have an on-the-fly and reciprocal HABTM relationship between PressRelease and whatever you define the attachment model as, by default Attachment. Speaking of which…

Configuration Options

There are a number of configuration options when attaching the Attachable behavior to a model, most of which are the same as the HABTM config options. A quick rundown:

  • attachmentClass – default ‘Attachment’: the class to which the Attachable models will be attaching.
  • joinTable – default ‘attachments_models’: the HABTM join table used to, well, join the tables.
  • attachmentTable – default ‘attachments’: the table with which attachmentClass is associated.
  • attachmentForeignKey – default ‘attachment_id’: the foreign key for attachmentClass in joinTable.
  • foreignKey – default ‘foreign_id’: the foreign key field for the Attachable model in joinTable.
  • modelField – default ‘model’: the field that stores the name of the Attachable model in joinTable.
  • And then the rest of the HABTM config options, with the except of condition, which is hard-coded in conjunction with modelField.

And that’s it – I’d love to hear questions, comments, suggestions, critiques…

Attachable Behavior

Posted in CakePHP on June 19th, 2009 by Jamie – 1 Comment

It’s not normally a pain to add new HABTM relationships in CakePHP: just edit two the two model files, throw in some almost-stock code, and bob’s yer uncle. But I found myself in a sticky situation: I’m writing (almost done!) a general-purpose CMS, the core of which will be used by multiple sites. The ‘base’ of the CMS, such as the pages controller, the user auth system, and the media attachment system, are mostly contained within the core – only the view files are installed on a site-by-site basis. Since my Attachment model (the model that deals with attachments such as images, videos and documents to other models) is part of the core, I couldn’t just edit the HABTM array in the Attachment class every time I wanted to add a new association (since all relationships should be reciprocal). So, the solution I came up with is a behavior that just binds the HABTM associations on the fly, with the only configuration being the addition of Attachable to a model’s $actsAs array.

Is this the best way to do things? I’m not sure. But it’s a solution that works for me and the CMS I’m making, though it would be easy enough to modify the behavior to make it completely dynamic. If there are enough requests for such a version I’ll do one up and post it.

Download the Attachable Behavior (zip)

To use the Attachable behavior, simply add it to the $actsAs array for a model. For example:

class PressRelease extends AppModel {
 var $name = 'PressRelease';
 var $actsAs = array('Attachable');
}

SQL

The table you use as the join table should look something like this (the default table):

CREATE TABLE IF NOT EXISTS `attachments_models` (
 `id` int(11) NOT NULL auto_increment,
 `created` datetime default NULL,
 `modified` datetime default NULL,
 `attachment_id` int(11) NOT NULL,
 `foreign_id` int(11) NOT NULL,
 `model` varchar(75) NOT NULL,
 `rank` int(11) NOT NULL default '1',
 PRIMARY KEY  (`id`)
) ENGINE=InnoDB ;

And that’s it. You’ll have an on-the-fly and reciprocal HABTM relationship between PressRelease and whatever you define the attachment model as, by default Attachment. Speaking of which…

Configuration Options

There are a number of configuration options when attaching the Attachable behavior to a model, most of which are the same as the HABTM config options. A quick rundown:

  • attachmentClass – default ‘Attachment’: the class to which the Attachable models will be attaching.
  • joinTable – default ‘attachments_models’: the HABTM join table used to, well, join the tables.
  • attachmentTable – default ‘attachments’: the table with which attachmentClass is associated.
  • attachmentForeignKey – default ‘attachment_id’: the foreign key for attachmentClass in joinTable.
  • foreignKey – default ‘foreign_id’: the foreign key field for the Attachable model in joinTable.
  • modelField – default ‘model’: the field that stores the name of the Attachable model in joinTable.
  • And then the rest of the HABTM config options, with the except of condition, which is hard-coded in conjunction with modelField.

New Theme

Posted in Personal on June 19th, 2009 by Jamie – Be the first to comment

Trying out a new WordPress theme (again) – the layout is still familiar but I think it’ll work out better. More CakePHP code coming tonight.

Cascading Dynamic Meta Tags and Page Titles in CakePHP 1.2

Posted in CakePHP on June 16th, 2009 by Jamie – 5 Comments

In the course of developing my general purpose CakePHP CMS, I needed a way to manipulate the meta tags, page title, and page heading for every URL in the website. Now, this task is pretty simple when dealing with the pages controller – just add the appropriate meta columns to the pages table and you’re off to the races. But what about URLs that handle other controllers, especially ones that don’t have CMS-editable page content?

Well, I hmmm’ed over the idea for a while before settling on my solution: a separate database table, metas. This table and its data would be manipulated like any other MVC component in the CMS, except that each row in the metas table would be associated with a URL on the website. And, since it would be a lot of work to cover absolutely every URL in a website, this Meta feature should cascade, so that if, for example, I don’t have meta information for “/places/canada/victoria”, the code would automatically pull the metas for “/places/canada” (and if that’s empty, “/places/”, or just “/”).

So I wrote it and, hey, it turned out to be pretty simple and useful, so much so that I whipped up a little tutorial to implement the Meta feature. So without further delay, here’s the tutorial, and here’s how it’s going to go down:

  1. Create a metas database table
  2. Create a controller, model, and a couple of views to manipulate our metas
  3. Write a function to get the meta details of the current page
  4. Add meta initialization to AppController
  5. Add the dynamic metas to our layout

Create a metas database table

Before we build any of the exciting stuff we need to create a metas table in our database (I’m using MySQL). Mine looks lke this:


CREATE TABLE `metas` (
 `id` int(11) NOT NULL auto_increment,
 `created` datetime default NULL,
 `modified` datetime default NULL,
 `url` varchar(255) NOT NULL,
 `page_header` varchar(255) NOT NULL,
 `meta_description` text,
 `meta_keywords` text,
 `head_title` varchar(255) NOT NULL,
 PRIMARY KEY  (`id`)
) ENGINE=MyISAM;

Pretty straightforward: ‘url’ is, predictably, the URL of a page we want to create meta information for. ‘page_header’ defines the title of the page within the content, while ‘head_title’ is used for the title tag in the page head.

Create a controller, model, and a couple of views to manipulate our metas

Now that we have our database table, we’ll need to add the standard MVC components. Let’s start with the controller.

The controller is simple – it’s basically a baked version of a standard controller. Nothing fancy at all, except that, instead of having separate admin_edit and admin_add functions, I combine them both in admin_edit (thanks to Matt Curry for the idea). So, /app/controllers/metas_controller.php:

<?php
class MetasController extends AppController {
	var $name = 'Metas';

	function admin_index() {
		$this->set('metas', $this->paginate());
	}

	function admin_edit($id = null) {
		if (!empty($this->data)) {
			if ($this->Meta->save($this->data)) {
				$this->Session->setFlash(__('The meta information has been saved', true));
				$this->redirect(array('action'=>'index'));
			} else {
			}
		}
		if ($id &amp;&amp; empty($this->data)) {
			$this->data = $this->Meta->read(null, $id);
		}
	}

	function admin_delete($id = null) {
		if (!$id) {
			$this->Session->setFlash(__('Invalid id for meta', true));
			$this->redirect(array('action'=>'index'));
		}
		if ($this->Meta->del($id)) {
			$this->Session->setFlash(__('Meta information deleted', true));
			$this->redirect(array('action'=>'index'));
		}
	}

}
?>

The views are just as simple as the controller. Again, mostly baked stuff.

/app/views/metas/admin_index.ctp:

<div id="controller_actions">
 <?php echo $html->link(__('New Meta Entry', true), array('action'=>'edit')); ?></div>
<div class="metas index">
<h2><?php __('Meta');?></h2>
<?php
echo $paginator->counter(array(
'format' => __('Page %page% of %pages%, showing %current% records out of %count% total, starting on record %start%, ending on %end%.', true)
));
?>
<table cellpadding="0" cellspacing="0" class="tbl_list">
<tr>
<th><?php echo $paginator->sort('url');?></th>
<th><?php echo $paginator->sort('page_header');?></th>
<th><?php echo $paginator->sort('created');?></th>
<th><?php echo $paginator->sort('modified');?></th>
<th class="actions"><?php __('Actions');?></th>
</tr>
<?php
$i = 0;
foreach ($metas as $meta):
 $class = null;
 if ($i++ % 2 == 0) {
 $class = ' class="altrow"';
 }
?>
<tr<?php echo $class;?>>
<td>
 <?php echo $meta['Meta']['url']; ?></td>
<td>
 <?php echo $meta['Meta']['page_header']; ?></td>
<td>
 <?php echo $time->nice($meta['Meta']['created']); ?></td>
<td>
 <?php echo $time->nice($meta['Meta']['modified']); ?></td>
<td class="actions">
 <?php echo $html->link(__('Edit', true), array('action'=>'edit', $meta['Meta']['id'])); ?>
 <?php echo $html->link(__('Delete', true), array('action'=>'delete', $meta['Meta']['id']), null, sprintf(__('Are you sure you want to delete # %s?', true), $meta['Meta']['id'])); ?></td>
</tr>
<?php endforeach; ?></table>
</div>
<div class="paging">
 <?php echo $paginator->prev('<< '.__('previous', true), array(), null, array('class'=>'disabled'));?>
 | 	<?php echo $paginator->numbers();?>
 <?php echo $paginator->next(__('next', true).' >>', array(), null, array('class'=>'disabled'));?></div>

/app/views/metas/admin_edit.ctp:

<div id="controller_actions">
<ul>
 <?php echo $html->link(__('Delete', true), array('action'=>'delete', $form->value('Meta.id')), null, sprintf(__('Are you sure you want to delete # %s?', true), $form->value('Meta.id'))); ?> |
 <?php echo $html->link(__('List Metas', true), array('action'=>'index'));?></ul>
</div>
<h2>Add/Edit Meta Entry</h2>
<?php echo $form->create('Meta', array('class' => 'editor_form'));?>
 <?php
 echo $form->input('id');
 echo $form->input('url');
 echo $form->input('title', array('label' => 'Page Title'));
 echo $form->input('head_title', array('label' => 'Head Title'));
 echo $form->input('meta_description');
 echo $form->input('meta_keywords');
 ?>
<?php echo $form->end('Submit');?>

And finally, the model. We’ll make a simple one now and then, in the next step, we’ll write our meta-finding method.

/apps/models/meta.php:

<?php
class Meta extends AppModel  {
 var $name = 'Meta';
}

Write a function to get the meta details of the current page

OK, so now that we’ve finished our MVC implementation of the Meta model, let’s write the meat of our new feature: a function that finds the meta information for the current URL. If no such information is found, we’ll ‘cascade’ down through the URL until we find a match, eventually settling on the root entry (”/”) if we can’t find any other metas.

So, put this function in /app/models/meta.php:

/*
 * Get the meta model for the current page ($this->here from AppController).
 */
function __findCurrentPage($options = array()) {

	if (!isset($options['url'])) {
		return NULL;
	}

	$url = rtrim($options['url'], '/');

	/*
	 * First we try to find a complete match for the URL. If we can find it, or if
	 * we're at the root of the site, return the results.
	 */
	$meta = $this->find('first', array('conditions' => array('url' => $url)));
	if (!empty($meta) || $url == '/') {
		return $meta;
	}

	/*
	 * We didn't find a match (or we're not in the root), so now we explode the URL
	 * into its parts (separated by /), and look for a match. In other words, we cascade
	 * down the URL until the root in order to find a meta entry.
	 */
	$urlParts = explode("/", trim($url, "/"));
	krsort($urlParts);

	foreach ((array)$urlParts as $part) {
		$url = str_replace('/'.$part, '', $url);
		if ($url) {
			$meta = $this->find('first', array('conditions' => array('url' => $url)));
			if (!empty($meta)) {
				return $meta;
			}
		}
	}

	/*
	 * Still no matching meta, so now we just return the metas for the root.
	 */
	$meta = $this->find('first', array('conditions' => array('url' => '/')));
	return $meta;
}

So, if our URL is “/canucks/the-team/ryan-kesler”, the function will first look for a complete URL match, then move down to “/canucks/the-team”, then just “/canucks”, and then simply “/”. A pretty simple and intuitive way to handle CMS-manipulated meta information.

Add meta initialization to AppController

Now that we’ve finished the bulk of our work, we just need to grab the meta info so it’s available to our page views. We’ll accomplish this by adding a handful of simple functions to AppController (/app/app_controller.php):

/**
 * Search the metas table for an entry matching the current URL. If no match is
 * found, keep cascading through the URL path separator ('/') until we find an entry.
 *
 */
private function _configureMeta()
{
	$meta = ClassRegistry::init('Meta')->find('currentPage', array('url' => $this->here));
	$meta = $meta['Meta'];
	$this->meta = $meta;

	return $this->meta;
}

// Page title (<title>)
private function _pageTitle()
{
   return ($this->meta['head_title'] ? $this->meta['page_header'] : NULL);
}

// Page header (<h1)
private function _pageHeader()
{
   return ($this->meta['page_header'] ? $this->meta['page_header'] : NULL);
}       

// Meta keywords
private function _metaKeywords()
{
	   return (!empty($this->meta['meta_keywords']) ? $this->meta['meta_keywords'] : NULL);
}

// Meta description
private function _metaDescription()
{
   return (!empty($this->meta['meta_description']) ? $this->meta['meta_description'] : NULL);
}

The methods are nothing fancy: _configureMeta() loads the meta info for the current page, calling up the findCurrentPage function we wrote in the Meta class. The other four methods simply grab the information. We’ll call all of these methods – thereby setting the view variables – in AppController::beforeRender():

function beforeRender() {
...
    // Grab our dynamic page <title>.
    $this->pageTitle = $this->_pageTitle();

    // Set the page header.
    $this->set('pageHeader', $this->_pageHeader());

    // Grab our dynamic meta keywords and description.
    $this->set('metaKeywords', $this->_metaKeywords());
    $this->set('metaDescription', $this->_metaDescription());
...
}

OK! We’ve set $title_for_layout with _pageTitle(), and we’ve added three new variables to the view, $pageHeader, $metaKeywords, and $metaDescription. There’s just one more thing to do…

Add the dynamic metas to our layout

I’m not going to give a complete layout file here, since this is a basic step. Basically I just want to remind everyone to use CakePHP’s HTML helper for the meta keywords and meta description (the other two variables just need simple echo calls):

echo $html->meta('keywords', $metaKeywords);
echo $html->meta('description', $metaDescription);

And that’s it. Now you can have dynamic, cascading meta tags and page titles for your CakePHP application. As always, suggestions, improvements, critiques welcome.

A Quick, Dirty and Useful Widget Component for CakePHP

Posted in CakePHP on June 13th, 2009 by Jamie – 1 Comment

In one of my side projects, which I’m currently refactoring to use CakePHP, I needed to have certain dynamic (i.e. database-retrieved) elements on every page: the three latest entries from the articles section, certain user details, etc. I could add a whole lot of models to the $uses array of my individual models or AppModel, but that results in a lot of overhead and it just doesn’t make sense, since, if I’m just displaying the latest three articles in a site-wide sidebar, my other models aren’t really “using” Article. So I searched for a better, more fluid solution.

The simplest fix is the requestAction method, but that results in a lot of overhead since it results in an almost-complete page load with every use (basically, it just loads another URL on the fly). I searched around the popular CakePHP blogs for a few hours looking for a good solution, but, after not finding anything I liked, I decided to make my own. I actually got the idea for this component from the comments on debuggable.com’s post ‘requestAction considered harmful‘ -Rafael Bandeira suggested a Widget view helper, which I thought was a great idea but decided to go with a controller component instead.

And that’s where the Widget component comes in. Basically, this component, which I attach to AppController, allows you to retrieve any information from any model, without directly interacting with the model itself and without using $uses, App::import, ClassRegistry, etc. The basic idea is that you write your own findX methods in your models, which you then call via the Widget component.

So, we’re going to work through implementing a Widget component, and we’ll use it to get the latest three published entries from the Articles table and display them across the site. Here’s what we’ll do:

  1. Implement support for custom find methods (skip this step if you’ve already done this)
  2. Write a method to find the latest Articles
  3. Write our Widget Component
  4. Add the Widget Component to AppController and write a function to set site-wide dynamic data

1. Implement support for custom find methods

Most CakePHP developers who’ve graduated from the ‘novice’ school have done this, but for those who haven’t: adding custom find methods is a great way to extend your application. And, it’s easy! I’m using the method found in Matt Curry’s excellent (and free!) Super Awesome Advanced CakePHP Tips e-book.

We just need to overwrite the default Model::find() method with a new find() in our AppModel. It looks something like this:

// For custom find('xxx') methods. The function looks for a __findFindType class method.
function find($type, $options = array()) {
     $method = null;
     if (is_string($type)) {
         $method = sprintf('__find%s', Inflector::camelize($type));
     }

     if ($method && method_exists($this, $method)) {
         return $this->{$method}($options);
     } else {
        $args = func_get_args();
        return call_user_func_array(array('parent', 'find'), $args);
     }
}

Basically, whenever we call find(), we’re going to look in the model doing the finding for a __findXXX method that matches find(’XXX’). If the method doesn’t exist then we fall back on the default find() method.

2. Write a sample custom find method

Now we’ll write a method in the Article model – /app/models/article.php – class to find the latest articles:

function __findLatest($limit = 3) {
    return $this->find('all', array('limit' => 3, 'published' => true, 'order' => 'Article.begin_publishing DESC'));
}

Pretty simple. To use it, we just call: $this->Article->find(’latest’);

Write our Widget component

Finally, the good stuff! The Widget component is simple too; it only has one method (of course, expand as you see fit). Put this in /app/controllers/components/widget.php:

<?php
class WidgetComponent extends Object {

    function retrieve($modelName, $findType, $options = NULL) {
        $model = ClassRegistry::init($modelName);
        return $model->find($findType, $options);
    }

}
?>

As you can see, we’re using a method called ‘retrieve’ to find our data. Its first argument is the name of the model from which you want to grab data, while the second argument denotes the name of the findXXX method. The third argument is any optional data you want to pass along. You’ll see an example usage in the next section. Speaking of which…

Add the Widget Component to AppController and write a function to set site-wide dynamic data

So now that we’ve written our custom find method and Widget Component, the only thing left to do is add the functionality to AppController. First, add it to the $components array in /app/app_controller.php:

var $components = array('Widget');

Next, write a function in AppController to set your common (site-wide) widgets. I like to call it “_setCommonWidgets()”:

function _setCommonWidgets()
{
    $this->set('latestArticles', $this->Widget->retrieve('Article', 'latest'));
}

And there’s the Widget Component in action. We call the retrieve() method, pass along the model name we want – Article – and the find type, ‘latest’. We set the resulting array as view variable, so it’ll be available in any view as $latestArticles. But wait!

We forgot one step: we need to invoke the _setCommonWidgets() function. Easy stuff: just call it in AppController::beforeRender():

// Set any common 'widget' variables.
$this->_setCommonWidgets();

And that’s it. Now you can use _setCommonWidgets to give your views access to any data you’d like, without resorting to requestAction, $uses, manual calls to App::import or ClassRegistry, etc.

I’d love feedback on this feature. Improvements especially are welcome. ;)