├── tests ├── Fixtures │ ├── fixtures │ │ └── config │ │ │ ├── config1.xml │ │ │ └── config2.xml │ └── App │ │ ├── config │ │ ├── routing.php │ │ ├── config.php │ │ ├── cmf_menu.yml │ │ ├── routing │ │ │ └── test.yml │ │ └── test-services.xml │ │ ├── Kernel.php │ │ ├── Admin │ │ └── TestContentAdmin.php │ │ ├── Document │ │ └── Content.php │ │ └── DataFixtures │ │ └── PHPCR │ │ └── LoadMenuData.php ├── Unit │ ├── DependencyInjection │ │ └── XmlSchemaTest.php │ ├── Voter │ │ ├── LegacyRequestContentIdentityVoterTest.php │ │ ├── RequestContentIdentityVoterTest.php │ │ ├── RequestContentIdentityVoterTestCase.php │ │ ├── UriPrefixVoterTest.php │ │ └── RequestParentContentIdentityVoterTest.php │ ├── QuietFactoryTest.php │ ├── Loader │ │ └── VotingNodeLoaderTest.php │ ├── Provider │ │ └── PhpcrMenuProviderTest.php │ ├── Extension │ │ └── ContentExtensionTest.php │ ├── Model │ │ └── MenuNodeTest.php │ └── PublishWorkflow │ │ └── Voter │ │ └── MenuContentVoterTest.php └── Functional │ ├── RenderingTest.php │ └── Doctrine │ └── Phpcr │ ├── MenuTest.php │ └── MenuNodeTest.php ├── src ├── Doctrine │ └── Phpcr │ │ ├── MenuNodeBase.php │ │ ├── Menu.php │ │ └── MenuNode.php ├── Model │ ├── Menu.php │ ├── MenuNodeReferrersInterface.php │ ├── MenuOptionsInterface.php │ ├── MenuNode.php │ └── MenuNodeBase.php ├── Resources │ ├── config │ │ ├── doctrine-model │ │ │ ├── Menu.phpcr.xml │ │ │ ├── MenuNode.phpcr.xml │ │ │ └── MenuNodeBase.phpcr.xml │ │ ├── doctrine-phpcr │ │ │ ├── MenuNodeBase.phpcr.xml │ │ │ ├── Menu.phpcr.xml │ │ │ └── MenuNode.phpcr.xml │ │ ├── voters.xml │ │ ├── publish-workflow.xml │ │ ├── validation-phpcr.xml │ │ ├── menu.xml │ │ ├── persistence-phpcr.xml │ │ └── schema │ │ │ └── menu-1.0.xsd │ └── meta │ │ └── LICENSE ├── Event │ ├── Events.php │ └── CreateMenuItemFromNodeEvent.php ├── DependencyInjection │ ├── Compiler │ │ ├── ValidationPass.php │ │ └── DecorateMenuFactoryPass.php │ ├── Configuration.php │ └── CmfMenuExtension.php ├── CmfMenuBundle.php ├── PublishWorkflow │ ├── CreateMenuItemFromNodeListener.php │ └── Voter │ │ └── MenuContentVoter.php ├── Loader │ └── VotingNodeLoader.php ├── Voter │ ├── RequestContentIdentityVoter.php │ ├── UriPrefixVoter.php │ └── RequestParentContentIdentityVoter.php ├── QuietFactory.php ├── Extension │ └── ContentExtension.php └── Provider │ └── PhpcrMenuProvider.php ├── phpunit.xml.dist ├── Makefile └── composer.json /tests/Fixtures/fixtures/config/config1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /tests/Fixtures/App/config/routing.php: -------------------------------------------------------------------------------- 1 | import(__DIR__.'/routing/test.yml'); 13 | -------------------------------------------------------------------------------- /src/Doctrine/Phpcr/MenuNodeBase.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | class Menu extends MenuNode 21 | { 22 | } 23 | -------------------------------------------------------------------------------- /src/Resources/config/doctrine-model/Menu.phpcr.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/Resources/config/doctrine-phpcr/MenuNodeBase.phpcr.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/Event/Events.php: -------------------------------------------------------------------------------- 1 | setParameter('cmf_testing.bundle_fqn', 'Symfony\Cmf\Bundle\MenuBundle'); 13 | 14 | $container->loadFromExtension('framework', [ 15 | 'serializer' => true, 16 | ]); 17 | 18 | $loader->import(CMF_TEST_CONFIG_DIR.'/default.php'); 19 | $loader->import(CMF_TEST_CONFIG_DIR.'/phpcr_odm.php'); 20 | $loader->import(__DIR__.'/cmf_menu.yml'); 21 | -------------------------------------------------------------------------------- /src/Resources/config/doctrine-phpcr/Menu.phpcr.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/Fixtures/App/config/cmf_menu.yml: -------------------------------------------------------------------------------- 1 | cmf_core: 2 | multilang: 3 | locales: ['en', 'de', 'fr'] 4 | publish_workflow: 5 | enabled: false 6 | 7 | cmf_menu: 8 | persistence: 9 | phpcr: 10 | menu_basepath: /test/menus 11 | voters: 12 | content_identity: true 13 | uri_prefix: ~ 14 | 15 | cmf_routing: 16 | dynamic: 17 | enabled: true 18 | persistence: 19 | phpcr: 20 | route_basepath: /test/routes 21 | enabled: true 22 | chain: 23 | routers_by_id: 24 | cmf_routing.dynamic_router: 20 25 | router.default: 100 26 | -------------------------------------------------------------------------------- /src/Resources/config/doctrine-phpcr/MenuNode.phpcr.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | 11 | ./tests/Unit 12 | 13 | 14 | 15 | ./tests/Functional 16 | 17 | 18 | 19 | 20 | 21 | src 22 | 23 | *Bundle.php 24 | Resources/ 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /tests/Unit/DependencyInjection/XmlSchemaTest.php: -------------------------------------------------------------------------------- 1 | assertSchemaAcceptsXml($xmlFiles, __DIR__.'/../../../src/Resources/config/schema/menu-1.0.xsd'); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Resources/config/voters.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | %cmf_menu.content_key% 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /tests/Fixtures/fixtures/config/config2.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /tests/Unit/Voter/LegacyRequestContentIdentityVoterTest.php: -------------------------------------------------------------------------------- 1 | setRequest($request); 26 | 27 | return $voter; 28 | } 29 | 30 | public function testSkipsWhenNoRequestIsAvailable() 31 | { 32 | $this->voter->setRequest(null); 33 | 34 | $this->assertNull($this->voter->matchItem($this->createItem())); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/Fixtures/App/config/routing/test.yml: -------------------------------------------------------------------------------- 1 | test_index: 2 | path: / 3 | defaults: 4 | _controller: Symfony\Cmf\Bundle\MenuBundle\Tests\Fixtures\App\Controller\TestController::indexAction 5 | 6 | render_test: 7 | path: /render-test 8 | defaults: 9 | _controller: Symfony\Cmf\Bundle\MenuBundle\Tests\Fixtures\App\Controller\TestController::renderAction 10 | 11 | link_test_route: 12 | path: /link_test_route 13 | 14 | link_test_route_with_params: 15 | path: /link_test_route/hello/{bar}/{foo} 16 | defaults: 17 | _controller: Symfony\Cmf\Bundle\MenuBundle\Tests\Fixtures\App\Controller\TestController::linkTestRouteAction 18 | 19 | current_menu_item_default: 20 | path: /cmi/default 21 | defaults: 22 | _controller: Symfony\Cmf\Bundle\MenuBundle\Tests\Fixtures\App\Controller\VoterController::defaultAction 23 | 24 | current_menu_item_request_content_identity: 25 | path: /cmi/request_content_identity 26 | defaults: 27 | _controller: Symfony\Cmf\Bundle\MenuBundle\Tests\Fixtures\App\Controller\VoterController::requestContentIdentityAction 28 | -------------------------------------------------------------------------------- /src/Resources/config/publish-workflow.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/Resources/config/validation-phpcr.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/DependencyInjection/Compiler/ValidationPass.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | class ValidationPass implements CompilerPassInterface 23 | { 24 | public function process(ContainerBuilder $container) 25 | { 26 | if ($container->hasParameter('cmf_menu.persistence.phpcr.menu_document_class')) { 27 | $container 28 | ->getDefinition('validator.builder') 29 | ->addMethodCall('addXmlMappings', [[__DIR__.'/../../Resources/config/validation-phpcr.xml']]); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/Unit/Voter/RequestContentIdentityVoterTest.php: -------------------------------------------------------------------------------- 1 | push($request); 24 | 25 | return new RequestContentIdentityVoter('_content', $requestStack); 26 | } 27 | 28 | public function testSkipsWhenNoRequestIsAvailable() 29 | { 30 | $voter = new RequestContentIdentityVoter('_content', new RequestStack()); 31 | 32 | $this->assertNull($voter->matchItem($this->createItem())); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Resources/config/doctrine-model/MenuNode.phpcr.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/Model/MenuNodeReferrersInterface.php: -------------------------------------------------------------------------------- 1 | requireBundleSets([ 22 | 'default', 23 | 'phpcr_odm', 24 | ]); 25 | 26 | $this->addBundles([ 27 | new \Knp\Bundle\MenuBundle\KnpMenuBundle(), 28 | new \Symfony\Cmf\Bundle\MenuBundle\CmfMenuBundle(), 29 | new \Symfony\Cmf\Bundle\CoreBundle\CmfCoreBundle(), 30 | new \Symfony\Cmf\Bundle\RoutingBundle\CmfRoutingBundle(), 31 | ]); 32 | } 33 | 34 | public function registerContainerConfiguration(LoaderInterface $loader) 35 | { 36 | $loader->load(__DIR__.'/config/config.php'); 37 | $loader->load(__DIR__.'/config/test-services.xml'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/Fixtures/App/config/test-services.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Symfony\Cmf\Bundle\MenuBundle\Tests\Fixtures\App\Document\Content 13 | SonataAdminBundle:CRUD 14 | 15 | 16 | 17 | 19 | contentDocument 20 | Symfony\Cmf\Bundle\MenuBundle\Tests\Fixtures\App\Document\Post 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/Resources/meta/LICENSE: -------------------------------------------------------------------------------- 1 | Symfony Cmf Menu Bundle 2 | 3 | The MIT License 4 | 5 | Copyright (c) 2011-2017 Symfony CMF 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /src/Resources/config/menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | %cmf_menu.allow_empty_items% 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/DependencyInjection/Compiler/DecorateMenuFactoryPass.php: -------------------------------------------------------------------------------- 1 | hasDefinition('knp_menu.factory')) { 29 | return; 30 | } 31 | 32 | $knpFactory = $container->getDefinition('knp_menu.factory'); 33 | $knpFactory->setPublic(false); 34 | 35 | // rename old service 36 | $container->setDefinition('cmf_menu.factory.quiet.inner', $knpFactory); 37 | 38 | $container->setAlias('knp_menu.factory', new Alias('cmf_menu.factory.quiet')); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Resources/config/persistence-phpcr.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | %cmf_menu.persistence.phpcr.menu_basepath% 14 | %cmf_menu.persistence.phpcr.manager_name% 15 | %cmf_menu.persistence.phpcr.prefetch% 16 | 17 | 18 | 19 | CmfMenuBundle 20 | 21 | %cmf_menu.persistence.phpcr.menu_basepath% 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/Resources/config/doctrine-model/MenuNodeBase.phpcr.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /tests/Fixtures/App/Admin/TestContentAdmin.php: -------------------------------------------------------------------------------- 1 | addIdentifier('id', 'text') 32 | ->add('title', 'text') 33 | ; 34 | 35 | $listMapper 36 | ->add('locales', 'choice', [ 37 | 'template' => 'SonataDoctrinePHPCRAdminBundle:CRUD:locales.html.twig', 38 | ]) 39 | ; 40 | } 41 | 42 | protected function configureFormFields(FormMapper $formMapper) 43 | { 44 | $formMapper 45 | ->with('form.group_general') 46 | ->add('title', 'text') 47 | ->end() 48 | ; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ####################################################### 2 | # DO NOT EDIT THIS FILE! # 3 | # # 4 | # It's auto-generated by symfony-cmf/dev-kit package. # 5 | ####################################################### 6 | 7 | ############################################################################ 8 | # This file is part of the Symfony CMF package. # 9 | # # 10 | # (c) 2011-2017 Symfony CMF # 11 | # # 12 | # For the full copyright and license information, please view the LICENSE # 13 | # file that was distributed with this source code. # 14 | ############################################################################ 15 | 16 | TESTING_SCRIPTS_DIR=vendor/symfony-cmf/testing/bin 17 | CONSOLE=${TESTING_SCRIPTS_DIR}/console 18 | VERSION=dev-master 19 | ifdef BRANCH 20 | VERSION=dev-${BRANCH} 21 | endif 22 | PACKAGE=symfony-cmf/menu-bundle 23 | export KERNEL_CLASS=Symfony\Cmf\Bundle\MenuBundle\Tests\Fixtures\App\Kernel 24 | list: 25 | @echo 'test: will run all tests' 26 | @echo 'unit_tests: will run unit tests only' 27 | @echo 'functional_tests_phpcr: will run functional tests with PHPCR' 28 | 29 | @echo 'test_installation: will run installation test' 30 | include ${TESTING_SCRIPTS_DIR}/make/unit_tests.mk 31 | include ${TESTING_SCRIPTS_DIR}/make/functional_tests_phpcr.mk 32 | include ${TESTING_SCRIPTS_DIR}/make/test_installation.mk 33 | 34 | .PHONY: test 35 | test: unit_tests functional_tests_phpcr 36 | -------------------------------------------------------------------------------- /src/CmfMenuBundle.php: -------------------------------------------------------------------------------- 1 | addCompilerPass(new DecorateMenuFactoryPass()); 27 | $container->addCompilerPass(new ValidationPass()); 28 | 29 | if (class_exists(DoctrinePhpcrMappingsPass::class)) { 30 | $container->addCompilerPass( 31 | DoctrinePhpcrMappingsPass::createXmlMappingDriver( 32 | [ 33 | realpath(__DIR__.'/Resources/config/doctrine-model') => 'Symfony\Cmf\Bundle\MenuBundle\Model', 34 | realpath(__DIR__.'/Resources/config/doctrine-phpcr') => 'Symfony\Cmf\Bundle\MenuBundle\Doctrine\Phpcr', 35 | ], 36 | ['cmf_menu.persistence.phpcr.manager_name'], 37 | false, 38 | ['CmfMenuBundle' => 'Symfony\Cmf\Bundle\MenuBundle\Doctrine\Phpcr'] 39 | ) 40 | ); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "symfony-cmf/menu-bundle", 3 | "type": "symfony-bundle", 4 | "description": "Symfony CMF Menu Bundle", 5 | "keywords": [ 6 | "Symfony CMF", 7 | "menu" 8 | ], 9 | "homepage": "http://cmf.symfony.com", 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "Symfony CMF Community", 14 | "homepage": "https://github.com/symfony-cmf/MenuBundle/contributors" 15 | } 16 | ], 17 | "require": { 18 | "php": "^7.1", 19 | "symfony/framework-bundle": "^2.8 || ^3.3 || ^4.0", 20 | "symfony/validator": "^2.8 || ^3.3 || ^4.0", 21 | "knplabs/knp-menu-bundle": "^2.2.0", 22 | "knplabs/knp-menu": "^2.0.0" 23 | }, 24 | "require-dev": { 25 | "symfony/monolog-bundle": "~3.1", 26 | "symfony/phpunit-bridge": "^3.3 || ^4.0", 27 | "symfony-cmf/routing-bundle": "^1.4 || ^2.0", 28 | "symfony-cmf/testing": "^2.1.8", 29 | "twig/twig": "^1.35 || ^2.4.4", 30 | "symfony-cmf/core-bundle": "^2.1", 31 | "doctrine/phpcr-odm": "^1.4.2 || ^2.0" 32 | }, 33 | "suggest": { 34 | "doctrine/phpcr-odm": "To enable support for the PHPCR ODM documents (^1.4)", 35 | "doctrine/phpcr-bundle": "To enable support for the PHPCR ODM documents", 36 | "symfony-cmf/core-bundle": "To enable support for publishing workflow" 37 | }, 38 | "autoload": { 39 | "psr-4": { 40 | "Symfony\\Cmf\\Bundle\\MenuBundle\\": "src/" 41 | } 42 | }, 43 | "autoload-dev": { 44 | "psr-4": { 45 | "Symfony\\Cmf\\Bundle\\MenuBundle\\Tests\\": "tests/" 46 | } 47 | }, 48 | "extra": { 49 | "branch-alias": { 50 | "dev-master": "2.2-dev" 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/PublishWorkflow/CreateMenuItemFromNodeListener.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | class CreateMenuItemFromNodeListener 25 | { 26 | /** 27 | * @var AuthorizationCheckerInterface 28 | */ 29 | private $publishWorkflowChecker; 30 | 31 | /** 32 | * The permission to check for when doing the publish workflow check. 33 | * 34 | * @var string 35 | */ 36 | private $publishWorkflowPermission; 37 | 38 | /** 39 | * @param AuthorizationCheckerInterface $publishWorkflowChecker The publish workflow checker 40 | * @param string $attribute The permission to check 41 | */ 42 | public function __construct(AuthorizationCheckerInterface $publishWorkflowChecker, $attribute = PublishWorkflowChecker::VIEW_ATTRIBUTE) 43 | { 44 | $this->publishWorkflowChecker = $publishWorkflowChecker; 45 | $this->publishWorkflowPermission = $attribute; 46 | } 47 | 48 | /** 49 | * Check if the node on the event is published, otherwise skip it. 50 | * 51 | * @param CreateMenuItemFromNodeEvent $event 52 | */ 53 | public function onCreateMenuItemFromNode(CreateMenuItemFromNodeEvent $event) 54 | { 55 | $node = $event->getNode(); 56 | 57 | if (!$this->publishWorkflowChecker->isGranted($this->publishWorkflowPermission, $node)) { 58 | $event->setSkipNode(true); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/Unit/QuietFactoryTest.php: -------------------------------------------------------------------------------- 1 | innerFactory = $this->prophesize('Knp\Menu\FactoryInterface'); 25 | $this->logger = $this->prophesize('Psr\Log\LoggerInterface'); 26 | } 27 | 28 | public function provideItemsWithNotExistingLinks() 29 | { 30 | return [ 31 | [['route' => 'not_existent'], ['route' => 'not_existent']], 32 | [['content' => 'not_existent'], ['content' => 'not_existent']], 33 | [['linkType' => 'route', 'route' => 'not_existent'], ['linkType' => 'route']], 34 | ]; 35 | } 36 | 37 | /** @dataProvider provideItemsWithNotExistingLinks */ 38 | public function testAllowEmptyItemsReturnsItemWithoutURL(array $firstOptions, array $secondOptions) 39 | { 40 | $this->innerFactory->createItem('Home', $firstOptions) 41 | ->willThrow('Symfony\Component\Routing\Exception\RouteNotFoundException'); 42 | 43 | $homeMenuItem = new \stdClass(); 44 | $this->innerFactory->createItem('Home', $secondOptions)->willReturn($homeMenuItem); 45 | 46 | $factory = new QuietFactory($this->innerFactory->reveal(), $this->logger->reveal(), true); 47 | 48 | $this->assertEquals($homeMenuItem, $factory->createItem('Home', $firstOptions)); 49 | } 50 | 51 | public function testDisallowEmptyItemsReturnsNull() 52 | { 53 | $this->innerFactory->createItem('Home', ['route' => 'not_existent']) 54 | ->willThrow('Symfony\Component\Routing\Exception\RouteNotFoundException'); 55 | 56 | $factory = new QuietFactory($this->innerFactory->reveal(), $this->logger->reveal(), false); 57 | 58 | $this->assertNull($factory->createItem('Home', ['route' => 'not_existent'])); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/Functional/RenderingTest.php: -------------------------------------------------------------------------------- 1 | db('PHPCR')->loadFixtures([ 21 | 'Symfony\Cmf\Bundle\MenuBundle\Tests\Fixtures\App\DataFixtures\PHPCR\LoadMenuData', 22 | ]); 23 | } 24 | 25 | public function testWithAutomaticLinkType() 26 | { 27 | $template = $this->getContainer()->get('twig')->createTemplate('{{ knp_menu_render("test-menu") }}'); 28 | $dom = new \DOMDocument(); 29 | $dom->loadXml($template->render([])); 30 | 31 | $items = [ 32 | 'item-1' => null, 33 | 'This node has a URI' => 'http://www.example.com', 34 | 'This node has content' => '/content-1', 35 | 'This node has an assigned route' => '/link_test_route', 36 | 'This node has an assigned route with parameters' => '/link_test_route/hello/foo/bar', 37 | 'item-3' => null, 38 | ]; 39 | 40 | $this->assertMenu($items, $dom); 41 | } 42 | 43 | public function testWithExplicitLinkType() 44 | { 45 | $template = $this->getContainer()->get('twig')->createTemplate('{{ knp_menu_render("another-menu") }}'); 46 | $dom = new \DOMDocument(); 47 | $dom->loadXml($template->render([])); 48 | 49 | $items = [ 50 | 'This node has uri, route and content set. but linkType is set to route' => '/link_test_route', 51 | 'item-2' => null, 52 | ]; 53 | 54 | $this->assertMenu($items, $dom); 55 | } 56 | 57 | protected function assertMenu($expectedItems, \DOMDocument $menu) 58 | { 59 | $xpath = new \DOMXpath($menu); 60 | $menuItems = []; 61 | foreach ($xpath->query('//ul/li/*[self::span or self::a]') as $menuItem) { 62 | $menuItems[$menuItem->textContent] = 'span' === $menuItem->nodeName 63 | ? null 64 | : $menuItem->getAttribute('href') 65 | ; 66 | } 67 | 68 | $this->assertEquals($expectedItems, $menuItems); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/Functional/Doctrine/Phpcr/MenuTest.php: -------------------------------------------------------------------------------- 1 | db('PHPCR')->createTestNode(); 32 | 33 | $this->dm = $this->db('PHPCR')->getOm(); 34 | $this->rootDocument = $this->dm->find(null, '/test'); 35 | } 36 | 37 | public function testPersist() 38 | { 39 | $menu = new Menu(); 40 | $menu->setPosition($this->rootDocument, 'main'); 41 | $this->dm->persist($menu); 42 | 43 | $menuNode = new MenuNode(); 44 | $menuNode->setName('home'); 45 | $menu->addChild($menuNode); 46 | $this->dm->persist($menuNode); 47 | 48 | $this->dm->flush(); 49 | $this->dm->clear(); 50 | 51 | $menu = $this->dm->find(null, '/test/main'); 52 | 53 | $this->assertNotNull($menu); 54 | $this->assertEquals('main', $menu->getName()); 55 | 56 | $children = $menu->getChildren(); 57 | $this->assertCount(1, $children); 58 | $this->assertEquals('home', $children[0]->getName()); 59 | } 60 | 61 | /** 62 | * @dataProvider getInvalidChildren 63 | * @expectedException \Doctrine\ODM\PHPCR\Exception\OutOfBoundsException 64 | */ 65 | public function testPersistInvalidChild($invalidChild) 66 | { 67 | $menu = new Menu(); 68 | $menu->setPosition($this->rootDocument, 'main'); 69 | $this->dm->persist($menu); 70 | 71 | $invalidChild->setParentDocument($menu); 72 | $this->dm->persist($invalidChild); 73 | 74 | $this->dm->flush(); 75 | } 76 | 77 | public function getInvalidChildren() 78 | { 79 | return [ 80 | [(new Menu())->setName('invalid')], 81 | [(new Generic())->setNodename('invalid')], 82 | ]; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Loader/VotingNodeLoader.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | class VotingNodeLoader extends NodeLoader 26 | { 27 | /** 28 | * @var EventDispatcherInterface 29 | */ 30 | private $dispatcher; 31 | 32 | /** 33 | * @var FactoryInterface 34 | */ 35 | private $menuFactory; 36 | 37 | public function __construct(FactoryInterface $factory, EventDispatcherInterface $dispatcher) 38 | { 39 | $this->menuFactory = $factory; 40 | $this->dispatcher = $dispatcher; 41 | } 42 | 43 | public function load($data) 44 | { 45 | if (!$this->supports($data)) { 46 | throw new \InvalidArgumentException(sprintf( 47 | 'NodeLoader can only handle data implementing NodeInterface, "%s" given.', 48 | is_object($data) ? get_class($data) : gettype($data) 49 | )); 50 | } 51 | $event = new CreateMenuItemFromNodeEvent($data); 52 | $this->dispatcher->dispatch(Events::CREATE_ITEM_FROM_NODE, $event); 53 | 54 | if ($event->isSkipNode()) { 55 | if ($data instanceof Menu) { 56 | // create an empty menu root to avoid the knp menu from failing. 57 | return $this->menuFactory->createItem(''); 58 | } 59 | 60 | return; 61 | } 62 | 63 | $item = $event->getItem() ?: $this->menuFactory->createItem($data->getName(), $data->getOptions()); 64 | 65 | if (empty($item)) { 66 | return; 67 | } 68 | 69 | if ($event->isSkipChildren()) { 70 | return $item; 71 | } 72 | 73 | foreach ($data->getChildren() as $childNode) { 74 | if ($childNode instanceof NodeInterface) { 75 | $child = $this->load($childNode); 76 | if (!empty($child)) { 77 | $item->addChild($child); 78 | } 79 | } 80 | } 81 | 82 | return $item; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Doctrine/Phpcr/Menu.php: -------------------------------------------------------------------------------- 1 | setParentObject($parent); 30 | } 31 | 32 | /** 33 | * Returns the parent of this menu node. 34 | * 35 | * @return object 36 | */ 37 | public function getParentDocument() 38 | { 39 | return $this->getParentObject(); 40 | } 41 | 42 | /** 43 | * @deprecated For BC with the PHPCR-ODM 1.4 HierarchyInterface 44 | * @see setParentDocument 45 | */ 46 | public function setParent($parent) 47 | { 48 | @trigger_error('The '.__METHOD__.'() method is deprecated and will be removed in version 3.0. Use setParentDocument() instead.', E_USER_DEPRECATED); 49 | 50 | return $this->setParentDocument($parent); 51 | } 52 | 53 | /** 54 | * @deprecated For BC with the PHPCR-ODM 1.4 HierarchyInterface 55 | * @see getParentDocument 56 | */ 57 | public function getParent() 58 | { 59 | @trigger_error('The '.__METHOD__.'() method is deprecated and will be removed in version 3.0. Use getParentDocument() instead.', E_USER_DEPRECATED); 60 | 61 | return $this->getParentDocument(); 62 | } 63 | 64 | /** 65 | * Convenience method to set parent and name at the same time. 66 | * 67 | * @param object $parent A mapped object 68 | * @param string $name 69 | * 70 | * @return Menu - this instance 71 | */ 72 | public function setPosition($parent, $name) 73 | { 74 | $this->setParentObject($parent); 75 | $this->setName($name); 76 | 77 | return $this; 78 | } 79 | 80 | /** 81 | * Add a child menu node, automatically setting the parent node. 82 | * 83 | * @param NodeInterface $child 84 | * 85 | * @return NodeInterface - The newly added child node 86 | */ 87 | public function addChild(NodeInterface $child) 88 | { 89 | if ($child instanceof MenuNode) { 90 | $child->setParentObject($this); 91 | } 92 | 93 | return parent::addChild($child); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Resources/config/schema/menu-1.0.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /src/Doctrine/Phpcr/MenuNode.php: -------------------------------------------------------------------------------- 1 | setParentObject($parent); 30 | } 31 | 32 | /** 33 | * Returns the parent of this menu node. 34 | * 35 | * @return object 36 | */ 37 | public function getParentDocument() 38 | { 39 | return $this->getParentObject(); 40 | } 41 | 42 | /** 43 | * @deprecated For BC with the PHPCR-ODM 1.4 HierarchyInterface 44 | * @see setParentDocument 45 | */ 46 | public function setParent($parent) 47 | { 48 | @trigger_error('The '.__METHOD__.'() method is deprecated and will be removed in version 3.0. Use setParentDocument() instead.', E_USER_DEPRECATED); 49 | 50 | return $this->setParentDocument($parent); 51 | } 52 | 53 | /** 54 | * @deprecated For BC with the PHPCR-ODM 1.4 HierarchyInterface 55 | * @see getParentDocument 56 | */ 57 | public function getParent() 58 | { 59 | @trigger_error('The '.__METHOD__.'() method is deprecated and will be removed in version 3.0. Use getParentDocument() instead.', E_USER_DEPRECATED); 60 | 61 | return $this->getParentDocument(); 62 | } 63 | 64 | /** 65 | * Convenience method to set parent and name at the same time. 66 | * 67 | * @param object $parent A mapped document 68 | * @param string $name 69 | * 70 | * @return MenuNode - this instance 71 | */ 72 | public function setPosition($parent, $name) 73 | { 74 | $this->setParentObject($parent); 75 | $this->setName($name); 76 | 77 | return $this; 78 | } 79 | 80 | /** 81 | * Add a child menu node, automatically setting the parent node. 82 | * 83 | * @param NodeInterface $child 84 | * 85 | * @return NodeInterface - The newly added child node 86 | */ 87 | public function addChild(NodeInterface $child) 88 | { 89 | if ($child instanceof self) { 90 | $child->setParentObject($this); 91 | } 92 | 93 | return parent::addChild($child); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /tests/Unit/Voter/RequestContentIdentityVoterTestCase.php: -------------------------------------------------------------------------------- 1 | request = $this->prophesize(Request::class); 34 | 35 | $this->voter = $this->buildVoter($this->request->reveal()); 36 | } 37 | 38 | abstract protected function buildVoter(Request $request); 39 | 40 | abstract public function testSkipsWhenNoRequestIsAvailable(); 41 | 42 | public function testSkipsWhenNoContentIsAvailable() 43 | { 44 | $this->assertNull($this->voter->matchItem($this->createItem())); 45 | } 46 | 47 | public function testSkipsWhenNoContentAttributeWasDefined() 48 | { 49 | $attributes = $this->prophesize(ParameterBag::class); 50 | $attributes->has('_content')->willReturn(false); 51 | $this->request->attributes = $attributes; 52 | 53 | $this->assertNull($this->voter->matchItem($this->createItem(new \stdClass()))); 54 | } 55 | 56 | public function testMatchesWhenContentIsEqualToCurrentContent() 57 | { 58 | $content = new \stdClass(); 59 | 60 | $attributes = $this->prophesize(ParameterBag::class); 61 | $attributes->has('_content')->willReturn(true); 62 | $attributes->get('_content')->willReturn($content); 63 | $this->request->attributes = $attributes; 64 | 65 | $this->assertTrue($this->voter->matchItem($this->createItem($content))); 66 | } 67 | 68 | public function testSkipsWhenContentIsNotEqual() 69 | { 70 | $attributes = $this->prophesize(ParameterBag::class); 71 | $attributes->has('_content')->willReturn(true); 72 | $attributes->get('_content')->willReturn(new \stdClass()); 73 | $this->request->attributes = $attributes; 74 | 75 | $this->assertNull($this->voter->matchItem($this->createItem(new \stdClass()))); 76 | } 77 | 78 | protected function createItem($content = null) 79 | { 80 | $item = $this->prophesize(ItemInterface::class); 81 | $item->getExtra('content')->willReturn($content); 82 | 83 | return $item->reveal(); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /tests/Unit/Loader/VotingNodeLoaderTest.php: -------------------------------------------------------------------------------- 1 | factory = $this->getMock('Knp\Menu\FactoryInterface'); 27 | $this->dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface'); 28 | $this->subject = new VotingNodeLoader($this->factory, $this->dispatcher); 29 | } 30 | 31 | /** 32 | * @dataProvider getCreateFromNodeData 33 | */ 34 | public function testCreateFromNode($options) 35 | { 36 | // promises 37 | $node2 = $this->getNode('node2'); 38 | $node3 = $this->getNode('node3'); 39 | $node1 = $this->getNode('node1', [], [$node2, $node3]); 40 | 41 | // predictions 42 | $options = array_merge([ 43 | 'node2_is_published' => true, 44 | ], $options); 45 | 46 | $dispatchMethodMock = $this->dispatcher->expects($this->exactly(3))->method('dispatch'); 47 | 48 | $nodes = 3; 49 | if (!$options['node2_is_published']) { 50 | $dispatchMethodMock->will($this->returnCallback(function ($name, $event) use ($node2) { 51 | if ($event->getNode() === $node2) { 52 | $event->setSkipNode(true); 53 | } 54 | })); 55 | $nodes = 2; 56 | } 57 | 58 | $that = $this; 59 | $this->factory->expects($this->exactly($nodes))->method('createItem')->will($this->returnCallback(function () use ($that) { 60 | return $that->getMock('Knp\Menu\ItemInterface'); 61 | })); 62 | 63 | // test 64 | $res = $this->subject->load($node1); 65 | $this->assertInstanceOf('Knp\Menu\ItemInterface', $res); 66 | } 67 | 68 | public function getCreateFromNodeData() 69 | { 70 | return [ 71 | [[ 72 | ]], 73 | [[ 74 | 'node2_is_published' => false, 75 | ]], 76 | ]; 77 | } 78 | 79 | protected function getNode($name, $options = [], $children = []) 80 | { 81 | $node = $this->getMock('Knp\Menu\NodeInterface'); 82 | 83 | $node->expects($this->any())->method('getName')->willReturn($name); 84 | $node->expects($this->any())->method('getOptions')->willReturn($options); 85 | $node->expects($this->any())->method('getChildren')->willReturn($children); 86 | 87 | return $node; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Voter/RequestContentIdentityVoter.php: -------------------------------------------------------------------------------- 1 | 27 | */ 28 | class RequestContentIdentityVoter implements VoterInterface 29 | { 30 | /** 31 | * @var string The key to look up the content in the request attributes 32 | */ 33 | private $requestKey; 34 | 35 | /** 36 | * @var RequestStack|null 37 | */ 38 | private $requestStack; 39 | 40 | /** 41 | * @var Request|null 42 | */ 43 | private $request; 44 | 45 | /** 46 | * @param string $requestKey The key to look up the content in the request 47 | * attributes 48 | */ 49 | public function __construct($requestKey, RequestStack $requestStack = null) 50 | { 51 | $this->requestKey = $requestKey; 52 | $this->requestStack = $requestStack; 53 | } 54 | 55 | /** 56 | * @deprecated since version 2.2. Pass a RequestStack to the constructor instead. 57 | */ 58 | public function setRequest(Request $request = null) 59 | { 60 | @trigger_error( 61 | sprintf( 62 | 'The %s() method is deprecated since version 2.2. 63 | Pass a Symfony\Component\HttpFoundation\RequestStack 64 | in the constructor instead.', 65 | __METHOD__), 66 | E_USER_DEPRECATED 67 | ); 68 | 69 | $this->request = $request; 70 | } 71 | 72 | /** 73 | * {@inheritdoc} 74 | */ 75 | public function matchItem(ItemInterface $item) 76 | { 77 | $request = $this->getRequest(); 78 | if (!$request) { 79 | return; 80 | } 81 | 82 | $content = $item->getExtra('content'); 83 | 84 | if (null !== $content 85 | && $request->attributes->has($this->requestKey) 86 | && $request->attributes->get($this->requestKey) === $content 87 | ) { 88 | return true; 89 | } 90 | } 91 | 92 | private function getRequest() 93 | { 94 | if ($this->requestStack) { 95 | return $this->requestStack->getMasterRequest(); 96 | } 97 | 98 | return $this->request; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | root('cmf_menu') 26 | ->fixXmlConfig('voter') 27 | ->children() 28 | ->arrayNode('persistence') 29 | ->addDefaultsIfNotSet() 30 | ->children() 31 | ->arrayNode('phpcr') 32 | ->addDefaultsIfNotSet() 33 | ->canBeEnabled() 34 | ->children() 35 | ->scalarNode('menu_basepath')->defaultValue('/cms/menu')->end() 36 | ->scalarNode('content_basepath')->defaultValue('/cms/content')->end() 37 | ->integerNode('prefetch')->defaultValue(10)->end() 38 | ->scalarNode('manager_name')->defaultNull()->end() 39 | ->scalarNode('menu_document_class')->defaultValue(Menu::class)->end() 40 | ->scalarNode('node_document_class')->defaultValue(MenuNode::class)->end() 41 | ->end() 42 | ->end() 43 | ->end() 44 | ->end() 45 | 46 | ->scalarNode('content_url_generator')->defaultValue('router')->end() 47 | ->booleanNode('allow_empty_items')->defaultFalse()->end() 48 | 49 | ->arrayNode('voters') 50 | ->children() 51 | ->arrayNode('content_identity') 52 | ->children() 53 | ->scalarNode('content_key')->defaultNull()->end() 54 | ->end() 55 | ->end() 56 | ->scalarNode('uri_prefix')->defaultFalse()->end() 57 | ->end() 58 | ->end() 59 | 60 | ->arrayNode('publish_workflow') 61 | ->addDefaultsIfNotSet() 62 | ->children() 63 | ->enumNode('enabled') 64 | ->values([true, false, 'auto']) 65 | ->defaultValue('auto') 66 | ->end() 67 | ->end() 68 | ->end() 69 | ->end() 70 | ; 71 | 72 | return $treeBuilder; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Voter/UriPrefixVoter.php: -------------------------------------------------------------------------------- 1 | 30 | * @author David Buchmann 31 | */ 32 | class UriPrefixVoter implements VoterInterface 33 | { 34 | /** 35 | * @var RequestStack|null 36 | */ 37 | private $requestStack; 38 | 39 | /** 40 | * @deprecated Use the request stack instead 41 | * 42 | * @var Request|null 43 | */ 44 | private $request; 45 | 46 | public function __construct(RequestStack $requestStack = null) 47 | { 48 | $this->requestStack = $requestStack; 49 | } 50 | 51 | /** 52 | * @deprecated since version 2.2. Pass a RequestStack to the constructor instead. 53 | */ 54 | public function setRequest(Request $request = null) 55 | { 56 | @trigger_error( 57 | sprintf( 58 | 'The %s() method is deprecated since version 2.2. 59 | Pass a Symfony\Component\HttpFoundation\RequestStack 60 | in the constructor instead.', 61 | __METHOD__), 62 | E_USER_DEPRECATED 63 | ); 64 | 65 | $this->request = $request; 66 | } 67 | 68 | /** 69 | * {@inheritdoc} 70 | */ 71 | public function matchItem(ItemInterface $item) 72 | { 73 | $request = $this->getRequest(); 74 | if (!$request) { 75 | return; 76 | } 77 | 78 | $content = $item->getExtra('content'); 79 | 80 | if ($content instanceof Route && $content->hasOption('currentUriPrefix')) { 81 | $currentUriPrefix = $content->getOption('currentUriPrefix'); 82 | $currentUriPrefix = str_replace('{_locale}', $request->getLocale(), $currentUriPrefix); 83 | if (0 === strncmp($request->getPathInfo(), $currentUriPrefix, strlen($currentUriPrefix))) { 84 | return true; 85 | } 86 | } 87 | } 88 | 89 | private function getRequest() 90 | { 91 | if ($this->requestStack) { 92 | return $this->requestStack->getMasterRequest(); 93 | } 94 | 95 | return $this->request; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/QuietFactory.php: -------------------------------------------------------------------------------- 1 | 25 | */ 26 | class QuietFactory implements FactoryInterface 27 | { 28 | /** 29 | * @var FactoryInterface 30 | */ 31 | private $innerFactory; 32 | 33 | /** 34 | * @var LoggerInterface|null 35 | */ 36 | private $logger; 37 | 38 | /** 39 | * Whether to return null (if value is false) or a MenuItem 40 | * without any URL (if value is true) if no URL can be found 41 | * for a MenuNode. 42 | * 43 | * @var bool 44 | */ 45 | private $allowEmptyItems; 46 | 47 | public function __construct(FactoryInterface $innerFactory, LoggerInterface $logger = null, $allowEmptyItems = false) 48 | { 49 | $this->innerFactory = $innerFactory; 50 | $this->logger = $logger; 51 | $this->allowEmptyItems = $allowEmptyItems; 52 | } 53 | 54 | /** 55 | * {@inheritdoc} 56 | */ 57 | public function createItem($name, array $options = []) 58 | { 59 | try { 60 | return $this->innerFactory->createItem($name, $options); 61 | } catch (RouteNotFoundException $e) { 62 | if (null !== $this->logger) { 63 | $this->logger->error( 64 | sprintf('An exception was thrown while creating a menu item called "%s"', $name), 65 | ['exception' => $e] 66 | ); 67 | } 68 | 69 | if (!$this->allowEmptyItems) { 70 | return; 71 | } 72 | 73 | // remove route and content options 74 | unset($options['route'], $options['content']); 75 | 76 | return $this->innerFactory->createItem($name, $options); 77 | } 78 | } 79 | 80 | /** 81 | * Forward adding extensions to the wrapped factory. 82 | * 83 | * @param ExtensionInterface $extension 84 | * @param int $priority 85 | * 86 | * @throws \Exception if the inner factory does not implement the addExtension method 87 | */ 88 | public function addExtension(ExtensionInterface $extension, $priority = 0) 89 | { 90 | if (!method_exists($this->innerFactory, 'addExtension')) { 91 | throw new LogicException(sprintf( 92 | 'Wrapped factory "%s" does not have the method "addExtension".', 93 | get_class($this->innerFactory) 94 | )); 95 | } 96 | $this->innerFactory->addExtension($extension, $priority); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/PublishWorkflow/Voter/MenuContentVoter.php: -------------------------------------------------------------------------------- 1 | container = $container; 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | public function supportsAttribute($attribute) 42 | { 43 | return PublishWorkflowChecker::VIEW_ATTRIBUTE === $attribute 44 | || PublishWorkflowChecker::VIEW_ANONYMOUS_ATTRIBUTE === $attribute 45 | ; 46 | } 47 | 48 | /** 49 | * {@inheritdoc} 50 | */ 51 | public function supportsClass($class) 52 | { 53 | return is_subclass_of($class, MenuNode::class); 54 | } 55 | 56 | /** 57 | * {@inheritdoc} 58 | * 59 | * @param MenuNode $object 60 | */ 61 | public function vote(TokenInterface $token, $object, array $attributes) 62 | { 63 | if (!$this->supportsClass(get_class($object))) { 64 | return self::ACCESS_ABSTAIN; 65 | } 66 | /** @var PublishWorkflowChecker $publishWorkflowChecker */ 67 | $publishWorkflowChecker = $this->container->get('cmf_core.publish_workflow.checker'); 68 | /** @var MenuNode $object */ 69 | $content = $object->getContent(); 70 | $decision = self::ACCESS_GRANTED; 71 | foreach ($attributes as $attribute) { 72 | if (!$this->supportsAttribute($attribute)) { 73 | // there was an unsupported attribute in the request. 74 | // now we only abstain or deny if we find a supported attribute 75 | // and the content is not publishable 76 | $decision = self::ACCESS_ABSTAIN; 77 | 78 | continue; 79 | } 80 | 81 | if ($content && 82 | false === $publishWorkflowChecker->isGranted($attribute, $content) 83 | ) { 84 | return self::ACCESS_DENIED; 85 | } 86 | } 87 | 88 | return $decision; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /tests/Unit/Provider/PhpcrMenuProviderTest.php: -------------------------------------------------------------------------------- 1 | manager = $this->prophesize(DocumentManagerInterface::class); 30 | $this->registry = $this->prophesize(ManagerRegistry::class); 31 | $this->document = $this->prophesize(NodeInterface::class); 32 | $this->item = $this->prophesize(ItemInterface::class); 33 | $this->nodeLoader = $this->prophesize(NodeLoader::class); 34 | $this->session = $this->prophesize(SessionInterface::class); 35 | 36 | $this->manager->getPhpcrSession()->willReturn($this->session->reveal()); 37 | $this->registry->getManager(null)->willReturn($this->manager->reveal()); 38 | } 39 | 40 | /** 41 | * @dataProvider provideMenuTests 42 | */ 43 | public function testGet($menuRoot, $name, $expectedPath) 44 | { 45 | $this->manager->find(null, $expectedPath) 46 | ->willReturn($this->document->reveal()); 47 | $this->nodeLoader->load($this->document->reveal()) 48 | ->willReturn($this->item->reveal()); 49 | 50 | $provider = $this->createProvider($menuRoot); 51 | $item = $provider->get($name); 52 | 53 | $this->assertSame($this->item->reveal(), $item); 54 | } 55 | 56 | /** 57 | * @dataProvider provideMenuTests 58 | */ 59 | public function testHas($menuRoot, $name, $expectedPath) 60 | { 61 | $this->manager->find(null, $expectedPath) 62 | ->willReturn($this->document->reveal()); 63 | 64 | $provider = $this->createProvider($menuRoot); 65 | $this->assertTrue($provider->has($name)); 66 | } 67 | 68 | public function testHasNot() 69 | { 70 | $this->session->getNode()->shouldNotBeCalled(); 71 | $this->session->getNamespacePrefixes() 72 | ->willReturn(['jcr', 'nt']); 73 | 74 | $this->manager->find(Argument::cetera())->shouldNotBeCalled(); 75 | 76 | $provider = $this->createProvider('/foo'); 77 | 78 | $this->assertFalse($provider->has('notavalidnamespace:bar')); 79 | $this->assertFalse($provider->has('not:a:valid:name')); 80 | } 81 | 82 | public function provideMenuTests() 83 | { 84 | return [ 85 | ['/test/menu', 'foo', '/test/menu/foo'], 86 | ['/test/menu', '/another/menu/path', '/another/menu/path'], 87 | ['/test/menu', 'jcr:namespaced', '/test/menu/jcr:namespaced'], 88 | ]; 89 | } 90 | 91 | private function createProvider($basePath) 92 | { 93 | return new PhpcrMenuProvider( 94 | $this->nodeLoader->reveal(), 95 | $this->registry->reveal(), 96 | $basePath 97 | ); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /tests/Unit/Voter/UriPrefixVoterTest.php: -------------------------------------------------------------------------------- 1 | request = $this->prophesize('Symfony\Component\HttpFoundation\Request'); 25 | $this->request->getLocale()->willReturn(''); 26 | 27 | $this->voter = new UriPrefixVoter(); 28 | $this->voter->setRequest($this->request->reveal()); 29 | } 30 | 31 | public function testSkipsWhenNoContentIsAvailable() 32 | { 33 | $this->assertNull($this->voter->matchItem($this->createItem())); 34 | } 35 | 36 | public function testSkipsWhenNoRequestIsAvailable() 37 | { 38 | $this->voter->setRequest(null); 39 | 40 | $this->assertNull($this->voter->matchItem($this->createItem())); 41 | } 42 | 43 | public function testSkipsIfContentDoesNotExtendRoute() 44 | { 45 | $this->assertNull($this->voter->matchItem($this->createItem(new \stdClass()))); 46 | } 47 | 48 | public function testSkipsIfContentHasNoCurrentUriPrefixOption() 49 | { 50 | $content = $this->prophesize('Symfony\Component\Routing\Route'); 51 | $content->hasOption('currentUriPrefix')->willReturn(false); 52 | 53 | $this->assertNull($this->voter->matchItem($this->createItem($content->reveal()))); 54 | } 55 | 56 | public function testMatchesCurrentUriPrefixOptionWithCurrentUri() 57 | { 58 | $content = $this->prophesize('Symfony\Component\Routing\Route'); 59 | $content->hasOption('currentUriPrefix')->willReturn(true); 60 | $content->getOption('currentUriPrefix')->willReturn('/some/prefix'); 61 | 62 | $this->request->getPathInfo()->willReturn('/some/prefix/page/12'); 63 | 64 | $this->assertTrue($this->voter->matchItem($this->createItem($content->reveal()))); 65 | } 66 | 67 | public function testSkipsWhenThereIsNoMatch() 68 | { 69 | $content = $this->prophesize('Symfony\Component\Routing\Route'); 70 | $content->hasOption('currentUriPrefix')->willReturn(true); 71 | $content->getOption('currentUriPrefix')->willReturn('/some/prefix'); 72 | 73 | $this->request->getPathInfo()->willReturn('/page/12'); 74 | 75 | $this->assertNull($this->voter->matchItem($this->createItem($content->reveal()))); 76 | } 77 | 78 | public function testReplacesSpecialLocalePlaceholderInCurrentUriPrefix() 79 | { 80 | $content = $this->prophesize('Symfony\Component\Routing\Route'); 81 | $content->hasOption('currentUriPrefix')->willReturn(true); 82 | $content->getOption('currentUriPrefix')->willReturn('/{_locale}/prefix'); 83 | 84 | $this->request->getPathInfo()->willReturn('/en/prefix/page/12'); 85 | $this->request->getLocale()->willReturn('en'); 86 | 87 | $this->assertTrue($this->voter->matchItem($this->createItem($content->reveal()))); 88 | } 89 | 90 | private function createItem($content = null) 91 | { 92 | $item = $this->prophesize('Knp\Menu\ItemInterface'); 93 | $item->getExtra('content')->willReturn($content); 94 | 95 | return $item->reveal(); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Voter/RequestParentContentIdentityVoter.php: -------------------------------------------------------------------------------- 1 | 28 | */ 29 | class RequestParentContentIdentityVoter implements VoterInterface 30 | { 31 | /** 32 | * @var string The key to look up the content in the request attributes 33 | */ 34 | private $requestKey; 35 | 36 | /** 37 | * @var string Class for content having a getParent method 38 | */ 39 | private $childClass; 40 | 41 | /** 42 | * @var RequestStack|null 43 | */ 44 | private $requestStack; 45 | 46 | /** 47 | * @var Request|null 48 | */ 49 | private $request; 50 | 51 | /** 52 | * @param string $requestKey The key to look up the content in the request 53 | * attributes 54 | * @param string $childClass Fully qualified class name of the model class 55 | * the content in the request must have to 56 | * attempt calling getParentDocument on it 57 | * @param RequestStack|null $requestStack 58 | */ 59 | public function __construct($requestKey, $childClass, RequestStack $requestStack = null) 60 | { 61 | $this->requestKey = $requestKey; 62 | $this->childClass = $childClass; 63 | $this->requestStack = $requestStack; 64 | } 65 | 66 | /** 67 | * @deprecated since version 2.2. Pass a RequestStack to the constructor instead. 68 | */ 69 | public function setRequest(Request $request = null) 70 | { 71 | @trigger_error( 72 | sprintf( 73 | 'The %s() method is deprecated since version 2.2. 74 | Pass a Symfony\Component\HttpFoundation\RequestStack 75 | in the constructor instead.', 76 | __METHOD__), 77 | E_USER_DEPRECATED 78 | ); 79 | 80 | $this->request = $request; 81 | } 82 | 83 | /** 84 | * {@inheritdoc} 85 | */ 86 | public function matchItem(ItemInterface $item) 87 | { 88 | $request = $this->getRequest(); 89 | if (!$request) { 90 | return; 91 | } 92 | 93 | $content = $item->getExtra('content'); 94 | 95 | if (null !== $content 96 | && $request->attributes->has($this->requestKey) 97 | && $request->attributes->get($this->requestKey) instanceof $this->childClass 98 | && $request->attributes->get($this->requestKey)->getParentDocument() === $content 99 | ) { 100 | return true; 101 | } 102 | } 103 | 104 | private function getRequest() 105 | { 106 | if ($this->requestStack) { 107 | return $this->requestStack->getMasterRequest(); 108 | } 109 | 110 | return $this->request; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /tests/Fixtures/App/Document/Content.php: -------------------------------------------------------------------------------- 1 | menuNodes = new ArrayCollection(); 68 | $this->routes = new ArrayCollection(); 69 | } 70 | 71 | public function getId() 72 | { 73 | return $this->id; 74 | } 75 | 76 | public function setId($id) 77 | { 78 | $this->id = $id; 79 | } 80 | 81 | public function getTitle() 82 | { 83 | return $this->title; 84 | } 85 | 86 | public function setTitle($title) 87 | { 88 | $this->title = $title; 89 | } 90 | 91 | public function getMenuNodes() 92 | { 93 | return $this->menuNodes; 94 | } 95 | 96 | public function addMenuNode(NodeInterface $menuNode) 97 | { 98 | $this->menuNodes->add($menuNode); 99 | } 100 | 101 | public function addRoute($route) 102 | { 103 | $this->routes->add($route); 104 | } 105 | 106 | public function removeMenuNode(NodeInterface $menuNode) 107 | { 108 | $this->menuNodes->remove($menuNode); 109 | } 110 | 111 | public function getRoutes() 112 | { 113 | foreach ($this->routes as $route) { 114 | } 115 | 116 | return $this->routes; 117 | } 118 | 119 | public function setParentDocument($parent) 120 | { 121 | $this->parent = $parent; 122 | } 123 | 124 | public function getParentDocument() 125 | { 126 | return $this->parent; 127 | } 128 | 129 | public function setName($name) 130 | { 131 | $this->name = $name; 132 | } 133 | 134 | public function getName() 135 | { 136 | return $this->name; 137 | } 138 | 139 | public function isPublishable() 140 | { 141 | return $this->published; 142 | } 143 | 144 | /** 145 | * Set the boolean flag whether this content is publishable or not. 146 | * 147 | * @param bool $publishable 148 | */ 149 | public function setPublishable($publishable) 150 | { 151 | $this->published = $publishable; 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /tests/Unit/Voter/RequestParentContentIdentityVoterTest.php: -------------------------------------------------------------------------------- 1 | request = $this->prophesize('Symfony\Component\HttpFoundation\Request'); 25 | 26 | $this->voter = new RequestParentContentIdentityVoter('_content', __CLASS__.'_ChildContent'); 27 | $this->voter->setRequest($this->request->reveal()); 28 | } 29 | 30 | public function testSkipsWhenNoContentIsAvailable() 31 | { 32 | $this->assertNull($this->voter->matchItem($this->createItem())); 33 | } 34 | 35 | public function testSkipsWhenNoRequestIsAvailable() 36 | { 37 | $this->voter->setRequest(null); 38 | 39 | $this->assertNull($this->voter->matchItem($this->createItem())); 40 | } 41 | 42 | public function testSkipsWhenNoContentAttributeWasDefined() 43 | { 44 | $attributes = $this->prophesize('Symfony\Component\HttpFoundation\ParameterBag'); 45 | $attributes->has('_content')->willReturn(false); 46 | $this->request->attributes = $attributes; 47 | 48 | $this->assertNull($this->voter->matchItem($this->createItem(new \stdClass()))); 49 | } 50 | 51 | public function testSkipsWhenContentObjectDoesNotImplementChildClass() 52 | { 53 | $attributes = $this->prophesize('Symfony\Component\HttpFoundation\ParameterBag'); 54 | $attributes->has('_content')->willReturn(false); 55 | $this->request->attributes = $attributes; 56 | 57 | $this->assertNull($this->voter->matchItem($this->createItem(new \stdClass()))); 58 | } 59 | 60 | public function testMatchesWhenParentContentIsEqualToCurrentContent() 61 | { 62 | $parent = new \stdClass(); 63 | $content = new RequestParentContentIdentityVoterTest_ChildContent($parent); 64 | 65 | $attributes = $this->prophesize('Symfony\Component\HttpFoundation\ParameterBag'); 66 | $attributes->has('_content')->willReturn(true); 67 | $attributes->get('_content')->willReturn($content); 68 | $this->request->attributes = $attributes; 69 | 70 | $this->assertTrue($this->voter->matchItem($this->createItem($parent))); 71 | } 72 | 73 | public function testSkipsWhenParentContentIsNotEqual() 74 | { 75 | $content = new RequestParentContentIdentityVoterTest_ChildContent(new \stdClass()); 76 | 77 | $attributes = $this->prophesize('Symfony\Component\HttpFoundation\ParameterBag'); 78 | $attributes->has('_content')->willReturn(true); 79 | $attributes->get('_content')->willReturn($content); 80 | $this->request->attributes = $attributes; 81 | 82 | $this->assertNull($this->voter->matchItem($this->createItem(new \stdClass()))); 83 | } 84 | 85 | private function createItem($content = null) 86 | { 87 | $item = $this->prophesize('Knp\Menu\ItemInterface'); 88 | $item->getExtra('content')->willReturn($content); 89 | 90 | return $item->reveal(); 91 | } 92 | } 93 | 94 | class RequestParentContentIdentityVoterTest_ChildContent 95 | { 96 | private $parent; 97 | 98 | public function __construct($parent) 99 | { 100 | $this->parent = $parent; 101 | } 102 | 103 | public function getParentDocument() 104 | { 105 | return $this->parent; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Model/MenuOptionsInterface.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | interface MenuOptionsInterface extends NodeInterface 22 | { 23 | /** 24 | * Whether or not to display this menu. 25 | * 26 | * @return bool 27 | */ 28 | public function getDisplay(); 29 | 30 | /** 31 | * Set whether or not this menu should be displayed. 32 | * 33 | * @param bool $bool 34 | * 35 | * @return MenuOptionsInterface 36 | */ 37 | public function setDisplay($bool); 38 | 39 | /** 40 | * Whether or not this menu should show its children. 41 | * 42 | * @return bool 43 | */ 44 | public function getDisplayChildren(); 45 | 46 | /** 47 | * Set whether or not this menu should show its children. 48 | * 49 | * @param bool $bool 50 | * 51 | * @return MenuOptionsInterface 52 | */ 53 | public function setDisplayChildren($bool); 54 | 55 | /** 56 | * Return the attributes associated with this menu node. 57 | * 58 | * @return array 59 | */ 60 | public function getAttributes(); 61 | 62 | /** 63 | * Set the attributes associated with this menu node. 64 | * 65 | * @param $attributes array 66 | * 67 | * @return MenuOptionsInterface The item to provide a fluent interface 68 | */ 69 | public function setAttributes(array $attributes); 70 | 71 | /** 72 | * Return the given attribute, optionally specifying a default value. 73 | * 74 | * @param string $name The name of the attribute to return 75 | * @param string $default The value to return if the attribute doesn't exist 76 | * 77 | * @return string 78 | */ 79 | public function getAttribute($name, $default = null); 80 | 81 | /** 82 | * Set the named attribute. 83 | * 84 | * @param string $name attribute name 85 | * @param string $value attribute value 86 | * 87 | * @return MenuOptionsInterface The item to provide a fluent interface 88 | */ 89 | public function setAttribute($name, $value); 90 | 91 | /** 92 | * Get the link HTML attributes. 93 | * 94 | * @return array 95 | */ 96 | public function getLinkAttributes(); 97 | 98 | /** 99 | * Set the link HTML attributes as associative array. 100 | * 101 | * @param array $linkAttributes 102 | * 103 | * @return MenuOptionsInterface The item to provide a fluent interface 104 | */ 105 | public function setLinkAttributes($linkAttributes); 106 | 107 | /** 108 | * Return the children attributes. 109 | * 110 | * @return array 111 | */ 112 | public function getChildrenAttributes(); 113 | 114 | /** 115 | * Set the children attributes. 116 | * 117 | * @param array $childrenAttributes 118 | * 119 | * @return MenuOptionsInterface The item to provide a fluent interface 120 | */ 121 | public function setChildrenAttributes(array $childrenAttributes); 122 | 123 | /** 124 | * Get the label HTML attributes. 125 | * 126 | * @return array 127 | */ 128 | public function getLabelAttributes(); 129 | 130 | /** 131 | * Set the label HTML attributes as associative array. 132 | * 133 | * @param array $labelAttributes 134 | * 135 | * @return MenuOptionsInterface The item to provide a fluent interface 136 | */ 137 | public function setLabelAttributes($labelAttributes); 138 | } 139 | -------------------------------------------------------------------------------- /src/Event/CreateMenuItemFromNodeEvent.php: -------------------------------------------------------------------------------- 1 | 26 | */ 27 | class CreateMenuItemFromNodeEvent extends Event 28 | { 29 | /** 30 | * @var NodeInterface 31 | */ 32 | private $node; 33 | 34 | /** 35 | * @var ItemInterface 36 | */ 37 | private $item; 38 | 39 | /** 40 | * Whether or not to skip processing of this node. 41 | * 42 | * @var bool 43 | */ 44 | private $skipNode = false; 45 | 46 | /** 47 | * Whether or not to skip processing of child nodes. 48 | * 49 | * @var bool 50 | */ 51 | private $skipChildren = false; 52 | 53 | /** 54 | * @param NodeInterface $node 55 | */ 56 | public function __construct(NodeInterface $node) 57 | { 58 | $this->node = $node; 59 | } 60 | 61 | /** 62 | * Get the menu node that is about to be built. 63 | * 64 | * @return NodeInterface 65 | */ 66 | public function getNode() 67 | { 68 | return $this->node; 69 | } 70 | 71 | /** 72 | * Get the menu item attached to this event. 73 | * 74 | * If this is non-null, it will be used instead of automatically converting 75 | * the NodeInterface into a MenuItem. 76 | * 77 | * @return ItemInterface 78 | */ 79 | public function getItem() 80 | { 81 | return $this->item; 82 | } 83 | 84 | /** 85 | * Set the menu item that represents the menu node of this event. 86 | * 87 | * Unless you set the skip children option, the children from the menu node 88 | * will still be built and added after eventual children this menu item 89 | * has. 90 | * 91 | * @param ItemInterface $item Menu item to use 92 | */ 93 | public function setItem(ItemInterface $item = null) 94 | { 95 | $this->item = $item; 96 | } 97 | 98 | /** 99 | * Set whether the node associated with this event is to be skipped 100 | * entirely. This has precedence over an eventual menu item attached to the 101 | * event. 102 | * 103 | * This automatically skips the whole subtree, as the children have no 104 | * place where they could be attached to. 105 | * 106 | * @param bool $skipNode 107 | */ 108 | public function setSkipNode($skipNode) 109 | { 110 | $this->skipNode = (bool) $skipNode; 111 | } 112 | 113 | /** 114 | * @return bool Whether the node associated to this event is to be skipped 115 | */ 116 | public function isSkipNode() 117 | { 118 | return $this->skipNode; 119 | } 120 | 121 | /** 122 | * Set whether the children of the *node* associated with this event should 123 | * be ignored. 124 | * 125 | * Use this for example when your event handler implements its own logic to 126 | * build children items for the node associated with this event. 127 | * 128 | * If this event has a menu *item*, those children won't be skipped. 129 | * 130 | * @param bool $skipChildren 131 | */ 132 | public function setSkipChildren($skipChildren) 133 | { 134 | $this->skipChildren = (bool) $skipChildren; 135 | } 136 | 137 | /** 138 | * @return bool Whether the children of the node associated to this event 139 | * should be handled or ignored 140 | */ 141 | public function isSkipChildren() 142 | { 143 | return $this->skipChildren; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/DependencyInjection/CmfMenuExtension.php: -------------------------------------------------------------------------------- 1 | processConfiguration(new Configuration(), $configs); 25 | $bundles = $container->getParameter('kernel.bundles'); 26 | 27 | $loader = new XmlFileLoader( 28 | $container, 29 | new FileLocator(__DIR__.'/../Resources/config') 30 | ); 31 | 32 | $loader->load('menu.xml'); 33 | $container->setAlias('cmf_menu.content_router', $config['content_url_generator']); 34 | $container->setParameter($this->getAlias().'.allow_empty_items', $config['allow_empty_items']); 35 | 36 | $this->loadVoters($config, $loader, $container); 37 | 38 | if ($config['persistence']['phpcr']['enabled']) { 39 | $this->loadPhpcr($config['persistence']['phpcr'], $loader, $container); 40 | } 41 | 42 | if (true === $config['publish_workflow']['enabled'] 43 | || ('auto' === $config['publish_workflow']['enabled'] && isset($bundles['CmfCoreBundle'])) 44 | ) { 45 | $loader->load('publish-workflow.xml'); 46 | } 47 | } 48 | 49 | public function loadVoters($config, XmlFileLoader $loader, ContainerBuilder $container) 50 | { 51 | $loader->load('voters.xml'); 52 | 53 | if (isset($config['voters']['content_identity'])) { 54 | if (empty($config['voters']['content_identity']['content_key'])) { 55 | if (!class_exists(DynamicRouter::class)) { 56 | throw new \RuntimeException('You need to set the content_key when not using the CmfRoutingBundle DynamicRouter'); 57 | } 58 | $contentKey = DynamicRouter::CONTENT_KEY; 59 | } else { 60 | $contentKey = $config['voters']['content_identity']['content_key']; 61 | } 62 | $container->setParameter($this->getAlias().'.content_key', $contentKey); 63 | } else { 64 | $container->removeDefinition('cmf_menu.current_item_voter.content_identity'); 65 | } 66 | 67 | if (isset($config['voters']) && !array_key_exists('uri_prefix', $config['voters'])) { 68 | $container->removeDefinition('cmf_menu.current_item_voter.uri_prefix'); 69 | } 70 | } 71 | 72 | public function loadPhpcr($config, XmlFileLoader $loader, ContainerBuilder $container) 73 | { 74 | $keys = [ 75 | 'menu_document_class' => 'menu_document.class', 76 | 'node_document_class' => 'node_document.class', 77 | 'menu_basepath' => 'menu_basepath', 78 | 'content_basepath' => 'content_basepath', 79 | 'manager_name' => 'manager_name', 80 | 'prefetch' => 'prefetch', 81 | ]; 82 | 83 | foreach ($keys as $sourceKey => $targetKey) { 84 | $container->setParameter( 85 | $this->getAlias().'.persistence.phpcr.'.$targetKey, 86 | $config[$sourceKey] 87 | ); 88 | } 89 | 90 | $loader->load('persistence-phpcr.xml'); 91 | } 92 | 93 | /** 94 | * Returns the base path for the XSD files. 95 | * 96 | * @return string The XSD base path 97 | */ 98 | public function getXsdValidationBasePath() 99 | { 100 | return __DIR__.'/../Resources/config/schema'; 101 | } 102 | 103 | public function getNamespace() 104 | { 105 | return 'http://cmf.symfony.com/schema/dic/menu'; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Extension/ContentExtension.php: -------------------------------------------------------------------------------- 1 | 25 | */ 26 | class ContentExtension implements ExtensionInterface 27 | { 28 | /** 29 | * @var UrlGeneratorInterface 30 | */ 31 | private $contentRouter; 32 | 33 | /** 34 | * @param UrlGeneratorInterface $contentRouter A router to generate URLs based on the content object 35 | */ 36 | public function __construct(UrlGeneratorInterface $contentRouter) 37 | { 38 | $this->contentRouter = $contentRouter; 39 | } 40 | 41 | /** 42 | * Builds the full option array used to configure the item. 43 | * 44 | * @param array $options The options processed by the previous extensions 45 | * 46 | * @return array 47 | */ 48 | public function buildOptions(array $options) 49 | { 50 | $options = array_merge([ 51 | 'content' => null, 52 | 'linkType' => null, 53 | 'extras' => [], 54 | ], $options); 55 | 56 | if (null === $options['linkType']) { 57 | $options['linkType'] = $this->determineLinkType($options); 58 | } 59 | 60 | $this->validateLinkType($options['linkType']); 61 | 62 | if ('content' === $options['linkType']) { 63 | if (!isset($options['content'])) { 64 | throw new \InvalidArgumentException(sprintf('Link type content configured, but could not find content option in the provided options: %s', implode(', ', array_keys($options)))); 65 | } 66 | 67 | $options['uri'] = $this->contentRouter->generate( 68 | $options['content'], 69 | isset($options['routeParameters']) ? $options['routeParameters'] : [], 70 | (isset($options['routeAbsolute']) && $options['routeAbsolute']) ? UrlGeneratorInterface::ABSOLUTE_URL : UrlGeneratorInterface::ABSOLUTE_PATH 71 | ); 72 | } 73 | 74 | if (isset($options['route']) && 'route' !== $options['linkType']) { 75 | unset($options['route']); 76 | } 77 | 78 | $options['extras']['content'] = $options['content']; 79 | 80 | return $options; 81 | } 82 | 83 | /** 84 | * Configures the item with the passed options. 85 | * 86 | * @param ItemInterface $item 87 | * @param array $options 88 | */ 89 | public function buildItem(ItemInterface $item, array $options) 90 | { 91 | } 92 | 93 | /** 94 | * If linkType not specified, we can determine it from existing options. 95 | * 96 | * @param array $options Menu node options 97 | * 98 | * @return string The type of link to use 99 | */ 100 | private function determineLinkType(array $options) 101 | { 102 | if (!empty($options['uri'])) { 103 | return 'uri'; 104 | } 105 | 106 | if (!empty($options['route'])) { 107 | return 'route'; 108 | } 109 | 110 | if (!empty($options['content'])) { 111 | return 'content'; 112 | } 113 | 114 | return 'uri'; 115 | } 116 | 117 | /** 118 | * Ensure that we have a valid link type. 119 | * 120 | * @param string $linkType 121 | * 122 | * @throws \InvalidArgumentException if $linkType is not one of the known 123 | * link types 124 | */ 125 | private function validateLinkType($linkType) 126 | { 127 | $linkTypes = ['uri', 'route', 'content']; 128 | if (!in_array($linkType, $linkTypes)) { 129 | throw new \InvalidArgumentException(sprintf( 130 | 'Invalid link type "%s", expected: "%s"', 131 | $linkType, 132 | implode(',', $linkTypes) 133 | )); 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /tests/Unit/Extension/ContentExtensionTest.php: -------------------------------------------------------------------------------- 1 | generator = $this->getMock('Symfony\Component\Routing\Generator\UrlGeneratorInterface'); 26 | $this->subject = new ContentExtension($this->generator); 27 | } 28 | 29 | public function getLinkTypeData() 30 | { 31 | return [ 32 | [true], 33 | [false], 34 | ]; 35 | } 36 | 37 | /** 38 | * @dataProvider getLinkTypeData 39 | */ 40 | public function testUriLinkType($typeSet) 41 | { 42 | $options = ['uri' => '/configured_uri']; 43 | if ($typeSet) { 44 | $options['linkType'] = 'uri'; 45 | } 46 | 47 | $this->assertEquals( 48 | ['uri' => '/configured_uri', 'linkType' => 'uri', 'content' => null, 'extras' => ['content' => null]], 49 | $this->subject->buildOptions($options) 50 | ); 51 | } 52 | 53 | /** 54 | * @dataProvider getLinkTypeData 55 | */ 56 | public function testRouteLinkType($typeSet) 57 | { 58 | $options = ['route' => 'configured_route']; 59 | if ($typeSet) { 60 | $options['linkType'] = 'route'; 61 | } 62 | 63 | $this->assertEquals( 64 | ['route' => 'configured_route', 'linkType' => 'route', 'content' => null, 'extras' => ['content' => null]], 65 | $this->subject->buildOptions($options) 66 | ); 67 | } 68 | 69 | /** 70 | * @dataProvider getLinkTypeData 71 | */ 72 | public function testContentLinkType($typeSet) 73 | { 74 | $options = ['content' => 'configured_content', 'routeParameters' => ['test' => 'foo'], 'routeAbsolute' => true]; 75 | if ($typeSet) { 76 | $options['linkType'] = 'content'; 77 | } 78 | 79 | $this->generator->expects($this->once()) 80 | ->method('generate') 81 | ->with('configured_content', ['test' => 'foo'], UrlGeneratorInterface::ABSOLUTE_URL) 82 | ->willReturn('/generated_uri'); 83 | 84 | $this->assertEquals( 85 | [ 86 | 'uri' => '/generated_uri', 87 | 'linkType' => 'content', 88 | 'content' => 'configured_content', 89 | 'extras' => ['content' => 'configured_content'], 90 | 'routeParameters' => ['test' => 'foo'], 91 | 'routeAbsolute' => true, 92 | ], 93 | $this->subject->buildOptions($options) 94 | ); 95 | } 96 | 97 | public function testOptionsAsRemovedWhenLinkTypeIsElse() 98 | { 99 | $options = [ 100 | 'uri' => '/configured_uri', 101 | 'route' => 'configured_route', 102 | 'content' => 'configured_content', 103 | 'linkType' => 'content', 104 | ]; 105 | 106 | $this->generator->expects($this->once()) 107 | ->method('generate') 108 | ->with('configured_content', [], UrlGeneratorInterface::ABSOLUTE_PATH) 109 | ->willReturn('/generated_uri'); 110 | 111 | $this->assertEquals( 112 | [ 113 | 'uri' => '/generated_uri', 114 | 'content' => 'configured_content', 115 | 'linkType' => 'content', 116 | 'extras' => ['content' => 'configured_content'], 117 | ], 118 | $this->subject->buildOptions($options) 119 | ); 120 | } 121 | 122 | /** 123 | * @expectedException \InvalidArgumentException 124 | * @expectedExceptionMessage Invalid link type 125 | */ 126 | public function testFailsOnInvalidLinkType() 127 | { 128 | $this->subject->buildOptions(['linkType' => 'not_valid']); 129 | } 130 | 131 | /** 132 | * @expectedException \InvalidArgumentException 133 | * @expectedExceptionMessage could not find content option 134 | */ 135 | public function testFailsWhenContentIsNotAvailable() 136 | { 137 | $this->subject->buildOptions(['linkType' => 'content']); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /tests/Fixtures/App/DataFixtures/PHPCR/LoadMenuData.php: -------------------------------------------------------------------------------- 1 | getPhpcrSession(), '/test/menus'); 32 | NodeHelper::createPath($manager->getPhpcrSession(), '/test/routes/contents'); 33 | $this->menuRoot = $manager->find(null, '/test/menus'); 34 | $this->routeRoot = $manager->find(null, '/test/routes'); 35 | 36 | $this->loadMenu($manager); 37 | 38 | $manager->flush(); 39 | } 40 | 41 | protected function loadMenu(DocumentManager $manager) 42 | { 43 | $route = new Route(); 44 | $route->setName('content-1'); 45 | $route->setParentDocument($this->routeRoot); 46 | $manager->persist($route); 47 | 48 | $content = new Content(); 49 | $content->setTitle('Menu Item Content 1'); 50 | $content->setId('/test/content-menu-item-1'); 51 | $content->addRoute($route); 52 | 53 | $menu = new Menu(); 54 | $menu->setName('test-menu'); 55 | $menu->setLabel('Test Menu'); 56 | $menu->setParentDocument($this->menuRoot); 57 | $manager->persist($menu); 58 | 59 | $menuNode = new MenuNode(); 60 | $menuNode->setParentDocument($menu); 61 | $menuNode->setLabel('item-1'); 62 | $menuNode->setName('item-1'); 63 | $manager->persist($menuNode); 64 | 65 | $menuNode = new MenuNode(); 66 | $menuNode->setParentDocument($menu); 67 | $menuNode->setLabel('This node has a URI'); 68 | $menuNode->setUri('http://www.example.com'); 69 | $menuNode->setName('item-2'); 70 | $manager->persist($menuNode); 71 | 72 | $subNode = new MenuNode(); 73 | $subNode->setParentDocument($menuNode); 74 | $subNode->setLabel('This node has content'); 75 | $subNode->setName('sub-item-1'); 76 | $subNode->setContent($content); 77 | $manager->persist($subNode); 78 | 79 | $content->addMenuNode($subNode); 80 | 81 | $subNode = new MenuNode(); 82 | $subNode->setParentDocument($menuNode); 83 | $subNode->setLabel('This node has an assigned route'); 84 | $subNode->setName('sub-item-2'); 85 | $subNode->setRoute('link_test_route'); 86 | $manager->persist($subNode); 87 | 88 | $subNode = new MenuNode(); 89 | $subNode->setParentDocument($menuNode); 90 | $subNode->setLabel('This node has an assigned route with parameters'); 91 | $subNode->setName('sub-item-3'); 92 | $subNode->setRoute('link_test_route_with_params'); 93 | $subNode->setRouteParameters(['foo' => 'bar', 'bar' => 'foo']); 94 | $manager->persist($subNode); 95 | 96 | $menuNode = new MenuNode(); 97 | $menuNode->setParentDocument($menu); 98 | $menuNode->setLabel('item-3'); 99 | $menuNode->setName('item-3'); 100 | $manager->persist($menuNode); 101 | 102 | $menu = new Menu(); 103 | $menu->setName('another-menu'); 104 | $menu->setLabel('Another Menu'); 105 | $menu->setParentDocument($this->menuRoot); 106 | $manager->persist($menu); 107 | 108 | $menuNode = new MenuNode(); 109 | $menuNode->setParentDocument($menu); 110 | $menuNode->setLabel('This node has uri, route and content set. but linkType is set to route'); 111 | $menuNode->setLinkType('route'); 112 | $menuNode->setUri('http://www.example.com'); 113 | $menuNode->setRoute('link_test_route'); 114 | $menuNode->setName('item-1'); 115 | $manager->persist($menuNode); 116 | 117 | $menuNode = new MenuNode(); 118 | $menuNode->setParentDocument($menu); 119 | $menuNode->setLabel('item-2'); 120 | $menuNode->setName('item-2'); 121 | $manager->persist($menuNode); 122 | 123 | $manager->persist($content); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /tests/Unit/Model/MenuNodeTest.php: -------------------------------------------------------------------------------- 1 | setLabel('Child 1'); 22 | $c2 = new MenuNode(); 23 | $c2->setLabel('Child 2'); 24 | $this->content = new \stdClass(); 25 | $this->parentNode = new MenuNode(); 26 | $this->node = new MenuNode(); 27 | $this->node->setId('/foo/bar') 28 | ->setParentDocument($this->parentNode) 29 | ->setName('test') 30 | ->setLabel('Test') 31 | ->setUri('http://www.example.com') 32 | ->setRoute('test_route') 33 | ->setContent($this->content) 34 | ->setAttributes(['foo' => 'bar']) 35 | ->setChildrenAttributes(['bar' => 'foo']) 36 | ->setExtras(['far' => 'boo']) 37 | ->setLinkAttributes(['link' => 'knil']) 38 | ->setLabelAttributes(['label' => 'lebal']) 39 | ->setDisplay(false) 40 | ->setDisplayChildren(false) 41 | ->setRouteAbsolute(true) 42 | ->setLinkType('linktype'); 43 | } 44 | 45 | public function testGetters() 46 | { 47 | $this->assertSame($this->parentNode, $this->node->getParentDocument()); 48 | $this->assertEquals('test', $this->node->getName()); 49 | $this->assertEquals('Test', $this->node->getLabel()); 50 | $this->assertEquals('http://www.example.com', $this->node->getUri()); 51 | $this->assertEquals('test_route', $this->node->getRoute()); 52 | $this->assertSame($this->content, $this->node->getContent()); 53 | $this->assertEquals(['foo' => 'bar'], $this->node->getAttributes()); 54 | $this->assertEquals('bar', $this->node->getAttribute('foo')); 55 | $this->assertEquals(['bar' => 'foo'], $this->node->getChildrenAttributes()); 56 | $this->assertEquals(['far' => 'boo'], $this->node->getExtras()); 57 | 58 | $this->parentNode = new MenuNode(); 59 | $this->node->setPosition($this->parentNode, 'FOOO'); 60 | $this->assertSame($this->parentNode, $this->node->getParentDocument()); 61 | $this->assertEquals('FOOO', $this->node->getName()); 62 | $this->assertEquals(['link' => 'knil'], $this->node->getLinkAttributes()); 63 | $this->assertEquals(['label' => 'lebal'], $this->node->getLabelAttributes()); 64 | $this->assertFalse($this->node->getDisplay()); 65 | $this->assertFalse($this->node->getDisplayChildren()); 66 | $this->assertTrue($this->node->getRouteAbsolute()); 67 | $this->assertEquals('linktype', $this->node->getLinkType()); 68 | } 69 | 70 | public function testAddChild() 71 | { 72 | $c1 = new MenuNode(); 73 | $c2 = new MenuNode(); 74 | $m = new MenuNode(); 75 | $m->addChild($c1); 76 | $ret = $m->addChild($c2); 77 | 78 | $children = $m->getChildren(); 79 | $this->assertCount(2, $children); 80 | $this->assertSame($m, $children[0]->getParentDocument()); 81 | $this->assertSame($c2, $ret); 82 | } 83 | 84 | public function testMultilang() 85 | { 86 | $n = new MenuNode(); 87 | $n->setLocale('fr'); 88 | $this->assertEquals('fr', $n->getLocale()); 89 | } 90 | 91 | public function testPublishTimePeriodInterface() 92 | { 93 | $startDate = new \DateTime('2013-01-01'); 94 | $endDate = new \DateTime('2013-02-01'); 95 | 96 | $n = new MenuNode(); 97 | 98 | $this->assertInstanceOf( 99 | 'Symfony\Cmf\Bundle\CoreBundle\PublishWorkflow\PublishTimePeriodInterface', 100 | $n 101 | ); 102 | 103 | // test defaults 104 | $this->assertTrue($n->isPublishable()); 105 | $this->assertNull($n->getPublishStartDate()); 106 | $this->assertNull($n->getPublishEndDate()); 107 | 108 | $n->setPublishable(false); 109 | $n->setPublishStartDate($startDate); 110 | $n->setPublishEndDate($endDate); 111 | 112 | $this->assertSame($startDate, $n->getPublishStartDate()); 113 | $this->assertSame($endDate, $n->getPublishEndDate()); 114 | } 115 | 116 | /** 117 | * @depends testGetters 118 | */ 119 | public function testGetOptions() 120 | { 121 | $this->assertEquals([ 122 | 'uri' => $this->node->getUri(), 123 | 'route' => $this->node->getRoute(), 124 | 'label' => $this->node->getLabel(), 125 | 'attributes' => $this->node->getAttributes(), 126 | 'childrenAttributes' => $this->node->getChildrenAttributes(), 127 | 'display' => $this->node->getDisplay(), 128 | 'displayChildren' => $this->node->getDisplayChildren(), 129 | 'content' => $this->node->getContent(), 130 | 'routeParameters' => $this->node->getRouteParameters(), 131 | 'routeAbsolute' => $this->node->getRouteAbsolute(), 132 | 'linkAttributes' => $this->node->getLinkAttributes(), 133 | 'labelAttributes' => $this->node->getLabelAttributes(), 134 | 'linkType' => $this->node->getLinkType(), 135 | ], $this->node->getOptions()); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/Model/MenuNode.php: -------------------------------------------------------------------------------- 1 | parent = $parent; 87 | 88 | return $this; 89 | } 90 | 91 | /** 92 | * {@inheritdoc} 93 | */ 94 | public function getParentObject() 95 | { 96 | return $this->parent; 97 | } 98 | 99 | /** 100 | * @return string the loaded locale of this menu node 101 | */ 102 | public function getLocale() 103 | { 104 | return $this->locale; 105 | } 106 | 107 | /** 108 | * Set the locale this menu node should be. When doing a flush, 109 | * this will have the translated fields be stored as that locale. 110 | * 111 | * @param string $locale the locale to use for this menu node 112 | */ 113 | public function setLocale($locale) 114 | { 115 | $this->locale = $locale; 116 | } 117 | 118 | /** 119 | * Return the content document associated with this menu node. 120 | * 121 | * @return object the content of this menu node 122 | */ 123 | public function getContent() 124 | { 125 | return $this->content; 126 | } 127 | 128 | /** 129 | * Set the content document associated with this menu node. 130 | * 131 | * NOTE: When using doctrine, the content must be mapped for doctrine and 132 | * be persisted or cascading be configured on the content field. 133 | * 134 | * @param object $content 135 | * 136 | * @return MenuNode - this instance 137 | */ 138 | public function setContent($content) 139 | { 140 | $this->content = $content; 141 | 142 | return $this; 143 | } 144 | 145 | /** 146 | * {@inheritdoc} 147 | */ 148 | public function getOptions() 149 | { 150 | $options = parent::getOptions(); 151 | 152 | return array_merge($options, [ 153 | 'linkType' => $this->linkType, 154 | 'content' => $this->getContent(), 155 | ]); 156 | } 157 | 158 | /** 159 | * {@inheritdoc} 160 | */ 161 | public function isPublishable() 162 | { 163 | return $this->publishable; 164 | } 165 | 166 | /** 167 | * Set the publishable workflow flag. 168 | * 169 | * @param bool $publishable 170 | */ 171 | public function setPublishable($publishable) 172 | { 173 | $this->publishable = $publishable; 174 | } 175 | 176 | /** 177 | * {@inheritdoc} 178 | */ 179 | public function getPublishStartDate() 180 | { 181 | return $this->publishStartDate; 182 | } 183 | 184 | /** 185 | * {@inheritdoc} 186 | */ 187 | public function setPublishStartDate(\DateTime $date = null) 188 | { 189 | $this->publishStartDate = $date; 190 | } 191 | 192 | /** 193 | * {@inheritdoc} 194 | */ 195 | public function getPublishEndDate() 196 | { 197 | return $this->publishEndDate; 198 | } 199 | 200 | /** 201 | * {@inheritdoc} 202 | */ 203 | public function setPublishEndDate(\DateTime $date = null) 204 | { 205 | $this->publishEndDate = $date; 206 | } 207 | 208 | /** 209 | * Get the link type. 210 | * 211 | * The link type is used to explicitly determine which of the uri, route 212 | * and content fields are used to determine the link which will bre 213 | * rendered for the menu item. If it is empty this will be determined 214 | * automatically. 215 | * 216 | * @return string 217 | */ 218 | public function getLinkType() 219 | { 220 | return $this->linkType; 221 | } 222 | 223 | /** 224 | * @see getLinkType 225 | * @see ContentAwareFactory::$validLinkTypes 226 | * 227 | * Valid link types are defined in ContenentAwareFactory 228 | * 229 | * @param $linkType string - one of uri, route or content 230 | */ 231 | public function setLinkType($linkType) 232 | { 233 | $this->linkType = $linkType; 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /tests/Unit/PublishWorkflow/Voter/MenuContentVoterTest.php: -------------------------------------------------------------------------------- 1 | pwfc = $this->getMockBuilder('Symfony\Cmf\Bundle\CoreBundle\PublishWorkflow\PublishWorkflowChecker') 45 | ->disableOriginalConstructor() 46 | ->getMock(); 47 | $this->container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface'); 48 | $this->container 49 | ->expects($this->any()) 50 | ->method('get') 51 | ->with('cmf_core.publish_workflow.checker') 52 | ->will($this->returnValue($this->pwfc)) 53 | ; 54 | $this->voter = new MenuContentVoter($this->container); 55 | $this->token = new AnonymousToken('', ''); 56 | } 57 | 58 | public function providePublishWorkflowChecker() 59 | { 60 | $content = $this->getMock('Symfony\Cmf\Bundle\CoreBundle\PublishWorkflow\PublishableReadInterface'); 61 | 62 | return [ 63 | [ 64 | 'expected' => VoterInterface::ACCESS_GRANTED, 65 | 'attributes' => PublishWorkflowChecker::VIEW_ATTRIBUTE, 66 | $content, 67 | 'isMenuPublishable' => true, 68 | 'isContentPublishable' => true, 69 | ], 70 | [ 71 | 'expected' => VoterInterface::ACCESS_DENIED, 72 | 'attributes' => PublishWorkflowChecker::VIEW_ATTRIBUTE, 73 | $content, 74 | 'isMenuPublishable' => false, 75 | 'isContentPublishable' => false, 76 | ], 77 | [ 78 | 'expected' => VoterInterface::ACCESS_GRANTED, 79 | 'attributes' => [ 80 | PublishWorkflowChecker::VIEW_ANONYMOUS_ATTRIBUTE, 81 | PublishWorkflowChecker::VIEW_ATTRIBUTE, 82 | ], 83 | $content, 84 | 'isMenuPublishable' => true, 85 | 'isContentPublishable' => true, 86 | ], 87 | [ 88 | 'expected' => VoterInterface::ACCESS_DENIED, 89 | 'attributes' => PublishWorkflowChecker::VIEW_ANONYMOUS_ATTRIBUTE, 90 | $content, 91 | 'isMenuPublishable' => false, 92 | 'isContentPublishable' => false, 93 | ], 94 | [ 95 | 'expected' => VoterInterface::ACCESS_ABSTAIN, 96 | 'attributes' => 'other', 97 | $content, 98 | 'isMenuPublishable' => true, 99 | 'isContentPublishable' => true, 100 | ], 101 | [ 102 | 'expected' => VoterInterface::ACCESS_ABSTAIN, 103 | 'attributes' => [PublishWorkflowChecker::VIEW_ATTRIBUTE, 'other'], 104 | $content, 105 | 'isMenuPublishable' => true, 106 | 'isContentPublishable' => true, 107 | ], 108 | [ 109 | 'expected' => VoterInterface::ACCESS_GRANTED, 110 | 'attributes' => [PublishWorkflowChecker::VIEW_ATTRIBUTE], 111 | null, 112 | 'isMenuPublishable' => true, 113 | 'isContentPublishable' => null, 114 | ], 115 | [ 116 | 'expected' => VoterInterface::ACCESS_ABSTAIN, 117 | 'attributes' => [PublishWorkflowChecker::VIEW_ATTRIBUTE, 'other'], 118 | null, 119 | 'isMenuPublishable' => true, 120 | 'isContentPublishable' => null, 121 | ], 122 | [ 123 | 'expected' => VoterInterface::ACCESS_DENIED, 124 | 'attributes' => [PublishWorkflowChecker::VIEW_ATTRIBUTE, 'other'], 125 | $content, 126 | 'isMenuPublishable' => true, 127 | 'isContentPublishable' => false, 128 | ], 129 | [ 130 | 'expected' => VoterInterface::ACCESS_DENIED, 131 | 'attributes' => PublishWorkflowChecker::VIEW_ATTRIBUTE, 132 | $content, 133 | 'isMenuPublishable' => true, 134 | 'isContentPublishable' => false, 135 | ], 136 | ]; 137 | } 138 | 139 | /** 140 | * @dataProvider providePublishWorkflowChecker 141 | */ 142 | public function testPublishWorkflowChecker($expected, $attributes, $content, $isMenuPusblishable, $isContentPublishable) 143 | { 144 | $attributes = (array) $attributes; 145 | $menuNode = $this->getMock('Symfony\Cmf\Bundle\MenuBundle\Model\MenuNode'); 146 | $menuNode->expects($this->any()) 147 | ->method('getContent') 148 | ->will($this->returnValue($content)) 149 | ; 150 | $this->pwfc->expects($this->any()) 151 | ->method('isGranted') 152 | ->will($this->returnValue($isContentPublishable)) 153 | ; 154 | 155 | $this->assertEquals($expected, $this->voter->vote($this->token, $menuNode, $attributes)); 156 | } 157 | 158 | public function testUnsupportedClass() 159 | { 160 | $result = $this->voter->vote( 161 | $this->token, 162 | $this, 163 | [PublishWorkflowChecker::VIEW_ATTRIBUTE] 164 | ); 165 | $this->assertEquals(VoterInterface::ACCESS_ABSTAIN, $result); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /tests/Functional/Doctrine/Phpcr/MenuNodeTest.php: -------------------------------------------------------------------------------- 1 | db('PHPCR')->createTestNode(); 39 | 40 | $this->dm = $this->db('PHPCR')->getOm(); 41 | $this->rootDocument = $this->dm->find(null, '/test'); 42 | 43 | $this->content = new Content(); 44 | $this->content->setId('/test/fake_weak_content'); 45 | $this->content->setTitle('fake_weak_content'); 46 | $this->dm->persist($this->content); 47 | 48 | $this->child1 = new MenuNode(); 49 | $this->child1->setName('child1'); 50 | } 51 | 52 | public function testMenuNode() 53 | { 54 | $data = [ 55 | 'name' => 'test-node', 56 | 'label' => 'label_foobar', 57 | 'uri' => 'http://www.example.com/foo', 58 | 'route' => 'foo_route', 59 | 'linkType' => 'route', 60 | 'content' => $this->content, 61 | 'publishable' => false, 62 | 'publishStartDate' => new \DateTime('2013-06-18'), 63 | 'publishEndDate' => new \DateTime('2013-06-18'), 64 | 'attributes' => [ 65 | 'attr_foobar_1' => 'barfoo', 66 | 'attr_foobar_2' => 'barfoo', 67 | ], 68 | 'childrenAttributes' => [ 69 | 'child_foobar_1' => 'barfoo', 70 | 'child_foobar_2' => 'barfoo', 71 | ], 72 | 'linkAttributes' => [ 73 | 'link_foobar_1' => 'barfoo', 74 | 'link_foobar_2' => 'barfoo', 75 | ], 76 | 'labelAttributes' => [ 77 | 'label_foobar_1' => 'barfoo', 78 | 'label_foobar_2' => 'barfoo', 79 | ], 80 | 'extras' => [ 81 | 'extra_foobar_1' => 'barfoo', 82 | 'extra_foobar_2' => 'barfoo', 83 | ], 84 | 'routeParameters' => [ 85 | 'route_param_foobar_1' => 'barfoo', 86 | 'route_param_foobar_2' => 'barfoo', 87 | ], 88 | 'routeAbsolute' => true, 89 | 'display' => false, 90 | 'displayChildren' => false, 91 | ]; 92 | 93 | $startDateString = $data['publishStartDate']->format('Y-m-d'); 94 | $endDateString = $data['publishEndDate']->format('Y-m-d'); 95 | 96 | $menuNode = new MenuNode(); 97 | $refl = new \ReflectionClass($menuNode); 98 | 99 | $menuNode->setParentDocument($this->rootDocument); 100 | 101 | foreach ($data as $key => $value) { 102 | $refl = new \ReflectionClass($menuNode); 103 | $prop = $refl->getProperty($key); 104 | $prop->setAccessible(true); 105 | $prop->setValue($menuNode, $value); 106 | } 107 | 108 | $menuNode->addChild($this->child1); 109 | 110 | $this->dm->persist($menuNode); 111 | $this->dm->flush(); 112 | $this->dm->clear(); 113 | 114 | $menuNode = $this->dm->find(null, '/test/test-node'); 115 | 116 | $this->assertNotNull($menuNode); 117 | 118 | foreach ($data as $key => $value) { 119 | $prop = $refl->getProperty($key); 120 | $prop->setAccessible(true); 121 | $v = $prop->getValue($menuNode); 122 | 123 | if (!is_object($value)) { 124 | $this->assertEquals($value, $v); 125 | } 126 | } 127 | 128 | // test objects 129 | $prop = $refl->getProperty('content'); 130 | $prop->setAccessible(true); 131 | $content = $prop->getValue($menuNode); 132 | $this->assertEquals('fake_weak_content', $content->getName()); 133 | 134 | // test children 135 | $this->assertCount(1, $menuNode->getChildren()); 136 | 137 | // test publish start and end 138 | $publishStartDate = $menuNode->getPublishStartDate(); 139 | $publishEndDate = $menuNode->getPublishEndDate(); 140 | 141 | $this->assertInstanceOf('\DateTime', $publishStartDate); 142 | $this->assertInstanceOf('\DateTime', $publishEndDate); 143 | $this->assertEquals($startDateString, $publishStartDate->format('Y-m-d')); 144 | $this->assertEquals($endDateString, $publishEndDate->format('Y-m-d')); 145 | 146 | // test multi-lang 147 | $menuNode->setLocale('fr'); 148 | $this->dm->persist($menuNode); 149 | $this->dm->flush(); 150 | $this->dm->clear(); 151 | 152 | $menuNode = $this->dm->findTranslation(null, '/test/test-node', 'fr'); 153 | $this->assertEquals('fr', $menuNode->getLocale()); 154 | 155 | $child = $this->dm->find(null, '/test/test-node/child1'); 156 | $menuNode = $child->getParentDocument(); 157 | $this->assertCount(1, $menuNode->getChildren()); 158 | $menuNode->removeChild($child); 159 | $this->dm->flush(); 160 | $this->dm->clear(); 161 | $menuNode = $this->dm->find(null, '/test/test-node'); 162 | $this->assertCount(0, $menuNode->getChildren()); 163 | } 164 | 165 | /** 166 | * @expectedException \Doctrine\ODM\PHPCR\Exception\OutOfBoundsException 167 | * @expectedExceptionMessage Allowed child classes "Symfony\Cmf\Bundle\MenuBundle\Doctrine\Phpcr\MenuNode" 168 | */ 169 | public function testPersistInvalidChild() 170 | { 171 | $menuNode = new MenuNode(); 172 | $menuNode->setName('menu-node'); 173 | $menuNode->setParentDocument($this->rootDocument); 174 | $this->dm->persist($menuNode); 175 | 176 | $generic = new Generic(); 177 | $generic->setParentDocument($menuNode); 178 | $generic->setNodename('invalid'); 179 | $this->dm->persist($generic); 180 | 181 | $this->dm->flush(); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/Provider/PhpcrMenuProvider.php: -------------------------------------------------------------------------------- 1 | 0, otherwise 41 | * no prefetch is attempted. 42 | * 43 | * @var int 44 | */ 45 | private $prefetch = 10; 46 | 47 | /** 48 | * If this is null, the manager registry will return the default manager. 49 | * 50 | * @var string|null Name of object manager to use 51 | */ 52 | private $managerName; 53 | 54 | /** 55 | * @var ManagerRegistry 56 | */ 57 | private $managerRegistry; 58 | 59 | /** 60 | * @param NodeLoader $loader Factory for the menu items 61 | * @param ManagerRegistry $managerRegistry manager registry service to use in conjunction 62 | * with the manager name to load the load menu root document 63 | * @param string $menuRoot root id of the menu 64 | */ 65 | public function __construct( 66 | NodeLoader $loader, 67 | ManagerRegistry $managerRegistry, 68 | $menuRoot 69 | ) { 70 | $this->loader = $loader; 71 | $this->managerRegistry = $managerRegistry; 72 | $this->menuRoot = $menuRoot; 73 | } 74 | 75 | /** 76 | * Set the object manager name to use for this loader. If not set, the 77 | * default manager as decided by the manager registry will be used. 78 | * 79 | * @param string|null $managerName 80 | */ 81 | public function setManagerName($managerName) 82 | { 83 | $this->managerName = $managerName; 84 | } 85 | 86 | /** 87 | * @param string $menuRoot 88 | */ 89 | public function setMenuRoot($menuRoot) 90 | { 91 | $this->menuRoot = $menuRoot; 92 | } 93 | 94 | /** 95 | * @return string 96 | */ 97 | public function getMenuRoot() 98 | { 99 | return $this->menuRoot; 100 | } 101 | 102 | /** 103 | * Define the depth of menu to prefetch when a menu is accessed. 104 | * 105 | * Note that if this PHPCR implementation is jackalope and there is a 106 | * global fetch depth, the menu provider will prefetch *all* menus at the 107 | * menu root when a menu is accessed. If it would not do that, loading the 108 | * parent for one menu root would fetch all menu roots and only one menu 109 | * would be completely prefetched. 110 | * 111 | * @param int $depth 112 | */ 113 | public function setPrefetch($depth) 114 | { 115 | $this->prefetch = (int) $depth; 116 | } 117 | 118 | /** 119 | * Get the depth to use. A depth <= 0 means no prefetching should be done. 120 | * 121 | * @return int The depth to use when fetching menus 122 | */ 123 | public function getPrefetch() 124 | { 125 | return $this->prefetch; 126 | } 127 | 128 | /** 129 | * Create the menu subtree starting from name. 130 | * 131 | * If the name is not already absolute, it is interpreted relative to the 132 | * menu root. You can thus pass a name or any relative path with slashes to 133 | * only load a submenu rather than a whole menu. 134 | * 135 | * @param string $name Name of the menu to load. This can be an 136 | * absolute PHPCR path or one relative to the menu root 137 | * @param array $options 138 | * 139 | * @return ItemInterface The menu (sub)tree starting with name 140 | * 141 | * @throws \InvalidArgumentException if the menu can not be found 142 | */ 143 | public function get($name, array $options = []) 144 | { 145 | $menu = $this->find($name, true); 146 | 147 | $menuItem = $this->loader->load($menu); 148 | if (!$menuItem) { 149 | throw new \InvalidArgumentException("Menu at '$name' is misconfigured (f.e. the route might be incorrect) and could therefore not be instanciated"); 150 | } 151 | 152 | return $menuItem; 153 | } 154 | 155 | /** 156 | * Check if a menu node exists. 157 | * 158 | * If this method returns true, it means that you can call get() without 159 | * an exception. 160 | * 161 | * @param string $name Name of the menu to load. This can be an 162 | * absolute PHPCR path or one relative to the menu root 163 | * @param array $options 164 | * 165 | * @return bool Whether a menu with this name can be loaded by this provider 166 | */ 167 | public function has($name, array $options = []) 168 | { 169 | return $this->find($name, false) instanceof NodeInterface; 170 | } 171 | 172 | /** 173 | * @param string $name Name of the menu to load 174 | * @param bool $throw Whether to throw an exception if the menu is not 175 | * found or no valid menu. Returns false if $throw is 176 | * false and there is no menu at $name 177 | * 178 | * @return object|bool The menu root found with $name or false if $throw 179 | * is false and the menu was not found 180 | * 181 | * @throws \InvalidArgumentException Only if $throw is true throws this 182 | * exception if the name is empty or no menu found 183 | */ 184 | private function find($name, $throw) 185 | { 186 | if (!$name) { 187 | if ($throw) { 188 | throw new \InvalidArgumentException('The menu name may not be empty'); 189 | } 190 | 191 | return false; 192 | } 193 | 194 | $dm = $this->getObjectManager(); 195 | $session = $dm->getPhpcrSession(); 196 | 197 | try { 198 | $path = PathHelper::absolutizePath($name, $this->getMenuRoot()); 199 | PathHelper::assertValidAbsolutePath($path, false, true, $session->getNamespacePrefixes()); 200 | } catch (RepositoryException $e) { 201 | if ($throw) { 202 | throw $e; 203 | } 204 | 205 | return false; 206 | } 207 | 208 | if ($this->getPrefetch() > 0) { 209 | if ($session instanceof Session 210 | && 0 < $session->getSessionOption(Session::OPTION_FETCH_DEPTH) 211 | && 0 === strncmp($path, $this->getMenuRoot(), strlen($this->getMenuRoot())) 212 | ) { 213 | // we have jackalope with a fetch depth. prefetch all menu 214 | // nodes of all menues. 215 | try { 216 | $session->getNode($this->getMenuRoot(), $this->getPrefetch() + 1); 217 | } catch (PathNotFoundException $e) { 218 | if ($throw) { 219 | throw new \InvalidArgumentException(sprintf( 220 | 'The menu root "%s" does not exist.', 221 | $this->getMenuRoot() 222 | )); 223 | } 224 | 225 | return false; 226 | } 227 | } else { 228 | try { 229 | $session->getNode($path, $this->getPrefetch()); 230 | } catch (PathNotFoundException $e) { 231 | if ($throw) { 232 | throw new \InvalidArgumentException(sprintf('No menu found at "%s".', $path)); 233 | } 234 | 235 | return false; 236 | } 237 | } 238 | } 239 | 240 | $menu = $dm->find(null, $path); 241 | if (null === $menu) { 242 | if ($throw) { 243 | throw new \InvalidArgumentException(sprintf('The menu "%s" is not defined.', $name)); 244 | } 245 | 246 | return false; 247 | } 248 | if (!$menu instanceof NodeInterface) { 249 | if ($throw) { 250 | throw new \InvalidArgumentException("Menu at '$name' is not a valid menu node"); 251 | } 252 | 253 | return false; 254 | } 255 | 256 | return $menu; 257 | } 258 | 259 | /** 260 | * Get the object manager named $managerName from the registry. 261 | * 262 | * @return DocumentManager 263 | */ 264 | protected function getObjectManager() 265 | { 266 | return $this->managerRegistry->getManager($this->managerName); 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /src/Model/MenuNodeBase.php: -------------------------------------------------------------------------------- 1 | 23 | * @author Daniel Leech 24 | */ 25 | class MenuNodeBase implements NodeInterface 26 | { 27 | /** 28 | * Id of this menu node. 29 | * 30 | * @var string 31 | */ 32 | protected $id; 33 | 34 | /** 35 | * Node name. 36 | * 37 | * @var string 38 | */ 39 | protected $name; 40 | 41 | /** 42 | * Child menu nodes. 43 | * 44 | * @var Collection 45 | */ 46 | protected $children; 47 | 48 | /** 49 | * Menu label. 50 | * 51 | * @var string 52 | */ 53 | protected $label = ''; 54 | 55 | /** 56 | * @var string 57 | */ 58 | protected $uri; 59 | 60 | /** 61 | * The name of the route to generate. 62 | * 63 | * @var string 64 | */ 65 | protected $route; 66 | 67 | /** 68 | * HTML attributes to add to the individual menu element. 69 | * 70 | * e.g. array('class' => 'foobar', 'style' => 'bar: foo') 71 | * 72 | * @var array 73 | */ 74 | protected $attributes = []; 75 | 76 | /** 77 | * HTML attributes to add to the children list element. 78 | * 79 | * e.g. array('class' => 'foobar', 'style' => 'bar: foo') 80 | * 81 | * @var array 82 | */ 83 | protected $childrenAttributes = []; 84 | 85 | /** 86 | * HTML attributes to add to items link. 87 | * 88 | * e.g. array('class' => 'foobar', 'style' => 'bar: foo') 89 | * 90 | * @var array 91 | */ 92 | protected $linkAttributes = []; 93 | 94 | /** 95 | * HTML attributes to add to the items label. 96 | * 97 | * e.g. array('class' => 'foobar', 'style' => 'bar: foo') 98 | * 99 | * @var array 100 | */ 101 | protected $labelAttributes = []; 102 | 103 | /** 104 | * Hashmap for extra stuff associated to the node. 105 | * 106 | * @var array 107 | */ 108 | protected $extras = []; 109 | 110 | /** 111 | * Parameters to use when generating the route. 112 | * 113 | * Used with the "route" option. 114 | * 115 | * @var array 116 | */ 117 | protected $routeParameters = []; 118 | 119 | /** 120 | * Set to false to not render. 121 | * 122 | * @var bool 123 | */ 124 | protected $display = true; 125 | 126 | /** 127 | * Set to false to not render the children. 128 | * 129 | * @var bool 130 | */ 131 | protected $displayChildren = true; 132 | 133 | /** 134 | * Generate an absolute route. 135 | * 136 | * To be used with the "content" or "route" option. 137 | * 138 | * @var bool 139 | */ 140 | protected $routeAbsolute = false; 141 | 142 | public function __construct($name = null) 143 | { 144 | $this->name = $name; 145 | $this->children = new ArrayCollection(); 146 | } 147 | 148 | /** 149 | * Return ID of this menu node. 150 | * 151 | * @return string 152 | */ 153 | public function getId() 154 | { 155 | return $this->id; 156 | } 157 | 158 | /** 159 | * Sets ID of this menu node. 160 | * 161 | * @param $id string 162 | * 163 | * @return MenuNodeBase - this instance 164 | */ 165 | public function setId($id) 166 | { 167 | $this->id = $id; 168 | 169 | return $this; 170 | } 171 | 172 | /** 173 | * {@inheritdoc} 174 | */ 175 | public function getName() 176 | { 177 | return $this->name; 178 | } 179 | 180 | /** 181 | * Set the name of this node (used in ID). 182 | * 183 | * @param string $name 184 | * 185 | * @return MenuNodeBase - this instance 186 | */ 187 | public function setName($name) 188 | { 189 | $this->name = $name; 190 | 191 | return $this; 192 | } 193 | 194 | /** 195 | * Return the label assigned to this menu node. 196 | * 197 | * @return string 198 | */ 199 | public function getLabel() 200 | { 201 | return $this->label; 202 | } 203 | 204 | /** 205 | * Set label for this menu node. 206 | * 207 | * @param $label string 208 | * 209 | * @return MenuNodeBase - this instance 210 | */ 211 | public function setLabel($label) 212 | { 213 | $this->label = $label; 214 | 215 | return $this; 216 | } 217 | 218 | /** 219 | * Return the URI this menu node points to. 220 | * 221 | * @return string URI 222 | */ 223 | public function getUri() 224 | { 225 | return $this->uri; 226 | } 227 | 228 | /** 229 | * Set the URI. 230 | * 231 | * @param $uri string 232 | * 233 | * @return MenuNodeBase - this instance 234 | */ 235 | public function setUri($uri) 236 | { 237 | $this->uri = $uri; 238 | 239 | return $this; 240 | } 241 | 242 | /** 243 | * Return the route name. 244 | * 245 | * @return string 246 | */ 247 | public function getRoute() 248 | { 249 | return $this->route; 250 | } 251 | 252 | /** 253 | * Set the route name. 254 | * 255 | * @param $route string - name of route 256 | * 257 | * @return MenuNodeBase - this instance 258 | */ 259 | public function setRoute($route) 260 | { 261 | $this->route = $route; 262 | 263 | return $this; 264 | } 265 | 266 | /** 267 | * Return the attributes associated with this menu node. 268 | * 269 | * @return array 270 | */ 271 | public function getAttributes() 272 | { 273 | return $this->attributes; 274 | } 275 | 276 | /** 277 | * Set the attributes associated with this menu node. 278 | * 279 | * @param $attributes array 280 | * 281 | * @return MenuNodeBase - this instance 282 | */ 283 | public function setAttributes(array $attributes) 284 | { 285 | $this->attributes = $attributes; 286 | 287 | return $this; 288 | } 289 | 290 | /** 291 | * Return the given attribute, optionally specifying a default value. 292 | * 293 | * @param string $name The name of the attribute to return 294 | * @param string $default The value to return if the attribute doesn't exist 295 | * 296 | * @return string 297 | */ 298 | public function getAttribute($name, $default = null) 299 | { 300 | if (isset($this->attributes[$name])) { 301 | return $this->attributes[$name]; 302 | } 303 | 304 | return $default; 305 | } 306 | 307 | /** 308 | * Set the named attribute. 309 | * 310 | * @param string $name attribute name 311 | * @param string $value attribute value 312 | * 313 | * @return MenuNodeBase - this instance 314 | */ 315 | public function setAttribute($name, $value) 316 | { 317 | $this->attributes[$name] = $value; 318 | 319 | return $this; 320 | } 321 | 322 | /** 323 | * Return the children attributes. 324 | * 325 | * @return array 326 | */ 327 | public function getChildrenAttributes() 328 | { 329 | return $this->childrenAttributes; 330 | } 331 | 332 | /** 333 | * Set the children attributes. 334 | * 335 | * @param array $attributes 336 | * 337 | * @return MenuNodeBase - this instance 338 | */ 339 | public function setChildrenAttributes(array $attributes) 340 | { 341 | $this->childrenAttributes = $attributes; 342 | 343 | return $this; 344 | } 345 | 346 | /** 347 | * Get all child menu nodes of this menu node. This will filter out all 348 | * non-NodeInterface children. 349 | * 350 | * @return NodeInterface[] 351 | */ 352 | public function getChildren() 353 | { 354 | $children = []; 355 | foreach ($this->children as $child) { 356 | if (!$child instanceof NodeInterface) { 357 | continue; 358 | } 359 | $children[] = $child; 360 | } 361 | 362 | return $children; 363 | } 364 | 365 | /** 366 | * Add a child menu node under this node. 367 | * 368 | * @param NodeInterface $child 369 | * 370 | * @return NodeInterface The newly added child node 371 | */ 372 | public function addChild(NodeInterface $child) 373 | { 374 | $this->children[] = $child; 375 | 376 | return $child; 377 | } 378 | 379 | /** 380 | * Remove a child menu node. 381 | * 382 | * @param NodeInterface $child 383 | * 384 | * @return MenuNodeBase $this 385 | */ 386 | public function removeChild(NodeInterface $child) 387 | { 388 | $this->children->removeElement($child); 389 | 390 | return $this; 391 | } 392 | 393 | /** 394 | * Gets the route parameters. 395 | * 396 | * @return array 397 | */ 398 | public function getRouteParameters() 399 | { 400 | return $this->routeParameters; 401 | } 402 | 403 | /** 404 | * Sets the route parameters. 405 | * 406 | * @param array $routeParameters 407 | * 408 | * @return MenuNodeBase - this instance 409 | */ 410 | public function setRouteParameters($routeParameters) 411 | { 412 | $this->routeParameters = $routeParameters; 413 | 414 | return $this; 415 | } 416 | 417 | /** 418 | * Get extra information associated with this node. 419 | * 420 | * @return array 421 | */ 422 | public function getExtras() 423 | { 424 | return $this->extras; 425 | } 426 | 427 | /** 428 | * Set extra information associated with this node. 429 | * 430 | * @param array $extras 431 | * 432 | * @return MenuNodeBase - this instance 433 | */ 434 | public function setExtras(array $extras) 435 | { 436 | $this->extras = $extras; 437 | 438 | return $this; 439 | } 440 | 441 | /** 442 | * Get the link HTML attributes. 443 | * 444 | * @return array 445 | */ 446 | public function getLinkAttributes() 447 | { 448 | return $this->linkAttributes; 449 | } 450 | 451 | /** 452 | * Set the link HTML attributes as associative array. 453 | * 454 | * @param array $linkAttributes 455 | * 456 | * @return MenuNodeBase - this instance 457 | */ 458 | public function setLinkAttributes($linkAttributes) 459 | { 460 | $this->linkAttributes = $linkAttributes; 461 | 462 | return $this; 463 | } 464 | 465 | /** 466 | * Get the label HTML attributes. 467 | * 468 | * @return array 469 | */ 470 | public function getLabelAttributes() 471 | { 472 | return $this->labelAttributes; 473 | } 474 | 475 | /** 476 | * Set the label HTML attributes as associative array. 477 | * 478 | * @param array $labelAttributes 479 | * 480 | * @return MenuNodeBase - this instance 481 | */ 482 | public function setLabelAttributes($labelAttributes) 483 | { 484 | $this->labelAttributes = $labelAttributes; 485 | 486 | return $this; 487 | } 488 | 489 | /** 490 | * Whether to display this menu node. 491 | * 492 | * @return bool 493 | */ 494 | public function getDisplay() 495 | { 496 | return $this->display; 497 | } 498 | 499 | /** 500 | * Set whether to display this menu node. 501 | * 502 | * @param bool $display 503 | * 504 | * @return MenuNodeBase - this instance 505 | */ 506 | public function setDisplay($display) 507 | { 508 | $this->display = $display; 509 | 510 | return $this; 511 | } 512 | 513 | /** 514 | * Whether to display the children of this menu node. 515 | * 516 | * @return bool 517 | */ 518 | public function getDisplayChildren() 519 | { 520 | return $this->displayChildren; 521 | } 522 | 523 | /** 524 | * Set whether to display the children of this menu node. 525 | * 526 | * @param bool $displayChildren 527 | * 528 | * @return MenuNodeBase - this instance 529 | */ 530 | public function setDisplayChildren($displayChildren) 531 | { 532 | $this->displayChildren = $displayChildren; 533 | 534 | return $this; 535 | } 536 | 537 | /** 538 | * Whether to generate absolute links for route or content. 539 | * 540 | * @return bool 541 | */ 542 | public function getRouteAbsolute() 543 | { 544 | return $this->routeAbsolute; 545 | } 546 | 547 | /** 548 | * Set whether to generate absolute links when generating from a route 549 | * or the content. 550 | * 551 | * @param bool $routeAbsolute 552 | * 553 | * @return MenuNodeBase - this instance 554 | */ 555 | public function setRouteAbsolute($routeAbsolute) 556 | { 557 | $this->routeAbsolute = $routeAbsolute; 558 | 559 | return $this; 560 | } 561 | 562 | /** 563 | * Whether this menu node can be displayed, meaning it is set to display 564 | * and it does have a non-empty label. 565 | * 566 | * @return bool 567 | */ 568 | public function isDisplayable() 569 | { 570 | return $this->getDisplay() && $this->getLabel(); 571 | } 572 | 573 | /** 574 | * {@inheritdoc} 575 | */ 576 | public function getOptions() 577 | { 578 | return [ 579 | 'uri' => $this->getUri(), 580 | 'route' => $this->getRoute(), 581 | 'label' => $this->getLabel(), 582 | 'attributes' => $this->getAttributes(), 583 | 'childrenAttributes' => $this->getChildrenAttributes(), 584 | 'display' => $this->isDisplayable(), 585 | 'displayChildren' => $this->getDisplayChildren(), 586 | 'routeParameters' => $this->getRouteParameters(), 587 | 'routeAbsolute' => $this->getRouteAbsolute(), 588 | 'linkAttributes' => $this->getLinkAttributes(), 589 | 'labelAttributes' => $this->getLabelAttributes(), 590 | ]; 591 | } 592 | } 593 | --------------------------------------------------------------------------------