Skip to content
Browse files

Initial import of KnpMenuBundle decoupled classes with a new namespace

  • Loading branch information...
0 parents commit a3d02ca8b619c91b832b2717ba84f045e1f57f16 @stof stof committed Aug 18, 2011
1 .gitignore
@@ -0,0 +1 @@
+/phpunit.xml
19 LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2011 KnpLabs - http://www.knplabs.com
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is furnished
+to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
94 README.markdown
@@ -0,0 +1,94 @@
+MenuBundle
+==========
+
+The KnnMenu library provides object oriented menus for PHP 5.3.
+
+ use Knp\Menu\MenuItem;
+
+ $menu = new MenuItem('My menu');
+ $menu->addChild('Home', $router->generate('homepage'));
+ $menu->addChild('Comments', $router->generate('comments'));
+ $menu->addChild('Symfony2', 'http://symfony-reloaded.org/');
+ echo $menu->render();
+
+The above menu would render the following HTML:
+
+ <ul>
+ <li class="first">
+ <a href="/">Home</a>
+ </li>
+ <li class="current">
+ <a href="/comments">Comments</a>
+ </li>
+ <li class="last">
+ <a href="http://symfony-reloaded.org/">Symfony2</a>
+ </li>
+ </ul>
+
+This way you can finally avoid writing an ugly template to show the selected item,
+the first and last items, submenus, ...
+
+> The bulk of the documentation can be found in the `doc` directory.
+
+## Installation
+
+### Get the bundle
+
+To install the bundle, place it in the `vendor/bundles/Knp/Bundle` directory of your project
+(so that it lives at `vendor/bundles/Knp/Bundle/MenuBundle`). You can do this by adding
+the bundle as a submodule, cloning it, or simply downloading the source.
+
+ git submodule add https://github.com/knplabs/KnpMenuBundle.git vendor/bundles/Knp/Bundle/MenuBundle
+
+### Add the namespace to your autoloader
+
+If it is the first Knp bundle you install in your Symfony 2 project, you
+need to add the `Knp` namespace to your autoloader:
+
+ // app/autoload.php
+ $loader->registerNamespaces(array(
+ 'Knp' => __DIR__.'/../vendor/bundles'
+ // ...
+ ));
+
+### Initialize the bundle
+
+To start using the bundle, initialize the bundle in your Kernel. This
+file is usually located at `app/AppKernel`:
+
+ public function registerBundles()
+ {
+ $bundles = array(
+ // ...
+ new Knp\Bundle\MenuBundle\KnpMenuBundle(),
+ );
+ )
+
+That's it! Other than a few templating helpers (explained next), the `MenuBundle`
+is a standalone PHP 5.3 library and can be used as soon as Symfony2's
+class autoloader is aware of it (this was just accomplished above).
+
+### What now?
+
+Now you probably want to use this bundle in your Symfony2 project.
+You will need 5 steps to get to the point where you can just type in your Twig template:
+
+ {{ knp_menu('main') }}
+
+* Create a Menu class
+* Declare a Menu service
+* Load your Menu service in the Dependency Injection Extension
+* Enable the Dependency Injection for your bundle
+* Render your menu with Twig
+
+Follow the tutorial in `Resources/doc/03-Twig-Integration.markdown` to
+discover how the `MenuBundle` will rock your world!
+
+## Credits
+
+This bundle was originally ported from [ioMenuPlugin](http://github.com/weaverryan/ioMenuPlugin),
+a menu plugin for symfony1. It has since been developed by [knpLabs](http://www.knplabs.com) and
+the Symfony community.
+
+> Although this bundle was written for Symfony2 projects, the core menu objects
+> can also be used outside of Symfony2!
190 doc/01-Basic-Menus.markdown
@@ -0,0 +1,190 @@
+Creating Menus: The Basics
+==========================
+
+Let's face it, creating menus sucks. Menus - a common aspect of any
+site - can range from being simple and mundane to giant monsters that
+become a headache to code and maintain.
+
+This bundle solves the issue by giving you a small, yet powerful and flexible
+framework for handling your menus. While most of the examples shown here
+are simple, the menus can grow arbitrarily large and deep.
+
+Creating a menu
+---------------
+
+The menu framework centers around one main class: `Knp\Menu\MenuItem`.
+It's best to think of each `MenuItem` object as an `<li>` tag that can
+hold children objects (`<li>` tags that are wrapped in a `<ul>` tag).
+For example:
+
+ use Knp\Menu\MenuItem;
+
+ $menu = new MenuItem('My menu');
+ $menu->addChild('Home', $router->generate('homepage'));
+ $menu->addChild('Comments', $router->generate('comments'));
+ $menu->addChild('Symfony2', 'http://symfony-reloaded.org/');
+ echo $menu->render();
+
+The above would render the following html code:
+
+ <ul>
+ <li class="first">
+ <a href="/">Home</a>
+ </li>
+ <li class="current">
+ <a href="/comments">Comments</a>
+ </li>
+ <li class="last">
+ <a href="http://symfony-reloaded.org/">Symfony2</a>
+ </li>
+ </ul>
+
+>**NOTE**
+>The menu framework automatically adds `first` and `last` classes to each
+>`<li>` tag at each level for easy styling. Notice also that a `current`
+>class is added to the "current" menu item by uri. The above example assumes
+>the menu is being rendered on the `/comments` page, making the Comments
+>menu the "current" item.
+
+>**NOTE**
+>When the menu is rendered, it's actually spaced correctly so that it appears
+>as shown in the source html. This is to allow for easier debugging and can
+>be turned off by calling `$menu->getRenderer()->setRenderCompressed(true)`.
+
+Working with your menu tree
+---------------------------
+
+Your menu tree works and acts like a multi-dimensional array. Specifically,
+it implements ArrayAccess, Countable and Iterator:
+
+ $menu = new MenuItem('My menu');
+ $menu->addChild('Home', $router->generate('homepage'));
+ $menu->addChild('Comments');
+
+ // ArrayAccess
+ $menu['Comments']->setUri($router->generate('comments'));
+ $menu['Comments']->addChild('My comments', $router->generate('my_comments'));
+
+ // Countable
+ echo count($menu); // returns 2
+
+ // Iterator
+ foreach ($menu as $child) {
+ echo $menu->getLabel();
+ }
+
+As you can see, the name you give your menu item (e.g. overview, comments)
+when creating it is the name you'll use when accessing it. By default,
+the name is also used when displaying the menu, but that can be overridden
+by setting the menu item's label (see below).
+
+Customizing each menu item
+--------------------------
+
+There are many ways to customize the output of each menu item.
+
+### The label
+
+By default, a menu item uses its name when rendering. You can easily
+change this without changing the name of your menu item by setting its label:
+
+ $menu->addChild('Home', $router->generate('homepage'));
+ $menu['Home']->setLabel('Back to homepage');
+
+### The uri
+
+When creating a new menu item (via the constructor or via `addChild()`),
+the second argument is the uri to your menu item. If a menu
+isn't given a url, then text will be output instead of a link:
+
+ $menu->addChild('Not a link');
+ $menu->addChild('Home', $router->generate('homepage'));
+ $menu->addChild('Symfony', 'http://www.symfony-reloaded.org');
+
+You can also specify the uri after creation via the `setUri()` method:
+
+ $menu['Home']->setUri($router->generate('homepage'));
+
+>**NOTE**
+>To generate Symfony uris, we use Symfony's `Router`. See chapter two for
+>more details on how to create menus that use the `Router` to generate uris.
+
+### Menu attributes
+
+In fact, you can add any attribute to the `<li>` tag of a menu item. This
+can be done via the optional 3rd argument when creating a menu item or
+via the `setAttribute()` method:
+
+ $menu->addChild('Home', null, array('id' => 'back_to_homepage'));
+ $menu['Home']->setAttribute('id', 'back_to_homepage');
+
+### Rendering only part of a menu
+
+If you need to render only part of your menu, the menu framework gives
+you unlimited control to do so:
+
+ // render only 2 levels deep (root, parents, children)
+ $menu->render(2);
+
+ // rendering everything except for the children of the Home branch
+ $menu['Home']->setShowChildren(false);
+ $menu->render();
+
+ // render everything except for Home AND its children
+ $menu['Home']->setShow(false);
+ $menu->render();
+
+Using the above controls, you can specify exactly which part of you menu
+you need to render at any given time.
+
+### The "root" is special
+
+Each menu is a tree containing exactly one root menu item. Let's revisit
+the previous example:
+
+ $menu = new MenuItem('My menu');
+ $menu->addChild('Home', $router->generate('homepage'));
+ $menu->addChild('Comments', $router->generate('comments'));
+
+In the above example, the `$menu` variable, corresponding to a menu item
+named "My menu" is the root. The root node is special in that no `<li>`
+tag is rendered and the name is never output:
+
+ <ul>
+ <li class="first">
+ <a href="/">Home</a>
+ </li>
+ <li class="current last">
+ <a href="/comments">Comments</a>
+ </li>
+ </ul>
+
+As you can see, the name "My menu" appears nowhere. The root menu item
+will always render its children, but not itself. However, any attributes
+that you set on your root will be output on the top-level `<ul`> element
+itself.
+
+To facilitate the creation of the root node, a special helper class, `Knp\Menu\Menu`
+was created:
+
+ use Knp\Menu\Menu;
+
+ $menu = new Menu(array('class' => 'root_menu');
+ $menu->addChild('Home', $router->generate('homepage'));
+ $menu->addChild('Comments', $router->generate('comments'));
+
+This will create the same menu as the previous option, but allows you to
+skip the specification of a name or route (the first and only argument
+is the array of attributes for the `<ul>`) for the root node.
+
+Creating a Menu from a Tree structure
+-------------------------------------
+
+You can create a menu easily from a Tree structure (a nested set for example) by
+making it implement ``Knp\Menu\NodeInterface``. You will then be able
+to create the menu easily (assuming ``$node`` is the root node of your structure):
+
+ <?php
+
+ $factory = new \Knp\Menu\MenuFactory();
+ $menu = $factory->createFromNode($node);
199 doc/02-Integrate-With-Symfony.markdown
@@ -0,0 +1,199 @@
+Using menus with Symfony2
+=========================
+
+The core menu classes of the MenuBundle, `Knp\Menu\Menu` and
+`Knp\Menu\MenuItem` are perfectly decoupled from Symfony2 and
+can be used in any PHP 5.3 project.
+
+This bundle also provides several classes that ease the integration of
+menus within a Symfony2 project.
+
+## Make your menu a service
+
+There a lot of benefits to making a menu a service. Its logic is then
+self-contained,and it can be accessed from anywhere in the project.
+
+### Create your menu class
+
+Create a `MainMenu` class for your `main` menu:
+
+ <?php // src/MyVendor/MyBundle/Menu/MainMenu.php
+
+ namespace MyVendor\MyBundle\Menu;
+
+ use Knp\Menu\Menu;
+ use Symfony\Component\HttpFoundation\Request;
+ use Symfony\Component\Routing\Router;
+
+ class MainMenu extends Menu
+ {
+ /**
+ * @param Request $request
+ * @param Router $router
+ */
+ public function __construct(Request $request, Router $router)
+ {
+ parent::__construct();
+
+ $this->setCurrentUri($request->getRequestUri());
+
+ $this->addChild('Home', $router->generate('homepage'));
+ // ... add more children
+ }
+ }
+
+The construction of the menu items is now contained inside the menu.
+It requires a Symfony Router in order to generate uris.
+
+### Declare and configure your menu service
+
+Next, declare you knp_menu service class via configuration. An example in XML
+is shown below:
+
+ # src/MyVendor/MyBundle/Resources/config/menu.xml
+ <?xml version="1.0" ?>
+
+ <container xmlns="http://symfony.com/schema/dic/services"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
+
+ <parameters>
+ <parameter key="knp_menu.main.class">MyVendor\MyBundle\Menu\MainMenu</parameter>
+ </parameters>
+
+ <services>
+ <service id="menu.main" class="%knp_menu.main.class%" scope="request">
+ <tag name="knp_menu.menu" alias="main" />
+ <argument type="service" id="request" />
+ <argument type="service" id="router" />
+ </service>
+ </services>
+
+ </container>
+
+If you include the menu configuration in your bundle (as shown above), you'll
+need to include it as a resource in your base configuration:
+
+ # app/config/config.yml
+ imports:
+ - { resource: "@MyVendorMyBundle/Resources/config/menu.xml" }
+ ...
+
+### Access the menu service
+
+You can now access your menu like any Symfony service:
+
+ $menu = $container->get('menu.main');
+
+From a controller, it's even easier:
+
+ $menu = $this->get('menu.main');
+
+The menu is lazy loaded, and will construct its children the first time
+you access it.
+
+## Create a template helper for your menu
+
+You will probably need to access the menu from a template.
+You _could_ render an entire action to get the menu from a controller class,
+pass it to a template and the render it. But it would be a bit overkill.
+You can easily enable a Symfony template helper instead. This bundle
+provides a generic menu template helper, all you need to do is enable the helper.
+
+### Enable the menu template helper
+
+ # app/config/config.yml
+ knp_menu:
+ templating: true
+
+### Access the menu from a template
+
+You now can render the menu in a template:
+
+ echo $view['menu']->get('main')->render(); // This uses the Renderer
+
+ echo $view['menu']->render('main'); // This uses the templates
+
+Or manipulate it:
+
+ $view['menu']['main']['Home']->setLabel('<span>Home</span>');
+ $view['menu']['main']['Home']->setIsCurrent(true);
+
+## Customize your Menu
+
+If you want to customize the way your menu are rendered, just create a
+custom `MenuItem` class
+
+ # src/MyVendor/MyBundle/Menu/MyCustomMenuItem.php
+ <?php
+ namespace MyVendor\MyBundle\Menu;
+ use Knp\Menu\MenuItem;
+
+ class MyCustomMenuItem extends MenuItem
+ {
+ /**
+ * Renders the anchor tag for this menu item.
+ *
+ * If no uri is specified, or if the uri fails to generate, the
+ * label will be output.
+ *
+ * @return string
+ */
+ public function renderLink()
+ {
+ $label = $this->renderLabel();
+ $uri = $this->getUri();
+ if (!$uri) {
+ return $label;
+ }
+
+ return sprintf('<a href="%s"><span></span>%s</a>', $uri, $label);
+ }
+ }
+
+This example overrides the `renderLink()` method. You can then use the new
+`CustomMenuItem` class as the default item class in your `MainMenu`:
+
+ // src/MyVendor/MyBundle/Menu/MainMenu.php
+ <?php
+ namespace MyVendor\MyBundle\Menu;
+ use Knp\Menu\Menu;
+ use Symfony\Component\HttpFoundation\Request;
+ use Symfony\Component\Routing\Router;
+
+ class MainMenu extends Menu
+ {
+ public function __construct(Request $request, Router $router)
+ {
+ parent::__construct(array(), 'MyVendor\MyBundle\Menu\MyCustomMenuItem');
+
+ $this->setCurrentUri($request->getRequestUri());
+
+ $this->addChild('Home', $router->generate('homepage'));
+ $this->addChild('Comments', $router->generate('comments'));
+ }
+ }
+
+Or, if you want to customize each child item, pass them as an argument of
+the `addChild()` method:
+
+ // src/MyVendor/MyBundle/Menu/MainMenu.php
+ <?php
+ namespace MyVendor\MyBundle\Menu;
+ use Knp\Menu\Menu;
+ use Symfony\Component\HttpFoundation\Request;
+ use Symfony\Component\Routing\Router;
+
+ class MainMenu extends Menu
+ {
+ public function __construct(Request $request, Router $router)
+ {
+ parent::__construct();
+
+ $this->setCurrentUri($request->getRequestUri());
+
+ $this->addChild(new MyCustomMenuItem('Home', $router->generate('homepage')));
+ $this->addChild(new MyCustomMenuItem('Comments', $router->generate('comments')));
+ }
+ }
+
134 doc/03-Twig-Integration.markdown
@@ -0,0 +1,134 @@
+Twig Integration
+================
+
+Services tagged as `knp_menu.menu` will be added automatically to the Twig helper. You
+can access those menus with the `{{ knp_menu('menu_alias') }}` tag in your Twig templates.
+
+Here is a complete but simple example for a Menu named `main`, used as your
+main navigation for the whole page (expecting you have a `MyVendor\MyBundle` bundle
+where you store your menu).
+
+Create a Menu class
+-------------------
+
+Create a `MainMenu` class for your `main` menu:
+
+ <?php // src/MyVendor/MyBundle/Menu/MainMenu.php
+
+ namespace MyVendor\MyBundle\Menu;
+
+ use Knp\Menu\Menu;
+ use Symfony\Component\HttpFoundation\Request;
+ use Symfony\Component\Routing\Router;
+
+ class MainMenu extends Menu
+ {
+ /**
+ * @param Request $request
+ * @param Router $router
+ */
+ public function __construct(Request $request, Router $router)
+ {
+ parent::__construct();
+
+ $this->setCurrentUri($request->getRequestUri());
+
+ $this->addChild('Home', $router->generate('homepage'));
+ // ... add more children
+ }
+ }
+
+Declare a Menu service
+----------------------
+
+First create a `menu.xml` to declare your `menu.main` service:
+
+ # src/MyVendor/MyBundle/Resources/config/menu.xml
+ <?xml version="1.0" ?>
+
+ <container xmlns="http://symfony.com/schema/dic/services"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
+
+ <parameters>
+ <parameter key="menu.main.class">MyVendor\MyBundle\Menu\MainMenu</parameter>
+ </parameters>
+
+ <services>
+ <service id="menu.main" class="%menu.main.class%" scope="request">
+ <tag name="knp_menu.menu" alias="main" />
+ <argument type="service" id="request" />
+ <argument type="service" id="router" />
+ </service>
+ </services>
+
+ </container>
+
+> **About `<tag>`:** Tagging your menu with the name `knp_menu.menu` tells
+> the Twig helper to load this menu and give it the alias `main`.
+> This way you can use a simple alias in your template to tell the twig helper
+> to render THIS menu.
+
+
+If you include the menu configuration in your bundle (as shown above), you'll
+need to include it as a resource in your base configuration:
+
+ # app/config/config.yml
+ imports:
+ - { resource: "@MyVendorMyBundle/Resources/config/menu.xml" }
+ ...
+
+
+Configure the bundle to use Twig
+--------------------------------
+
+Finally you should enable the Twig extension of the bundle:
+
+ # app/config/config.yml
+ knp_menu:
+ twig: true
+
+Render your menu with Twig
+--------------------------
+
+Now its time to render the menu in your main `layout.twig`:
+
+ {# app/views/layout.twig #}
+ <html>
+ {# ... #}
+ <body>
+ {# ... #}
+ <nav id="main">
+ {{ knp_menu('main') }}
+ </nav>
+ {# ... #}
+ </body>
+ </html>
+
+
+You can optionally provide a `depth` parameter to control how much of your menu
+you want to render:
+
+ {{ knp_menu('main', 3) }}
+
+Render your menu using the Renderer of the MenuItem object
+----------------------------------------------------------
+
+You can also use the Renderer which allows you not to use the PHP templating
+engine and to customize the rendering by changing the renderer of the menu.
+Just get the menu object and call the ``render`` method:
+
+ {# app/views/layout.twig #}
+ <html>
+ {# ... #}
+ <body>
+ {# ... #}
+ <nav id="main">
+ {{ knp_menu_get('main').render|raw }}
+ </nav>
+ {# ... #}
+ </body>
+ </html>
+
+> Using the ``raw`` filter is needed when the autoescaping is enabled as the
+> ``render`` method returns HTML code.
26 phpunit.xml.dist
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<phpunit backupGlobals="false"
+ backupStaticAttributes="false"
+ colors="false"
+ convertErrorsToExceptions="true"
+ convertNoticesToExceptions="true"
+ convertWarningsToExceptions="true"
+ processIsolation="false"
+ stopOnFailure="false"
+ syntaxCheck="false"
+ bootstrap="tests/bootstrap.php"
+>
+
+ <testsuites>
+ <testsuite name="KnpMenu Test Suite">
+ <directory>./tests/</directory>
+ </testsuite>
+ </testsuites>
+
+ <filter>
+ <whitelist>
+ <directory>./src</directory>
+ </whitelist>
+ </filter>
+</phpunit>
62 src/Knp/Menu/Menu.php
@@ -0,0 +1,62 @@
+<?php
+
+namespace Knp\Menu;
+
+/**
+ * A convenience class for creating the root node of a menu.
+ * Decoupled from Symfony2, can be used in any PHP 5.3 project.
+ * Originally taken from ioMenuPlugin (http://github.com/weaverryan/ioMenuPlugin)
+ *
+ * When creating the root menu object, you can use this class or the
+ * normal MenuItem class. For example, the following are equivalent:
+ * $menu = new Menu(array('class' => 'root'));
+ * $menu = new MenuItem(null, null, array('class' => 'root'));
+ */
+class Menu extends MenuItem
+{
+ /**
+ * @var string
+ */
+ protected $childClass;
+
+ /**
+ * Class constructor
+ *
+ * @see MenuItem
+ * @param array $attributes
+ * @param string $childClass The class to use if instantiating children menu items
+ */
+ public function __construct($attributes = array(), $childClass = 'Knp\Menu\MenuItem')
+ {
+ $this->childClass = $childClass;
+
+ parent::__construct(null, null, $attributes);
+ }
+
+ public function initialize(array $options = array())
+ {
+ }
+
+ /**
+ * Overridden to specify what the child class should be
+ */
+ protected function createChild($name, $route = null, $attributes = array(), $class = null)
+ {
+ if (null === $class)
+ {
+ $class = $this->childClass;
+ }
+
+ return parent::createChild($name, $route, $attributes, $class);
+ }
+
+ /**
+ * Get the class used to instanciate children
+ *
+ * @return string
+ **/
+ public function getChildClass()
+ {
+ return $this->childClass;
+ }
+}
57 src/Knp/Menu/MenuFactory.php
@@ -0,0 +1,57 @@
+<?php
+
+namespace Knp\Menu;
+
+/**
+ * Factory to create a menu from a tree
+ */
+class MenuFactory
+{
+ /**
+ * Create a menu item from a NodeInterface
+ *
+ * @param NodeInterface $node
+ * @return MenuItem
+ */
+ public function createFromNode(NodeInterface $node)
+ {
+ $item = new MenuItem($node->getName(), $this->getUriFromNode($node), $node->getAttributes());
+ $item->setLabel($node->getLabel());
+
+ foreach ($node->getChildren() as $childNode) {
+ $item->addChild($this->createFromNode($childNode));
+ }
+
+ return $item;
+ }
+
+ /**
+ * Creates a new menu item (and tree if $data['children'] is set).
+ *
+ * The source is an array of data that should match the output from MenuItem->toArray().
+ *
+ * @param array $data The array of data to use as a source for the menu tree
+ * @return MenuItem
+ */
+ public function createFromArray(array $data)
+ {
+ $class = isset($data['class']) ? $data['class'] : '\Knp\Menu\MenuItem';
+
+ $name = isset($data['name']) ? $data['name'] : null;
+ $menu = new $class($name);
+ $menu->fromArray($data);
+
+ return $menu;
+ }
+
+ /**
+ * Get the uri for the given node
+ *
+ * @param NodeInterface $node
+ * @return string
+ */
+ protected function getUriFromNode(NodeInterface $node)
+ {
+ return $node->getUri();
+ }
+}
1,311 src/Knp/Menu/MenuItem.php
@@ -0,0 +1,1311 @@
+<?php
+
+namespace Knp\Menu;
+use Knp\Menu\Renderer\RendererInterface;
+use Knp\Menu\Renderer\ListRenderer;
+
+/**
+ * This is your base menu item. It roughly represents a single <li> tag
+ * and is what you should interact with most of the time by default.
+ * Decoupled from Symfony2, can be used in any PHP 5.3 project.
+ * Originally taken from ioMenuPlugin (http://github.com/weaverryan/ioMenuPlugin)
+ */
+class MenuItem implements \ArrayAccess, \Countable, \IteratorAggregate
+{
+ /**
+ * Properties on this menu item
+ */
+ protected
+ $name = null, // the name of this menu item (used for id by parent menu)
+ $label = null, // the label to output, name is used by default
+ $linkAttributes = array(), // an array of attributes for the item link
+ $labelAttributes = array(), // an array of attributes for the item text
+ $uri = null, // the uri to use in the anchor tag
+ $attributes = array(); // an array of attributes for the li
+
+ /**
+ * Options related to rendering
+ */
+ protected
+ $show = true, // boolean to render this menu
+ $showChildren = true; // boolean to render the children of this menu
+
+ /**
+ * Metadata on this menu item
+ */
+ protected
+ $children = array(), // an array of MenuItem children
+ $num = null, // the order number this menu is in its parent
+ $parent = null, // parent MenuItem
+ $isCurrent = null, // whether or not this menu item is current
+ $currentUri = null, // the current uri to use for selecting current menu
+ $currentAsLink = true; // boolean to render the current uri as a link or not
+
+ /**
+ * The renderer used to render this menu
+
+ * @var RendererInterface
+ */
+ protected $renderer = null;
+
+ /**
+ * Class constructor
+ *
+ * @param string $name The name of this menu, which is how its parent will
+ * reference it. Also used as label if label not specified
+ * @param string $uri The uri for this menu to use. If not specified,
+ * text will be shown without a link
+ * @param array $attributes Attributes to place on the li tag of this menu item
+ */
+ public function __construct($name, $uri = null, $attributes = array())
+ {
+ $this->name = (string) $name;
+ $this->uri = $uri;
+ $this->attributes = $attributes;
+ }
+
+ /**
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * @param string $name
+ * @return MenuItem
+ */
+ public function setName($name)
+ {
+ if ($this->name == $name) {
+ return $this;
+ }
+
+ if ($this->getParent() && $this->getParent()->getChild($name)) {
+ throw new \InvalidArgumentException('Cannot rename item, name is already used by sibling.');
+ }
+
+ $oldName = $this->name;
+ $this->name = $name;
+
+ if ($this->getParent()) {
+ $this->getParent()->updateChildId($this, $oldName);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Updates id for child based on new name.
+ *
+ * Used internally after renaming item which has parent.
+ *
+ * @param MenuItem $child Item whose name has been changed.
+ * @param string $oldName Old (previous) name of item.
+ *
+ */
+ protected function updateChildId(MenuItem $child, $oldName)
+ {
+ $names = array_keys($this->getChildren());
+ $items = array_values($this->getChildren());
+
+ $offset = array_search($oldName, $names);
+ $names[$offset] = $child->getName();
+
+ $children = array_combine($names, $items);
+ $this->setChildren($children);
+ }
+
+ /**
+ * Get the uri for a menu item
+ *
+ * @return string
+ */
+ public function getUri()
+ {
+ return $this->uri;
+ }
+
+
+ /**
+ * Set the uri for a menu item
+ *
+ * @param string $uri The uri to set on this menu item
+ * @return MenuItem
+ */
+ public function setUri($uri)
+ {
+ $this->uri = $uri;
+
+ return $this;
+ }
+
+ /**
+ * Returns the label that will be used to render this menu item
+ *
+ * Defaults to the name of no label was specified
+ *
+ * @return string
+ */
+ public function getLabel()
+ {
+ return ($this->label !== null) ? $this->label : $this->name;
+ }
+
+ /**
+ * @param string $label The text to use when rendering this menu item
+ * @return MenuItem
+ */
+ public function setLabel($label)
+ {
+ $this->label = $label;
+
+ return $this;
+ }
+
+ /**
+ * @return array
+ */
+ public function getAttributes()
+ {
+ return $this->attributes;
+ }
+
+ /**
+ * @param array $attributes
+ * @return MenuItem
+ */
+ public function setAttributes(array $attributes)
+ {
+ $this->attributes = $attributes;
+
+ return $this;
+ }
+
+ /**
+ * @param string $name The name of the attribute to return
+ * @param mixed $default The value to return if the attribute doesn't exist
+ *
+ * @return mixed
+ */
+ public function getAttribute($name, $default = null)
+ {
+ if (isset($this->attributes[$name])) {
+ return $this->attributes[$name];
+ }
+
+ return $default;
+ }
+
+ public function setAttribute($name, $value)
+ {
+ $this->attributes[$name] = $value;
+
+ return $this;
+ }
+
+ /**
+ * @return array
+ */
+ public function getLinkAttributes()
+ {
+ return $this->linkAttributes;
+ }
+
+ /**
+ * @param array $linkAttributes
+ * @return MenuItem
+ */
+ public function setLinkAttributes(array $linkAttributes)
+ {
+ $this->linkAttributes = $linkAttributes;
+
+ return $this;
+ }
+
+ /**
+ * @param string $name The name of the attribute to return
+ * @param mixed $default The value to return if the attribute doesn't exist
+ *
+ * @return mixed
+ */
+ public function getLinkAttribute($name, $default = null)
+ {
+ if (isset($this->linkAttributes[$name])) {
+ return $this->linkAttributes[$name];
+ }
+
+ return $default;
+ }
+
+ /**
+ * @param string $name
+ * @param string $value
+ *
+ * @return MenuItem
+ */
+ public function setLinkAttribute($name, $value)
+ {
+ $this->linkAttributes[$name] = $value;
+
+ return $this;
+ }
+
+ /**
+ * @return array
+ */
+ public function getLabelAttributes()
+ {
+ return $this->labelAttributes;
+ }
+
+ /**
+ * @param array $labelAttributes
+ * @return MenuItem
+ */
+ public function setLabelAttributes(array $labelAttributes)
+ {
+ $this->labelAttributes = $labelAttributes;
+
+ return $this;
+ }
+
+ /**
+ * @param string $name The name of the attribute to return
+ * @param mixed $default The value to return if the attribute doesn't exist
+ *
+ * @return mixed
+ */
+ public function getLabelAttribute($name, $default = null)
+ {
+ if (isset($this->labelAttributes[$name])) {
+ return $this->labelAttributes[$name];
+ }
+
+ return $default;
+ }
+
+ public function setLabelAttribute($name, $value)
+ {
+ $this->labelAttributes[$name] = $value;
+
+ return $this;
+ }
+
+ /**
+ * @return bool Whether or not this menu item should show its children.
+ */
+ public function getShowChildren()
+ {
+ return $this->showChildren;
+ }
+
+ /**
+ * Set whether or not this menu item should show its children
+ *
+ * @param bool $bool
+ * @return MenuItem
+ */
+ public function setShowChildren($bool)
+ {
+ $this->showChildren = (bool) $bool;
+
+ return $this;
+ }
+
+ /**
+ * @return bool Whether or not to show this menu item
+ */
+ public function getShow()
+ {
+ return $this->show;
+ }
+
+ /**
+ * Set whether or not this menu to show this menu item
+ *
+ * @param bool $bool
+ * @return MenuItem
+ */
+ public function setShow($bool)
+ {
+ $this->show = (bool) $bool;
+
+ return $this;
+ }
+
+ /**
+ * Whether or not this menu item should be rendered or not based on all the available factors
+ *
+ * @return boolean
+ */
+ public function shouldBeRendered()
+ {
+ return $this->getShow();
+ }
+
+ /**
+ * Whether or not this menu item should be rendered as a link based on the available factors
+ *
+ * @return boolean
+ */
+ public function shouldBeRenderedAsLink()
+ {
+ return ($this->getIsCurrent() && $this->getParent()->getCurrentAsLink())
+ || (!$this->getIsCurrent() && $this->getUri());
+ }
+
+ /**
+ * Add a child menu item to this menu
+ *
+ * @param mixed $child An MenuItem object or the name of a new menu to create
+ * @param string $uri If creating a new menu, the uri for that menu
+ * @param string $attributes If creating a new menu, the attributes for that menu
+ * @param string $class The class for menu item, if it needs to be created
+ *
+ * @return MenuItem The child menu item
+ */
+ public function addChild($child, $uri = null, $attributes = array(), $class = null)
+ {
+ if (!$child instanceof MenuItem) {
+ $child = $this->createChild($child, $uri, $attributes, $class);
+ }
+ elseif ($child->getParent()) {
+ throw new \InvalidArgumentException('Cannot add menu item as child, it already belongs to another menu (e.g. has a parent).');
+ }
+
+ $child->setParent($this);
+ $child->setShowChildren($this->getShowChildren());
+ $child->setCurrentUri($this->getCurrentUri());
+ $child->setNum($this->count());
+
+ $this->children[$child->getName()] = $child;
+
+ return $child;
+ }
+
+ /**
+ * Returns the child menu identified by the given name
+ *
+ * @param string $name Then name of the child menu to return
+ * @return MenuItem|null
+ */
+ public function getChild($name)
+ {
+ return isset($this->children[$name]) ? $this->children[$name] : null;
+ }
+
+ /**
+ * Moves child to specified position. Rearange other children accordingly.
+ *
+ * @param numeric $position Position to move child to.
+ *
+ */
+ public function moveToPosition($position)
+ {
+ $this->getParent()->moveChildToPosition($this, $position);
+ }
+
+ /**
+ * Moves child to specified position. Rearange other children accordingly.
+ *
+ * @param MenuItem $child Child to move.
+ * @param numeric $position Position to move child to.
+ */
+ public function moveChildToPosition(MenuItem $child, $position)
+ {
+ $name = $child->getName();
+ $order = array_keys($this->children);
+
+ $oldPosition = array_search($name, $order);
+ unset($order[$oldPosition]);
+
+ $order = array_values($order);
+
+ array_splice($order, $position, 0, $name);
+ $this->reorderChildren($order);
+ }
+
+ /**
+ * Moves child to first position. Rearange other children accordingly.
+ */
+ public function moveToFirstPosition()
+ {
+ $this->moveToPosition(0);
+ }
+
+ /**
+ * Moves child to last position. Rearange other children accordingly.
+ */
+ public function moveToLastPosition()
+ {
+ $this->moveToPosition($this->getParent()->count());
+ }
+
+ /**
+ * Reorder children.
+ *
+ * @param array $order New order of children.
+ */
+ public function reorderChildren($order)
+ {
+ if (count($order) != $this->count()) {
+ throw new \InvalidArgumentException('Cannot reorder children, order does not contain all children.');
+ }
+
+ $newChildren = array();
+
+ foreach($order as $name) {
+ if (!isset($this->children[$name])) {
+ throw new \InvalidArgumentException('Cannot find children named '.$name);
+ }
+
+ $child = $this->children[$name];
+ $newChildren[$name] = $child;
+ }
+
+ $this->children = $newChildren;
+ $this->resetChildrenNum();
+ }
+
+ /**
+ * Makes a deep copy of menu tree. Every item is copied as another object.
+ *
+ * @return MenuItem
+ */
+ public function copy()
+ {
+ $newMenu = clone $this;
+ $newMenu->children = array();
+ $newMenu->setParent(null);
+ foreach($this->getChildren() as $child) {
+ $newMenu->addChild($child->copy());
+ }
+
+ return $newMenu;
+ }
+
+ /**
+ * Get slice of menu as another menu.
+ *
+ * If offset and/or length are numeric, it works like in array_slice function:
+ *
+ * If offset is non-negative, slice will start at the offset.
+ * If offset is negative, slice will start that far from the end.
+ *
+ * If length is zero, slice will have all elements.
+ * If length is positive, slice will have that many elements.
+ * If length is negative, slice will stop that far from the end.
+ *
+ * It's possible to mix names/object/numeric, for example:
+ * slice("child1", 2);
+ * slice(3, $child5);
+ *
+ * @param mixed $offset Name of child, child object, or numeric offset.
+ * @param mixed $length Name of child, child object, or numeric length.
+ * @return MenuItem Slice of menu.
+ */
+ public function slice($offset, $length = 0)
+ {
+ $count = $this->count();
+
+ $names = array_keys($this->getChildren());
+ if (is_numeric($offset)) {
+ $offset = ($offset >= 0) ? $offset : $count + $offset;
+ $from = (isset($names[$offset])) ? $names[$offset] : "";
+ }
+ else {
+ $child = ($offset instanceof MenuItem) ? $offset : $this->getChild($offset);
+ $offset = ($child) ? $child->getNum() : 0;
+ $from = ($child) ? $child->getName() : "";
+ }
+
+ if (is_numeric($length)) {
+ if ($length == 0) {
+ $offset2 = $count - 1;
+ }
+ else {
+ $offset2 = ($length > 0) ? $offset + $length - 1 : $count - 1 + $length;
+ }
+ $to = (isset($names[$offset2])) ? $names[$offset2] : "";
+ }
+ else {
+ $to = ($length instanceof MenuItem) ? $length->getName() : $length;
+ }
+
+ return $this->sliceFromTo($from, $to);
+ }
+
+ /**
+ * Get slice of menu as another menu.
+ *
+ * Internal method.
+ *
+ * @param string $offset Name of child.
+ * @param string $length Name of child.
+ * @return MenuItem
+ */
+ private function sliceFromTo($from, $to)
+ {
+ $newMenu = $this->copy();
+ $newChildren = array();
+
+ $copy = false;
+ foreach($newMenu->getChildren() as $child) {
+ if ($child->getName() == $from) {
+ $copy = true;
+ }
+
+ if ($copy == true) {
+ $newChildren[$child->getName()] = $child;
+ }
+
+ if ($child->getName() == $to) {
+ break;
+ }
+ }
+
+ $newMenu->setChildren($newChildren);
+ $newMenu->resetChildrenNum();
+
+ return $newMenu;
+ }
+
+ /**
+ * Split menu into two distinct menus.
+ *
+ * @param mixed $length Name of child, child object, or numeric length.
+ * @return array Array with two menus, with "primary" and "secondary" key
+ */
+ public function split($length)
+ {
+ $count = $this->count();
+
+ if (!is_numeric ($length)) {
+ if (!($length instanceof MenuItem)) {
+ $length = $this->getChild($length);
+ }
+
+ $length = ($length != null) ? $length->getNum() + 1 : $count;
+ }
+
+ $ret = array();
+ $ret['primary'] = $this->slice(0, $length);
+ $ret['secondary'] = $this->slice($length);
+
+ return $ret;
+ }
+
+ /**
+ * Returns the level of this menu item
+ *
+ * The root menu item is 0, followed by 1, 2, etc
+ *
+ * @return integer
+ */
+ public function getLevel()
+ {
+ return $this->parent ? $this->parent->getLevel() + 1 : 0;
+ }
+
+ /**
+ * Returns the root MenuItem of this menu tree
+ *
+ * @return MenuItem
+ */
+ public function getRoot()
+ {
+ $obj = $this;
+ do {
+ $found = $obj;
+ }
+ while ($obj = $obj->getParent());
+
+ return $found;
+ }
+
+ /**
+ * Returns whether or not this menu item is the root menu item
+ *
+ * @return bool
+ */
+ public function isRoot()
+ {
+ return (bool) !$this->getParent();
+ }
+
+ /**
+ * @return MenuItem|null
+ */
+ public function getParent()
+ {
+ return $this->parent;
+ }
+
+ /**
+ * Used internally when adding and removing children
+ *
+ * @param MenuItem $parent
+ * @return MenuItem
+ */
+ public function setParent(MenuItem $parent = null)
+ {
+ return $this->parent = $parent;
+ }
+
+ /**
+ * @return array of MenuItem objects
+ */
+ public function getChildren()
+ {
+ return $this->children;
+ }
+
+ /**
+ * @param array $children An array of MenuItem objects
+ * @return MenuItem
+ */
+ public function setChildren(array $children)
+ {
+ $this->children = $children;
+
+ return $this;
+ }
+
+ /**
+ * Returns the index that this child is within its parent.
+ *
+ * Primarily used internally to calculate first and last
+ *
+ * @return integer
+ */
+ public function getNum()
+ {
+ return $this->num;
+ }
+
+ /**
+ * Sets the index that this child is within its parent.
+ *
+ * Primarily used internally to calculate first and last
+ *
+ * @return void
+ */
+ public function setNum($num)
+ {
+ $this->num = $num;
+ }
+
+ /**
+ * Reset children nums.
+ *
+ * Primarily called after changes to children (removing, reordering, etc)
+ *
+ * @return void
+ */
+ protected function resetChildrenNum()
+ {
+ $i = 0;
+ foreach ($this->children as $child) {
+ $child->setNum($i++);
+ }
+ }
+
+ /**
+ * Creates a new MenuItem to be the child of this menu
+ *
+ * @param string $name
+ * @param string $uri
+ * @param array $attributes
+ *
+ * @return MenuItem
+ */
+ protected function createChild($name, $uri = null, $attributes = array(), $class = null)
+ {
+ if ($class === null) {
+ $class = get_class($this);
+ }
+
+ return new $class($name, $uri, $attributes);
+ }
+
+ /**
+ * Removes a child from this menu item
+ *
+ * @param mixed $name The name of MenuItem instance to remove
+ */
+ public function removeChild($name)
+ {
+ $name = ($name instanceof MenuItem) ? $name->getName() : $name;
+
+ if (isset($this->children[$name])) {
+ // unset the child and reset it so it looks independent
+ $this->children[$name]->setParent(null);
+ $this->children[$name]->setNum(null);
+ unset($this->children[$name]);
+
+ $this->resetChildrenNum();
+ }
+ }
+
+ /**
+ * @return MenuItem
+ */
+ public function getFirstChild()
+ {
+ return reset($this->children);
+ }
+
+ /**
+ * @return MenuItem
+ */
+ public function getLastChild()
+ {
+ return end($this->children);
+ }
+
+ /**
+ * Returns whether or not this menu items has viewable children
+ *
+ * This menu MAY have children, but this will return false if the current
+ * user does not have access to vew any of those items
+ *
+ * @return boolean;
+ */
+ public function hasChildren()
+ {
+ foreach ($this->children as $child) {
+ if ($child->shouldBeRendered()) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Renders the menu tree by using the statically set renderer.
+ *
+ * Depth values corresppond to:
+ * * 0 - no children displayed at all (would return a blank string)
+ * * 1 - directly children only
+ * * 2 - children and grandchildren
+ *
+ * @param integer $depth The depth of children to render
+ *
+ * @return string
+ */
+ public function render($depth = null)
+ {
+ return $this->getRenderer()->render($this, $depth);
+ }
+
+ /**
+ * Sets renderer which will be used to render menu items.
+ *
+ * @param RendererInterface $renderer Renderer.
+ */
+ public function setRenderer(RendererInterface $renderer)
+ {
+ $this->renderer = $renderer;
+ }
+
+ /**
+ * Gets renderer which is used to render menu items.
+ *
+ * @return RendererInterface $renderer Renderer.
+ */
+ public function getRenderer()
+ {
+ if(null === $this->renderer) {
+ if($this->isRoot()) {
+ $this->setRenderer(new ListRenderer());
+ }
+ else {
+ return $this->getParent()->getRenderer();
+ }
+ }
+
+ return $this->renderer;
+ }
+
+ /**
+ * @return string
+ */
+ public function __toString()
+ {
+ return $this->render();
+ }
+
+ /**
+ * Renders the anchor tag for this menu item.
+ *
+ * If no uri is specified, or if the uri fails to generate, the
+ * label will be output.
+ *
+ * @return string
+ */
+ public function renderLink()
+ {
+ $label = $this->renderLabel();
+ $uri = $this->getUri();
+ if (!$uri) {
+ return $label;
+ }
+
+ return sprintf('<a href="%s">%s</a>', $uri, $label);
+ }
+
+ /**
+ * Renders the label of this menu
+ *
+ * @return string
+ */
+ public function renderLabel()
+ {
+ return $this->getLabel();
+ }
+
+ /**
+ * A string representation of this menu item
+ *
+ * e.g. Top Level > Second Level > This menu
+ *
+ * @param string $separator
+ * @return string
+ */
+ public function getPathAsString($separator = ' > ')
+ {
+ $children = array();
+ $obj = $this;
+
+ do {
+ $children[] = $obj->renderLabel();
+ }
+ while ($obj = $obj->getParent());
+
+ return implode($separator, array_reverse($children));
+ }
+
+ /**
+ * Renders an array of label => uri pairs ready to be used for breadcrumbs.
+ *
+ * The subItem can be one of the following forms
+ * * 'subItem'
+ * * array('subItem' => '@homepage')
+ * * array('subItem1', 'subItem2')
+ *
+ * @example
+ * // drill down to the Documentation menu item, then add "Chapter 1" to the breadcrumb
+ * $arr = $menu['Documentation']->getBreadcrumbsArray('Chapter 1');
+ * foreach ($arr as $name => $url)
+ * {
+ *
+ * }
+ *
+ * @param mixed $subItem A string or array to append onto the end of the array
+ * @return array
+ */
+ public function getBreadcrumbsArray($subItem = null)
+ {
+ $breadcrumbs = array();
+ $obj = $this;
+
+ if ($subItem) {
+ if (!is_array($subItem)) {
+ $subItem = array((string) $subItem => null);
+ }
+ $subItem = array_reverse($subItem);
+ foreach ($subItem as $key => $value) {
+ if (is_numeric($key)) {
+ $key = $value;
+ $value = null;
+ }
+ $breadcrumbs[(string) $key] = $value;
+ }
+ }
+
+ do {
+ $label = $obj->renderLabel();
+ $breadcrumbs[$label] = $obj->getUri();
+ }
+ while ($obj = $obj->getParent());
+
+ return array_reverse($breadcrumbs);
+ }
+
+ /**
+ * Returns the current menu item if it is a child of this menu item
+ *
+ * @return bool|MenuItem
+ */
+ public function getCurrent()
+ {
+ if ($this->getIsCurrent()) {
+ return $this;
+ }
+
+ foreach ($this->children as $child) {
+ if ($current = $child->getCurrent()) {
+ return $current;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Set whether or not this menu item is "current"
+ *
+ * @param boolean $bool Specify that this menu item is current
+ * @return boolean
+ */
+ public function setIsCurrent($bool)
+ {
+ $this->isCurrent = (bool) $bool;
+
+ return $this;
+ }
+
+ /**
+ * Get whether or not this menu item is "current"
+ *
+ * @return bool
+ */
+ public function getIsCurrent()
+ {
+ if (null === $this->isCurrent) {
+ $currentUri = $this->getCurrentUri();
+ $this->isCurrent = null !== $currentUri && ($this->getUri() === $currentUri);
+ }
+
+ return $this->isCurrent;
+ }
+
+ /**
+ * Returns whether or not this menu is an ancestor of the current menu item
+ *
+ * @return boolean
+ */
+ public function getIsCurrentAncestor()
+ {
+ foreach ($this->getChildren() as $child) {
+ if ($child->getIsCurrent() || $child->getIsCurrentAncestor()) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * @return bool Whether or not this menu item is last in its parent
+ */
+ public function isLast()
+ {
+ // if this is root, then return false
+ if ($this->isRoot()) {
+ return false;
+ }
+
+ return $this->getNum() == $this->getParent()->count() - 1 ? true : false;
+ }
+
+ /**
+ * @return bool Whether or not this menu item is first in its parent
+ */
+ public function isFirst()
+ {
+ // if this is root, then return false
+ if ($this->isRoot()) {
+ return false;
+ }
+
+ return ($this->getNum() == 0);
+ }
+
+ /**
+ * Whereas isFirst() returns if this is the first child of the parent
+ * menu item, this function takes into consideration whether children are rendered or not.
+ *
+ * This returns true if this is the first child that would be rendered
+ * for the current user
+ *
+ * @return boolean
+ */
+ public function actsLikeFirst()
+ {
+ // root items are never "marked" as first
+ if ($this->isRoot()) {
+ return false;
+ }
+
+ // if we're first and visible, we're first, period.
+ if ($this->shouldBeRendered() && $this->isFirst()) {
+ return true;
+ }
+
+ $children = $this->getParent()->getChildren();
+ foreach ($children as $child) {
+ // loop until we find a visible menu. If its this menu, we're first
+ if ($child->shouldBeRendered()) {
+ return $child->getName() == $this->getName();
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Whereas isLast() returns if this is the last child of the parent
+ * menu item, this function takes into consideration whether children are rendered or not.
+ *
+ * This returns true if this is the last child that would be rendered
+ * for the current user
+ *
+ * @return boolean
+ */
+ public function actsLikeLast()
+ {
+ // root items are never "marked" as last
+ if ($this->isRoot()) {
+ return false;
+ }
+
+ // if we're last and visible, we're last, period.
+ if ($this->shouldBeRendered() && $this->isLast()) {
+ return true;
+ }
+
+ $children = array_reverse($this->getParent()->getChildren());
+ foreach ($children as $child) {
+ // loop until we find a visible menu. If its this menu, we're first
+ if ($child->shouldBeRendered()) {
+ return $child->getName() == $this->getName();
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns the current uri, which is used for determining the current
+ * menu item.
+ *
+ * If the uri isn't set, this asks the parent menu for its current uri.
+ * This would recurse up the tree until the root is hit. Once the root
+ * is hit, if it still doesn't know the currentUri, it gets it from the
+ * request object.
+ *
+ * @return string
+ */
+ public function getCurrentUri()
+ {
+ if ($this->currentUri === null) {
+ if ($this->getParent() && ($currentUri = $this->getParent()->getCurrentUri())) {
+ /**
+ * This should look strange. But, if we ask our parent for the
+ * current uri, and it returns it successfully, then one of two
+ * different things just happened:
+ *
+ * 1) The parent already had the currentUri calculated, but it
+ * hadn't been passed down to the child yet. This technically
+ * should not happen, but we allow for the possibility. In
+ * that case, currentUri is still blank and we set it here.
+ * 2) The parent did not have the currentUri calculated, and upon
+ * calculating it, it set it on itself and all of its children.
+ * In that case, this menu item and all of its children will
+ * now have the currentUri just by asking the parent.
+ */
+ if ($this->currentUri === null) {
+ $this->setCurrentUri($currentUri);
+ }
+ }
+ }
+
+ return $this->currentUri;
+ }
+
+ /**
+ * Sets the current uri, used when determining the current menu item
+ *
+ * This will set the current uri on the root menu item, which all other
+ * menu items will use
+ *
+ * @return void
+ */
+ public function setCurrentUri($uri)
+ {
+ $this->currentUri = $uri;
+
+ foreach ($this->getChildren() as $child) {
+ $child->setCurrentUri($uri);
+ }
+ }
+
+ /**
+ * Sets if the current item should render a link or not
+ *
+ * @param bool $currentAsLink
+ */
+ public function setCurrentAsLink($currentAsLink = true)
+ {
+ $this->currentAsLink = (bool)$currentAsLink;
+ }
+
+ /**
+ * Returns the currentAsLink
+ *
+ * Used to determine if the current item must render
+ * its text as a link or not
+ *
+ * @return bool
+ */
+ public function getCurrentAsLink()
+ {
+ return $this->currentAsLink;
+ }
+
+
+ /**
+ * Calls a method recursively on all of the children of this item
+ *
+ * @example
+ * $menu->callRecursively('setShowChildren', false);
+ *
+ * @return MenuItem
+ */
+ public function callRecursively()
+ {
+ $args = func_get_args();
+ $arguments = $args;
+ unset($arguments[0]);
+
+ call_user_func_array(array($this, $args[0]), $arguments);
+
+ foreach ($this->children as $child) {
+ call_user_func_array(array($child, 'callRecursively'), $args);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Exports this menu item to an array
+ *
+ * @param boolean $withChildren Whether to
+ * @return array
+ */
+ public function toArray($withChildren = true)
+ {
+ $fields = array(
+ 'name' => 'name',
+ 'label' => 'label',
+ 'uri' => 'uri',
+ 'attributes' => 'attributes'
+ );
+
+ $array = array();
+
+ foreach ($fields as $propName => $field) {
+ $array[$field] = $this->$propName;
+ }
+
+ // record this class name so this item can be recreated with the same class
+ $array['class'] = get_class($this);
+
+ // export the children as well, unless explicitly disabled
+ if ($withChildren) {
+ $array['children'] = array();
+ foreach ($this->children as $key => $child) {
+ $array['children'][$key] = $child->toArray();
+ }
+ }
+
+ return $array;
+ }
+
+ /**
+ * Imports a menu item array into this menu item
+ *
+ * @param array $array The menu item array
+ * @return MenuItem
+ */
+ public function fromArray($array)
+ {
+ if (isset($array['name'])) {
+ $this->setName($array['name']);
+ }
+
+ if (isset($array['label'])) {
+ $this->label = $array['label'];
+ }
+
+ if (isset($array['uri'])) {
+ $this->setUri($array['uri']);
+ }
+
+ if (isset($array['attributes'])) {
+ $this->setAttributes($array['attributes']);
+ }
+
+ if (isset($array['children'])) {
+ foreach ($array['children'] as $name => $child) {
+ $class = isset($child['class']) ? $child['class'] : get_class($this);
+ // create the child with the correct class
+ $this->addChild($name, null, array(), $class)->fromArray($child);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Implements Countable
+ */
+ public function count()
+ {
+ return count($this->children);
+ }
+
+ /**
+ * Implements IteratorAggregate
+ */
+ public function getIterator()
+ {
+ return new \ArrayObject($this->children);
+ }
+
+ /**
+ * Implements ArrayAccess
+ */
+ public function offsetExists($name)
+ {
+ return isset($this->children[$name]);
+ }
+
+ /**
+ * Implements ArrayAccess
+ */
+ public function offsetGet($name)
+ {
+ return $this->getChild($name);
+ }
+
+ /**
+ * Implements ArrayAccess
+ */
+ public function offsetSet($name, $value)
+ {
+ return $this->addChild($name)->setLabel($value);
+ }
+
+ /**
+ * Implements ArrayAccess
+ */
+ public function offsetUnset($name)
+ {
+ $this->removeChild($name);
+ }
+}
46 src/Knp/Menu/NodeInterface.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace Knp\Menu;
+
+/**
+ * Interface implemented by a node to construct a menu from a tree.
+ */
+interface NodeInterface
+{
+ /**
+ * Get the name of the node
+ *
+ * Each child of a node must have a unique name
+ *
+ * @return string
+ */
+ function getName();
+
+ /**
+ * Get the label of the link
+ *
+ * @return string
+ */
+ function getLabel();
+
+ /**
+ * Get the uri of the link
+ *
+ * @return string
+ */
+ function getUri();
+
+ /**
+ * Get the attributes of the MenuItem generated by this node
+ *
+ * @return array
+ */
+ function getAttributes();
+
+ /**
+ * Get the child nodes implementing NodeInterface
+ *
+ * @return \Traversable a collection of NodeInterface instances
+ */
+ function getChildren();
+}
26 src/Knp/Menu/Provider/BasicProvider.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace Knp\Menu\Provider;
+use Knp\Menu\ProviderInterface;
+
+class BasicProvider implements ProviderInterface
+{
+ protected $menus = array();
+
+ public function addMenu($name, $menu)
+ {
+ $this->menus[$name] = $menu;
+ }
+
+ public function getMenu($name)
+ {
+ if(!isset($this->menus[$name])) {
+ throw new \InvalidArgumentException(sprintf(
+ 'Menu "%s" does not exist. Available menus are %s',
+ $name, implode(', ', array_keys($this->menus))
+ ));
+ }
+
+ return $this->menus[$name];
+ }
+}
8 src/Knp/Menu/ProviderInterface.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace Knp\Menu;
+
+interface ProviderInterface
+{
+ function getMenu($name);
+}
189 src/Knp/Menu/Renderer/ListRenderer.php
@@ -0,0 +1,189 @@
+<?php
+
+namespace Knp\Menu\Renderer;
+use Knp\Menu\MenuItem;
+
+/**
+ * Renders MenuItem tree as unordered list
+ */
+class ListRenderer extends Renderer implements RendererInterface
+{
+ /**
+ * @see RendererInterface::render
+ */
+ public function render(MenuItem $item, $depth = null)
+ {
+ return $this->doRender($item, $depth);
+ }
+
+ /**
+ * Renders menu tree. Internal method.
+ *
+ * @param MenuItem $item Menu item
+ * @param integer $depth The depth of children to render
+ * @param boolean $renderAsChild Render with attributes on the li (true) or the ul around the children (false)
+ *
+ * @return string
+ */
+ protected function doRender(MenuItem $item, $depth = null, $renderAsChild = false)
+ {
+ /**
+ * Return an empty string if any of the following are true:
+ * a) The menu has no children eligible to be displayed
+ * b) The depth is 0
+ * c) This menu item has been explicitly set to hide its children
+ */
+ if (!$item->hasChildren() || $depth === 0 || !$item->getShowChildren()) {
+ return '';
+ }
+
+ if ($renderAsChild) {
+ $attributes = array('class' => 'menu_level_'.$item->getLevel());
+ }
+ else {
+ $attributes = $item->getAttributes();
+ }
+
+ // render children with a depth - 1
+ $childDepth = ($depth === null) ? null : ($depth - 1);
+
+ $html = $this->format('<ul'.$this->renderHtmlAttributes($attributes).'>', 'ul', $item->getLevel());
+ $html .= $this->renderChildren($item, $childDepth);
+ $html .= $this->format('</ul>', 'ul', $item->getLevel());
+
+ return $html;
+ }
+
+ /**
+ * Renders all of the children of this menu.
+ *
+ * This calls ->renderItem() on each menu item, which instructs each
+ * menu item to render themselves as an <li> tag (with nested ul if it
+ * has children).
+ *
+ * @param integer $depth The depth each child should render
+ * @return string
+ */
+ public function renderChildren($item, $depth = null)
+ {
+ $html = '';
+ foreach ($item->getChildren() as $child) {
+ $html .= $this->renderItem($child, $depth);
+ }
+ return $html;
+ }
+
+ /**
+ * Called by the parent menu item to render this menu.
+ *
+ * This renders the li tag to fit into the parent ul as well as its
+ * own nested ul tag if this menu item has children
+ *
+ * @param integer $depth The depth each child should render
+ * @return string
+ */
+ public function renderItem($item, $depth = null)
+ {
+ // if we don't have access or this item is marked to not be shown
+ if (!$item->shouldBeRendered()) {
+ return;
+ }
+
+ // explode the class string into an array of classes
+ $class = ($item->getAttribute('class')) ? explode(' ', $item->getAttribute('class')) : array();
+
+ if ($item->getIsCurrent()) {
+ $class[] = 'current';
+ }
+ elseif ($item->getIsCurrentAncestor($depth)) {
+ $class[] = 'current_ancestor';
+ }
+
+ if ($item->actsLikeFirst()) {
+ $class[] = 'first';
+ }
+ if ($item->actsLikeLast()) {
+ $class[] = 'last';
+ }
+
+ // retrieve the attributes and put the final class string back on it
+ $attributes = $item->getAttributes();
+ if (!empty($class)) {
+ $attributes['class'] = implode(' ', $class);
+ }
+
+ // opening li tag
+ $html = $this->format('<li'.$this->renderHtmlAttributes($attributes).'>', 'li', $item->getLevel());
+
+ // render the text/link inside the li tag
+ //$html .= $this->format($item->getUri() ? $item->renderLink() : $item->renderLabel(), 'link', $item->getLevel());
+ $html .= $this->renderLink($item);
+
+ // renders the embedded ul if there are visible children
+ $html .= $this->doRender($item, $depth, true);
+
+ // closing li tag
+ $html .= $this->format('</li>', 'li', $item->getLevel());
+
+ return $html;
+ }
+
+ /**
+ * Renders the link in a a tag with link attributes or
+ * the label in a span tag with label attributes
+ *
+ * Tests if item has a an uri and if not tests if it's
+ * the current item and if the text has to be rendered
+ * as a link or not.
+ *
+ * @param MenuItem $item The item to render the link or label for
+ * @return string
+ */
+ public function renderLink($item)
+ {
+ $text = '';
+ if (!$item->getUri()) {
+ $text = sprintf('<span%s>%s</span>', $this->renderHtmlAttributes($item->getLabelAttributes()), $item->renderLabel());
+ }
+ else {
+ if (($item->getIsCurrent() && $item->getParent()->getCurrentAsLink())
+ || !$item->getIsCurrent()) {
+ $text = sprintf('<a href="%s"%s>%s</a>', $item->getUri(), $this->renderHtmlAttributes($item->getLinkAttributes()), $item->renderLabel());
+ }
+ else {
+ $text = sprintf('<span%s>%s</span>', $this->renderHtmlAttributes($item->getLabelAttributes()), $item->renderLabel());
+ }
+ }
+
+ return $this->format($text, 'link', $item->getLevel());
+ }
+
+ /**
+ * If $this->renderCompressed is on, this will apply the necessary
+ * spacing and line-breaking so that the particular thing being rendered
+ * makes up its part in a fully-rendered and spaced menu.
+ *
+ * @param string $html The html to render in an (un)formatted way
+ * @param string $type The type [ul,link,li] of thing being rendered
+ * @return string
+ */
+ protected function format($html, $type, $level)