CakePHP: Adding Dynamic Content to Layouts

For the past few months, I’ve had this lingering item on my TODO list to Learn CakePHP. When that item was added to the list, we had decided to use it as our default framework for building new apps at work and I wanted to at least be aware of its basic capabilities, techniques and toolkits. Unfortunately, I don’t get as much time as I’d like to put my head down and write code at work, so this item has been taunting me until this week when some time presented itself.

I come into this brand new to CakePHP, but as an experienced web and OOP developer. I have certain expectations of encapsulation, elegance and maintainability that I want to bring to my CakePHP project. As I run across scenarios where I don’t think that such elegance is obvious, I’ll document the solution I used here. Maybe someone else can benefit from my experience and maybe I can benefit from that of others.

The Project

Since I learn most effectively by doing, I needed a project. The project I chose was a rebuild of a site I did about a year ago; I was familiar with the needs as well as the things I would have liked to do differently, had there been enough time.

For the site I’m building, I have not only a header and a footer, but also a sidebar. The site has navigation menus in each of these areas of the template that I wanted to be data driven and several menu items are shared across menu locations. Not an uncommon need, to be sure, but I wasn’t sure how to handle it with CakePHP because layouts don’t have models – or controllers – of their own to access the data and this layout would be shared by every page in the site – I didn’t want to add the logic to retrieve and manipulate menu data in more than one place.

The Scenario

The scenario, then, is the need to place dynamic content within a layout. Without going into excruciating detail, a CakePHP layout is analogous to a template in a CMS. It’s the part of the page that is fixed and usually shared across multiple pages of a site. On robwilkerson.org, for example, the layout would include the header, the footer and the sidebar on the right. Everything else is the page content or, in CakePHP’s MVC-speak, the view template.

The Data

The backbone of a data driven solution, of course, is the database. For my menus problem, I created three tables: nav_menus, nav_menu_items and nav_menu_items_nav_menus following all of the conventions of CakePHP so that everything would work without any more effort than necessary. The last table, of course, is a linking table to identify which items exist on which menus. The many-to-many relationship it enables is what allows a single menu item to appear on more than one menu.

The Models & Controllers

With the database prepared, I needed to provide access from my application and this is where CakePHP and most other frameworks I’m familiar with really shine. Using CakePHP, I just “baked” models and controllers using the command line utility:

$ cd path/to/cake/console
$ ./cake bake model nav_menu
$ ./cake bake controller nav_menus
$ ./cake bake model nav_menu_items
$ ./cake bake controller nav_menu_items

That’s it. A few simple command line requests and I had all the files I needed. All that was left to get this scaffolded up was to define the associations between the models I’d just created. To do so, I opened each model file (located in app/models/) and added a single line of code to each. To app/models/nav_menu.php, I added the following:

public $hasAndBelongsToMany = array ( 'NavMenuItem' );

Similarly, I added the following to app/models/nav_menu_item.php:

public $hasAndBelongsToMany = array ( 'NavMenu' );

That’s all I needed to do to be able to create new menus and menu items. By requesting http://localhost/nav_menus/add, I was able to use CakePHP’s scaffolded interface to create new menus. Likewise, by accessing http://localhost/nav_menu_items/add, I could create new menu items and assign them to the appropriate menus.

Sharing the Logic

The non-obvious question in my mind was how do I get this dynamic data displayed on a layout? A layout doesn’t have its own controller and I sure has hell don’t want to have to tell every controller how to render my navigation. Inheritance to the rescue.

CakePHP’s controllers all extend the AppController class, so I dropped my menu logic there:

class AppController extends Controller {
   public $uses = array ( ‘NavMenu’ );

public function beforeRender() { /** * Populate and expose menu data */ $raw_menus = $this->NavMenu->find ( ‘all’ ); $menus = $this->NavMenu->unobfuscateMenus ( $raw_menus ); $this->set ( ‘nav_menus’, $menus ); } }

In my mind, this is perfectly reasonable since this logic really will be used on every single page request; there are no CPU cycles being wasted.

An Associative Recordset

If anyone’s wondering about the unobfuscateMenus() method, I created a custom method in my NavMenus model that would allow me to access and key on my menus as an associative array rather than as an indexed array. I have three menus: Primary, Secondary and (surprise!) Tertiary. By default, CakePHP exposes these as $nav_menus[ 0 ][‘Primary’], $nav_menus[ 1 ][‘Secondary’], etc. I wanted to be able to key on the menu name, though, so that in my layout, I had the ability to access the precise menu I wanted when I wanted it.

