Skip to content

Latest commit

 

History

History
526 lines (429 loc) · 18.5 KB

symfony.md

File metadata and controls

526 lines (429 loc) · 18.5 KB

Install Gedmo Doctrine extensions in Symfony

Configure full featured Doctrine extensions for your Symfony 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 :)

This post will put some light over the shed of extension installation and mapping configuration of Doctrine. It does not require any additional dependencies and gives you full power over management of extensions.

Content:

Symfony application

First of all, we will need a symfony startup application, let's say symfony-standard edition with composer

  • composer create-project symfony/skeleton [project name]

Now let's add the gedmo/doctrine-extensions

You can find the doctrine-extensions project on packagist: https://packagist.org/packages/gedmo/doctrine-extensions

To add it to your project:

  • composer require gedmo/doctrine-extensions

Mapping

Let's start from the mapping. In case you use the translatable, tree or loggable extension you will need to map those abstract mapped superclasses for your ORM to be aware of. To do so, add some mapping info to your doctrine.orm configuration, edit config/doctrine.yaml:

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: attribute # or annotation or xml
                alias: Gedmo
                prefix: Gedmo\Translatable\Entity
                # make sure vendor library location is correct
                dir: "%kernel.project_dir%/vendor/gedmo/doctrine-extensions/src/Translatable/Entity"

After that, running php bin/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 a 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: attribute # or annotation or xml
        alias: Gedmo
        prefix: Gedmo\Translatable\Entity
        # make sure vendor library location is correct
        dir: "%kernel.project_dir%/vendor/gedmo/doctrine-extensions/src/Translatable/Entity/MappedSuperclass"

The configuration above, adds a /MappedSuperclass into directory depth, after running php bin/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 now let's map everything the 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: attribute # or annotation or xml
            alias: Gedmo
            prefix: Gedmo\Translatable\Entity
            # make sure vendor library location is correct
            dir: "%kernel.project_dir%/vendor/gedmo/doctrine-extensions/src/Translatable/Entity"
        loggable:
            type: attribute # or annotation or xml
            alias: Gedmo
            prefix: Gedmo\Loggable\Entity
            dir: "%kernel.project_dir%/vendor/gedmo/doctrine-extensions/src/Loggable/Entity"
        tree:
            type: attribute # or annotation or xml
            alias: Gedmo
            prefix: Gedmo\Tree\Entity
            dir: "%kernel.project_dir%/vendor/gedmo/doctrine-extensions/src/Tree/Entity"

Filters

The softdeleteable ORM filter also needs to be configured, so that soft deleted records are filtered when querying. To do so, add this filter info to your doctrine.orm configuration, edit config/doctrine.yaml:

doctrine:
    dbal:
    # your dbal config here
    orm:
        auto_generate_proxy_classes: '%kernel.debug%'
        auto_mapping: true
        # only these lines are added additionally
        filters:
            softdeleteable:
                class: Gedmo\SoftDeleteable\Filter\SoftDeleteableFilter

Doctrine extension listener services

Next, the heart of extensions are behavioral listeners which pours all the sugar. We will create a yaml service file in our config directory. The setup can be different, your config could be located in the bundle, it depends on your preferences. Edit config/packages/doctrine_extensions.yaml

