|
@@ -0,0 +1,438 @@
|
|
|
+# Install Gedmo Doctrine2 extensions in Symfony2
|
|
|
+
|
|
|
+Configure full featured [Doctrine2 extensions](http://github.com/l3pp4rd/DoctrineExtensions) for your symfony2 project.
|
|
|
+This post will show you - how to create a simple configuration file to manage extensions with
|
|
|
+ability to use all features it provides.
|
|
|
+Interested? then bear with me! and don't be afraid, we're not diving into security component :)
|
|
|
+
|
|
|
+[blog_reference]: http://gediminasm.org/article/mapping-extension-for-doctrine2 "Mapping extension for Doctrine2 makes it easy to create extensions based on annotation, xml, yaml mapping drivers"
|
|
|
+[blog_test]: http://gediminasm.org/test "Test extensions on this blog"
|
|
|
+
|
|
|
+This post will put some light over the shed of extension installation and mapping configuration
|
|
|
+of Doctrine2. It does not require any additional dependencies and gives you full power
|
|
|
+over management of extensions.
|
|
|
+
|
|
|
+Content:
|
|
|
+
|
|
|
+- [Symfony2](#sf2-app) application
|
|
|
+- Extensions metadata [mapping](#ext-mapping)
|
|
|
+- Extension [listeners](#ext-listeners)
|
|
|
+- Usage [example](#ext-example)
|
|
|
+- Some [tips](#more-tips)
|
|
|
+- [Alternative](#alternative) over configuration
|
|
|
+
|
|
|
+<a name="sf2-app"></a>
|
|
|
+
|
|
|
+## Symfony2 application
|
|
|
+
|
|
|
+First of all, we will need a symfony2 startup application, lets say [symfony-standard edition
|
|
|
+with composer](http://github.com/KnpLabs/symfony-with-composer). Follow the standard setup:
|
|
|
+
|
|
|
+- `git clone git://github.com/KnpLabs/symfony-with-composer.git example`
|
|
|
+- `cd example && rm -rf .git && php bin/vendors install`
|
|
|
+- ensure your application loads and meet requirements, by following the url: **http://your_virtual_host/app_dev.php**
|
|
|
+
|
|
|
+Now lets add the **gedmo/doctrine-extensions** into **composer.json**
|
|
|
+
|
|
|
+ {
|
|
|
+ "require": {
|
|
|
+ "php": ">=5.3.2",
|
|
|
+ "symfony/symfony": ">=2.0.9,<2.1.0-dev",
|
|
|
+ "doctrine/orm": ">=2.1.0,<2.2.0-dev",
|
|
|
+ "twig/extensions": "*",
|
|
|
+
|
|
|
+ "symfony/assetic-bundle": "*",
|
|
|
+ "sensio/generator-bundle": "2.0.*",
|
|
|
+ "sensio/framework-extra-bundle": "2.0.*",
|
|
|
+ "sensio/distribution-bundle": "2.0.*",
|
|
|
+ "jms/security-extra-bundle": "1.0.*",
|
|
|
+ "gedmo/doctrine-extensions": "master-dev"
|
|
|
+ },
|
|
|
+
|
|
|
+ "autoload": {
|
|
|
+ "psr-0": {
|
|
|
+ "Acme": "src/"
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+Update vendors, run: **php bin/vendors update**
|
|
|
+Initially in this package you have **doctrine2 orm** included, so we will base our setup
|
|
|
+and configuration for this specific connection. Do not forget to configure your database
|
|
|
+connection parameters, edit **app/config/parameters.ini**
|
|
|
+
|
|
|
+<a name="ext-mapping"></a>
|
|
|
+
|
|
|
+## Mapping
|
|
|
+
|
|
|
+Lets start from the mapping, in case if you use **translatable**, **tree** or **loggable**
|
|
|
+extension you will need to map those abstract mappedsuperclasses for you ORM to be aware of.
|
|
|
+To do so, add some mapping info to your **doctrine.orm** configuration, edit **app/config.yml**:
|
|
|
+
|
|
|
+ doctrine:
|
|
|
+ dbal:
|
|
|
+ # your dbal config here
|
|
|
+
|
|
|
+ orm:
|
|
|
+ auto_generate_proxy_classes: %kernel.debug%
|
|
|
+ auto_mapping: true
|
|
|
+ # only these lines are added additionally
|
|
|
+ mappings:
|
|
|
+ translatable:
|
|
|
+ type: annotation
|
|
|
+ alias: Gedmo
|
|
|
+ prefix: Gedmo\Translatable\Entity
|
|
|
+ # make sure vendor library location is correct
|
|
|
+ dir: "%kernel.root_dir%/../vendor/gedmo/doctrine-extensions/lib/Gedmo/Translatable/Entity"
|
|
|
+
|
|
|
+After that, running **php app/console doctrine:mapping:info** you should see the output:
|
|
|
+
|
|
|
+ Found 3 entities mapped in entity manager default:
|
|
|
+ [OK] Gedmo\Translatable\Entity\MappedSuperclass\AbstractPersonalTranslation
|
|
|
+ [OK] Gedmo\Translatable\Entity\MappedSuperclass\AbstractTranslation
|
|
|
+ [OK] Gedmo\Translatable\Entity\Translation
|
|
|
+
|
|
|
+Well we mapped only **translatable** for now, it really depends on your needs, which extensions
|
|
|
+your application uses.
|
|
|
+
|
|
|
+**Note:** there is **Gedmo\Translatable\Entity\Translation** which is not a super class, in that case
|
|
|
+if you create doctrine schema, it will add **ext_translations** table, which might not be useful
|
|
|
+to you also. To skip mapping of these entities, you can map **only superclasses**
|
|
|
+
|
|
|
+ mappings:
|
|
|
+ translatable:
|
|
|
+ type: annotation
|
|
|
+ alias: Gedmo
|
|
|
+ prefix: Gedmo\Translatable\Entity
|
|
|
+ # make sure vendor library location is correct
|
|
|
+ dir: "%kernel.root_dir%/../vendor/gedmo/doctrine-extensions/lib/Gedmo/Translatable/Entity/MappedSuperclass"
|
|
|
+
|
|
|
+The configuration above, adds a **/MappedSuperclass** into directory depth, after running
|
|
|
+**php app/console doctrine:mapping:info** you should only see now:
|
|
|
+
|
|
|
+ Found 2 entities mapped in entity manager default:
|
|
|
+ [OK] Gedmo\Translatable\Entity\MappedSuperclass\AbstractPersonalTranslation
|
|
|
+ [OK] Gedmo\Translatable\Entity\MappedSuperclass\AbstractTranslation
|
|
|
+
|
|
|
+This is very useful for advanced requirements and quite simple to understand. So lets map now
|
|
|
+everything extensions provide:
|
|
|
+
|
|
|
+ # only orm config branch of doctrine
|
|
|
+ orm:
|
|
|
+ auto_generate_proxy_classes: %kernel.debug%
|
|
|
+ auto_mapping: true
|
|
|
+ # only these lines are added additionally
|
|
|
+ mappings:
|
|
|
+ translatable:
|
|
|
+ type: annotation
|
|
|
+ alias: Gedmo
|
|
|
+ prefix: Gedmo\Translatable\Entity
|
|
|
+ # make sure vendor library location is correct
|
|
|
+ dir: "%kernel.root_dir%/../vendor/gedmo/doctrine-extensions/lib/Gedmo/Translatable/Entity"
|
|
|
+ loggable:
|
|
|
+ type: annotation
|
|
|
+ alias: Gedmo
|
|
|
+ prefix: Gedmo\Loggable\Entity
|
|
|
+ dir: "%kernel.root_dir%/../vendor/gedmo/doctrine-extensions/lib/Gedmo/Loggable/Entity"
|
|
|
+ tree:
|
|
|
+ type: annotation
|
|
|
+ alias: Gedmo
|
|
|
+ prefix: Gedmo\Tree\Entity
|
|
|
+ dir: "%kernel.root_dir%/../vendor/gedmo/doctrine-extensions/lib/Gedmo/Tree/Entity"
|
|
|
+
|
|
|
+<a name="ext-listeners"></a>
|
|
|
+
|
|
|
+## Doctrine extension listener services
|
|
|
+
|
|
|
+Next, the heart of extensions are behavioral listeners which pours all the sugar. We will
|
|
|
+create a **yml** service file in our config directory. The setup can be different and located
|
|
|
+in the bundle, it depends what you prefer, edit **app/config/doctrine_extensions.yml**
|
|
|
+
|
|
|
+ # services to handle doctrine extensions
|
|
|
+ # import it in config.yml
|
|
|
+ services:
|
|
|
+ # KernelRequest listener
|
|
|
+ extension.listener:
|
|
|
+ class: Acme\DemoBundle\Listener\DoctrineExtensionListener
|
|
|
+ calls:
|
|
|
+ - [ setContainer, [ @service_container ] ]
|
|
|
+ tags:
|
|
|
+ # translatable sets locale after router processing
|
|
|
+ - { name: kernel.event_listener, event: kernel.request, method: onLateKernelRequest, priority: -10 }
|
|
|
+ # loggable hooks user username if one is in security context
|
|
|
+ - { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
|
|
|
+
|
|
|
+
|
|
|
+ # Doctrine Extension listeners to handle behaviors
|
|
|
+ gedmo.listener.tree:
|
|
|
+ class: Gedmo\Tree\TreeListener
|
|
|
+ tags:
|
|
|
+ - { name: doctrine.event_subscriber, connection: default }
|
|
|
+ calls:
|
|
|
+ - [ setAnnotationReader, [ @annotation_reader ] ]
|
|
|
+
|
|
|
+ gedmo.listener.translatable:
|
|
|
+ class: Gedmo\Translatable\TranslatableListener
|
|
|
+ tags:
|
|
|
+ - { name: doctrine.event_subscriber, connection: default }
|
|
|
+ calls:
|
|
|
+ - [ setAnnotationReader, [ @annotation_reader ] ]
|
|
|
+ - [ setDefaultLocale, [ %locale% ] ]
|
|
|
+ - [ setTranslationFallback, [ false ] ]
|
|
|
+
|
|
|
+ gedmo.listener.timestampable:
|
|
|
+ class: Gedmo\Timestampable\TimestampableListener
|
|
|
+ tags:
|
|
|
+ - { name: doctrine.event_subscriber, connection: default }
|
|
|
+ calls:
|
|
|
+ - [ setAnnotationReader, [ @annotation_reader ] ]
|
|
|
+
|
|
|
+ gedmo.listener.sluggable:
|
|
|
+ class: Gedmo\Sluggable\SluggableListener
|
|
|
+ tags:
|
|
|
+ - { name: doctrine.event_subscriber, connection: default }
|
|
|
+ calls:
|
|
|
+ - [ setAnnotationReader, [ @annotation_reader ] ]
|
|
|
+
|
|
|
+ gedmo.listener.sortable:
|
|
|
+ class: Gedmo\Sortable\SortableListener
|
|
|
+ tags:
|
|
|
+ - { name: doctrine.event_subscriber, connection: default }
|
|
|
+ calls:
|
|
|
+ - [ setAnnotationReader, [ @annotation_reader ] ]
|
|
|
+
|
|
|
+ gedmo.listener.loggable:
|
|
|
+ class: Gedmo\Loggable\LoggableListener
|
|
|
+ tags:
|
|
|
+ - { name: doctrine.event_subscriber, connection: default }
|
|
|
+ calls:
|
|
|
+ - [ setAnnotationReader, [ @annotation_reader ] ]
|
|
|
+
|
|
|
+So what it includes in general? Well it creates services for all extension listeners.
|
|
|
+You can remove some which you do not use or change at will. **Translatable** for instance,
|
|
|
+sets default locale to `%locale%` parameter, you can configure it differently.
|
|
|
+
|
|
|
+**Note:** if you noticed, theres **Acme\DemoBundle\Listener\DoctrineExtensionListener**
|
|
|
+you will need to create this listener class if you use **loggable** or **translatable**
|
|
|
+behaviors. This listener will set the **locale used** from request and **username** to
|
|
|
+loggable. So, to finish the setup create **Acme\DemoBundle\Listener\DoctrineExtensionListener**
|
|
|
+
|
|
|
+ <?php
|
|
|
+
|
|
|
+ // file: src/Acme/DemoBundle/Listener/DoctrineExtensionListener.php
|
|
|
+
|
|
|
+ namespace Acme\DemoBundle\Listener;
|
|
|
+
|
|
|
+ use Symfony\Component\HttpKernel\Event\GetResponseEvent;
|
|
|
+ use Symfony\Component\DependencyInjection\ContainerAwareInterface;
|
|
|
+ use Symfony\Component\DependencyInjection\ContainerInterface;
|
|
|
+
|
|
|
+ class DoctrineExtensionListener implements ContainerAwareInterface
|
|
|
+ {
|
|
|
+ /**
|
|
|
+ * @var ContainerInterface
|
|
|
+ */
|
|
|
+ protected $container;
|
|
|
+
|
|
|
+ public function setContainer(ContainerInterface $container = null)
|
|
|
+ {
|
|
|
+ $this->container = $container;
|
|
|
+ }
|
|
|
+
|
|
|
+ public function onLateKernelRequest(GetResponseEvent $event)
|
|
|
+ {
|
|
|
+ $translatable = $this->container->get('gedmo.listener.translatable');
|
|
|
+ $translatable->setTranslatableLocale($event->getRequest()->getLocale());
|
|
|
+ }
|
|
|
+
|
|
|
+ public function onKernelRequest(GetResponseEvent $event)
|
|
|
+ {
|
|
|
+ $securityContext = $this->container->get('security.context', ContainerInterface::NULL_ON_INVALID_REFERENCE);
|
|
|
+ if (null !== $securityContext && null !== $securityContext->getToken() && $securityContext->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
|
|
|
+ $loggable = $this->container->get('gedmo.listener.loggable');
|
|
|
+ $loggable->setUsername($securityContext->getToken()->getUsername());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+Do not forget to import **doctrine_extensions.yml** in your **app/config/config.yml** etc.:
|
|
|
+
|
|
|
+ # file: app/config/config.yml
|
|
|
+ imports:
|
|
|
+ - { resource: parameters.ini }
|
|
|
+ - { resource: security.yml }
|
|
|
+ - { resource: doctrine_extensions.yml }
|
|
|
+
|
|
|
+ # ... configuration follows
|
|
|
+
|
|
|
+<a name="ext-example"></a>
|
|
|
+
|
|
|
+## Example
|
|
|
+
|
|
|
+Well after that, you have your extensions setup and ready to be used! Too easy right? Well
|
|
|
+if you do not believe me, lets create a simple entity in our **Acme** project:
|
|
|
+
|
|
|
+ <?php
|
|
|
+
|
|
|
+ // file: src/Acme/DemoBundle/Entity/BlogPost.php
|
|
|
+
|
|
|
+ namespace Acme\DemoBundle\Entity;
|
|
|
+
|
|
|
+ use Gedmo\Mapping\Annotation as Gedmo; // gedmo annotations
|
|
|
+ use Doctrine\ORM\Mapping as ORM; // doctrine orm annotations
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @ORM\Entity
|
|
|
+ */
|
|
|
+ class BlogPost
|
|
|
+ {
|
|
|
+ /**
|
|
|
+ * @Gedmo\Slug(fields={"title"}, updatable=false, separator="_")
|
|
|
+ * @ORM\Id
|
|
|
+ * @ORM\Column(length=32, unique=true)
|
|
|
+ */
|
|
|
+ private $id;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @Gedmo\Translatable
|
|
|
+ * @ORM\Column(length=64)
|
|
|
+ */
|
|
|
+ private $title;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @Gedmo\Timestampable(on="create")
|
|
|
+ * @ORM\Column(name="created", type="datetime")
|
|
|
+ */
|
|
|
+ private $created;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @ORM\Column(name="updated", type="datetime")
|
|
|
+ * @Gedmo\Timestampable(on="update")
|
|
|
+ */
|
|
|
+ private $updated;
|
|
|
+
|
|
|
+ public function getId()
|
|
|
+ {
|
|
|
+ return $this->id;
|
|
|
+ }
|
|
|
+
|
|
|
+ public function setTitle($title)
|
|
|
+ {
|
|
|
+ $this->title = $title;
|
|
|
+ }
|
|
|
+
|
|
|
+ public function getTitle()
|
|
|
+ {
|
|
|
+ return $this->title;
|
|
|
+ }
|
|
|
+
|
|
|
+ public function getCreated()
|
|
|
+ {
|
|
|
+ return $this->created;
|
|
|
+ }
|
|
|
+
|
|
|
+ public function getUpdated()
|
|
|
+ {
|
|
|
+ return $this->updated;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+Now, lets have some fun:
|
|
|
+
|
|
|
+- if you have not created database yet, run `php app/console doctrine:database:create`
|
|
|
+- create the schema `php app/console doctrine:schema:create`
|
|
|
+
|
|
|
+Well, everything will work just fine, you can modify the **Acme\DemoBundle\Controller\DemoController**
|
|
|
+and add an action to test how it works:
|
|
|
+
|
|
|
+ // file: src/Acme/DemoBundle/Controller/DemoController.php
|
|
|
+ // include this code portion
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @Route("/posts", name="_demo_posts")
|
|
|
+ */
|
|
|
+ public function postsAction()
|
|
|
+ {
|
|
|
+ $em = $this->getDoctrine()->getEntityManager();
|
|
|
+ $repository = $em->getRepository('AcmeDemoBundle:BlogPost');
|
|
|
+ // create some posts in case if there aren't any
|
|
|
+ if (!$repository->findOneById('hello_world')) {
|
|
|
+ $post = new \Acme\DemoBundle\Entity\BlogPost();
|
|
|
+ $post->setTitle('Hello world');
|
|
|
+
|
|
|
+ $next = new \Acme\DemoBundle\Entity\BlogPost();
|
|
|
+ $next->setTitle('Doctrine extensions');
|
|
|
+
|
|
|
+ $em->persist($post);
|
|
|
+ $em->persist($next);
|
|
|
+ $em->flush();
|
|
|
+ }
|
|
|
+ $posts = $em
|
|
|
+ ->createQuery('SELECT p FROM AcmeDemoBundle:BlogPost p')
|
|
|
+ ->getArrayResult()
|
|
|
+ ;
|
|
|
+ die(var_dump($posts));
|
|
|
+ }
|
|
|
+
|
|
|
+Now if you follow the url: **http://your_virtual_host/app_dev.php/demo/posts** you
|
|
|
+should see a print of posts, this is only an extension demo, we will not create template.
|
|
|
+
|
|
|
+<a name="more-tips"></a>
|
|
|
+
|
|
|
+## More tips
|
|
|
+
|
|
|
+Regarding, the setup, I do not think its too complicated to use, in general it is simple
|
|
|
+enough, and lets you understand at least small parts on how you can hook mapping into doctrine,
|
|
|
+how easily extension services are added. This configuration does not hide anything behind
|
|
|
+curtains and allows you to modify the configuration as you require.
|
|
|
+
|
|
|
+### Multiple entity managers
|
|
|
+
|
|
|
+If you use more than one entity manager, you can simply tag the listener
|
|
|
+with other manager name:
|
|
|
+
|
|
|
+ services:
|
|
|
+ # tree behavior
|
|
|
+ gedmo.listener.tree:
|
|
|
+ class: Gedmo\Tree\TreeListener
|
|
|
+ tags:
|
|
|
+ - { name: doctrine.event_subscriber, connection: default }
|
|
|
+ # additional ORM subscriber
|
|
|
+ - { name: doctrine.event_subscriber, connection: other_connection }
|
|
|
+ # ODM MongoDb subscriber, where **default** is manager name
|
|
|
+ - { name: doctrine.odm.mongodb.default_event_subscriber }
|
|
|
+ calls:
|
|
|
+ - [ setAnnotationReader, [ @annotation_reader ] ]
|
|
|
+
|
|
|
+Well regarding, mapping of ODM mongodb, its basically the same:
|
|
|
+
|
|
|
+ doctrine_mongodb:
|
|
|
+ default_database: 'my_database'
|
|
|
+ default_connection: 'default'
|
|
|
+ default_document_manager: 'default'
|
|
|
+ connections:
|
|
|
+ default: ~
|
|
|
+ document_managers:
|
|
|
+ default:
|
|
|
+ connection: 'default'
|
|
|
+ auto_mapping: true
|
|
|
+ mappings:
|
|
|
+ translatable:
|
|
|
+ type: annotation
|
|
|
+ alias: GedmoDocument
|
|
|
+ prefix: Gedmo\Translatable\Document
|
|
|
+ # make sure vendor library location is correct
|
|
|
+ dir: "%kernel.root_dir%/../vendor/gedmo/doctrine-extensions/lib/Gedmo/Translatable/Document"
|
|
|
+
|
|
|
+This also shows, how to make mappings based on single manager. All what differs is **Document**
|
|
|
+instead of **Entity** used. Haven't tested it with mongo though.
|
|
|
+
|
|
|
+**Note:** [extension repository](http://github.com/l3pp4rd/DoctrineExtensions) contains all
|
|
|
+[documentation](http://github.com/l3pp4rd/DoctrineExtensions/tree/master/doc) you may need
|
|
|
+to understand how you can use it in your projects.
|
|
|
+
|
|
|
+<a name="alternative"></a>
|
|
|
+
|
|
|
+## Alternative over configuration
|
|
|
+
|
|
|
+You can use [StofDoctrineExtensionsBundle](http://github.com/stof/StofDoctrineExtensionsBundle) which is a wrapper of these extensions
|