The unobfuscateMenus() method reorganizes the array slightly so that I can access $nav_menus[‘Primary’] when I want to display the primary menu without looping over all menus and testing for the menu name. This should become more clear in the code that renders the menu below.

With the code added to the AppController class, all of my controllers and, by extension, all of my layouts will have access to my menu data. Since I have several menus on my layout, I not only wanted to share the logic, but also the output.

Sharing the Output

Duplicating presentation code is almost as distasteful to me as duplicating business logic, so I created a navigation element that would accept my $nav_menus array and iterate over each item to create an unordered list. The menu array whose items will be displayed is passed to the element as the $menu variable.

<ul>
   <?php $i_item = 0; ?>
   <?php foreach ( $menu['items'] as $item ): ?>
   <?php
      $i_item++;
      $item_class = '';
      if ( $i_item  1 ) {
         $item_class = ' class="first"';
      }
      else if ( $i_item  count ( $menu['items'] ) ) {
         $item_class = ' class="last"';
      }
   ?>
   <li<?php echo $item_class; ?>>
      <a href="<?php echo $item['target_uri']; ?>" 
         title="<?php echo $item['description']; ?>"
         ><?php echo $item['title']; ?></a>
   </li>
   <?php endforeach; ?>
</ul>

Wiring It All Together

My logic, data and presentation code are all in place now, so the only thing left is to apply the context – the layout. In my layout markup, I included the following code to display my primary menu:

<?php echo $this->element (
      'navigation',
      array (
         'id'   => 'primary',
         'menu' => $nav_menus['PRIMARY']
      )
   );
?>

And for my secondary menu:

<?php echo
      $this->element (
      'navigation',
      array (
         'id'   => 'secondary',
         'menu' => $nav_menus['SECONDARY']
      )
   );
?>

Key Points

Here’s what I like about this solution, in no particular order:

  1. One database query. I have three menus and I certainly could have retrieved each menu individually. Whenever it’s feasible, though, I like to avoid querying the database. In my experience, the database is almost always the bottleneck for an application; the less I have to communicate with it, the better. With a little application logic (the unobfuscateMenus() method), I get all the benefits of three queries at the performance cost of just one.
  2. It will scale. If I need six navigation menus, all that’s required is a little more data. The code can handle the additional menus without any changes.
  3. There’s no waste. I’m very wary of dropping code in a super class, but in this case it works. The AppController class is loaded on every request and the logic it includes is used on every request. Symmetry.
  4. Encapsulation of presentation. Within the element, there’s no logic that isn’t used exclusively for presentation. Because I unobfuscated the menus array, I’m able to pass in only the menu that needs to be rendered by the element at the time it’s called.

Subscribe6 Comments on CakePHP: Adding Dynamic Content to...

  1. Kyril Revels said...

    Good solution, Rob. I’ve been using Cake for quite some time, and it’s flexibility definitely impresses me. My frustrations w/ Cake mainly come from the lack of solid online documentation rather than any beefs w/ the framework itself.

    One other thing: I think you might be able to remove your unobfuscateMenus() model method if you were to select from NavMenu using find(‘list’) rather than find(‘all’).

    The ‘list’ parameter has a nice little feature that will auto-index your results based on the primary key by default. If you wish to index on some other field you can specify it by using:

    NavMenu->find(‘list’, array(‘fields’ => array(‘NavMenu.indexfield’, ‘NavMenu.field1’, ‘NavMenu.fieldn’)));

  2. Rob Wilkerson said...

    I actually think that there’s a pretty decent amount of documentation, my bitch at times is that it only covers the trivial cases. I often find myself wishing that it went one step further.

    I worry about bloat with Cake – it’s not a small footprint – but it seems pretty manageable. There’s a lot of stuff, but it’s pretty confined and it seems like I can use as much or as little I need without incurring too much of a performance hit if I’m carefule; I haven’t benchmarked anything, though.

    I’ll look into list(). If it does what you say, then I’m all about it. Thanks.

  3. Ник said...

    Hi, I’ve wrote about this here: http://nik.chankov.net/2007/10/10/cakephp-and-layout-secret-of-data-passing-through/ about an year ago. :)

    So basically we are using the same approach, but instead of element I am using Helper. I think that it’s more good looking, but it’s a matter of taste anyway.

  4. Rob Wilkerson said...

    Great solution, Ник. I like your approach better than mine since it’s much more extensible. I only need menus in my layout, but that may not be the only place I’ll ever need them. Your solution gives me a lot more options.

    Don’t be shocked if I, um, borrow your approach. :-)

  5. webgeeks said...

    i am a newbie and could not understand more than few lines of code, further simplification will help..

  6. bla said...

    Hey,

    can you make the SQL-Code public?