Skip to content
Snippets Groups Projects
index.md 12.8 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
    
    
    Greg0ire's avatar
    Greg0ire committed
        public function getId(): int
    
        {
            return $this->id;
        }
    
    Greg0ire's avatar
    Greg0ire committed
        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.
    
    Greg0ire's avatar
    Greg0ire committed
    ---
    ## 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
    
    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
    
    
    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 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:
    
    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;
        }
    }
    ```
    
    ## É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
    
    
    Greg0ire's avatar
    Greg0ire committed
    #### 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.
    
    
    
    ---
    <!-- .slide: data-background="./iwantmore.gif" -->
    
    ---
    # Protips
    
    
    Greg0ire's avatar
    Greg0ire committed
    ### 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>
    
    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
    ---
    
    ## 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();
    ```
    
    
    Greg0ire's avatar
    Greg0ire committed
    ### 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>