Creating a routing-based menu in symfony 1.2, part 3: the builder

April 3rd, 2009 | by David |

After defining a menu structure and describing the sfMenuItem object it’s time to show you how to pack the whole menu thing up to generate the menu with a few lines of code.

Up until now we have defined a tree structure in yaml and created a PHP class to generate menu items recursively as well as render their content. We are able to load the object assigned to an sfObjectRoute and even find related objects and create route links for those through the sfObjectResolver class.

The next step

Now I want to show you how to use those sfMenuItem objects to generate your menu HTML code. There are 2 typical menu variants I want to implement:

  • Generate one flat level of a menu (e.g. for tab navigation)
  • Generate a complete tree menu (e.g. for a sidebar menu)

During the first part I only showed the HTML code for the tab row version – this is mainly because I haven’t used the menu code for trees myself yet, but now I think the tree version might be even more important for some users, so I decided to implement it, too.

Flat (tab) menu generation

When designing a tab menu based on a tree structure you have to keep a few things in mind. Most important is that when providing sub level tabs, only tabs that are descendants (children) of the active tab one level above should be shown.

To achieve rendering of a single level, I added a renderLevel() method to sfMenuItem.class.php:

  public function renderLevel($level, array &$rendered) {
    if ($level == 0) {
      $rendered[] = $this->render();
    } elseif ($level > 0 && in_array($this->routeName, $this->allRouteNames)) {
      $level--;
      foreach ($this->children as $child) {
        $child->renderLevel($level, $rendered);
      }
    }
  }

The code more or less explains itself. If the $level parameter is 0 (when the level is the one that is to be rendered) the item is rendered and added to an array passed by reference. If the level is too high and the current item is parent of the route to display, the level is decreased by 1 and renderLevel() is called for all child items. Thus only the items of a specific level and inside the required route path are rendered.

To create the complete menu I added another class: sfMenuBuilder.class.php:

class sfMenuBuilder {
  protected $menuItems = array();
 
  protected $route = null;
  protected $routeName = null;
  protected $routeObject = null;
 
  public function __construct(array &$itemsData) {
    foreach ($itemsData as $routeName => $itemData) {
      $this->menuItems[$routeName] = new sfMenuItem($routeName, $itemData);
    }
 
    $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();
      }
    }
 
    foreach ($this->menuItems as $item) {
      $item->initialize($this->routeName, $this->route, $this->routeObject);
    }
  }
 
  public function renderLevel($level = 0) {
    $rendered = array();
    foreach ($this->menuItems as $item) {
      $item->renderLevel($level, $rendered);
    }
    return $rendered;
  }
}

Now you can generate your menu like this:

  $builder = new sfMenuBuilder(sfConfig::get('app_menu_items'));
  $mainItems = $builder->renderLevel(0);
  $subItems = $builder->renderLevel(1);
 
  $mainItemsHtml = content_tag('ul', implode("\n", $mainItems));
  $subItemsHtml = content_tag('ul', implode("\n", $subItems));

You could place this code inside a helper function or directly in the view. But don’t do it until I’ve showed you how to do this even nicer in part 4 ;-)

Tree menu generation

To generate a tree menu we have to split up sfMenuItem::render() a little because we need to reach inside the li elements:

  public function isCurrent() {
    return in_array($this->routeName, $this->allRouteNames);
  }
 
  public function renderContent() {
    $i18n = sfContext::getInstance()->getI18n();
 
    if (!$this->isEnabled()) {
      return content_tag('a', $i18n->__($this->title));
    } else {
      if (is_null($this->requireObjectClass)) {
        return link_to($i18n->__($this->title), '@'.$this->myRouteName);
      } else {
        return link_to($i18n->__($this->title), $this->myRouteName, sfObjectResolver::resolve($this->routeObject, $this->requireObjectClass));
      }
    }
  }
 
  public function render() {
    $text = $this->renderContent();
    if (!$this->isEnabled()) {
      return $this->containerTag($text, array('class' => 'disabled'));
    } elseif ($this->isCurrent()) {
      return $this->containerTag($text, array('class' => 'current'));
    }
    return $this->containerTag($text);
  }

Now we can define a method to render the tree:

  public function renderTree($container = 'ul') {
    $children = '';
    foreach ($this->children as $child) {
      $children .= $child->renderTree($container);
    }
 
    $text = $this->renderContent();
    if ($children != '') {
      $text .= content_tag($container, $children);
    }
    if (!$this->isEnabled()) {
      return $this->containerTag($text, array('class' => 'disabled'));
    } elseif ($this->isCurrent()) {
      return $this->containerTag($text, array('class' => 'current'));
    }
    return $this->containerTag($text);
  }

In sfMenuBuilder.class.php we also define a method to render the tree:

  public function renderTree($container = 'ul') {
    $rendered = '';
    foreach ($this->menuItems as $item) {
      $rendered .= $item->renderTree($container);
    }
    return content_tag($container, $rendered);
  }

Creating the tree is even easier than creating the tab rows:

  $builder = new sfMenuBuilder(sfConfig::get('app_menu_items'));
  $treeHtml = $builder->renderTree();

Using the code above we are able to create tree-like menus as well as tab menu rows out of the same sfMenuItem objects.

We are not done yet

Now you know how it all works out. But do you think that was all? Of course not! Just give me a little more time to write the 4th and final part to this series, showing the full classes including phpDoc comments and adding the following features to our menu system:

  • Tab groups
  • Custom containers
  • Callback menu items
  • Static url menu items
  • A multi-instance sfMenuBuilder with named instances for several independent menus at once
  • … and some thoughts about how all this could be used dynamically

Curious? You better be!

Have fun!

  1. 2 Responses to “Creating a routing-based menu in symfony 1.2, part 3: the builder”

  2. By Hugo on Apr 4, 2009 | Reply

    Hello,

    That’s a great serie of symfony articles but there is one bad practice you show. You shouldn’t have to call the context inside your class but pass it from the outside. The problem is that calling sfContext::getInstance() inside a classe evolves a string coupling with your class and avoid it to be unit tested… The solution is to create a getter and a setter to pass the context from outside the class.

      public function setContext($context)
      {
        $this->context = $context;
      }
     
      public function getContext()
      {
        return $this->context;
      }
     
      // ...
    }

    You can also pass the context as a constructor argument.

    ++

  3. By David on Apr 4, 2009 | Reply

    Thanks for your input, I will include this in the final version. For the same reason I inject the routing information into the sfMenuItem objects, but of course the context should also be injected.

Post a Comment