# NE SOYEZ PLUS L'ESCLAVE DE DOCTRINE
---
<!-- .slide: id="hello" -->
## Hello!
<div style="width: 50%; float: left;">
<span id="gregoire"></span><br />
**Grégoire PARIS**<br />
<span class="job">
Software Engineer<br />
Universciné → ManoMano<br />
</span>
</div>
<div style="width: 50%; float: right;" id="maxime">
<br />
**Maxime VEBER**<br />
<span class="job">
Backend Developer<br />
Agence BiiG
</span>
</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.
---
<!-- .slide: class="lot_of_code" -->
## 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
---
## ️ 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
<span style="font-size: 1.5em;">❤️</span>
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 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 type `uuid` dans Postgres,
jetez-y un oeil.
---
## 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
---
## Instanciation avec des value object
Instanciation:
```php
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
```php
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
---
<!-- .slide: class="lot_of_code" -->
## 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
---
<!-- .slide: class="lot_of_code" -->
## 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: 2em;color: red;">❌</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: before
```php
class ArticleRepository extends EntityRepository
{
// your methods
}
```
```yaml
app.infrastructure.doctrine.user_repository:
class: App\Infrastructure\Doctrine\UserRepository
factory: ["@doctrine.orm.entity_manager", getRepository]
arguments:
- App\Model\User
```
```
[Doctrine\ORM\ORMException]
The EntityManager is closed.
```
---
## Repository as a Service: after
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
---
## L'interface ServiceEntityRepositoryInterface
```php
final class DoctrineArticleRepository implements
ArticleRepositoryInterface,
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
---
<!-- .slide: class="big_code" -->
```php
// Récupère votre repository d'article custom !
$repository = $entityManager->getRepository(Article::class);
```
<span style="font-size: 1.5em;">👍</span>
---
## Les repositories, ça peut grossir
```php
interface ArticleRepositoryInterface
{
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 GetLatestArticlesInterface
{
private $registry;
public function __construct(RegistryInterface $registry)
{
$this->registry = $registry;
}
public function __invoke(int $size): iterable
{
return $this->registry->getManager()->createQueryBuilder()
->select('a')
->from(Article::class, 'a')
->orderBy('a.id', 'DESC')
->setMaxResults($size)
->getQuery()
->execute()
;
}
}
```
Note:
- Tout de suite beaucoup plus simple à réimplémenter en elasticsearch
- Interface Segregation Principle
---
## Aliasing d'interfaces
```yaml
services:
App\Domain\Article\GetLatestArticlesInterface:
'@App\Infrastructure\Article\DoctrineGetLatestArticles'
```
```php
use App\Domain\Article\GetLatestArticlesInterface;
final class LatestArticlesAction
{
private $latestArticles;
public function __construct(GetLatestArticlesInterface $latestArticles)
{
$this->latestArticles = $latestArticles;
}
}
```
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
- L'utilisation est rare, en vrai on veut souvent filtrer ou paginer, faut
faire ça en SQL
---
<!-- .slide: class="long_text" -->
#### 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, 'a')
->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.
---
## Doctrine impose peu de choses
- `final` possible pour les méthodes en version 3
- `__clone` & `__wakeup` implémentables librement en version 3
---
<div class="tweet" data-src="https://twitter.com/Ocramius/status/975399920202080256" style="transform: scale(1.5) translate(-33%,0%);"></div>
---
- Découplage de Doctrine ✓
- Découplage de `symfony/form`
- Découplage de `{symfony,jms}/serializer`
---
<!-- .slide: data-background="./iwantmore.gif" -->
---
## 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.
---
<!-- .slide: class="lot_of_code long_text" -->
## Result Set Mapping
```php
$rsm = new ResultSetMappingBuilder($entityManager);
$rsm->addRootEntityFromClassMetadata(Comment::class, 'c');
$rsm->addJoinedEntityFromClassMetadata(
Article::class,
'a',
'c',
'article',
['id' => 'article_id', 'content' => 'article_content']
);
$query = $entityManager->createNativeQuery(
<<<'SQL'
SELECT c.*, a.title AS title, a.content AS article_content, a.id AS article_id
FROM comment c
INNER JOIN article a ON c.article_id = a.id
WHERE article_id = ?
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%" />
---
<!-- .slide: class="big_code" -->
## Resolve target entities
```yaml
doctrine:
orm:
resolve_target_entities:
Lib\Address\UserInterface: My\Project\Model\User
```
---
# What's next?
---
# Support de MariaDB dans Doctrine 3
---
<!-- .slide: data-background="./explosion2.gif" -->
# La configuration Doctrine 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>