Skip to content
Snippets Groups Projects
Grégoire Paris's avatar
Greg0ire authored
39d8226a
History

Ne soyez plus l'esclave de Doctrine


Hello!

Grégoire Paris
Maxime Veber

Une application classique

.
├── AppBundle
    ├── Admin
    ├── AppBundle.php
    ├── Controller
    ├── DataFixtures
    ├── DependencyInjection
    ├── Entity
    ├── Form
    ├── Listeners
    ├── OAuth
    ├── Resources
    ├── Tag
    └── Twig

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é ?

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)

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

class NotThatTypicalArticle
{
    private $id;
    private $content;

    public function __construct(string $content)
    {
        $this->content = $content;
    }

    // getters
}

Notes:

  • intégrité du domaine
  • Pas de setters par défaut, ni Doctrine ni sf n'en ont besoin

Les règles métier

class Article
{
    private $id;
    private $content;

    public function __construct(string $content)
    {
        if (empty($content)) {
            throw new ArticleHasNoContent();
        }
        $this->content = $content;
    }

    // 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

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

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

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

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

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

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

Emoji test

💩

Note: speaker notes FTW!