Creating a routing-based menu in symfony 1.2, part 2: the MenuItem object
April 3rd, 2009 | by David |During my last article I described how the structure of my routing-based menu is defined. This time I want to show you how to create menu item objects using this structure as the source. You will also learn how the items interact with the symfony environment.
The structure again
To make sure you understand what comes next I want to bring the last version of the menu structure we defined in app.yml back to mind:
all:
menu:
items:
homepage:
title: Home
children:
homepage:
title: Home
about:
title: About
article_index:
title: Articles
children:
article_index:
title: Article index
article_view:
title: View article details
require_object_class: Article
article_edit:
title: Edit article
require_object_class: Article
alias_routes: [article_update]
article_comments_edit:
title: Edit article comments
require_object_class: Article
alias_routes: [article_comments_update]
user_view:
title: View article author
require_object_class: UserThe object constructor
I want each item out of this menu definition to be a single PHP object. The object should be created using the route name (the key in the yaml structure) and an array containing the node’s data. Both parameters are passed to the constructor of sfMenuItem.class.php:
class sfMenuItem { protected $myRouteName; protected $children = array(); protected $allRouteNames = array(); protected $title = ''; protected $requireObjectClass = null; public function __construct($myRouteName, array $itemData) { $this->myRouteName = $myRouteName; $this->allRouteNames[] = $myRouteName; if (isset($itemData['title'])) { $this->title = $itemData['title']; } if (isset($itemData['alias_routes'])) { $this->allRouteNames = array_merge($this->allRouteNames, (array) $itemData['alias_routes']); } if (isset($itemData['require_object_class'])) { $this->requireObjectClass = $itemData['require_object_class']; } if (isset($itemData['children']) && is_array($itemData['children'])) { foreach ($itemData['children'] as $subRouteName => $subItemData) { $this->children[$subRouteName] = new sfMenuItem($subRouteName, $subItemData); $this->allRouteNames = array_merge($this->allRouteNames, $this->children[$subRouteName]->getAllRouteNames()); } } }
As you can see the item’s data is stored in the object and child items are automatically added recursively. You can also see that both the own route name as well as all child route names are stored in the allRouteNames property. This will make it easy to find out if a specific node represents any route somewhere down below and thus has to be displayed.
Injecting the routing information
To make the intelligence of the sfMenuItem object work we need to initialize it using the real routing information. This information can be retrieved from the symfony context like this:
$this->routeName = sfContext::getInstance()->getRouting()->getCurrentRouteName(); $this->route = sfContext::getInstance()->getRequest()->getAttribute('sf_route'); if ($this->route instanceof sfObjectRoute) { $options = $this->route->getOptions(); if ($options['type'] == 'object') { $this->routeObject = $this->route->getObject(); } }
Since it would be a little bloating to do this in all menu items and also prevents you from injecting testing data the best way is to retrieve the data outside the sfMenuItem class (I will show later where to do this) and inject it into each sfMenuItem. This is done in the initialize() method:
public function initialize($routeName, sfRoute $route, $routeObject) { $this->routeName = $routeName; $this->route = $route; $this->routeObject = $routeObject; foreach ($this->children as $child) { $child->initialize($this->routeName, $this->route, $this->routeObject); } }
Rendering the output
Now the sfMenuItem object is ready to be rendered. The basic render() method looks like this:
public function render() { $i18n = sfContext::getInstance()->getI18n(); if (!$this->isEnabled()) { return $this->containerTag(content_tag('a', $i18n->__($this->title)), array('class' => 'disabled')); } else { if (is_null($this->requireObjectClass)) { $link = link_to($i18n->__($this->title), '@'.$this->myRouteName); } else { $link = link_to($i18n->__($this->title), $this->myRouteName, sfObjectResolver::resolve($this->routeObject, $this->requireObjectClass)); } if (in_array($this->routeName, $this->allRouteNames)) { return $this->containerTag($link, array('class' => 'current')); } else { return $this->containerTag($link); } } } public function isEnabled() { return is_null($this->requireObjectClass) || !is_null(sfObjectResolver::resolve($this->routeObject, $this->requireObjectClass)); } protected function containerTag($text, $options = array()) { return content_tag('li', $text, $options); }
I’m loading the i18n instance inside the render() method to translate the title texts. Of course this could be left out if you either don’t need i18n for your menu or just translate the titles before they are inserted into the sfMenuItems.
Why an object resolver is useful here
You can see 3 additional method dependencies of render(): isEnabled(), containerTag() and the external calls to sfObjectResolver::resolve(). The containerTag() is easy: this way the embracing tag for the menu item (li by default here) can be modified. isEnabled() checks if the item requires a specific object class and looks it up in sfObjectResolver. If no matching object is found, no link can be generated, so the a tag is just left without the href attribute.
See Managing “soft” relations between PHP objects to learn more about the resolver – here it comes in really handy, because the sfMenuItem class has absolutely no clue about the objects it has to link to, but using the resolver moves this dependency into each object’s context. Using this little trick you can have a menu that automatically generates reference route links to objects without the need to know anything about the objects but their class names. Could it be any more comfortable?
That’s it again for today (the articles just grow so long!). In my next article I will show you how to wrap the menu building process in a small builder class and what additional options sfMenuItem may provide.
One small side note: you may have noticed that the code presented here doesn’t follow symfony’s PHP coding style guidelines in terms of bracket placement. I just like them more this way and symfony’s style would make the article even longer, but feel free to change them as you like
Have fun!