# Ne soyez plus l'esclave de Doctrine --- ## Hello! <div style="width: 50%; float: left;"> Grégoire Paris<br /> Software Engineer<br /> <span id="gregoire"></span><br /> <div id="gregoire_company"> <div id="universcine">  </div> <div id="universcine">➡ </div> <div id="manomano">  </div> </div> </div> <div style="width: 50%; float: right;" id="maxime"> Maxime Veber<br /> Backend developer<br />  </div> --- <img src="./doctrine.svg" /> - 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é ? ```php 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 --- <div class="tweet" data-src="https://twitter.com/Ocramius/status/975399920202080256"></div> --- ## ️❤️ 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 ```php 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 ```php class Article { private $id; private $content; public function __construct(string $content) { if (empty($content)) { throw new ArticleHasNoContent(); } $this->content = $content; } public function getId(): int { return $this->id; } public function getContent(): string { 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 UUID - Universally unique - Indevinables - Ne nécessitent pas de base de données - `ramsey/uuid` à la rescousse Notes: - Idéalement, prendre une clé naturelle. - Doctrine a un type `guid` qui correspond au type `uuid` dans Postgres, jetez-y un oeil. --- ## Les value objects ```php 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 ```php class Article { public static function createFromNative(string $content) { return new self(new ArticleContent($content)); } } ``` Instanciation: ```php 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 ```php 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'; } } ``` ```yaml # 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 --- ## Les embeddables ```php 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; } } ``` ```php /** @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 ```php use Doctrine\ORM\Annotation as ORM; class Article { private $uuid; /** @ORM\Embedded(class = "ArticleContent", nullable=true) */ private $articleContent; } ``` <span style="font-size: 1em;">❌</span> --- ### 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) : ```php class ArticleRepository extends ServiceEntityRepository { public function __construct(RegistryInterface $registry) { parent::__construct($registry, Article::class); } // your methods } ``` ✔ Enregistrement en tant que service simple --- ## Les repositories ```php 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 --- ```php // Récupère votre repository d'article custom ! $repository = $entityManager->getRepository(Article::class); ``` --- ## Les repositories, ça peut grossir ```php 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 ```php 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 ```yaml 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` ```php 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**. ```php $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(); ``` <video data-autoplay height="400" src="./UntidyCraftyCanadagoose.webm"> </video> Note: - utile si on a besoin de rajouter des conditions sans nécessiter d'hydrater des objets de la classe jointe. --- <!-- .slide: data-background="./iwantmore.gif" --> --- # Protips --- ### Quelle API pour interroger la base de données? <table> <tr> <th>Clause conditionnelle</th> <td>`Doctrine\ORM\QueryBuilder`</td> <td>`Doctrine\DBAL\Query\QueryBuilder`</td> </tr> <tr> <th>Clause non conditionnelle</th> <td>DQL</td> <td>SQL</td> </tr> <tr> <th>Résultat</th> <th>Objets</th> <th>Scalaires</th> </tr> </table> 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 ```php $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 --- ### 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 <img src="./relationcirculaire.svg" style="width: 90%" /> --- ## Resolve target entities ```yaml 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 --- <!-- .slide: data-background="./explosion2.gif" --> # La configuration YAML est dépréciée --- <div style="width: 50%; float: left;"> <h1>Thanks!</h1> </div> <div style="width: 50%; float: right;" class="no-img-marge verticalAlign"> <table> <tr> <td style="padding-bottom: 50px">Grégoire Paris</td> <td><img src="./twi1.svg" width="45" /> greg0ire</td> </tr> <tr> <td style="padding-top: 50px">Maxime Veber</td> <td><img src="./twi1.svg" width="45" /> nekdev</td> </tr> </table> </div>