Ne soyez plus l'esclave de Doctrine
Hello!
- Conçu par Benjamin Eberlei
- Maintenu par Marco Pivetta et Guilherme Blanco
- 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é ?
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;
return $this;
}
}
👆 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
️❤️ 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
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
{
private $id;
private $content;
public function __construct(string $content)
{
$this->content = $content;
}
public function getId()
{
return $this->id;
}
public function getContent()
{
return $this->content;
}
}
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;
}
public function getId(): int
{
return $this->id;
}
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.
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 typeuuid
dans Postgres, jetez-y un oeil.
Les value objects
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
Instanciation avec des value object
Instanciation:
use App\Domain\Article\Article;
use App\Domain\Article\ArticleContent;
use App\Domain\Article\ArticleTitle;
new Article(
new ArticleTitle('my title'),
new ArticleContent('This is a very short but nice article'),
new \DateTimeImmutable(),
);
Notes:
- ça peut vite devenir très pénible
Les constructeurs nommés
class Article
{
public static function createFromNative(string $title, string $content): self
{
return new self(
new ArticleTitle($title),
new ArticleContent($content),
new \DateTimeImmutable()
);
}
}
Note:
- On peut faire plusieurs constructeurs nommés
- On peut passer le constructeur en privé pour encourager l'utilisation des constructeurs nommés.
Persister des VO avec Doctrine
- les custom types
- les embeddables
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';
}
}
# 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
Les embeddables
use Doctrine\ORM\Annotation as ORM;
class Article
{
private $uuid;
/** @ORM\Embedded(class = "ArticleContent") */
private $articleContent;
public function __construct(ArticleContent $articleContent)
{
$this->uuid = Uuid::generate();
$this->articleContent = $articleContent;
}
}
/** @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
use Doctrine\ORM\Annotation as ORM;
class Article
{
private $uuid;
/** @ORM\Embedded(class = "ArticleContent", nullable=true) */
private $articleContent;
}
❌
tarifhaus/doctrine-nullable-embeddable
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
Les repositories
final class DoctrineArticleRepository implements ArticleRepository, ServiceEntityRepositoryInterface
{
private $entityManager;
public function __construct(ManagerRegistry $registry)
{
$this->entityManager = $registry->getManagerForClass(Article::class);
}
// your methods…
}
✔️ Repository simplifié
Note:
-
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
// Récupère votre repository d'article custom !
$repository = $entityManager->getRepository(Article::class);
Les repositories, ça peut grossir
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
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
services:
App\Domain\Article\GetLatestArticles:
'@App\Infrastructure\Article\DoctrineGetLatestArticles'
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.
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 avecCollection
etarray
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
Faire une jointure avec une relation uni-directionnelle
Récupérer tous les articles dont les commentaires contiennent Doctrine sans les commentaires.
$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();
Note:
- utile si on a besoin de rajouter des conditions sans nécessiter d'hydrater des objets de la classe jointe.
Doctrine impose peu de choses
-
final
possible pour les méthodes en version 3 -
__clone
&__wakeup
implémentables librement en version 3
Protips
Quelle API pour interroger la base de données?
Clause conditionnelle | `Doctrine\ORM\QueryBuilder` | `Doctrine\DBAL\Query\QueryBuilder` |
---|---|---|
Clause non conditionnelle | DQL | SQL |
Résultat | Objets | Scalaires |
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.
Result Set Mapping
$rsm = new ResultSetMappingBuilder($entityManager);
$rsm->addRootEntityFromClassMetadata(Comment::class, 'c');
$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();
SQL + Objets
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
Resolve target entities
doctrine:
orm:
resolve_target_entities:
Lib\Address\UserInterface: My\Project\Model\User