Skip to content
Snippets Groups Projects
index.md 5.6 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)
    
    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
    
    ---
    ### 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
      pas avoir besoin de demander à la DB de le calculer
    
    ---
    ### 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!