├── .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 | [![Build Status](https://secure.travis-ci.org/kbond/ZenstruckContentBundle.png)](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 | ``` --------------------------------------------------------------------------------