├── 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 |
13 |
14 |
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 |
13 |
14 |
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 |
16 |
17 |
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 |
16 |
17 |
22 |
23 |
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 |
17 |
18 |
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 |
--------------------------------------------------------------------------------