Newer
Older
# 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>
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.
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)
- Pas de setter pour id, il est setté par Doctrine après la persistence
---
### 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
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 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
```php
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.
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
---
### 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
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
---
### 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, ç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 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.
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
---
### 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