Skip to content
Snippets Groups Projects
index.md 6.04 KiB
Newer Older
  • Learn to ignore specific revisions
  • # Ne soyez plus l'esclave de Doctrine
    
    ---
    ## Hello!
    
    <div style="width: 50%; float: left;">
    Grégoire Paris
    </div>
    <div style="width: 50%; float: right;">
    Maxime Veber
    </div>
    
    ---
    ### Une application classique
    
    <pre style="width: 50%; float: left;">
    .
    ├── AppBundle
        ├── Admin
        ├── AppBundle.php
        ├── Controller
        ├── DataFixtures
        ├── DependencyInjection
        ├── Entity
        ├── Form
        ├── Listeners
        ├── OAuth
        ├── Resources
        ├── Tag
        └── Twig
    </pre>
    
    
    Greg0ire's avatar
    Greg0ire committed
    Notes:
    - Aucune idée de ce que fait l'application
    - Tout ce qu'on reconnait, c'est des répertoires qui sont présents sur d'autres
      applications.
    
    
    ---
    ### Vous avez dit entité ?
    
    Greg0ire's avatar
    Greg0ire committed
    
    ```php
    
    class TypicalArticle
    {
        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;
        }
    }
    ```
    
    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
    
    
    ---
    ### Nous aimons le DDD
    
    - Séparer le **domaine** de l'infrastructure
    - Représenter les **règles métier** dans les entités
    - Avoir une API expressive
    
    
    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
    
    ```php
    class NotThatTypicalArticle
    
    Greg0ire's avatar
    Greg0ire committed
    {
    
        private $id;
        private $content;
    
        public function __construct(string $content)
        {
            $this->content = $content;
        }
    
        // getters
    
    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
    
    
        // getters
    }
    ```
    
    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;
        private $lastModification;
    
        public function __construct(string $content)
        {
            if (empty($content)) {
                throw new ArticleHasNoContent();
            }
            $this->content = $content;
            $this->lastModification = new \DatetimeImmutable();
        }
    }
    ```
    
    Note:
    - Déportation de la validation dans les value objects
    - Début d'arborescence
    - Doctrine Embeddables
    - Custom types
    
    
    ---
    ### Les embeddables
    
    ```php
    use Doctrine\ORM\Annotation as ORM;
    
    class NiceArticle
    {
        private $uuid;
    
        /** @ORM\Embedded(class = "ArticleContent") */
        private $articleContent;
    }
    ```
    
    Note:
    - À utiliser en cas de Value Object composite
    - Des soucis avec la nullabilité, contournables avec un package
    - Ne peuvent contenir des colonnes complexes
    
    ---
    ### Les custom types
    
    ```php
    use Doctrine\DBAL\Platforms\AbstractPlatform as P;
    
    final class ArticleContentType extends Type
    {
        public function convertToPHPValue($value, P $p): ArticleId
        {
            return new ArticleId($value);
        }
    
        public function convertToDatabaseValue($value, P $p): string
        {
            return (string) $value;
        }
    }
    ```
    
    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
    
    Greg0ire's avatar
    Greg0ire committed
      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 constructeurs nommés
    
    ```php
    class BetterArticle
    {
        public static function createFromNative(string $content)
        {
            return new self(new ArticleContent($content));
        }
    }
    ```
    
    
    Note:
    - On peut faire plusieurs constructeurs nommés
    - On peut passer le constructeur en privé pour encourager l'utilisation des
      constructeurs nommés.
    
    ---
    ### Les repositories
    
    ```php
    final class DoctrineArticleRepository implements ArticleRepository
    {
        public function __construct(EntityManagerInterface $entityManager)
        {
            $this->entityManager = $entityManager;
        }
    
        public function latestArticles(int $size): iterable
        {
            $this->entityManager->createQueryBuilder()
              ->select('a.*')
              ->from(Article::class)
              ->orderBy('a.createdAt', 'DESC')
              ->setMaxResults($size)
              ->getQuery()
              ->getResults();
        }
    }
    ```
    
    Note:
    - `Repository`, c'est un pattern, et il vaut mieux définir les vôtres sous
      forme d'interface.
    - 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
    
    ---
    # Les Query functions
    
    ```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
    
    Greg0ire's avatar
    Greg0ire committed
    # Emoji test
    
    💩
    
    Note:
    speaker notes FTW!