diff --git a/.travis.yml b/.travis.yml
index af5f89cf..dbe7001b 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -15,7 +15,7 @@ cache:
- $HOME/.composer/cache/files
env:
- global: SYMFONY_DEPRECATIONS_HELPER=533
+ global: SYMFONY_DEPRECATIONS_HELPER=548
matrix:
include:
diff --git a/DependencyInjection/CmfMenuExtension.php b/DependencyInjection/CmfMenuExtension.php
index 9be7d370..12dc0ed2 100644
--- a/DependencyInjection/CmfMenuExtension.php
+++ b/DependencyInjection/CmfMenuExtension.php
@@ -32,7 +32,15 @@ public function load(array $configs, ContainerBuilder $container)
$loader->load('menu.xml');
$container->setAlias('cmf_menu.content_router', $config['content_url_generator']);
- $container->setParameter($this->getAlias().'.allow_empty_items', $config['allow_empty_items']);
+
+ $settingToParameterMap = array(
+ 'allow_empty_items' => 'allow_empty_items',
+ 'content_key' => 'request_content_key',
+ 'route_name_key' => 'request_route_name_key',
+ );
+ foreach ($settingToParameterMap as $setting => $parameter) {
+ $container->setParameter('cmf_menu.'.$parameter, $config[$setting]);
+ }
$this->loadVoters($config, $loader, $container);
diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php
index 789fc63e..6ddc87db 100644
--- a/DependencyInjection/Configuration.php
+++ b/DependencyInjection/Configuration.php
@@ -13,11 +13,13 @@
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
+use Symfony\Cmf\Bundle\RoutingBundle\Routing\DynamicRouter;
class Configuration implements ConfigurationInterface
{
public function getConfigTreeBuilder()
{
+ $cmfRoutingAvailable = class_exists('Symfony\Cmf\Bundle\RoutingBundle\Routing\DynamicRouter');
$treeBuilder = new TreeBuilder();
$treeBuilder->root('cmf_menu')
@@ -51,6 +53,8 @@ public function getConfigTreeBuilder()
->scalarNode('content_url_generator')->defaultValue('router')->end()
->booleanNode('allow_empty_items')->defaultFalse()->end()
+ ->scalarNode('content_key')->defaultValue($cmfRoutingAvailable ? DynamicRouter::CONTENT_KEY : '')->end()
+ ->scalarNode('route_name_key')->defaultValue($cmfRoutingAvailable ? DynamicRouter::ROUTE_KEY : '')->end()
->arrayNode('voters')
->children()
diff --git a/Resources/config/persistence-phpcr.xml b/Resources/config/persistence-phpcr.xml
index 1d3d93f4..8ce64b61 100644
--- a/Resources/config/persistence-phpcr.xml
+++ b/Resources/config/persistence-phpcr.xml
@@ -28,6 +28,20 @@
+
+
+
+
diff --git a/Resources/config/schema/menu-1.0.xsd b/Resources/config/schema/menu-1.0.xsd
index 82ff01d3..e7a1d605 100644
--- a/Resources/config/schema/menu-1.0.xsd
+++ b/Resources/config/schema/menu-1.0.xsd
@@ -16,6 +16,8 @@
+
+
diff --git a/Templating/MenuHelper.php b/Templating/MenuHelper.php
new file mode 100644
index 00000000..5a68d979
--- /dev/null
+++ b/Templating/MenuHelper.php
@@ -0,0 +1,179 @@
+
+ */
+class MenuHelper extends Helper
+{
+ /**
+ * @var ManagerRegistry
+ */
+ private $managerRegistry;
+
+ /**
+ * @var FactoryInterface
+ */
+ private $menuFactory;
+ private $managerName;
+ private $contentObjectKey;
+ private $routeNameKey;
+
+ /**
+ * @param ManagerRegistry $managerRegistry
+ * @param FactoryInterface $menuFactory
+ * @param string $contentObjectKey The name of the request attribute holding
+ * the current content object
+ * @param string $routeNameKey The name of the request attribute holding
+ * the name of the current route
+ */
+ public function __construct(ManagerRegistry $managerRegistry, FactoryInterface $menuFactory, $contentObjectKey = DynamicRouter::CONTENT_KEY, $routeNameKey = DynamicRouter::ROUTE_KEY)
+ {
+ $this->managerRegistry = $managerRegistry;
+ $this->menuFactory = $menuFactory;
+ $this->contentObjectKey = $contentObjectKey;
+ $this->routeNameKey = $routeNameKey;
+ }
+
+ /**
+ * Set the object manager name to use for this loader. If not set, the
+ * default manager as decided by the manager registry will be used.
+ *
+ * @param string|null $managerName
+ */
+ public function setManagerName($managerName)
+ {
+ $this->managerName = $managerName;
+ }
+
+ /**
+ * Generates an array of breadcrumb items by traversing
+ * up the tree from the current node.
+ *
+ * @param NodeInterface $node The current menu node (use {@link getCurrentNode} to get it)
+ * @param bool $includeMenuRoot Whether to include the menu root as breadcrumb item
+ *
+ * @return array An array with breadcrumb items (each item has the following keys: label, uri, item)
+ */
+ public function getBreadcrumbsArray(NodeInterface $node, $includeMenuRoot = true)
+ {
+ $item = $this->menuFactory->createItem($node->getName(), $node->getOptions());
+
+ $breadcrumbs = array(
+ array(
+ 'label' => $item->getLabel(),
+ 'uri' => $item->getUri(),
+ 'item' => $item,
+ ),
+ );
+
+ $parent = $node->getParentObject();
+ if (!$parent instanceof MenuNode) {
+ // We assume the root of the menu is reached
+ return $includeMenuRoot ? $breadcrumbs : array();
+ }
+
+ return array_merge($this->getBreadcrumbsArray($parent, $includeMenuRoot), $breadcrumbs);
+ }
+
+ /**
+ * Tries to find the current item from the request.
+ *
+ * The returned item does *not* include the parent and children,
+ * in order to minimalize the overhead.
+ *
+ * @param Request $request
+ *
+ * @return ItemInterface|null
+ */
+ public function getCurrentItem(Request $request)
+ {
+ $node = $this->getCurrentNode($request);
+
+ if (!$node instanceof NodeInterface) {
+ return;
+ }
+
+ return $this->menuFactory->createItem($node->getName(), $node->getOptions());
+ }
+
+ /**
+ * Retrieves the current node based on a Request.
+ *
+ * It uses some special Request attributes that are managed by
+ * the CmfRoutingBundle:
+ *
+ * * DynamicRouter::CONTENT_KEY to match a menu node by the refering content
+ * * DynamicRouter::ROUTE_KEY to match a menu node by the refering route name
+ *
+ * @return NodeInterface|null
+ */
+ public function getCurrentNode(Request $request)
+ {
+ $repository = $this->managerRegistry->getManager($this->managerName)
+ ->getRepository('Symfony\Cmf\Bundle\MenuBundle\Doctrine\Phpcr\MenuNode');
+
+ if ($request->attributes->has($this->contentObjectKey)) {
+ $content = $request->attributes->get($this->contentObjectKey);
+
+ if ($content instanceof MenuNodeReferrersInterface) {
+ $node = $this->filterByLinkType(new ArrayCollection($content->getMenuNodes()), 'content');
+
+ if ($node) {
+ return $node;
+ }
+ }
+ }
+
+ if ($request->attributes->has($this->routeNameKey)) {
+ $route = $request->attributes->get($this->routeNameKey);
+
+ return $this->filterByLinkType($repository->findBy(array('route' => $route)), 'route');
+ }
+ }
+
+ private function filterByLinkType(\Traversable $nodes, $type)
+ {
+ if (1 === count($nodes)) {
+ return $nodes->first();
+ } else {
+ foreach ($nodes as $node) {
+ if ($type === $node->getLinkType()) {
+ return $node;
+ }
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return 'cmf_menu';
+ }
+}
diff --git a/Tests/Functional/Templating/MenuHelperTest.php b/Tests/Functional/Templating/MenuHelperTest.php
new file mode 100644
index 00000000..2029cb4c
--- /dev/null
+++ b/Tests/Functional/Templating/MenuHelperTest.php
@@ -0,0 +1,154 @@
+db('PHPCR')->loadFixtures(array(
+ 'Symfony\Cmf\Bundle\MenuBundle\Tests\Resources\DataFixtures\PHPCR\LoadMenuData',
+ ));
+
+ $container = $this->getContainer();
+ $this->helper = new MenuHelper($container->get('doctrine_phpcr'), $container->get('knp_menu.factory'));
+ }
+
+ /**
+ * @dataProvider provideGetBreadcrumbsArrayData
+ */
+ public function testGetBreadcrumbsArray($includeMenuRoot)
+ {
+ $currentNode = $this->db('PHPCR')->getOm()->find(null, '/test/menus/test-menu/item-2/sub-item-2');
+
+ $breadcrumbs = $this->helper->getBreadcrumbsArray($currentNode, $includeMenuRoot);
+
+ // simplify the returned breadcrumb array
+ $breadcrumbs = array_map(function ($breadcrumb) {
+ return array('uri' => $breadcrumb['uri'], 'item_name' => $breadcrumb['item']->getName());
+ }, $breadcrumbs);
+
+ $expectedBreadcrumbs = array_merge(
+ $includeMenuRoot ? array(array('uri' => null, 'item_name' => 'test-menu')) : array(),
+ array(
+ array('uri' => 'http://www.example.com', 'item_name' => 'item-2'),
+ array('uri' => '/link_test_route', 'item_name' => 'sub-item-2'),
+ )
+ );
+
+ $this->assertEquals($expectedBreadcrumbs, $breadcrumbs);
+ }
+
+ public function provideGetBreadcrumbsArrayData()
+ {
+ return array('menu root included' => array(true), 'menu route excluded' => array(false));
+ }
+
+ /**
+ * @dataProvider provideGetCurrentNodeWithRouteData
+ */
+ public function testGetCurrentNodeWithRoute($routeName, $nodeName)
+ {
+ $attributes = $this->prophesize('Symfony\Component\HttpFoundation\ParameterBag');
+ $attributes->has(DynamicRouter::CONTENT_KEY)->willReturn(false);
+ $attributes->has(DynamicRouter::ROUTE_KEY)->willReturn(true);
+ $attributes->get(DynamicRouter::ROUTE_KEY)->willReturn($routeName);
+
+ $request = $this->prophesize('Symfony\Component\HttpFoundation\Request');
+ $request->attributes = $attributes->reveal();
+
+ $node = $this->helper->getCurrentNode($request->reveal());
+ $this->assertInstanceOf('Knp\Menu\NodeInterface', $node);
+ $this->assertEquals($nodeName, $node->getName());
+ }
+
+ public function provideGetCurrentNodeWithRouteData()
+ {
+ return array(
+ 'simple route refering node' => array('link_test_route_with_params', 'sub-item-3'),
+ 'multiple matching nodes' => array('link_test_route', 'item-1'),
+ );
+ }
+
+ public function testGetCurrentNodeWithContent()
+ {
+ $content = new MenuHelperTest_NodeReferrer();
+ $content->addMenuNode($this->db('PHPCR')->getOm()->find(null, '/test/menus/test-menu/item-1'));
+
+ $attributes = $this->prophesize('Symfony\Component\HttpFoundation\ParameterBag');
+ $attributes->has(DynamicRouter::CONTENT_KEY)->willReturn(true);
+ $attributes->has(DynamicRouter::ROUTE_KEY)->willReturn(true);
+ $attributes->get(DynamicRouter::CONTENT_KEY)->willReturn($content);
+
+ $request = $this->prophesize('Symfony\Component\HttpFoundation\Request');
+ $request->attributes = $attributes->reveal();
+
+ $node = $this->helper->getCurrentNode($request->reveal());
+ $this->assertInstanceOf('Knp\Menu\NodeInterface', $node);
+ $this->assertEquals('item-1', $node->getName());
+ }
+
+ public function testGetCurrentNodeWithoutMatch()
+ {
+ $attributes = $this->prophesize('Symfony\Component\HttpFoundation\ParameterBag');
+ $attributes->has(DynamicRouter::CONTENT_KEY)->willReturn(false);
+ $attributes->has(DynamicRouter::ROUTE_KEY)->willReturn(false);
+
+ $request = $this->prophesize('Symfony\Component\HttpFoundation\Request');
+ $request->attributes = $attributes->reveal();
+
+ $this->assertEquals(null, $this->helper->getCurrentNode($request->reveal()));
+ }
+
+ public function testGetCurrentItemWithMatch()
+ {
+ $attributes = $this->prophesize('Symfony\Component\HttpFoundation\ParameterBag');
+ $attributes->has(DynamicRouter::CONTENT_KEY)->willReturn(false);
+ $attributes->has(DynamicRouter::ROUTE_KEY)->willReturn(true);
+ $attributes->get(DynamicRouter::ROUTE_KEY)->willReturn('link_test_route_with_params');
+
+ $request = $this->prophesize('Symfony\Component\HttpFoundation\Request');
+ $request->attributes = $attributes->reveal();
+
+ $item = $this->helper->getCurrentItem($request->reveal());
+ $this->assertInstanceOf('Knp\Menu\ItemInterface', $item);
+ $this->assertEquals('sub-item-3', $item->getName());
+ }
+}
+
+class MenuHelperTest_NodeReferrer implements MenuNodeReferrersInterface
+{
+ private $nodes = array();
+
+ public function getMenuNodes()
+ {
+ return $this->nodes;
+ }
+
+ public function addMenuNode(NodeInterface $menu)
+ {
+ $this->nodes[] = $menu;
+ }
+
+ public function removeMenuNode(NodeInterface $menu)
+ {
+ // dummy
+ }
+}
diff --git a/Tests/Resources/Fixtures/config/config2.xml b/Tests/Resources/Fixtures/config/config2.xml
index d387187a..fa5dac0b 100644
--- a/Tests/Resources/Fixtures/config/config2.xml
+++ b/Tests/Resources/Fixtures/config/config2.xml
@@ -1,6 +1,8 @@
+ allow-empty-items="false"
+ content-key="_custom_content"
+ route-name-key="_custom_route_name">
helper = $helper;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFunctions()
+ {
+ return array(
+ new \Twig_SimpleFunction('cmf_menu_get_breadcrumbs_array', array($this->helper, 'getBreadcrumbsArray')),
+ new \Twig_SimpleFunction('cmf_menu_get_current_item', array($this->helper, 'getCurrentItem')),
+ new \Twig_SimpleFunction('cmf_menu_get_current_node', array($this->helper, 'getCurrentNode')),
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return 'cmf_menu';
+ }
+}