# Ne soyez plus l'esclave de Doctrine
---
## Hello!
<div style="width: 50%; float: left;">
Grégoire Paris<br />
Software Engineer<br />
<span id="gregoire"></span><br />
<div id="gregoire_company">
<div id="universcine">

</div>
<div id="universcine">➡ </div>
<div id="manomano">

</div>
</div>
</div>
<div style="width: 50%; float: right;" id="maxime">
Maxime Veber<br />
Backend developer<br />

</div>
---
<img src="./doctrine.svg" />
- 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é ?
```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;
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
---
<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
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 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
```php
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 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
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
```php
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
> - `ArticleId` devrait être une clé naturelle ou un UUID, le principal c'est de
> 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 embeddables
```php
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;
}
}
```
```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
{
private $uuid;
/** @ORM\Embedded(class = "ArticleContent", nullable=true) */
private $articleContent;
}
```
<span style="font-size: 1em;">❌</span>
---
### 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) :
```php
class ArticleRepository extends ServiceEntityRepository
{
public function __construct(RegistryInterface $registry)
{
parent::__construct($registry, Article::class);
}
// your methods
}
```
✔ Enregistrement en tant que service simple
---
## Les repositories
```php
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
---
```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:
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 avec `Collection` et `array`
```php
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**.
```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();
```
<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
---
### 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>
</table>
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
```php
$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
<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 3
---
<!-- .slide: data-background="./explosion2.gif" -->
# La configuration YAML est dépréciée
---
<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>