# services to handle doctrine extensions
# import it in config/packages/doctrine_extensions.yaml
services:
    # Doctrine Extension listeners to handle behaviors
    gedmo.listener.tree:
        class: Gedmo\Tree\TreeListener
        tags:
            - { name: doctrine.event_listener, event: 'prePersist'}
            - { name: doctrine.event_listener, event: 'preUpdate'}
            - { name: doctrine.event_listener, event: 'preRemove'}
            - { name: doctrine.event_listener, event: 'onFlush'}
            - { name: doctrine.event_listener, event: 'loadClassMetadata'}
            - { name: doctrine.event_listener, event: 'postPersist'}
            - { name: doctrine.event_listener, event: 'postUpdate'}
            - { name: doctrine.event_listener, event: 'postRemove'}
        calls:
            - [ setAnnotationReader, [ "@annotation_reader" ] ]

    Gedmo\Translatable\TranslatableListener:
        tags:
            - { name: doctrine.event_listener, event: 'postLoad' }
            - { name: doctrine.event_listener, event: 'postPersist' }
            - { name: doctrine.event_listener, event: 'preFlush' }
            - { name: doctrine.event_listener, event: 'onFlush' }
            - { name: doctrine.event_listener, event: 'loadClassMetadata' }
        calls:
            - [ setAnnotationReader, [ "@annotation_reader" ] ]
            - [ setDefaultLocale, [ "%locale%" ] ]
            - [ setTranslationFallback, [ false ] ]

    gedmo.listener.timestampable:
        class: Gedmo\Timestampable\TimestampableListener
        tags:
            - { name: doctrine.event_listener, event: 'prePersist' }
            - { name: doctrine.event_listener, event: 'onFlush' }
            - { name: doctrine.event_listener, event: 'loadClassMetadata' }
        calls:
            - [ setAnnotationReader, [ "@annotation_reader" ] ]

    gedmo.listener.sluggable:
        class: Gedmo\Sluggable\SluggableListener
        tags:
            - { name: doctrine.event_listener, event: 'prePersist' }
            - { name: doctrine.event_listener, event: 'onFlush' }
            - { name: doctrine.event_listener, event: 'loadClassMetadata' }
        calls:
            - [ setAnnotationReader, [ "@annotation_reader" ] ]

    gedmo.listener.sortable:
        class: Gedmo\Sortable\SortableListener
        tags:
            - { name: doctrine.event_listener, event: 'onFlush' }
            - { name: doctrine.event_listener, event: 'loadClassMetadata' }
            - { name: doctrine.event_listener, event: 'prePersist' }
            - { name: doctrine.event_listener, event: 'postPersist' }
            - { name: doctrine.event_listener, event: 'preUpdate' }
            - { name: doctrine.event_listener, event: 'postRemove' }
            - { name: doctrine.event_listener, event: 'postFlush' }
        calls:
            - [ setAnnotationReader, [ "@annotation_reader" ] ]

    gedmo.listener.softdeleteable:
        class: Gedmo\SoftDeleteable\SoftDeleteableListener
        tags:
            - { name: doctrine.event_listener, event: 'onFlush' }
            - { name: doctrine.event_listener, event: 'loadClassMetadata' }
        calls:
            - [ setAnnotationReader, [ "@annotation_reader" ] ]
            
    Gedmo\Loggable\LoggableListener:
        tags:
            - { name: doctrine.event_listener, event: 'onFlush' }
            - { name: doctrine.event_listener, event: 'loadClassMetadata' }
            - { name: doctrine.event_listener, event: 'postPersist' }
        calls:
            - [ setAnnotationReader, [ "@annotation_reader" ] ]

    Gedmo\Blameable\BlameableListener:
        tags:
            - { name: doctrine.event_listener, event: 'prePersist' }
            - { name: doctrine.event_listener, event: 'onFlush' }
            - { name: doctrine.event_listener, event: 'loadClassMetadata' }
        calls:
            - [ setAnnotationReader, [ "@annotation_reader" ] ]
            
    Gedmo\IpTraceable\IpTraceableListener:
        tags:
            - { name: doctrine.event_listener, event: 'prePersist' }
            - { name: doctrine.event_listener, event: 'onFlush' }
            - { name: doctrine.event_listener, event: 'loadClassMetadata' }
        calls:
            - [ setAnnotationReader, [ "@annotation_reader" ] ]            

So what does it include in general? Well, it creates services for all extension listeners. You can remove some which you do not use, or change them as you need. Translatable for instance, sets the default locale to the value of your %locale% parameter, you can configure it differently.

Note: In case you noticed, there is EventSubscriber\DoctrineExtensionSubscriber. You will need to create this subscriber class if you use loggable , translatable or blameable behaviors. This listener will set the locale used from request and username to loggable and blameable. So, to finish the setup create EventSubscriber\DoctrineExtensionSubscriber

Register event listener for Symfony Doctrine MongoDB Bundle

You also need to manually tag the listeners. Otherwise, the listeners will not be listening to the triggered events of Doctrine.

Gedmo\Loggable\LoggableListener:
        tags:
            - { name: doctrine_mongodb.odm.event_listener, event: 'onFlush' }
            - { name: doctrine_mongodb.odm.event_listener, event: 'loadClassMetadata' }
            - { name: doctrine_mongodb.odm.event_listener, event: 'postPersist' }
        calls:
            - [ setAnnotationReader, [ "@annotation_reader" ] ]
<?php

namespace App\EventSubscriber;

use Gedmo\Blameable\BlameableListener;
use Gedmo\Loggable\LoggableListener;
use Gedmo\Translatable\TranslatableListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\FinishRequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;

final class DoctrineExtensionSubscriber implements EventSubscriberInterface
{
    private BlameableListener $blameableListener;

    private TokenStorageInterface $tokenStorage;

    private TranslatableListener $translatableListener;

    private LoggableListener $loggableListener;


    public function __construct(
        BlameableListener $blameableListener,
        TokenStorageInterface $tokenStorage,
        TranslatableListener $translatableListener,
        LoggableListener $loggableListener
    ) {
        $this->blameableListener = $blameableListener;
        $this->tokenStorage = $tokenStorage;
        $this->translatableListener = $translatableListener;
        $this->loggableListener = $loggableListener;
    }


    public static function getSubscribedEvents(): array
    {
        return [
            KernelEvents::REQUEST => 'onKernelRequest',
            KernelEvents::FINISH_REQUEST => 'onLateKernelRequest'
        ];
    }
    public function onKernelRequest(): void
    {
        if (
            $this->tokenStorage->getToken() !== null &&
            $this->tokenStorage->getToken()->getUser() !== null
        ) {
            $this->blameableListener->setUserValue($this->tokenStorage->getToken()->getUser());
        }
    }

