├── .gitignore
├── Resources
├── views
│ └── Node
│ │ └── node.html.twig
├── config
│ ├── routing.xml
│ ├── form.xml
│ ├── controller.xml
│ ├── validator.xml
│ ├── validator.20.xml
│ ├── validator.21.xml
│ ├── manager.xml
│ ├── sitemap.xml
│ ├── listener.xml
│ ├── validation.xml
│ └── doctrine
│ │ └── Node.orm.xml
└── meta
│ └── LICENSE
├── Tests
├── Fixtures
│ └── App
│ │ ├── config
│ │ ├── routing.yml
│ │ └── default.yml
│ │ ├── Bundle
│ │ ├── Resources
│ │ │ └── views
│ │ │ │ └── Content
│ │ │ │ ├── blog_post.html.twig
│ │ │ │ └── node.html.twig
│ │ ├── ContentTestBundle.php
│ │ └── Entity
│ │ │ ├── BlogPost.php
│ │ │ └── Page.php
│ │ └── TestKernel.php
├── bootstrap.php
├── Entity
│ └── NodeTest.php
└── Functional
│ └── ApplicationTest.php
├── .travis.yml
├── ZenstruckContentBundle.php
├── Form
└── Type
│ └── NodeBaseFormType.php
├── phpunit.xml.dist
├── Validator
├── InheritedUniqueEntity.php
├── BC
│ ├── InheritedUniqueEntityValidator21.php
│ └── InheritedUniqueEntityValidator20.php
└── InheritedUniqueEntityValidator.php
├── composer.json
├── Sitemap
└── NodeGenerator.php
├── Controller
└── NodeController.php
├── DependencyInjection
├── Configuration.php
└── ZenstruckContentExtension.php
├── Listener
└── Doctrine
│ └── DiscriminatorListener.php
├── Entity
├── Node.php
└── NodeManager.php
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | vendor/
2 | composer.lock
--------------------------------------------------------------------------------
/Resources/views/Node/node.html.twig:
--------------------------------------------------------------------------------
1 | {% block title %}{{ node.title }}{% endblock %}
2 |
3 | {% block body %}
4 |
{{ node.title }} ({{ node.contentType }})
5 | {% endblock %}
6 |
--------------------------------------------------------------------------------
/Tests/Fixtures/App/config/routing.yml:
--------------------------------------------------------------------------------
1 | DpnXmlSitemapBundle:
2 | resource: "@DpnXmlSitemapBundle/Resources/config/routing.xml"
3 |
4 | ZenstruckContentBundle:
5 | resource: "@ZenstruckContentBundle/Resources/config/routing.xml"
6 |
--------------------------------------------------------------------------------
/Tests/Fixtures/App/Bundle/Resources/views/Content/blog_post.html.twig:
--------------------------------------------------------------------------------
1 | {% extends 'ContentTestBundle:Content:node.html.twig' %}
2 |
3 | {% block content %}
4 | {{ parent() }}
5 | {{ node.tags }}
6 | {{ node.body }}
7 | {% endblock %}
8 |
--------------------------------------------------------------------------------
/Tests/Fixtures/App/Bundle/ContentTestBundle.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | class ContentTestBundle extends Bundle
11 | {
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/Tests/bootstrap.php:
--------------------------------------------------------------------------------
1 |
2 | {% for node in breadcrumbs %}
3 | {{ node }} »
4 | {% endfor %}
5 |
6 |
7 | {% block content %}
8 | {{ node.title }}
9 | {{ node.contentType }}
10 | {% endblock %}
11 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 |
3 | php:
4 | - 5.3
5 | - 5.4
6 | - 5.5
7 | - 5.6
8 |
9 | env:
10 | - SYMFONY_VERSION=2.2.*
11 | - SYMFONY_VERSION=2.3.*
12 | - SYMFONY_VERSION=2.4.*
13 | - SYMFONY_VERSION=2.5.*
14 | - SYMFONY_VERSION=2.6.*
15 |
16 | before_script:
17 | - composer self-update
18 | - composer require symfony/symfony:${SYMFONY_VERSION} --dev --prefer-source
19 |
--------------------------------------------------------------------------------
/ZenstruckContentBundle.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\Bundle\ContentBundle;
13 |
14 | use Symfony\Component\HttpKernel\Bundle\Bundle;
15 |
16 | /**
17 | * @author Kevin Bond
18 | */
19 | class ZenstruckContentBundle extends Bundle
20 | {
21 | }
22 |
--------------------------------------------------------------------------------
/Resources/config/routing.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 | zenstruck_content.controller:showAction
8 | <front>
9 | [a-zA-Z0-9_\-\/]+
10 |
11 |
12 |
--------------------------------------------------------------------------------
/Resources/config/form.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/Tests/Fixtures/App/Bundle/Entity/BlogPost.php:
--------------------------------------------------------------------------------
1 | tags = $tags;
21 | }
22 |
23 | public function getTags()
24 | {
25 | return $this->tags;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Tests/Fixtures/App/Bundle/Entity/Page.php:
--------------------------------------------------------------------------------
1 | body = $body;
22 | }
23 |
24 | public function getBody()
25 | {
26 | return $this->body;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Resources/config/controller.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 | Zenstruck\Bundle\ContentBundle\Controller\NodeController
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/Resources/config/validator.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/Resources/config/validator.20.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/Resources/config/validator.21.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/Resources/config/manager.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 | Zenstruck\Bundle\ContentBundle\Entity\NodeManager
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/Form/Type/NodeBaseFormType.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\Bundle\ContentBundle\Form\Type;
13 |
14 | use Symfony\Component\Form\AbstractType;
15 | use Symfony\Component\Form\FormBuilder;
16 |
17 | /**
18 | * @author Kevin Bond
19 | */
20 | class NodeBaseFormType extends AbstractType
21 | {
22 | public function buildForm(FormBuilder $builder, array $options)
23 | {
24 | $builder->add('title');
25 | $builder->add('path');
26 | }
27 |
28 | public function getName()
29 | {
30 | return 'zenstruck_content_node_base';
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/Resources/config/sitemap.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 | Zenstruck\Bundle\ContentBundle\Sitemap\NodeGenerator
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/Tests/Fixtures/App/config/default.yml:
--------------------------------------------------------------------------------
1 | zenstruck_content:
2 | node_class: Zenstruck\Bundle\ContentBundle\Tests\Fixtures\App\Bundle\Entity\Page
3 | node_type_name: page
4 | use_controller: true
5 | default_template: ContentTestBundle:Content:node.html.twig
6 | sitemap:
7 | enabled: true
8 | content_types:
9 | blog_post: Zenstruck\Bundle\ContentBundle\Tests\Fixtures\App\Bundle\Entity\BlogPost
10 |
11 | framework:
12 | secret: test
13 | test: ~
14 | session:
15 | storage_id: session.storage.filesystem
16 | router: { resource: "%kernel.root_dir%/config/routing.yml" }
17 | validation: { enable_annotations: true }
18 | templating: { engines: ['twig'] }
19 |
20 | doctrine:
21 | dbal:
22 | driver: pdo_sqlite
23 | path: %kernel.cache_dir%/db.sqlite
24 | charset: UTF8
25 | orm:
26 | auto_generate_proxy_classes: true
27 | auto_mapping: true
28 |
--------------------------------------------------------------------------------
/Resources/config/listener.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 | Zenstruck\Bundle\ContentBundle\Listener\Doctrine\DiscriminatorListener
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | ./Tests/
21 |
22 |
23 |
24 |
25 |
26 | ./
27 |
28 | ./Tests
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/Validator/InheritedUniqueEntity.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\Bundle\ContentBundle\Validator;
13 |
14 | use Symfony\Component\Validator\Constraint;
15 |
16 | /**
17 | * @author Kevin Bond
18 | * @Annotation
19 | */
20 | class InheritedUniqueEntity extends Constraint
21 | {
22 | public $message = 'This value is already used.';
23 | public $field = null;
24 |
25 | public function getRequiredOptions()
26 | {
27 | return array('field');
28 | }
29 |
30 | public function validatedBy()
31 | {
32 | return 'zenstruck_content.validator.inherited_unique_entity';
33 | }
34 |
35 | public function getTargets()
36 | {
37 | return self::CLASS_CONSTRAINT;
38 | }
39 |
40 | public function getDefaultOption()
41 | {
42 | return 'field';
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Resources/meta/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2011 Kevin Bond
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is furnished
8 | to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "zenstruck/content-bundle",
3 | "description": "Simple CMS for Symfony2 using Doctrine2 Class Table Inheritance",
4 | "keywords": ["content management"],
5 | "homepage": "http://zenstruck.com/project/ZenstruckContentBundle",
6 | "type": "symfony-bundle",
7 | "license": "MIT",
8 | "authors": [
9 | {
10 | "name": "Kevin Bond",
11 | "email": "kevinbond@gmail.com"
12 | }
13 | ],
14 | "require": {
15 | "symfony/symfony" : "~2.2",
16 | "doctrine/orm" : "~2.2,>=2.2.3",
17 | "doctrine/doctrine-bundle" : "~1.0"
18 | },
19 | "require-dev": {
20 | "dpn/xml-sitemap-bundle" : "~1.0"
21 | },
22 | "suggest": {
23 | "dpn/xml-sitemap-bundle" : "For generating sitemaps"
24 | },
25 | "autoload": {
26 | "psr-0": { "Zenstruck\\Bundle\\ContentBundle": "" }
27 | },
28 | "extra": {
29 | "branch-alias": {
30 | "dev-master": "1.x-dev"
31 | }
32 | },
33 | "target-dir": "Zenstruck/Bundle/ContentBundle"
34 | }
35 |
--------------------------------------------------------------------------------
/Resources/config/validation.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 |
--------------------------------------------------------------------------------
/Resources/config/doctrine/Node.orm.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/Sitemap/NodeGenerator.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\Bundle\ContentBundle\Sitemap;
13 |
14 | use Dpn\XmlSitemapBundle\Sitemap\GeneratorInterface;
15 | use Dpn\XmlSitemapBundle\Sitemap\Entry;
16 | use Zenstruck\Bundle\ContentBundle\Entity\NodeManager;
17 |
18 | /**
19 | * @author Kevin Bond
20 | */
21 | class NodeGenerator implements GeneratorInterface
22 | {
23 | protected $repository;
24 |
25 | protected $method;
26 |
27 | public function __construct(NodeManager $manager, $method)
28 | {
29 | $this->repository = $manager->getRepository();
30 | $this->method = $method;
31 | }
32 |
33 | /**
34 | * @return Entry[]
35 | */
36 | public function generate()
37 | {
38 | /** @var $nodes \Zenstruck\Bundle\ContentBundle\Entity\Node[] */
39 | $nodes = call_user_func(array($this->repository, $this->method));
40 |
41 | $entries = array();
42 |
43 | foreach ($nodes as $node) {
44 | $entry = new Entry();
45 | $entry->setUri($node->getPath());
46 | $entry->setLastMod($node->getUpdatedAt());
47 |
48 | $entries[] = $entry;
49 | }
50 |
51 | return $entries;
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/Tests/Fixtures/App/TestKernel.php:
--------------------------------------------------------------------------------
1 |
8 | */
9 | class TestKernel extends Kernel
10 | {
11 | public function registerBundles()
12 | {
13 | $bundles = array(
14 | new \Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
15 | new \Zenstruck\Bundle\ContentBundle\ZenstruckContentBundle(),
16 | new \Zenstruck\Bundle\ContentBundle\Tests\Fixtures\App\Bundle\ContentTestBundle(),
17 | new \Symfony\Bundle\TwigBundle\TwigBundle(),
18 | new \Dpn\XmlSitemapBundle\DpnXmlSitemapBundle()
19 | );
20 |
21 | // check for Symfony 2.0
22 | if (version_compare(self::VERSION, '2.1', '<')) {
23 | $bundles[] = new \Symfony\Bundle\DoctrineBundle\DoctrineBundle();
24 | } else {
25 | $bundles[] = new \Doctrine\Bundle\DoctrineBundle\DoctrineBundle();
26 | }
27 |
28 | return $bundles;
29 | }
30 |
31 | public function registerContainerConfiguration(LoaderInterface $loader)
32 | {
33 | $loader->load(__DIR__.'/config/default.yml');
34 | }
35 |
36 | public function getRootDir()
37 | {
38 | return __DIR__;
39 | }
40 |
41 | public function getCacheDir()
42 | {
43 | return sys_get_temp_dir().'/'.Kernel::VERSION.'/cache/'.$this->environment;
44 | }
45 |
46 | public function getLogDir()
47 | {
48 | return sys_get_temp_dir().'/'.Kernel::VERSION.'/logs';
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Controller/NodeController.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\Bundle\ContentBundle\Controller;
13 |
14 | use Symfony\Component\DependencyInjection\Container;
15 | use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
16 |
17 | /**
18 | * @author Kevin Bond
19 | */
20 | class NodeController
21 | {
22 | protected $container;
23 | protected $defaultTemplate;
24 |
25 | /**
26 | * @param EntityManager $em
27 | * @param string $defaultTemplate
28 | */
29 | public function __construct(Container $container, $defaultTemplate)
30 | {
31 | $this->container = $container;
32 | $this->defaultTemplate = $defaultTemplate;
33 | }
34 |
35 | public function showAction($uri)
36 | {
37 | $manager = $this->container->get('zenstruck_content.manager');
38 | $node = $manager->findOneByPath($uri);
39 |
40 | if (!$node) {
41 | throw new NotFoundHttpException('Node not found.');
42 | }
43 |
44 | $breadcrumbs = $manager->getAncestors($node);
45 |
46 | $templating = $this->container->get('templating');
47 |
48 | $parameters = array(
49 | 'node' => $node,
50 | 'breadcrumbs' => $breadcrumbs
51 | );
52 |
53 | $template = str_replace(':node.', ':'.$node->getContentType().'.', $this->defaultTemplate);
54 |
55 | if ($templating->exists($template)) {
56 | return $templating->renderResponse($template, $parameters);
57 | } else {
58 | return $templating->renderResponse($this->defaultTemplate, $parameters);
59 | }
60 | }
61 |
62 | }
63 |
--------------------------------------------------------------------------------
/DependencyInjection/Configuration.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\Bundle\ContentBundle\DependencyInjection;
13 |
14 | use Symfony\Component\Config\Definition\Builder\TreeBuilder;
15 | use Symfony\Component\Config\Definition\ConfigurationInterface;
16 |
17 | /**
18 | * @author Kevin Bond
19 | */
20 | class Configuration implements ConfigurationInterface
21 | {
22 | public function getConfigTreeBuilder()
23 | {
24 | $treeBuilder = new TreeBuilder();
25 | $rootNode = $treeBuilder->root('zenstruck_content');
26 |
27 | $rootNode
28 | ->children()
29 | ->scalarNode('node_class')->isRequired()->end()
30 | ->scalarNode('node_type_name')->defaultValue('node')->end()
31 | ->scalarNode('manager_class')->defaultNull()->end()
32 | ->booleanNode('use_controller')->defaultFalse()->end()
33 | ->booleanNode('use_form')->defaultFalse()->end()
34 | ->scalarNode('inheritance_type')->defaultValue('class_table')->end()
35 | ->scalarNode('discriminator_column')->defaultValue('content_type')->end()
36 | ->scalarNode('default_template')->defaultValue('ZenstruckContentBundle:Node:node.html.twig')->end()
37 | ->arrayNode('content_types')
38 | ->useAttributeAsKey('key')
39 | ->prototype('scalar')->end()
40 | ->end()
41 | ->arrayNode('sitemap')
42 | ->addDefaultsIfNotSet()
43 | ->children()
44 | ->booleanNode('enabled')->defaultFalse()->end()
45 | ->scalarNode('entity_manager_method')->defaultValue('findAll')->end()
46 | ->end()
47 | ->end()
48 | ->end()
49 | ;
50 |
51 | return $treeBuilder;
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/Tests/Entity/NodeTest.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\Bundle\ContentBundle\Tests\Entity;
13 |
14 | use Zenstruck\Bundle\ContentBundle\Tests\Fixtures\App\Bundle\Entity\Page;
15 | use Zenstruck\Bundle\ContentBundle\Tests\Fixtures\App\Bundle\Entity\BlogPost;
16 |
17 | /**
18 | * @author Kevin Bond
19 | */
20 | class PathTest extends \PHPUnit_Framework_TestCase
21 | {
22 |
23 | public function testSlashesRemoved()
24 | {
25 | $node = new Page();
26 | $node->setPath('foo/bar');
27 |
28 | $this->assertEquals('foo/bar', $node->getPath());
29 |
30 | $node = new Page();
31 | $node->setPath('/foo/bar');
32 |
33 | $this->assertEquals('foo/bar', $node->getPath());
34 | }
35 |
36 | public function testGetContentType()
37 | {
38 | $entity = new Page();
39 |
40 | $this->assertEquals('page', $entity->getContentType());
41 |
42 | $entity = new BlogPost();
43 |
44 | $this->assertEquals('blog_post', $entity->getContentType());
45 | }
46 |
47 | public function testGetAncestorArray()
48 | {
49 | $node = new Page();
50 | $node->setPath('foo/bar/baz/biz');
51 | $array = $node->getAncestorArray();
52 |
53 | $this->assertTrue(is_array($array));
54 | $this->assertEquals(3, count($array));
55 | $this->assertEquals('foo', $array[0]);
56 | $this->assertEquals('foo/bar', $array[1]);
57 | $this->assertEquals('foo/bar/baz', $array[2]);
58 |
59 | $node = new Page();
60 | $node->setPath('foo/bar');
61 | $array = $node->getAncestorArray();
62 |
63 | $this->assertTrue(is_array($array));
64 | $this->assertEquals(1, count($array));
65 | $this->assertEquals('foo', $array[0]);
66 |
67 | $node = new Page();
68 | $node->setPath('foo');
69 | $array = $node->getAncestorArray();
70 |
71 | $this->assertTrue(is_array($array));
72 | $this->assertEquals(0, count($array));
73 | }
74 |
75 | }
76 |
--------------------------------------------------------------------------------
/Listener/Doctrine/DiscriminatorListener.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\Bundle\ContentBundle\Listener\Doctrine;
13 |
14 | use Doctrine\Common\EventSubscriber;
15 | use Doctrine\ORM\Events;
16 | use Doctrine\ORM\Event\LoadClassMetadataEventArgs;
17 | use Doctrine\ORM\Mapping\ClassMetadataInfo;
18 |
19 | /**
20 | * @author Kevin Bond
21 | */
22 | class DiscriminatorListener implements EventSubscriber
23 | {
24 |
25 | protected $contentTypes;
26 | protected $inheritanceType;
27 | protected $discriminatorColumn;
28 |
29 | /**
30 | * @param string $contentTypes
31 | */
32 | public function __construct($contentTypes, $inhertianceType, $discriminatorColumn)
33 | {
34 | $this->contentTypes = $contentTypes;
35 | $this->inheritanceType = $inhertianceType;
36 | $this->discriminatorColumn = $discriminatorColumn;
37 | }
38 |
39 | public function getSubscribedEvents()
40 | {
41 | return array(Events::loadClassMetadata);
42 | }
43 |
44 | public function loadClassMetadata(LoadClassMetadataEventArgs $eventArgs)
45 | {
46 | $classMetadata = $eventArgs->getClassMetadata();
47 |
48 | // add subclasses to node
49 | $subclasses = array_flip($this->contentTypes);
50 |
51 | if ($classMetadata->name == $subclasses['node']) {
52 | unset($subclasses['node']);
53 | $classMetadata->subClasses = $subclasses;
54 |
55 | switch ($this->inheritanceType) {
56 | case 'single_table':
57 | $classMetadata->setInheritanceType(ClassMetadataInfo::INHERITANCE_TYPE_SINGLE_TABLE);
58 | break;
59 |
60 | case 'table_per_class':
61 | $classMetadata->setInheritanceType(ClassMetadataInfo::INHERITANCE_TYPE_TABLE_PER_CLASS);
62 | break;
63 |
64 | default:
65 | $classMetadata->setInheritanceType(ClassMetadataInfo::INHERITANCE_TYPE_JOINED);
66 | break;
67 | }
68 |
69 | $classMetadata->setDiscriminatorColumn(array(
70 | 'name' => $this->discriminatorColumn,
71 | 'type' => 'string',
72 | 'length' => 50
73 | ));
74 | }
75 |
76 | // check if class is defined in config
77 | if (isset($this->contentTypes[$classMetadata->name])) {
78 | // set discriminator
79 | $classMetadata->discriminatorMap = array_flip($this->contentTypes);
80 | $classMetadata->discriminatorValue = $this->contentTypes[$classMetadata->name];
81 | }
82 | }
83 |
84 | }
85 |
--------------------------------------------------------------------------------
/Validator/BC/InheritedUniqueEntityValidator21.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\Bundle\ContentBundle\Validator\BC;
13 |
14 | use Symfony\Component\Validator\Constraint;
15 | use Symfony\Component\Validator\ConstraintValidator;
16 |
17 | use Zenstruck\Bundle\ContentBundle\Entity\NodeManager;
18 |
19 | /**
20 | * This file is for Symfony 2.0 BC
21 | *
22 | * @author Kevin Bond
23 | */
24 | class InheritedUniqueEntityValidator21 extends ConstraintValidator
25 | {
26 | /** @var \Zenstruck\Bundle\ContentBundle\Entity\NodeManager */
27 | protected $nodeManager;
28 |
29 | /**
30 | * @param \Doctrine\ORM\EntityRepository $repository
31 | */
32 | public function __construct(NodeManager $nodeManager)
33 | {
34 | $this->nodeManager = $nodeManager;
35 | }
36 |
37 | /**
38 | * @param \Zenstruck\Bundle\ContentBundle\Entity\Node $node
39 | * @param \Symfony\Component\Validator\Constraint $constraint
40 | * @return bool
41 | */
42 | public function isValid($node, Constraint $constraint)
43 | {
44 | if (null === $node) {
45 | return true;
46 | }
47 |
48 | $em = $this->nodeManager->getEntityManager();
49 |
50 | $className = $this->context->getCurrentClass();
51 | $class = $em->getClassMetadata($className);
52 | $fieldName = $constraint->field;
53 |
54 | // make sure to set class name at topmost parent that has field
55 | $className = $class->reflFields[$fieldName]->class;
56 |
57 | if ($className === 'Zenstruck\Bundle\ContentBundle\Entity\Node') {
58 | // avoid instanciating top level abstract class
59 | $repo = $this->nodeManager->getRepository();
60 | } else {
61 | $repo = $em->getRepository($className);
62 | }
63 |
64 | if (!isset($class->reflFields[$fieldName])) {
65 | throw new ConstraintDefinitionException("Only field names mapped by Doctrine can be validated for uniqueness.");
66 | }
67 |
68 | $fieldValue = $class->reflFields[$fieldName]->getValue($node);
69 |
70 | // leave alone if blank (let NotBlank constraint handle)
71 | if (null === $fieldValue) {
72 | return true;
73 | }
74 |
75 | $conflicts = $repo->findBy(array($fieldName => $fieldValue));
76 |
77 | if (empty($conflicts)) {
78 | return true;
79 | }
80 |
81 | foreach ($conflicts as $conflict) {
82 | /* @var \Zenstruck\Bundle\ContentBundle\Entity\Node $conflict */
83 | if ($conflict->getId() != $node->getId()) {
84 | $this->context->addViolationAtSubPath($fieldName, $constraint->message);
85 | }
86 | }
87 |
88 | return true;
89 | }
90 |
91 | }
92 |
--------------------------------------------------------------------------------
/Validator/InheritedUniqueEntityValidator.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\Bundle\ContentBundle\Validator;
13 |
14 | use Symfony\Component\Validator\Constraint;
15 | use Symfony\Component\Validator\ConstraintValidator;
16 |
17 | use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
18 | use Zenstruck\Bundle\ContentBundle\Entity\NodeManager;
19 |
20 | /**
21 | * @author Kevin Bond
22 | */
23 | class InheritedUniqueEntityValidator extends ConstraintValidator
24 | {
25 | /** @var \Zenstruck\Bundle\ContentBundle\Entity\NodeManager */
26 | protected $nodeManager;
27 |
28 | /**
29 | * @param \Doctrine\ORM\EntityRepository $repository
30 | */
31 | public function __construct(NodeManager $nodeManager)
32 | {
33 | $this->nodeManager = $nodeManager;
34 | }
35 |
36 | /**
37 | * @param \Zenstruck\Bundle\ContentBundle\Entity\Node $node
38 | * @param \Symfony\Component\Validator\Constraint $constraint
39 | * @return bool
40 | */
41 | public function validate($node, Constraint $constraint)
42 | {
43 | if (null === $node) {
44 | return true;
45 | }
46 |
47 | $em = $this->nodeManager->getEntityManager();
48 |
49 | $className = $this->context->getClassName();
50 | $class = $em->getClassMetadata($className);
51 | $fieldName = $constraint->field;
52 |
53 | // make sure to set class name at topmost parent that has field
54 | $className = $class->reflFields[$fieldName]->class;
55 |
56 | if ($className === 'Zenstruck\Bundle\ContentBundle\Entity\Node') {
57 | // avoid instanciating top level abstract class
58 | $repo = $this->nodeManager->getRepository();
59 | } else {
60 | $repo = $em->getRepository($className);
61 | }
62 |
63 | if (!isset($class->reflFields[$fieldName])) {
64 | throw new ConstraintDefinitionException("Only field names mapped by Doctrine can be validated for uniqueness.");
65 | }
66 |
67 | $fieldValue = $class->reflFields[$fieldName]->getValue($node);
68 |
69 | // leave alone if blank (let NotBlank constraint handle)
70 | if (null === $fieldValue) {
71 | return true;
72 | }
73 |
74 | $conflicts = $repo->findBy(array($fieldName => $fieldValue));
75 |
76 | if (empty($conflicts)) {
77 | return true;
78 | }
79 |
80 | foreach ($conflicts as $conflict) {
81 | /* @var \Zenstruck\Bundle\ContentBundle\Entity\Node $conflict */
82 | if ($conflict->getId() != $node->getId()) {
83 | $this->context->addViolationAt($fieldName, $constraint->message);
84 | }
85 | }
86 |
87 | return true;
88 | }
89 |
90 | }
91 |
--------------------------------------------------------------------------------
/DependencyInjection/ZenstruckContentExtension.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\Bundle\ContentBundle\DependencyInjection;
13 |
14 | use Symfony\Component\HttpKernel\DependencyInjection\Extension;
15 | use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
16 | use Symfony\Component\DependencyInjection\ContainerBuilder;
17 | use Symfony\Component\Config\FileLocator;
18 | use Symfony\Component\Config\Definition\Processor;
19 | use Symfony\Component\HttpKernel\Kernel;
20 |
21 | /**
22 | * @author Kevin Bond
23 | */
24 | class ZenstruckContentExtension extends Extension
25 | {
26 | public function load(array $configs, ContainerBuilder $container)
27 | {
28 | $processor = new Processor();
29 | $configuration = new Configuration();
30 |
31 | $config = $processor->processConfiguration($configuration, $configs);
32 |
33 | $loader = new XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
34 | $loader->load('manager.xml');
35 | $loader->load('listener.xml');
36 |
37 | if (version_compare(Kernel::VERSION, '2.1.0', '<')) {
38 | $loader->load('validator.20.xml');
39 | } elseif (version_compare(Kernel::VERSION, '2.2.0', '<')) {
40 | $loader->load('validator.21.xml');
41 | } else {
42 | $loader->load('validator.xml');
43 | }
44 |
45 | $container->getDefinition('zenstruck_content.manager')
46 | ->replaceArgument(1, $config['node_class'])
47 | ->replaceArgument(2, array_merge(array($config['node_type_name'] => $config['node_class']), $config['content_types']));
48 |
49 | // get content types defined in config
50 | $content_types = array_flip($config['content_types']);
51 | $content_types[$config['node_class']] = 'node';
52 |
53 | $container->getDefinition('zenstruck_content.listener.discriminator')
54 | ->replaceArgument(0, $content_types)
55 | ->replaceArgument(1, $config['inheritance_type'])
56 | ->replaceArgument(2, $config['discriminator_column']);
57 |
58 | if (null !== $config['manager_class']) {
59 | $container->setParameter('zenstruck_content.manager.class', $config['manager_class']);
60 | }
61 |
62 | if ($config['use_controller']) {
63 | $loader->load('controller.xml');
64 |
65 | $container->getDefinition('zenstruck_content.controller')
66 | ->replaceArgument(1, $config['default_template']);
67 | }
68 | if ($config['use_form']) {
69 | $loader->load('form.xml');
70 | }
71 |
72 | if ($config['sitemap']['enabled']) {
73 | $loader->load('sitemap.xml');
74 | $container->getDefinition('zenstruck_content.sitemap_generator')
75 | ->replaceArgument(1, $config['sitemap']['entity_manager_method']);
76 | }
77 | }
78 |
79 | }
80 |
--------------------------------------------------------------------------------
/Validator/BC/InheritedUniqueEntityValidator20.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\Bundle\ContentBundle\Validator\BC;
13 |
14 | use Symfony\Component\Validator\Constraint;
15 | use Symfony\Component\Validator\ConstraintValidator;
16 |
17 | use Zenstruck\Bundle\ContentBundle\Entity\NodeManager;
18 |
19 | /**
20 | * This file is for Symfony 2.0 BC
21 | *
22 | * @author Kevin Bond
23 | */
24 | class InheritedUniqueEntityValidator20 extends ConstraintValidator
25 | {
26 | /** @var \Zenstruck\Bundle\ContentBundle\Entity\NodeManager */
27 | protected $nodeManager;
28 |
29 | /**
30 | * @param \Doctrine\ORM\EntityRepository $repository
31 | */
32 | public function __construct(NodeManager $nodeManager)
33 | {
34 | $this->nodeManager = $nodeManager;
35 | }
36 |
37 | /**
38 | * @param \Zenstruck\Bundle\ContentBundle\Entity\Node $node
39 | * @param \Symfony\Component\Validator\Constraint $constraint
40 | * @return bool
41 | */
42 | public function isValid($node, Constraint $constraint)
43 | {
44 | if (null === $node) {
45 | return true;
46 | }
47 |
48 | $em = $this->nodeManager->getEntityManager();
49 |
50 | $className = $this->context->getCurrentClass();
51 | $class = $em->getClassMetadata($className);
52 | $fieldName = $constraint->field;
53 |
54 | // make sure to set class name at topmost parent that has field
55 | $className = $class->reflFields[$fieldName]->class;
56 |
57 | if ($className === 'Zenstruck\Bundle\ContentBundle\Entity\Node') {
58 | // avoid instanciating top level abstract class
59 | $repo = $this->nodeManager->getRepository();
60 | } else {
61 | $repo = $em->getRepository($className);
62 | }
63 |
64 | if (!isset($class->reflFields[$fieldName])) {
65 | throw new ConstraintDefinitionException("Only field names mapped by Doctrine can be validated for uniqueness.");
66 | }
67 |
68 | $fieldValue = $class->reflFields[$fieldName]->getValue($node);
69 |
70 | // leave alone if blank (let NotBlank constraint handle)
71 | if (null === $fieldValue) {
72 | return true;
73 | }
74 |
75 | $conflicts = $repo->findBy(array($fieldName => $fieldValue));
76 |
77 | if (empty($conflicts)) {
78 | return true;
79 | }
80 |
81 | foreach ($conflicts as $conflict) {
82 | /* @var \Zenstruck\Bundle\ContentBundle\Entity\Node $conflict */
83 | if ($conflict->getId() != $node->getId()) {
84 | $old = $this->context->getPropertyPath();
85 | $this->context->setPropertyPath( empty($old) ? $fieldName : $old.".".$fieldName);
86 | $this->context->addViolation($constraint->message, array(), $fieldValue);
87 | $this->context->setPropertyPath($old);
88 | }
89 | }
90 |
91 | return true;
92 | }
93 |
94 | }
95 |
--------------------------------------------------------------------------------
/Entity/Node.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\Bundle\ContentBundle\Entity;
13 |
14 | /**
15 | * @author Kevin Bond
16 | */
17 | abstract class Node
18 | {
19 | protected $id;
20 |
21 | protected $title;
22 |
23 | protected $path;
24 |
25 | protected $updatedAt;
26 |
27 | protected $createdAt;
28 |
29 | public function getId()
30 | {
31 | return $this->id;
32 | }
33 |
34 | public function getTitle()
35 | {
36 | return $this->title;
37 | }
38 |
39 | public function setTitle($title)
40 | {
41 | $this->title = $title;
42 | }
43 |
44 | public function getUpdatedAt()
45 | {
46 | return $this->updatedAt;
47 | }
48 |
49 | public function setUpdatedAt($updatedAt)
50 | {
51 | $this->updatedAt = $updatedAt;
52 | }
53 |
54 | public function getCreatedAt()
55 | {
56 | return $this->createdAt;
57 | }
58 |
59 | public function setCreatedAt($createdAt)
60 | {
61 | $this->createdAt = $createdAt;
62 | }
63 |
64 | public function prePersist()
65 | {
66 | $this->createdAt = new \DateTime();
67 | $this->updatedAt = new \DateTime();
68 | }
69 |
70 | public function preUpdate()
71 | {
72 | $this->updatedAt = new \DateTime();
73 | }
74 |
75 | public function __toString()
76 | {
77 | return (string) $this->getTitle();
78 | }
79 |
80 | /**
81 | * Returns the machine name of the class (without namespace)
82 | */
83 | public function getContentType()
84 | {
85 | preg_match('#([\w]+)$#', get_class($this), $matches);
86 | $className = $matches[1];
87 |
88 | // camel case
89 | $className = strtolower(preg_replace('#([a-z])([A-Z])#', '$1_$2', $className));
90 |
91 | return $className;
92 | }
93 |
94 | public function getPath()
95 | {
96 | return $this->path;
97 | }
98 |
99 | public function setPath($path)
100 | {
101 | $this->path = trim($path, '/');
102 | }
103 |
104 | /**
105 | * Returns the an array of ancestors based on the path
106 | *
107 | * Example:
108 | *
109 | * If path = /foo/bar/baz/bin
110 | *
111 | * Returns: array(
112 | * 'foo',
113 | * 'foo/bar',
114 | * 'foo/bar/baz'
115 | * )
116 | *
117 | * @return array
118 | */
119 | public function getAncestorArray()
120 | {
121 | $pathArray = explode('/', $this->path);
122 |
123 | array_pop($pathArray);
124 |
125 | $ancestors = array();
126 | $pathString = null;
127 |
128 | foreach ($pathArray as $path) {
129 |
130 | if (!$pathString) {
131 | $pathString = $path;
132 | } else {
133 | $pathString .= '/'.$path;
134 | }
135 |
136 | $ancestors[] = $pathString;
137 | }
138 |
139 | return $ancestors;
140 | }
141 |
142 | }
143 |
--------------------------------------------------------------------------------
/Entity/NodeManager.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\Bundle\ContentBundle\Entity;
13 |
14 | use Doctrine\ORM\EntityManager;
15 |
16 | /**
17 | * @author Kevin Bond
18 | */
19 | class NodeManager
20 | {
21 | protected $em;
22 | protected $class;
23 | protected $repository;
24 | protected $contentTypes;
25 |
26 | /**
27 | * @param EntityManager $em
28 | * @param string $class
29 | */
30 | public function __construct(EntityManager $em, $class, $contentTypes)
31 | {
32 | $this->em = $em;
33 | $this->repository = $em->getRepository($class);
34 |
35 | $metadata = $em->getClassMetadata($class);
36 | $this->class = $metadata->name;
37 |
38 | $this->contentTypes = $contentTypes;
39 | }
40 |
41 | /**
42 | * @param string $path
43 | * @return object
44 | */
45 | public function findOneByPath($path)
46 | {
47 | return $this->repository->findOneByPath($path);
48 | }
49 |
50 | /**
51 | * Returns the line of ancestors starting with highest
52 | *
53 | * @param Node $node
54 | */
55 | public function getAncestors(Node $node)
56 | {
57 | $ancestorArray = $node->getAncestorArray();
58 |
59 | if (empty ($ancestorArray)) {
60 | return array();
61 | }
62 |
63 | $class = $this->getClass();
64 |
65 | // @todo find a better sorting method
66 | // @todo find out why I can't set a :class parameter
67 | $query = $this->em->createQuery("SELECT n, LENGTH(n.path) as length FROM $class n WHERE n.path IN (:ids) ORDER BY length");
68 |
69 | $query->setParameters(array(
70 | 'ids' => $ancestorArray
71 | ));
72 |
73 | $results = $query->getResult();
74 |
75 | $ret = array();
76 |
77 | // @todo find a one step hyrdate solution
78 | // loop thu mixed results to convert to pure
79 | foreach ($results as $result) {
80 | $ret[] = $result[0];
81 | }
82 |
83 | return $ret;
84 | }
85 |
86 | public function findDescendants(Node $node)
87 | {
88 | return $this->findPathDescendants($node->getPath());
89 | }
90 |
91 | public function findPathDescendants($path)
92 | {
93 | $class = $this->getClass();
94 |
95 | // @todo find a better sorting method
96 | /* @var \Doctrine\ORM\Query $query */
97 | $query = $this->em->createQuery("SELECT n, LENGTH(n.path) as length FROM $class n WHERE n.path LIKE :path ORDER BY length");
98 |
99 | $path = $path . '/%';
100 |
101 | $query->setParameters(array(
102 | 'path' => $path
103 | ));
104 |
105 | $results = $query->getResult();
106 |
107 | $ret = array();
108 |
109 | // @todo find a one step hyrdate solution
110 | // loop thu mixed results to convert to pure
111 | foreach ($results as $result) {
112 | $ret[] = $result[0];
113 | }
114 |
115 | return $ret;
116 | }
117 |
118 | /**
119 | * @return \Doctrine\ORM\EntityRepository
120 | */
121 | public function getRepository()
122 | {
123 | return $this->repository;
124 | }
125 |
126 | public function getContentTypes()
127 | {
128 | return $this->contentTypes;
129 | }
130 |
131 | /**
132 | * @return \Doctrine\ORM\EntityManager
133 | */
134 | public function getEntityManager()
135 | {
136 | return $this->em;
137 | }
138 |
139 | /**
140 | * @return string
141 | */
142 | public function getClass()
143 | {
144 | return $this->class;
145 | }
146 |
147 | }
148 |
--------------------------------------------------------------------------------
/Tests/Functional/ApplicationTest.php:
--------------------------------------------------------------------------------
1 |
13 | */
14 | class ApplicationTest extends WebTestCase
15 | {
16 | /** @var \Doctrine\ORM\EntityManager */
17 | protected $em;
18 |
19 | public function testHomepage()
20 | {
21 | $client = $this->prepareEnvironment();
22 |
23 | $crawler = $client->request('GET', '/');
24 | $this->assertTrue(200 === $client->getResponse()->getStatusCode());
25 | $this->assertTrue($crawler->filter('h1:contains("Homepage")')->count() > 0);
26 | $this->assertTrue($crawler->filter('h2:contains("page")')->count() > 0);
27 | }
28 |
29 | public function testPage()
30 | {
31 | $client = $this->prepareEnvironment();
32 |
33 | $crawler = $client->request('GET', '/foo');
34 | $this->assertEquals(200, $client->getResponse()->getStatusCode());
35 | $this->assertEquals(1, $crawler->filter('h1:contains("Page 1")')->count());
36 | $this->assertEquals(1, $crawler->filter('h2:contains("page")')->count());
37 | }
38 |
39 | public function testBreadcrumbs()
40 | {
41 | $client = $this->prepareEnvironment();
42 |
43 | $crawler = $client->request('GET', '/foo/bar/baz/bam');
44 | $this->assertTrue(200 === $client->getResponse()->getStatusCode());
45 | $this->assertTrue($crawler->filter('h1:contains("Page 3")')->count() > 0);
46 | $this->assertTrue($crawler->filter('#breadcrumbs a')->count() > 1);
47 | }
48 |
49 | public function testBlogPost()
50 | {
51 | $client = $this->prepareEnvironment();
52 |
53 | $crawler = $client->request('GET', '/foo/bar');
54 | $this->assertTrue(200 === $client->getResponse()->getStatusCode());
55 | $this->assertTrue($crawler->filter('h1:contains("Post 1")')->count() > 0);
56 | $this->assertTrue($crawler->filter('h2:contains("blog_post")')->count() > 0);
57 | $this->assertTrue($crawler->filter('h3:contains("symfony, php")')->count() > 0);
58 | $this->assertTrue($crawler->filter('p:contains("body")')->count() > 0);
59 | $this->assertTrue($crawler->filter('#breadcrumbs a')->count() > 0);
60 | }
61 |
62 | public function testSitemap()
63 | {
64 | $client = $this->prepareEnvironment();
65 | $today = new \DateTime();
66 | $today = $today->format('Y-m-d');
67 |
68 | $crawler = $client->request('GET', '/sitemap.xml');
69 |
70 | if (version_compare(Kernel::VERSION, '2.4.0', '<')) {
71 | $this->assertTrue(200 === $client->getResponse()->getStatusCode());
72 | $this->assertEquals(4, $crawler->filter('url')->count());
73 | $this->assertTrue($crawler->filter('loc:contains("http://localhost/foo/bar/baz/bam")')->count() > 0);
74 | $this->assertTrue($crawler->filter('lastmod:contains("'.$today.'")')->count() > 0);
75 | } else {
76 | $this->assertTrue(200 === $client->getResponse()->getStatusCode());
77 | $this->assertEquals(4, $crawler->filter('default|url')->count());
78 | $this->assertTrue($crawler->filter('default|loc:contains("http://localhost/foo/bar/baz/bam")')->count() > 0);
79 | $this->assertTrue($crawler->filter('default|lastmod:contains("'.$today.'")')->count() > 0);
80 | }
81 |
82 | }
83 |
84 | protected function prepareEnvironment()
85 | {
86 | $client = parent::createClient();
87 |
88 | $application = new Application($client->getKernel());
89 | $application->setAutoExit(false);
90 | $this->runConsole($application, "doctrine:database:drop", array("--force" => true));
91 | $this->runConsole($application, "doctrine:database:create");
92 | $this->runConsole($application, "doctrine:schema:create");
93 |
94 | $this->em = $client->getContainer()->get('doctrine')->getManager();
95 | $this->addTestData();
96 |
97 | return $client;
98 | }
99 |
100 | protected function runConsole(Application $application, $command, array $options = array())
101 | {
102 | $options["-e"] = "test";
103 | $options["-q"] = null;
104 | $options = array_merge($options, array('command' => $command));
105 |
106 | return $application->run(new \Symfony\Component\Console\Input\ArrayInput($options));
107 | }
108 |
109 | protected function addTestData()
110 | {
111 | // empty db
112 | $this->em->createQuery('DELETE ContentTestBundle:Page')
113 | ->execute()
114 | ;
115 |
116 | $page1 = new Page();
117 | $page1->setTitle('Page 1');
118 | $page1->setPath('/foo');
119 | $page1->setBody('Page 1 body');
120 |
121 | $page2 = new Page();
122 | $page2->setTitle('Homepage');
123 | $page2->setPath('');
124 |
125 | $post1 = new BlogPost();
126 | $post1->setTitle('Post 1');
127 | $post1->setPath('/foo/bar');
128 | $post1->setBody('Post 1 body');
129 | $post1->setTags('symfony, php');
130 |
131 | $page3 = new Page();
132 | $page3->setTitle('Page 3');
133 | $page3->setPath('/foo/bar/baz/bam');
134 |
135 | $this->em->persist($page1);
136 | $this->em->persist($page2);
137 | $this->em->persist($page3);
138 | $this->em->persist($post1);
139 |
140 | $this->em->flush();
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Information
2 |
3 | [](http://travis-ci.org/kbond/ZenstruckContentBundle)
4 |
5 | This Bundle allows for various *content-types* using Doctrine2's Inheritance
6 | (see http://www.doctrine-project.org/docs/orm/2.1/en/reference/inheritance-mapping.html
7 | for more information). It allows for all your content-types to inherit from a single ``Node``.
8 | The problem with Doctrine2's implementation is it requires
9 | you set all your inherited Entities in the top most Entity. With this Bundle
10 | they are setup in your ``config.yml``.
11 |
12 | # Configuration
13 |
14 | 1. Create a ``Node`` class:
15 |
16 | ```php
17 | // path/to/your/bundle/Entity/Node.php
18 |
19 | namespace YourApplicationBundle\Entity;
20 |
21 | use Zenstruck\Bundle\ContentBundle\Entity\Node as BaseNode;
22 |
23 | /**
24 | * @orm:Entity
25 | */
26 | class Node extends BaseNode
27 | {
28 | // add any node fields (or leave empty)
29 | }
30 | ```
31 |
32 | 2. Create one or more content-type Entities (extending from your ``Node`` entity):
33 |
34 | ```php
35 | // path/to/your/bundle/Entity/BlogPost.php
36 |
37 | namespace YourApplicationBundle\Entity;
38 |
39 | /**
40 | * @orm:Entity
41 | */
42 | class BlogPost extends Node
43 | {
44 | /**
45 | * @orm:Column(type="text", nullable=true)
46 | */
47 | protected $body;
48 |
49 | public function getBody()
50 | {
51 | return $this->body;
52 | }
53 |
54 | public function setBody($body)
55 | {
56 | $this->body = $body;
57 | }
58 | }
59 | ```
60 |
61 | **Note**: They can also extend eachother (``BlogPost->Page->Node``)
62 |
63 | 3. Add your node class and any new content-types to your ``config.yml``:
64 |
65 | ```yaml
66 | zenstruck_content:
67 | node_class: YourApplicationBundle\Entity\Node
68 | content_types:
69 | blog_post: YourApplicationBundle\Entity\BlogPost
70 | ...
71 | ```
72 |
73 | **Note:** in the above example the *machine name* of class ``BlogPost`` is ``blog_post``.
74 | This naming convention is important.
75 |
76 | 4. (optional) To use the controller that this bundle provides activate it in your ``config.yml``:
77 |
78 | ```yaml
79 | zenstruck_content:
80 | use_controller: true
81 | ```
82 |
83 | 5. (optional) If you used the controller in step 4, add the routing:
84 |
85 | ```yaml
86 | zenstruck_content:
87 | resource: "@ZenstruckContentBundle/Resources/config/routing.xml"
88 | ```
89 |
90 | # Reference
91 |
92 | ## Manager
93 |
94 | There is a manager class that is available from the service container via the id
95 | ``zenstruck_content.manager``.
96 |
97 | ### Breadcrumbs
98 |
99 | The manager contains a function called ``getAncestors(Node $node)``. This returns
100 | an array of Ancestor nodes based on the path of the current ``Node`` given.
101 |
102 | For instance, if you pass a node with path ``foo/bar/baz`` it will return an array of nodes
103 | with path's ``foo`` and ``foo/bar`` if they exist and in that order.
104 |
105 | #### Usage
106 |
107 | ```php
108 | // controller
109 | $manager = $this->container->get('zenstruck_content.manager');
110 | $manager->getAncestors($node);
111 | ```
112 |
113 | ## Inheritance Type
114 |
115 | By default Doctrine2's *Class Table Inheritance* is used. This means each content-type
116 | extending from ``Node`` is it's own table linking back to the base ``node`` table. There
117 | is also the option of using *Class Table Inheritance*. All content types will be
118 | stored in the same table.
119 |
120 | You can enable this in your ``config.yml``:
121 |
122 | ```yaml
123 | zenstruck_content:
124 | inheritance_type: single_table
125 | ```
126 |
127 | ## Template
128 |
129 | To provide your own templates set the ``default_template`` option in your ``config.yml``:
130 |
131 | ```yaml
132 | zenstruck_content:
133 | default_template: YourApplicationBundle:Content:node.html.twig
134 | ```
135 |
136 | **Note:** the default template name must be ``node``.
137 |
138 | ## InheritedUniqueEntity constraint
139 |
140 | This bundles comes with a custom ``UniqueEntity`` validation constraint. The default Doctrine
141 | one has problems with inheritance. It only checks the values of entities within it's current
142 | scope and children. For instance if you have this structure: ``BlogPost->Page->Node``
143 | and place the default Doctrine ``UniqueEntity`` constraint on a field in ``Page``. Saving a
144 | ``BlogPost`` with a field that is the same as one in ``Page`` will not cause the constraint
145 | to become invalid.
146 |
147 | The ``InheritedUniqueEntity`` constraint packaged with this
148 | bundle does.
149 |
150 | ### Usage
151 |
152 | The following demonstrates adding a UniqueEntity constraint on the ``body`` field of the ``Page``
153 | entity. All classes that inherit from ``Page`` will have this constraint in the ``Page`` scope.
154 |
155 | ```php
156 | namespace Acme\DemoBundle\Entity;
157 |
158 | use Doctrine\ORM\Mapping as ORM;
159 | use Zenstruck\Bundle\ContentBundle\Validator\InheritedUniqueEntity;
160 |
161 | /**
162 | * Acme\DemoBundle\Entity\Node
163 | *
164 | * @ORM\Table(name="page")
165 | * @ORM\Entity
166 | * @InheritedUniqueEntity(field="body")
167 | */
168 | class Page extends Node
169 | {
170 |
171 | /**
172 | * @var string $body
173 | *
174 | * @ORM\Column(name="body", type="string", length=255, nullable=true)
175 | */
176 | protected $body;
177 |
178 | //...
179 | }
180 | ```
181 |
182 | ## Sitemap Generation
183 |
184 | This bundle comes with a Sitemap Generator for use with [DpnXmlSitemapBundle](https://github.com/dreipunktnull/DpnXmlSitemapBundle)
185 |
186 | ## Usage
187 |
188 | 1. Install and configure [DpnXmlSitemapBundle](https://github.com/dreipunktnull/DpnXmlSitemapBundle)
189 | 2. Enable sitemap generator in your `config.yml`:
190 |
191 | ```yaml
192 | zenstruck_content:
193 | sitemap:
194 | enabled: true
195 | ```
196 |
197 | 3. By default, the generator uses the `findAll` method on the `Node`'s entity manager. To use a different method change
198 | it in your `config.yml`:
199 |
200 | ```yaml
201 | zenstruck_content:
202 | sitemap:
203 | entity_manager_method: myCustomMethod
204 | ```
205 |
206 | The sitemap should be available at `/sitemap.xml`.
207 |
208 | ## Full Default Configuration
209 |
210 | ```yaml
211 | zenstruck_content:
212 | node_class: ~ # Required
213 | node_type_name: node
214 | manager_class: ~
215 | use_controller: false
216 | use_form: false
217 | inheritance_type: class_table
218 | discriminator_column: content_type
219 | default_template: ZenstruckContentBundle:Node:node.html.twig
220 | content_types: []
221 | sitemap:
222 | enabled: false
223 | entity_manager_method: findAll
224 | ```
--------------------------------------------------------------------------------