Skip to content
Snippets Groups Projects
Grégoire Paris's avatar
Greg0ire authored
Might be a bit small, I do not know.
df48fa5a
History

Ne soyez plus l'esclave de Doctrine


Hello!

Grégoire Paris
Software Engineer
PhotoDeGregoire
LogoUniversCine
➡️
LogoManomano
Maxime Veber
Backend developer
PhotoDeMaxime

  • Conçu par Benjamin Eberlei
  • Maintenu par Marco Pivetta et Guilherme Blanco
  • Boosté par Michael Moravec et Luís Cobucci

👏

Notes: Juste pour dire que nous n'avons rien à voir avec la team de Doctrine et qu'il faut les remercier.


Vous avez dit entité ?

class Article
{
    private $id;
    private $content;

    public function getId()
    {
        return $this->id;
    }

    public function getContent()
    {
        return $this->content;
    }

    public function setContent($content)
    {
        $this->content = $content;

        return $this;
    }
}

👆 modèle anémique

Notes:

  • Pas de validation
  • Pas de règles métier
  • Aucune logique (pas de tests nécessaires)
  • Pas de setter pour id, il est setté par Doctrine après la persistence


️❤️ Domain Driven Design ❤️

  • Représenter les règles métier dans les entités
  • Avoir une API expressive
  • Respecter l'encapsulation
  • Séparer le domaine de l'infrastructure

Notes:

  • Il existe d'autres architectures, cf le talk sur le clean code de Romain Kuzniak

Doctrine n'a pas besoin de setters

class Article
{
    private $id;
    private $content;

    public function __construct(string $content)
    {
        $this->content = $content;
    }

    public function getId()
    {
        return $this->id;
    }

    public function getContent()
    {
        return $this->content;
    }
}

Notes:

  • intégrité du domaine
  • Pas de setters par défaut, ni Doctrine ni sf n'en ont besoin

Les règles métier

class Article
{
    private $id;
    private $content;

    public function __construct(string $content)
    {
        if (empty($content)) {
            throw new ArticleHasNoContent();
        }
        $this->content = $content;
    }

    public function getId()
    {
        return $this->id;
    }

    public function getContent()
    {
        return $this->content;
    }
}

Note:

  • Impossible de persister une entité invalide
  • Validation compliquée quand on a trop de propriétés
  • Pour qu'on puisse utiliser des constructeurs sans que ça crée de conflit avec Doctrine, Doctrine utilise de la réflection ou de la désérialisation pour hydrater les entités.

Les value objects

class ArticleContent
{
    private $content;

    public function __construct(string $content)
    {
        if (empty($content)) {
            throw new ArticleHasNoContent();
        }
        $this->content = $content;
    }
}

Note:

  • Déportation de la validation dans les value objects
  • Début d'arborescence
  • Doctrine Embeddables
  • Custom types

Les constructeurs nommés

class Article
{
    public static function createFromNative(string $content)
    {
        return new self(new ArticleContent($content));
    }
}

Instanciation:

new Article(new ArticleContent('This is a very short but nice article'));
// devient
Article::createFromNative('This is a very short but nice article');

Note:

  • On peut faire plusieurs constructeurs nommés
  • On peut passer le constructeur en privé pour encourager l'utilisation des constructeurs nommés.

Les custom types

use Doctrine\DBAL\Types\Type;

final class ArticleContentType extends Type
{
    public function convertToPHPValue($value, AbstractPlatform $platform): ArticleContent
    {
        return new ArticleContent($value);
    }

    public function convertToDatabaseValue($value, AbstractPlatform $platform): string
    {
        return (string) $value;
    }

    public function getName()
    {
        return 'article_content';
    }
}
# config/packages/doctrine.yaml
doctrine:
    dbal:
        types:
            article_content:  App\Infrastructure\Persistence\ArticleContentType

Note:

  • Permet de contrôler qu'on respecte toujours les règles métier à l'hydratation. Si ça crashe, c'est qu'il manque des migrations.
  • La méthode getName() fait doublon avec le nom utilisé lors de l'enregistrement du type dans le registre de type, et disparaître dès Doctrine 3
  • ArticleId devrait être une clé naturelle ou un UUID, le principal c'est de pas avoir besoin de demander à la DB de le calculer, ça évite des attaques pour cause d'ID devinables, et ça évite d'exposer le nombre d'entités présentes dans une table. Ça évite aussi des collisions lorsque vous migrez des données d'une base vers une autre, et que la nouvelle base peut elle aussi être alimentée directement.

Les embeddables

use Doctrine\ORM\Annotation as ORM;

class Article
{
    private $uuid;

    /** @ORM\Embedded(class = "ArticleContent") */
    private $articleContent;

    public function __construct(ArticleContent $articleContent)
    {
        $this->uuid = Uuid::generate();
        $this->articleContent = $articleContent;
    }
}
/** @ORM\Embeddable() **/
class ArticleContent
{
    /** @ORM\Column() **/
    private $content;

    /** @ORM\Column() **/
    private $lastModification;
}

Note:

  • À utiliser en cas de Value Object composite
  • Des soucis avec la nullabilité, contournables avec un package
  • Ne peuvent contenir des colonnes complexes

Les embeddables nullables

use Doctrine\ORM\Annotation as ORM;

class Article
{
    private $uuid;

    /** @ORM\Embedded(class = "ArticleContent", nullable=true) */
    private $articleContent;
}


tarifhaus/doctrine-nullable-embeddable

Nécessite un setter.


Les repositories

  • Vos repositories en service facilement
  • Les repositories non pollués par toutes les méthodes par défaut

sisi, c'est possible


Repository as a Service

Depuis DoctrineBundle 1.8.0 (novembre 2017) :