    public function onLateKernelRequest(FinishRequestEvent $event): void
    {
        $this->translatableListener->setTranslatableLocale($event->getRequest()->getLocale());
    }
}

Example

After that, you have your extensions set up and ready to be used! Too easy right? Well, if you do not believe me, let's create a simple entity in our project:

<?php

// file: src/Entity/BlogPost.php

namespace App\Entity;

use DateTimeImmutable;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;

/**
 * @ORM\Entity
 * @Gedmo\SoftDeleteable(fieldName="deletedAt", timeAware=false)
 */
#[ORM\Entity]
#[Gedmo\SoftDeleteable(fieldName: 'deletedAt', timeAware: false)]
class BlogPost
{
    /**
     * @Gedmo\Slug(fields={"title"}, updatable=false, separator="_")
     * @ORM\Id
     * @ORM\Column(length=32, unique=true)
     */
    #[Gedmo\Slug(fields: ['title',  updatable: false, separator: '_'])]
    #[ORM\Id]
    #[ORM\Column(lenght: 32, unique: true)]
    private ?int $id;

    /**
     * @Gedmo\Translatable
     * @ORM\Column(length=64)
     */
    #[Gedmo\Translatable]
    #[ORM\Column(length: 64)]
    private ?string $title;

    /**
     * @Gedmo\Timestampable(on="create")
     * @ORM\Column(name="created", type="datetime_immutable")
     */
    #[Gedmo\Timestampable(on: 'create')]
    #[ORM\Column(name: 'created', type: Types::DATETIME_IMMUTABLE)]
    private ?DateTimeImmutable $created;

    /**
     * @ORM\Column(name="updated", type="datetime_immutable")
     * @Gedmo\Timestampable(on="update")
     */
    #[Gedmo\Timestampable(on: 'update')]
    #[ORM\Column(name: 'updated', type: Types::DATETIME_IMMUTABLE)]
    private ?DateTimeImmutable $updated;
    
    /**
     * @ORM\Column(type="datetime_immutable", nullable=true)
     */
    #[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)]
    private ?DateTimeImmutable $deletedAt;
    
    public function getId(): ?int
    {
        return $this->id;
    }

    public function setTitle(?string $title): void
    {
        $this->title = $title;
    }

    public function getTitle(): ?string
    {
        return $this->title;
    }

    public function getCreated(): ?DateTimeImmutable
    {
        return $this->created;
    }

    public function getUpdated(): ?DateTimeImmutable
    {
        return $this->updated;
    }

    public function getDeletedAt(): ?DateTimeImmutable
    {
        return $this->deletedAt;
    }
    
    public function setDeletedAt(?DateTimeImmutable $deletedAt): void
    {
        $this->deletedAt = $deletedAt;
    }
}

Now, let's have some fun:

  • if you have not created the database yet, run php bin/console doctrine:database:create
  • create the schema php bin/console doctrine:schema:create

Everything will work just fine, you can modify the App\Controller\DemoController and add an action to test how it works:

// file: src/Controller/DemoController.php
// include this code portion

/**
 * @Route("/posts", name="_demo_posts")
 */
public function postsAction(EntityManagerInterface $em): Response
{
    $repository = $em->getRepository(App\Entity\BlogPost::class);
    // create some posts in case if there aren't any
    if (!$repository->find('hello_world')) {
        $post = new App\Entity\BlogPost();
        $post->setTitle('Hello world');

        $next = new App\Entity\BlogPost();
        $next->setTitle('Doctrine extensions');

        $em->persist($post);
        $em->persist($next);
        $em->flush();
    }
    $posts = $repository->findAll();
    dd($posts);
}

Now if you follow the url: http://your_virtual_host/demo/posts you should see a print of posts, this is only an extension demo, we will not create a template.

More tips

Regarding, the setup, I do not think it's too complicated to use, in general it is simple enough, and lets you understand at least small parts on how you can hook mappings into doctrine, and 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 subscriber with other the manager name:

Regarding, mapping of ODM mongodb, it's 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: attribute # or annotation or xml
                    alias: GedmoDocument
                    prefix: Gedmo\Translatable\Document
                    # make sure vendor library location is correct
                    dir: "%kernel.project_dir%/vendor/gedmo/doctrine-extensions/src/Translatable/Document"

This also shows, how to make mappings based on single manager. All what differs is that Document instead of Entity is used. I haven't tested it with mongo though.

Note: extension repository contains all documentation you may need to understand how you can use it in your projects.

Alternative over configuration

You can use StofDoctrineExtensionsBundle which is a wrapper of these extensions

Troubleshooting

  • Make sure there are no *.orm.yml or *.orm.xml files for your Entities in your bundles Resources/config/doctrine directory. With those files in place the annotations won't be taken into account.