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 @@ + + + + %cmf_menu.request_content_key% + %cmf_menu.request_route_name_key% + %cmf_menu.persistence.phpcr.manager_name% + + + + + + + + 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'; + } +}