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 Slide

  2. Romaric Drigon,
    Développeur et consultant à netinfluence
    @romaricdrigon / [email protected]fluence.ch

    View Slide


  3. = à utiliser avec précaution

    View Slide

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

    View Slide

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

    View 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 Slide

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

    View 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 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 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 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 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 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 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 Slide

  29. [intermède]

    View Slide

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

    View 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 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 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 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 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 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 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 Slide

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

    View 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 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 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 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 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 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 Slide

  45. View Slide

  46. MAPPING
    & Configuration & dernière astuces

    View Slide

  47. 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 Slide

  48. 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 Slide

  49. 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 Slide

  50. 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 Slide

  51. 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 Slide

  52. 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 Slide

  53. É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 Slide

  54. Conclusion & questions
    @romaricdrigon / [email protected]fluence.ch

    View Slide