class ArticleRepository extends ServiceEntityRepository
{
    public function __construct(RegistryInterface $registry)
    {
        parent::__construct($registry, Article::class);
    }

    // your methods
}

✔️ Enregistrement en tant que service simple


Les repositories

final class DoctrineArticleRepository implements ArticleRepository, ServiceEntityRepositoryInterface
{
    private $entityManager;

    public function __construct(ManagerRegistry $registry)
    {
        $this->entityManager = $registry->getManagerForClass(Article::class);
    }

    // your methods…
}

✔️ Repository simplifié

Note:

  • Repository, c'est un pattern, et c'est vous qui devriez en définir l'interface.
  • EntityRepository et ses méthodes magiques à éviter si vous voulez des type hints de retour et donc de l'autocompletion. Il est maintenant possible d'en faire des services: https://github.com/doctrine/DoctrineBundle/pull/727, mais ça ne résout pas le problème. Si vous tenez à les utiliser, injectez les dans vos repositories plutôt que de les étendre.
  • Plus l'interface est grosse, plus elle devient difficile à implémenter, et le code qui consomme l'API a rarement besoin de faire beaucoup d'appels, du coup… slide suivant

// Récupère votre repository d'article custom !
$repository = $entityManager->getRepository(Article::class);

Les repositories, ça peut grossir

interface ArticleRepository
{
    public function latestArticles(int size): iterable;
    public function mostReadArticles(int size): iterable;
    public function mostCommentedArticles(int size): iterable;
    public function byTopic(ArticleTopic $topic): iterable;
    public function findRelated(Article $article): iterable;

    // more and more methods…
}

Les Query functions

Quand les repositories ne respectent pas l'ISP

final class DoctrineGetLatestArticles implements GetLatestArticles
{
    public function __construct(EntityManagerInterface $entityManager)
    {
        $this->entityManager = $entityManager;
    }

    public function __invoke(int $size): iterable
    {
        $this->entityManager->createQueryBuilder()
          ->select('a.*')
          ->from(Article::class)
          ->orderBy('a.createdAt', 'DESC')
          ->setMaxResults($size)
          ->getQuery()
          ->getResults();
    }
}

Note:

  • Tout de suite beaucoup plus simple à réimplémenter en elasticsearch
  • Interface Segregation Principle

Aliasing d'interfaces

services:
    App\Domain\Article\GetLatestArticles:
        '@App\Infrastructure\Article\DoctrineGetLatestArticles'

Note:

  • Inutile si il n'y a qu'une seule implémentation et que la feature de discovery est configurée sur un namespace commun.

Et les collections ?

  • Elles permettent à Doctrine de repérer les changements
  • Elles permettent à Doctrine de faire du lazy-loading

Ignorez les collections

  • Champs initialisés en tableaux
  • Utilisez le type iterable compatible avec Collection et array
class Article
{
    private $comments;

    public function __construct()
    {
        $this->comments = [];
    }

    public function getComments(): iterable
    {
        return $this->comments;
    }
}

Éviter le mapping One-To-Many

  • Permet d'éviter des collections inutiles
  • Leur utilisation est très rare
  • Elles ont un impact important sur les performances

Note:

  • Ne pas faire de relations par réflexe, surtout bidirectionnelles

Faire une jointure avec une relation uni-directionnelle

Récupérer tous les articles dont les commentaires contiennent Doctrine sans les commentaires.

$queryBuilder
  ->select('a.*')
  ->from(Article::class)
  ->innerJoin(Comment::class, 'c', Expr\Join::WITH, 'c.article_id = a.id')
  ->where("c.content LIKE '%Doctrine%'")
  ->getQuery()
  ->getResults();

 

Note:

  • utile si on a besoin de rajouter des conditions sans nécessiter d'hydrater des objets de la classe jointe.

Choisir la bonne API pour interroger la base de données

Clause conditionnelle `Doctrine\ORM\QueryBuilder` `Doctrine\DBAL\Query\QueryBuilder`
Clause non conditionnelle DQL SQL
Résultat Objets Scalaires

Note:

  • Si vous n'avez pas besoin d'objets, le SQL peut s'avérer plus simple, plus puissant, et plus performant (pas d'hydratation).
  • Si vous voulez des objets mais ne pouvez pas utiliser du DQL, tournez vous vers le ResultSetMapping
  • Si vous n'avez pas besoin de faire des requêtes dynamiques, vous pouvez vous passer du Query Builder.


Result Set Mapping

$rsm = new ResultSetMappingBuilder($entityManager);

$rsm->addRootEntityFromClassMetadata(Comment::class, 'c');

$rsm->addJoinedEntityFromClassMetadata(
  Article::class,
  'a',
  'c',
  'article',
  ['uuid' => 'article_uuid']
);

$query = $entityManager->createNativeQuery(
<<<'SQL'
    SELECT *, a.uuid AS article_uuid, c.uuid AS comment_uuid
    FROM comment c
    INNER JOIN article ON c.article_uuid = a.uuid
    WHERE article_uuid = ?
SQL
, $rsm);
$query->setParameter(1, $article->getUuid());

// Comment instances
$comments = $query->getResult();

SQL + Objets


Protips


Les dépendances circulaires entre paquets

Problème :

  • Je développe un système d'adresse pour tous mes projets
  • L'adresse est liée à mon utilisateur par une relation

Resolve target entities

doctrine:
    orm:
        resolve_target_entities:
            Lib\Address\UserInterface: My\Project\Model\User

What's next


__clone & __wakeup

Ces deux méthodes sont utilisées par Doctrine 2

Mais pas par Doctrine 3


Support de MariaDB dans Doctrine 3


La configuration YAML est dépréciée


Thanks!

Grégoire Paris greg0ire
Maxime Veber nekdev