Posts Tagged ‘ database

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!