Skip to content
Snippets Groups Projects
index.md 12.5 KiB
Newer Older
  • Learn to ignore specific revisions
  • # Ne soyez plus l'esclave de Doctrine
    
    ---
    ## Hello!
    
    
    <div style="width: 50%; float: left;">
    
    Maxime Veber's avatar
    Maxime Veber committed
    Grégoire Paris<br />
    
    Software Engineer<br />
    <span id="gregoire">![PhotoDeGregoire](./PARIS-150x150.jpg)</span><br />
    
    Greg0ire's avatar
    Greg0ire committed
      <div id="gregoire_company">
        <div id="universcine">
    
    Greg0ire's avatar
    Greg0ire committed
          ![LogoUniversCine](./universcine_light.svg)
    
    Greg0ire's avatar
    Greg0ire committed
        </div>
        <div id="universcine"></div>
        <div id="manomano">
          ![LogoManomano](./Logo_MAnoMano_HD.svg)
        </div>
      </div>
    
    Maxime Veber's avatar
    Maxime Veber committed
    <div style="width: 50%; float: right;" id="maxime">
    Maxime Veber<br />
    
    Backend developer<br />
    ![PhotoDeMaxime](./MaximeNB.png)
    
    Maxime Veber's avatar
    Maxime Veber committed
    ---
    
    <img src="./doctrine.svg" />
    
    Maxime Veber's avatar
    Maxime Veber committed
    
    - Conçu par **Benjamin Eberlei**
    
    - Maintenu par **Marco Pivetta** et **Guilherme Blanco**
    
    Maxime Veber's avatar
    Maxime Veber committed
    - 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é ?
    
    Greg0ire's avatar
    Greg0ire committed
    
    ```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;
    
    👆 _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
    
    Greg0ire's avatar
    Greg0ire committed
    
    
    
    <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
    
    Greg0ire's avatar
    Greg0ire committed
    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
    
    Greg0ire's avatar
    Greg0ire committed
    {
    
        private $id;
        private $content;
    
        public function __construct(string $content)
        {
            $this->content = $content;
        }
    
    
        public function getId()
        {
            return $this->id;
        }
    
        public function getContent()
        {
            return $this->content;
        }
    
    Greg0ire's avatar
    Greg0ire committed
    }
    ```
    
    
    Notes:
    - intégrité du domaine
    - Pas de setters par défaut, ni Doctrine ni sf n'en ont besoin
    
    
    Greg0ire's avatar
    Greg0ire committed
    ---
    
    ## Les règles métier
    
    
    ```php
    class Article
    {
        private $id;
        private $content;
    
    Greg0ire's avatar
    Greg0ire committed
    
    
        public function __construct(string $content)
        {
            if (empty($content)) {
                throw new ArticleHasNoContent();
            }
            $this->content = $content;
        }
    
    Greg0ire's avatar
    Greg0ire committed
    
    
        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
    
    
    ```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
    
    Greg0ire's avatar
    Greg0ire committed
    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';
    
    ```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
    
    > - `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.
    
    Maxime Veber's avatar
    Maxime Veber committed
    ---
    ## Les embeddables
    
    ```php
    use Doctrine\ORM\Annotation as ORM;
    
    
    class Article
    
    Maxime Veber's avatar
    Maxime Veber committed
    {
        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
    
    Maxime Veber's avatar
    Maxime Veber committed
    {
        private $uuid;
    
        /** @ORM\Embedded(class = "ArticleContent", nullable=true) */
        private $articleContent;
    }
    ```
    
    <span style="font-size: 1em;"></span>
    
    ---
    
    Greg0ire's avatar
    Greg0ire committed
    ### tarifhaus/doctrine-nullable-embeddable
    
    Maxime Veber's avatar
    Maxime Veber committed
    
    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
    
    Greg0ire's avatar
    Greg0ire committed
    
    
    ## Les repositories
    
    final class DoctrineArticleRepository implements ArticleRepository, ServiceEntityRepositoryInterface
    
        private $entityManager;
    
        public function __construct(ManagerRegistry $registry)
    
            $this->entityManager = $registry->getManagerForClass(Article::class);
    
    Greg0ire's avatar
    Greg0ire committed
        // your methods…
    
    ✔ Repository simplifié
    
    
    - `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 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:
    
    Greg0ire's avatar
    Greg0ire committed
        App\Domain\Article\GetLatestArticles:
            '@App\Infrastructure\Article\DoctrineGetLatestArticles'
    
    Greg0ire's avatar
    Greg0ire committed
    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.
    
    
    Greg0ire's avatar
    Greg0ire committed
    ## 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;
        }
    }
    ```
    
    ## Evitez les One-To-Many
    
    
    - Leur utilisation est très rare
    - Elles ont un impact important sur les performances
    
    ## 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();
    ```
    
    
    &nbsp;
    
    <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.
    
    
    ## Choisir la bonne 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>
    
    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.
    
    Maxime Veber's avatar
    Maxime Veber committed
    ---
    
    <!-- .slide: data-background="./iwantmore.gif" -->
    
    ## Result Set Mapping
    
    ```php
    $rsm = new ResultSetMappingBuilder($entityManager);
    
    $rsm->addRootEntityFromClassMetadata(Comment::class, 'c');
    
    
    Greg0ire's avatar
    Greg0ire committed
    $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();
    ```
    
    
    ## 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&nbsp;3
    
    Maxime Veber's avatar
    Maxime Veber committed
    
    ---
    <!-- .slide: data-background="./explosion2.gif" -->
    # La configuration YAML est dépréciée
    
    
    Greg0ire's avatar
    Greg0ire committed
    
    
    <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>