Posts Tagged ‘ cake

How to Maintain Simple, Static Pages in CakePHP

Cake’s default way of handling simple static content is to use the built-in PagesController to serve up .tpl files from /app/views/pages. This is a simple and straightforward approach and works for very small websites, but comes with some obvious drawbacks:

  • Making changes to the content of the pages requires editing template files;
  • There’s no easy way (generally) to edit these pages the way you’d edit other content on your site, using controllers with admin actions, for example;
  • There’s no way to specify a hierarchy of pages, which can be quite useful for large websites;
  • The URL structure of the pages, although it follows Cake’s URL conventions, isn’t intuitive and looks pretty clunky and unprofessional. It would be much nicer to have /about rather than /pages/about, and so forth.

Logically, it makes more sense to place static content in the database so that it can be manipulated just like any other model. Additionally, we want our URLs to be pretty.

This is surprisingly easy to accomplish, thanks in part to this article, which shows how to load a model from within our routes configuration. I use the same technique, but my code below is updated to run on CakePHP 1.2, and I’ll show you how to create a hierarchy of nested pages as well.

Database

Let’s start by creating the database schema for our model. I call mine “StaticPage”, but you can name yours whatever you’d like. I don’t use “Page” to avoid controller conflicts down the road should I ever decide to make use of Cake’s PagesController for anything.

CREATE TABLE IF NOT EXISTS `static_pages` (
  `id` INTEGER(12) NOT NULL AUTO_INCREMENT,
  `parent_id` INTEGER(12) NULL,
  `title` VARCHAR(128) NOT NULL,
  `slug` VARCHAR(128) NOT NULL,
  `content` LONGTEXT NOT NULL,
  PRIMARY_KEY(`id`)
);

Model

Next, let’s create the actual model in CakePHP. We need to define the belongsTo and hasMany relationships for the tree hierarchy to work properly:

<?php
class StaticPage extends AppModel {

  var $belongsTo = array(
    'ParentPage' => array(
      'className' => 'StaticPage',
      'foreignKey' => 'parent_id'
  ));

  var $hasMany = array(
    'ChildPage' => array(
      'className' => 'StaticPage',
      'foreignKey' => 'parent_id',
      'dependent' => false
  ));
}
?>

Controller

Moving on, we create our StaticPagesController. You’ll likely want to create your own admin CRUD actions (the entire point of this, afterall, is to be able to manage these pages dynamically!), but for simplicity I’m just going to define the one action we need to display our pages. Conveniently, we’re just going to use the “index” action:

<?php
function index( $slug = null ) {
  if (!$slug) {
    $this->Session->setFlash(__('Invalid StaticPage.', true));
    $this->redirect(array('action'=>'index'));
  }
  $staticPage = $this->StaticPage->find('first', array(
    'conditions' => array(
      'StaticPage.slug' => $slug
  )));
  $this->set(compact('staticPage'));
  $this->pageTitle = $staticPage['StaticPage']['title'];
}
?>

Notice we’re going to be using the slug as the unique identifier when looking up the page. But what happens if we have two pages with the same slug? As you’ll see, that won’t be a problem as long as they’re nested under separate parent pages.

Routing

Now for the key part: we need to set up a custom route to handle our pages. In our routes.php file, we’re going to pull a list of static page slugs from the database and use them as the regular expression to match against with our route:

<?php
// routes.php

App::import('Model', 'StaticPage');
$page = new StaticPage();
$slugs = $page->find('list', array(
  'fields' => array('StaticPage.slug'),
  'order' => 'StaticPage.slug DESC'
));

Router::connect('/:slug/',
  array('controller' => 'static_pages', 'action' => 'index'),
  array(
    'pass' => array('slug'),
    'slug' => implode($slugs, '|')
));
?>

Now that everything is set up, we can start creating some static pages in the database. The key thing to remember is that the slug should be the full path to the page. So, for example, if we create a page called “about”, the slug should simply be “about”. The page will then be accessible at yourdomain.com/about. If we want to create a subpage of that page called “projects”, the slug for that page should be “about/projects”. Some people may not like storing the full path as the slug, but I find that it has two main advantages: it prevents ambiguity among pages with the same name/slug, and it makes managing your pages easier since you can immediately know the location of any given page.

This is also the reason that we load the slugs from the database in decending order for our regular expression: the route can first try to match the full URL of a subpage before the parent page is considered for matching.

If you want to take this one step further, you could write some sort of method, getFullSlug(), for the StaticPage model that generates the full slug (rather that storing it) by recursively appending the simple slug from parent pages. The obvious downside to this is that more SQL queries will be required, something we want to avoid, especially when dealing with what should be static content.

Happy baking!

A Calendar Element for CakePHP

Here’s a quick calendar element I whipped up for a CakePHP application I’m writing.  I needed to use the calendar in a number of different places, so creating a view element for it in Cake made the most sense.

<<< March 2009 >>>

Su Mo Tu We Th Fr Sa
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31

Download the source (zipped .ctp file) here: CakePHP Calendar Element

The code is pretty simple.  Pass the element a year and a month when rendering it and it will render that month in a simple table.  It also accepts two additional arguments, a day link and a month link.  The day link allows you to specify what the base URL for each day should be, and similarly, the month link specifies the base URL for traversing months.

Drop the calendar.ctp into your elements directory in your Cake app (usually /app/views/elements) and render it an any view you’d like.  Below is an example of rendering the element with all of its parameters:

<?php
$this->renderElement('calendar', array(
    'year' => 2009,
    'month' => '11',
    'month_link' => '/controller/showmonth/',
    'day_link' => '/controller/showday/'
));
?>

The month link and day link provide the base URLs for linking to additional months and days, allowing you to modify the element for particular controller actions.

Feel free to use this component for anything you’d like, no strings attached.  If you make any improvements or enhancements to it, be sure to share!

Creating a Tree Structure within a CakePHP Model

My assumption going into this was that creating a tree-like relationship within one of my models would be fairly straightforward with Cake’s model relationships.  As it turns out, I was absolutely correct.

Let’s say we want to create a model called “Category”, and each category can belong to a parent category.  In other words, we want our application to recognize that every category may potentially belong to a higher-level category, and likewise, that every category may also have child categories beneath it.

Consider a Category model similar to the following:

CREATE TABLE `categories` (
  `id` INTEGER(12) NOT NULL AUTO_INCREMENT,
  `name` VARCHAR(64) NOT NULL,
  `parent_id` INTEGER(12) NULL,

  PRIMARY KEY(`id`)
);

The first step is to define our model (obviously):

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

Next, let’s tell Cake that a category can belong to other categories using the belongsTo association:

< ?php

class Category extends AppModel {
  var $name = 'Category';

  var $belongsTo = array(
    'ParentCategory' => array(
      'className' => 'Category',
      'foreignKey' => 'parent_id'
  ));
}
?>

Finally, we want to tell Cake that a category can have other categories beneath it.  We’ll do this using the hasMany association:

< ?php

class Category extends AppModel {
  var $name = 'Category';

  var $belongsTo = array(
    'ParentCategory' => array(
      'className' => 'Category',
      'foreignKey' => 'parent_id'
  ));

  var $hasMany = array(
    'ChildCategory' => array(
      'className' => 'Category',
      'foreignKey' => 'parent_id'
  ));
}
?>

That’s it.  Create a controller for categories and turn on scaffolding, and you’ll see how nicely this all works out.