Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Doctrine en dehors des sentiers battus

Doctrine en dehors des sentiers battus

L'ORM Doctrine offre beaucoup plus de flexibilité qu'il n'y paraît. Dans cette présentation, nous allons nous intéresser à son fonctionnement interne et à ses fonctionnalités moins connues, pour découvrir comment mieux l'utiliser. Au programme, évènements et listeners, filtres, tracking policy, mais aussi des astuces sur des architectures possibles pour son code...

Présenté au SymfonyLive Lille 2019

Romaric Drigon

March 01, 2019
Tweet

More Decks by Romaric Drigon

Other Decks in Programming

Transcript

  1. Doctrine en dehors
    des sentiers ba!us

    View full-size slide

  2. Romaric Drigon,
    Développeur et consultant à netinfluence
    @romaricdrigon / romaric@netinfluence.ch

    View full-size slide


  3. = à utiliser avec précaution

    View full-size slide

  4. SELECT
    Que se passe-t-il lorsque je requête une entité?

    View full-size slide

  5. Une requête basique
    $blog = $entityManager->getRepository(Blog::class)->find(1);

    View full-size slide

  6. Une requête basique
    $blog = $entityManager->getRepository(Blog::class)->find(1);
    — Doctrine ORM regarde dans le mapping (ClassMetadata) quel est
    l'identifiant (ID) de notre entité

    View full-size slide

  7. Une requête basique
    $blog = $entityManager->getRepository(Blog::class)->find(1);
    — Doctrine ORM regarde dans le mapping (ClassMetadata) quel est
    l'identifiant (ID) de notre entité
    — dans le cas d'une requête par ID, il regarde dans l'identityMap si
    l'entité n'a pas déjà été chargée

    View full-size slide

  8. Une requête basique
    $blog = $entityManager->getRepository(Blog::class)->find(1);
    — Doctrine ORM regarde dans le mapping (ClassMetadata) quel est
    l'identifiant (ID) de notre entité
    — dans le cas d'une requête par ID, il regarde dans l'identityMap si
    l'entité n'a pas déjà été chargée
    — si non, Doctrine va générer une requête SQL

    View full-size slide

  9. Une requête basique
    $blog = $entityManager->getRepository(Blog::class)->find(1);
    — Doctrine ORM regarde dans le mapping (ClassMetadata) quel est
    l'identifiant (ID) de notre entité
    — dans le cas d'une requête par ID, il regarde dans l'identityMap si
    l'entité n'a pas déjà été chargée
    — si non, Doctrine va générer une requête SQL
    — Doctrine DBAL va l'exécuter

    View full-size slide

  10. Une requête basique
    $blog = $entityManager->getRepository(Blog::class)->find(1);
    — Doctrine ORM regarde dans le mapping (ClassMetadata) quel est
    l'identifiant (ID) de notre entité
    — dans le cas d'une requête par ID, il regarde dans l'identityMap si
    l'entité n'a pas déjà été chargée
    — si non, Doctrine va générer une requête SQL
    — Doctrine DBAL va l'exécuter
    — Doctrine ORM va construire un objet à partir du résultat
    (hydratation)

    View full-size slide

  11. Une requête basique
    $blog = $entityManager->getRepository(Blog::class)->find(1);
    — Doctrine ORM regarde dans le mapping (ClassMetadata) quel est
    l'identifiant (ID) de notre entité
    — dans le cas d'une requête par ID, il regarde dans l'identityMap si
    l'entité n'a pas déjà été chargée
    — si non, Doctrine va générer une requête SQL
    — Doctrine DBAL va l'exécuter
    — Doctrine ORM va construire un objet à partir du résultat
    (hydratation)
    — elle sera ajoutée à l'identityMap, puis retournée

    View full-size slide

  12. Changer l'hydratation
    Plusieurs modes sont disponibles :
    $result = query->getResult(Query::HYDRATE_OBJECT); // Blog, objet construit par Reflection
    $result = query->getResult(Query::HYDRATE_ARRAY); // tableau associatif avec id, name...
    $result = query->getResult(Query::HYDRATE_SCALAR); // tableaux avec b_id, b_name... (non-dédupliqué !)
    $result = query->getResult(Query::HYDRATE_SINGLE_SCALAR); // non supporté ici
    $result = query->getResult(Query::HYDRATE_SIMPLEOBJECT); // Blog mais sans objets joints (1-to-1) (!)
    Pourquoi?
    Principalement pour des raisons de performance, l'hydratation en
    objet, surtout avec des objets associés, est très lourde.

    pour HYDRATE_SIMPLEOBJECT, surtout si vos getters sont typés

    View full-size slide

  13. Une autre optimisation : partial object
    // Dans BlogRepository
    $query = $this->createQueryBuilder('b')
    ->select('PARTIAL b.{id, name}')
    ->where('b.name = :name')
    ->setParameter('name', 'Romaric')
    ->getQuery();
    $blogs = query->getResult();
    dump($blogs[0] instanceof Blog); // true
    dump($blogs[0]->getId()); // 1
    dump($blogs[0]->getName()); // Romaric
    dump($blogs[0]->getDescription()); // null

    Tous les autres champs seront null, les associations et collections
    auront des proxys non initialisés / vides.

    View full-size slide

  14. Ou encore : les Partial References
    Lorsqu'on veut une entité que pour son ID, il est possible d'utiliser un
    autre type d'objet partiel, une référence :
    $blog1 = $entityManager->getPartialReference(Blog::class, 1);
    dump($blog1 instanceof Blog); // true
    dump($blog1->getId()); // 1
    $articles = $entityManager->getRepository(Article::class)
    ->findBy(['blog' => $blog1]);

    View full-size slide

  15. Relations / associations : base
    Dans une relation, il y a l'owning side (requis), et l'inverse side.
    /** @ORM\Entity */
    class Article
    {
    /**
    * @ORM\ManyToOne(targetEntity="Blog", inversedBy="articles")
    */
    private $blog;
    }
    /** @ORM\Entity */
    class Blog
    {
    /**
    * @ORM\OneToMany(targetEntity="Article", mappedBy="blog",
    * cascade={"remove"},
    * onDelete="CASCADE",
    * orphanRemoval=true)
    */
    private $articles;
    }

    View full-size slide

  16. Relations / associations : base
    Dans une relation, il y a l'owning side (requis), et l'inverse side.
    /** @ORM\Entity */
    class Article
    {
    /**
    * @ORM\ManyToOne(targetEntity="Blog", inversedBy="articles")
    */
    private $blog;
    }
    /** @ORM\Entity */
    class Blog
    {
    /**
    * @ORM\OneToMany(targetEntity="Article", mappedBy="blog",
    * cascade={"remove"},
    * onDelete="CASCADE",
    * orphanRemoval=true)
    */
    private $articles;
    }

    View full-size slide

  17. Relations / associations : base
    Dans une relation, il y a l'owning side (requis), et l'inverse side.
    /** @ORM\Entity */
    class Article
    {
    /**
    * @ORM\ManyToOne(targetEntity="Blog", inversedBy="articles")
    */
    private $blog;
    }
    /** @ORM\Entity */
    class Blog
    {
    /**
    * @ORM\OneToMany(targetEntity="Article", mappedBy="blog",
    * cascade={"remove"},
    * onDelete="CASCADE",
    * orphanRemoval=true)
    */
    private $articles;
    }

    View full-size slide

  18. Relations / associations : base
    Dans une relation, il y a l'owning side (requis), et l'inverse side.
    /** @ORM\Entity */
    class Article
    {
    /**
    * @ORM\ManyToOne(targetEntity="Blog", inversedBy="articles")
    */
    private $blog;
    }
    /** @ORM\Entity */
    class Blog
    {
    /**
    * @ORM\OneToMany(targetEntity="Article", mappedBy="blog",
    * cascade={"remove"},
    * onDelete="CASCADE",
    * orphanRemoval=true)
    */
    private $articles;
    }

    View full-size slide

  19. Relations et proxy
    Dans la mesure du possible, Doctrine propose du lazy
    loading, c'est-à-dire de mettre soit un proxy soit une
    PersistentCollection à la place de(s) entité(s) jointe(s).
    Type Côté Peut être proxy ?
    One-to-One Owning Oui (ou null)
    One-to-One Inverse Jamais ❗
    Many-to-One Owning Oui (ou null)
    Many-to-One (One-to-Many) Inverse Oui, Collection
    Many-to-Many Owning Oui, Collection
    Many-to-Many Inverse Oui, Collection

    View full-size slide

  20. Lazy-loading...
    $blogs = $entityManager->getRepository(Blog::class)->findAll();
    // Chaque Blog a des articles (One-to-Many),
    // et des contributeurs/authors (One-to-Many)
    foreach ($blogs as $blog) {
    foreach ($blog->getArticles() as $article) {
    $title = $article->getTitle();
    // ...
    }
    foreach ($blog->getAuthors() as $author) {
    $name = $author->getName();
    // ...
    }
    }

    View full-size slide

  21. ...et kaboom, problème du N+1

    View full-size slide

  22. N+1 : une possible solution
    On peut demander à Doctrine de récupérer en même temps les
    articles et les contributeurs :
    $blogs = $this->createQueryBuilder('blog')
    ->addSelect('article, author')
    ->join('blog.articles', 'article')
    ->join('blog.authors', 'author')
    ->getQuery()
    ->getResult();

    on garde le problème du coût de l'hydratation (coût en O(n*m*q) ici,
    avec n blogs, m articles et q contributeurs).

    View full-size slide

  23. Solution2 : multi-step hydratation
    $blogs = $entityManager->createQuery('
    SELECT blog, article
    FROM Blog blog
    LEFT JOIN blog.articles article
    ')
    ->getResult();
    $entityManager->createQuery('
    SELECT PARTIAL blog.{id}, author
    FROM Blog blog
    LEFT JOIN blog.authors author
    ')
    ->getResult(); // Résultat inutile
    $blogs[0]->getArticles()->first()->getTitle(); // Ne déclenche pas de requête
    2 Plus de détails sur le blog de Marco Pivetta (Ocramius), exemples sur Github ici

    View full-size slide


  24. Note sur les proxies
    // Un Blog a un Logo (One-To-One, owning side)
    $logo = $blog->getLogo();
    // Les proxies supportent mal la sérialisation
    $str = serialize($logo);
    $logo2 = unserialize($str); //
    !
    Error at offset 0...
    // À la place, soit charger le proxy
    if ($logo instanceof Doctrine\ORM\Proxy\Proxy) {
    $logo->__load();
    }
    $logo2 = unserialize(serialize($logo)); //
    // Soit utiliser l'identité
    $logoId = unserialize(serialize($logo->getId()));

    View full-size slide

  25. Requêtage, astuce : les Criteria
    class Blog
    {
    public function getDraftArticles(): Collection
    {
    $criteria = Criteria::create()
    ->where(Criteria::expr()->eq('status', 'draft'))
    ->orderBy(['position' => Criteria::ASC]);
    return $this->articles->matching($criteria);
    }
    }
    Si la collection n'est pas chargée, une requête SQL avec un WHERE sera
    générée, sinon le filtrage aura lieu sur les éléments en mémoire.

    View full-size slide

  26. Astuce 2 : appliquer un filtre à toutes les requêtes
    class NoDraftFilter extends SQLFilter
    {
    public function addFilterConstraint(ClassMetadata $entityMetadata, $targetTableAlias)
    {
    if (Article::class !== $entityMetadata->reflClass->getName()) {
    return '';
    }
    return $targetTableAlias.'.status != \'draft\''; // DQL qui sera injecté dans le WHERE
    }
    }
    orm:
    entity_managers:
    default:
    filters:
    draft_filter:
    class: App\Filter\NoDraftFilter
    enabled: true
    $publishedArticles = $entityManager->getRepository(Article::class)->findAll();
    $entityManager->getFilters()->disable('draft_filter');
    $allArticles = $entityManager->getRepository(Article::class)->findAll();

    View full-size slide

  27. Astuce 3 : organiser ses repositories...
    class ArticleRepository
    {
    public function findOnlineArticlesIWroteOnBlog(User $user, Blog $blog)
    {
    $queryBuilder = $this->createQueryBuilder('a');
    self::withIsOnline($queryBuilder, 'a');
    self::withIWrote($queryBuilder, 'a', $user);
    self::withFromBlog($queryBuilder, 'a', $blog);
    return $queryBuilder->getQuery()->getResult();
    }
    private static function withIsOnline(QueryBuilder $queryBuilder, string $alias)
    {
    $queryBuilder
    ->andWhere($alias.'.status != \'draft\'')
    ->andWhere($alias.'.publishOn >= CURRENT_TIMESTAMP()')
    ;
    }
    // withIWrote(), withFromBlog(), etc
    }

    View full-size slide

  28. ...ou construire des requêtes complexes
    class ArticleQueryBuilderBuilder
    {
    private $queryBuilder;
    private $tableAlias;
    public function __construct(QueryBuilder $queryBuilder, string $tableAlias)
    {
    $this->queryBuilder = $queryBuilder;
    $this->tableAlias = $tableAlias;
    }
    public function withIsOnline()
    {
    $this->queryBuilder
    ->andWhere($this->tableAlias.'.status != \'draft\'')
    ->andWhere($this->tableAlias.'.publishOn >= CURRENT_TIMESTAMP()');
    }
    // withIWrote(), withFromBlog(), etc
    }
    $articles = $entityManager->getRepository(Article::class)
    ->getArticleQueryBuilderBuilder() // À ajouter dans ArticleRepository
    ->withIsOnline()
    ->withIWrote($user)
    ->withFromBlog($blog)
    ->getQueryBuilder()->getQuery()->getResult();

    View full-size slide

  29. [intermède]

    View full-size slide

  30. INSERT
    Que se passe-t-il quand j'insère une nouvelle entité ?

    View full-size slide

  31. Cycle de Vie d'un persist()
    $article = new Article();
    // Doctrine détecte qu'il ne connaît pas l'entité,
    // il va l'ajouter dans UnitOfWork::entityInsertions
    $entityManager->persist($article);
    // Maintenant, Doctrine va synchroniser l'UnitOfWork avec la BDD :
    // il regarde s'il y a de nouvelles entités,
    // ouvre une transaction,
    // génère et exécute un INSERT SQL,
    // puis commit et l'UnitOfWork se "nettoie"
    $entityManager->flush();

    View full-size slide

  32. Cycle de Vie d'un persist()
    $article = new Article();
    // Doctrine détecte qu'il ne connaît pas l'entité,
    // il va l'ajouter dans UnitOfWork::entityInsertions
    $entityManager->persist($article);
    // Maintenant, Doctrine va synchroniser l'UnitOfWork avec la BDD :
    // il regarde s'il y a de nouvelles entités,
    // ouvre une transaction,
    // génère et exécute un INSERT SQL,
    // puis commit et l'UnitOfWork se "nettoie"
    $entityManager->flush();

    View full-size slide

  33. Cycle de Vie d'un persist()
    $article = new Article();
    // Doctrine détecte qu'il ne connaît pas l'entité,
    // il va l'ajouter dans UnitOfWork::entityInsertions
    $entityManager->persist($article);
    // Maintenant, Doctrine va synchroniser l'UnitOfWork avec la BDD :
    // il regarde s'il y a de nouvelles entités,
    // ouvre une transaction,
    // génère et exécute un INSERT SQL,
    // puis commit et l'UnitOfWork se "nettoie"
    $entityManager->flush();

    View full-size slide

  34. Évènements
    Action Quand ? Évènements
    Nouvelle entité EM:flush() prePersist et postPersist
    Mise à jour EM:flush() preUpdate et postUpdate
    Suppression EM:flush() preRemove et postRemove
    Toujours EM:flush() preFlush, onFlush et postFlush
    Lecture de la BDD find()... postLoad
    Première opération find(), EM:persist()... loadClassMetadata
    Nettoyage EM:clear() onClear

    View full-size slide

  35. Lifecycle callbacks (1/3)
    use Doctrine\Common\Persistence\Event\PreUpdateEventArgs;
    /**
    * @ORM\Entity
    * @ORM\HasLifecycleCallbacks
    */
    class Blog
    {
    /** @ORM\PreUpdate */
    public function onPreUpdate(PreUpdateEventArgs $event)
    {
    $this->updatedAt = new \DateTimeImmutable();
    }
    }

    View full-size slide

  36. Events listeners/subscriber (2/3)
    use Doctrine\ORM\Events;
    use Doctrine\Common\EventSubscriber;
    use Doctrine\Common\Persistence\Event\LifecycleEventArgs;
    class LocalizationPersister implements EventSubscriber
    {
    public function getSubscribedEvents()
    {
    return [Events::prePersist];
    }
    public function onPrePersist(LifecycleEventArgs $args)
    {
    if (!$args->getObject() instanceof Article) {
    return;
    }
    $args->getObject()->setLocale('fr'); // ou injectée...
    }
    }

    View full-size slide

  37. Entity listeners3

    (3/3)
    use Doctrine\Common\Persistence\Event\LifecycleEventArgs;
    class BlogLogoListener
    {
    public function preRemove(Logo $logo, LifecycleEventArgs $args)
    {
    unlink($logo->getPath());
    }
    }
    services:
    blog_logo_listener:
    class: App\Listener\BlogLogoListener
    tags:
    - { name: doctrine.orm.entity_listener, event: preRemove, entity: App\Entity\Logo }
    3 Syntaxe avec Doctrine 2.5+.

    La documentation n'est pas très claire pour l'instant.

    View full-size slide

  38. UPDATE
    Et lors de la mise à jour d'une entité ?

    View full-size slide

  39. Lors du flush()
    $article->setTitle('Retex SymfonyLive 2019');
    $entityManager->flush($article); // Ou flush() tout court
    Par défaut, pas besoin de persist() !
    Doctrine va regarder si chaque champ de chaque entité a été modifié.
    Il est possible de changer cela, c'est-à-dire la tracking policy.

    View full-size slide

  40. Les tracking policies
    Deferred Implicit : stratégie par défaut. Doctrine garde en mémoire les
    valeurs récupérées de la BDD, et lors du flush() va comparer chaque
    champ des entités qu'il connaît pour voir s'il a été modifié.

    Peut utiliser beaucoup de ressource.
    Deferred Explicit : seules les entités explicitement persistées
    ($entityManager->persist($article)) ont leurs champs comparés.

    Utilise moins de ressources, mais attention aux cascades.
    Notify : chaque entité doit signaler ses modifications à un listener.
    Le plus optimisé, mais lourd à mettre en place.

    View full-size slide

  41. Exemple avec Deferred explicit
    /**
    * @ORM\Entity
    * @ORM\ChangeTrackingPolicy("DEFERRED_EXPLICIT")
    */
    class Article
    {
    // ...
    }
    $article->setTitle('Hello World');
    $entityManager->persist($article);
    $entityManager->flush();

    Il faut appeler exactement $entityManager->persist() sur chaque
    entité, cascade: {"persist"} dans les annotations ne suffit pas. Donc il
    faut pouvoir/vouloir accéder à chaque entité depuis son contrôleur...

    View full-size slide

  42. Listeners : particularité de l'update
    use Doctrine\Common\Persistence\Event\PreUpdateEventArgs;
    class BlogListener
    {
    public function preUpdate(Blog $blog, PreUpdateEventArgs $args)
    {
    if ($eventArgs->hasChangedField('name') && $eventArgs->getNewValue('name')) {
    // Attention de cette manière seuls les champs déjà modifiés peuvent être remodifiés
    $eventArgs->setNewValue('name', 'Nouveau nom: '.$eventArgs->getNewValue());
    $blog->setSlug(canonicalize($blog->getName()));
    // Si on souhaite modifier une autre propriété, il faut lancer une recomparaison
    $classMetadata = $args->getEntityManager()->getClassMetadata(Blog::class);
    $args->getEntityManager()->getUnitOfWork()->recomputeSingleEntityChangeSet($classMetadata, $blog);
    }
    }
    }

    On ne peut pas créer de nouvelles entités ici.

    View full-size slide

  43. Listeners : créer de nouvelles entités
    class BlogSubscriber implements EventSubscriber
    {
    public function getSubscribedEvents()
    {
    return [Events::onFlush];
    }
    public function onFlush(OnFlushEventArgs $args)
    {
    $entityManager = $args->getEntityManager();
    $unitOfWork = $entityManager->getUnitOfWork();
    foreach ($unitOfWork->getScheduledEntityUpdates() as $entity) {
    if ($entity instanceof Blog) {
    $changeset = $unitOfWork->getEntityChangeSet($entity);
    if (isset($changeset['name'])) {
    $notification = new Notification(); // etc
    $entityManager->persist($notification);
    // On ne peut pas déclencher de flush() - on doit recompute "à la main"
    $classMetadata = $entityManager->getClassMetadata(Notification::class);
    $unitOfWork->computeChangeSet($classMetadata, $notification);
    }
    }
    }
    }
    }

    View full-size slide

  44. Un piège : suivi des objets
    Par défaut, Doctrine compare les valeurs des propriétés pour détecter
    si elles ont été modifiées.

    Cela ne marche pas avec les objets: UploadedFile, DateTime...
    Il faut alors soit modifier la valeur d'une autre propriété, d'un type
    primitif, soit utiliser des objets immutables.
    class Article
    {
    /**
    * @var \DateTimeImmutable
    * @ORM\Column(type="datetime_immutable")
    */
    private $updatedAt;
    }

    View full-size slide

  45. MAPPING
    & Configuration & dernière astuces

    View full-size slide

  46. Embeddables
    /** @ORM\Embeddable */
    class Address
    {
    /** @ORM\Column() */
    private $street;
    /** @ORM\Column() */
    private $city;
    }
    /** @ORM\Entity */
    class Blog
    {
    /** @ORM\Embedded(class="Address") */
    private $address;
    public function __construct()
    {
    $this->address = new Address();
    }
    }
    // Pour requêter en DQL, on pourra écrire :
    // SELECT b FROM Blog b WHERE b.address.city = ...

    View full-size slide

  47. Ajouter un type Doctrine (1/2)
    use Doctrine\DBAL\Platforms\AbstractPlatform;
    use Doctrine\DBAL\Types\ConversionException;
    use Doctrine\DBAL\Types\Type;
    class DatetimeUtcType extends Type
    {
    public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
    {
    return $platform->getDateTimeTypeDeclarationSQL($fieldDeclaration);
    }
    public function convertToDatabaseValue($value, AbstractPlatform $platform)
    {
    if (null === $value) {
    return $value;
    }
    if (!$value instanceof \DateTime) {
    throw ConversionException::conversionFailedInvalidType($value, $this->getName(), ['null', 'DateTime']);
    }
    $value->setTimezone(new \DateTimeZone('UTC'));
    return $value->format($platform->getDateTimeFormatString());
    }
    public function convertToPHPValue($value, AbstractPlatform $platform)
    {
    if ($value === null || $value instanceof \DateTime) {
    return $value;
    }
    $val = \DateTime::createFromFormat($platform->getDateTimeFormatString(), $value, new \DateTimeZone('UTC'));
    if (!$val) {
    throw ConversionException::conversionFailedFormat($value, $this->getName(), $platform->getDateTimeFormatString());
    }
    return $val;
    }
    public function getName()
    {
    return 'datetime_utc';
    }
    }

    View full-size slide

  48. Ajouter un type Doctrine (2/2)
    doctrine:
    dbal:
    types:
    datetime_utc: App\Type\DatetimeUtcType
    Exemple, pour stocker une date avec un timezone (en MySQL...) :
    /** ORM\Entity */
    class Article
    {
    /** ORM\Column(type="datetime_utc") */
    private $utcDate;
    /** ORM\Column(type="string") */
    private $timezone;
    public function getDate()
    {
    return (clone $this->utcDate)->setTimezone(new \DateTimeZone($this->timezone));
    }
    public function setDate(\DateTime $date)
    {
    $this->timezone = $date->getTimezone()->getName();
    $this->utcDate = clone $date;
    }
    }

    View full-size slide

  49. Héritage
    Cas typique : définir dans un bundle une entité qui sera étendue.
    /** @ORM\MappedSuperclass */ /** @ORM\Entity */
    abstract class AbstractUser class User extends AbstractUser
    { {
    /** @ORM\Column() */ /** @ORM\Id @ORM\Column(type="integer") */
    protected $email; private $id;
    /** @ORM\Column() */ /** @ORM\Column() */
    protected $username; private $myfield;
    } }

    aux limites (associations...)
    Doctrine supporte les Traits, cela est généralement plus simple.
    ❗ ❗ ❗
    pour Single Table Inheritance et Multiple Table Inheritance

    View full-size slide

  50. Caches
    doctrine:
    orm:
    # Ces 2 caches sont INDISPENSABLES en "prod" :
    # (et activés par Symfony Flex par défaut)
    metadata_cache_driver: apcu # cache les annotations
    query_cache_driver: apcu # cache le DQL généré
    # Optionnel et manuel :
    result_cache_driver: apcu
    Le Result cache devra être rempli manuellement :
    $query = $em->createQuery('SELECT b FROM App\Entity\Blog b');
    $query->useResultCache(true, 3600); // 1 heure

    View full-size slide

  51. Second-level cache1
    doctrine:
    orm:
    entity_managers:
    default:
    second_level_cache:
    enabled: true
    region_cache_driver: apcu
    regions:
    my_entity_region:
    type: apcu
    /**
    * @ORM\Entity
    * @ORM\Cache(usage="READ_ONLY", region="my_entity_region")
    */
    class Blog
    {
    // ...
    }
    1 Il y a un nombre important d'options, voir la documentation

    View full-size slide

  52. Étendre Doctrine
    Le plus connu, les Doctrine extensions :
    Sortable, SoftDeleteable, Sluggable...
    ⭐ ⭐
    steevanb/DoctrineStatsBundle
    ⭐ ⭐
    Des fonctions DQL supplémentaires :
    - spécifiques à chaque plate-forme
    - pour requêter/manipuler du JSON
    - pour les types spatiaux
    Des utilitaires pour les batchs

    View full-size slide

  53. Conclusion & questions
    @romaricdrigon / romaric@netinfluence.ch

    View full-size slide