diff --git a/hello.css b/hello.css index 31567f06867c35f1101c881ccec9be9712074afe..d221643df06a134fc19ece1f7c9cb9e25e703280 100644 --- a/hello.css +++ b/hello.css @@ -11,6 +11,9 @@ code { .reveal { color: white; + background-image: url(./sflive_paris2018_big_transp.png); + background-repeat: no-repeat; + background-position: top right; } @font-face { diff --git a/index.md b/index.md index 5a2c1102e8a196dba542aa373a896c6cacebc07b..aee4386fa8f52300553093f2fd609ea9c9227d61 100644 --- a/index.md +++ b/index.md @@ -24,8 +24,6 @@ Backend developer<br /> </div> --- -### Nous parlons de Doctrine ORM - <img src="./doctrine.svg" /> - Conçu par **Benjamin Eberlei** @@ -41,7 +39,7 @@ Notes: Juste pour dire que nous n'avons rien à voir avec la team de Doctrine et ## Vous avez dit entité ? ```php -class TypicalArticle +class Article { private $id; private $content; @@ -73,12 +71,6 @@ Notes: - 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 ❤️ @@ -94,7 +86,7 @@ Notes: ## Doctrine n'a pas besoin de setters ```php -class NotThatTypicalArticle +class Article { private $id; private $content; @@ -137,12 +129,12 @@ class Article $this->content = $content; } - public function getId() + public function getId(): int { return $this->id; } - public function getContent() + public function getContent(): string { return $this->content; } @@ -156,6 +148,19 @@ Note: 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 @@ -163,7 +168,6 @@ hydrater les entités. class ArticleContent { private $content; - private $lastModification; public function __construct(string $content) { @@ -171,58 +175,74 @@ class ArticleContent throw new ArticleHasNoContent(); } $this->content = $content; - $this->lastModification = new \DatetimeImmutable(); } } ``` -Instanciation: -```php -new NiceArticle(new ArticleContent('This is a very short but nice article')); -``` - 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 BetterArticle +class Article { - public static function createFromNative(string $content) + public static function createFromNative(string $title, string $content): self { - return new self(new ArticleContent($content)); + return new self( + new ArticleTitle($title), + new ArticleContent($content), + new \DateTimeImmutable() + ); } } ``` -Instanciation: -```php -BetterArticle::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. +--- +## Persister des VO avec Doctrine + +- les custom types +- les embeddables + --- ## Les custom types ```php -class ArticleId extends Uuid { } -``` +use Doctrine\DBAL\Types\Type; -```php -final class ArticleIdType extends Type +final class ArticleContentType extends Type { - public function convertToPHPValue($value, AbstractPlatform $platform): ArticleId + public function convertToPHPValue($value, AbstractPlatform $platform): ArticleContent { - return new ArticleId($value); + return new ArticleContent($value); } public function convertToDatabaseValue($value, AbstractPlatform $platform): string @@ -232,7 +252,7 @@ final class ArticleIdType extends Type public function getName() { - return 'article_id'; + return 'article_content'; } } ``` @@ -242,7 +262,7 @@ final class ArticleIdType extends Type doctrine: dbal: types: - article_id: App\Infrastructure\Persistence\ArticleIdType + article_content: App\Infrastructure\Persistence\ArticleContentType ``` Note: @@ -250,12 +270,6 @@ Note: 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 @@ -263,7 +277,7 @@ alimentée directement. ```php use Doctrine\ORM\Annotation as ORM; -class NiceArticle +class Article { private $uuid; @@ -301,7 +315,7 @@ Note: ```php use Doctrine\ORM\Annotation as ORM; -class NiceArticle +class Article { private $uuid; @@ -349,7 +363,7 @@ class ArticleRepository extends ServiceEntityRepository ## Les repositories ```php -final class DoctrineArticleRepository implements ArticleRepository, ServiceEntityRepositoryInterface +final class DoctrineArticleRepository implements ArticleRepositoryInterface, ServiceEntityRepositoryInterface { private $entityManager; @@ -358,16 +372,7 @@ final class DoctrineArticleRepository implements ArticleRepository, ServiceEntit $this->entityManager = $registry->getManagerForClass(Article::class); } - public function latestArticles(int $size): iterable - { - $this->entityManager->createQueryBuilder() - ->select('a.*') - ->from(Article::class) - ->orderBy('a.createdAt', 'DESC') - ->setMaxResults($size) - ->getQuery() - ->getResults(); - } + // your methods… } ``` @@ -392,13 +397,29 @@ coup… slide suivant $repository = $entityManager->getRepository(Article::class); ``` +--- +## 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 GetLatestArticles +final class DoctrineGetLatestArticles implements GetLatestArticlesInterface { public function __construct(EntityManagerInterface $entityManager) { @@ -427,15 +448,16 @@ Note: ```yaml services: - App\Domain\Article\GetLatestArticles: '@App\Infrastructure\Article\DoctrineGetLatestArticles' - - App\Infrastructure\Article\DoctrineGetLatestArticles: - arguments: - $entityManager: '@doctrine.orm.default_entity_manager' + App\Domain\Article\GetLatestArticlesInterface: + '@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. + --- -## À quoi servent les collections ? +## Et les collections ? - Elles permettent à Doctrine de repérer les changements - Elles permettent à Doctrine de faire du lazy-loading @@ -446,7 +468,7 @@ services: - Utilisez le type `iterable` compatible avec `Collection` et `array` ```php -class FarBetterArticle +class Article { private $comments; @@ -463,13 +485,19 @@ class FarBetterArticle ``` --- -## Evitez les One-To-Many +## É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 + --- -## Faire une jointure avec une relation uni-directionnelle +#### Faire une jointure avec une relation uni-directionnelle Récupérer tous les articles dont les commentaires contiennent Doctrine **sans les commentaires**. ```php @@ -492,7 +520,24 @@ Note: des objets de la classe jointe. --- -## Choisir la bonne API pour interroger la base de données +# 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"></div> + + +--- +<!-- .slide: data-background="./iwantmore.gif" --> + +--- +# Protips + +--- +### Quelle API pour interroger la base de données? <table> <tr> @@ -521,9 +566,6 @@ Note: - Si vous n'avez pas besoin de faire des requêtes dynamiques, vous pouvez vous passer du Query Builder. ---- -<!-- .slide: data-background="./iwantmore.gif" --> - --- ## Result Set Mapping @@ -557,10 +599,7 @@ $comments = $query->getResult(); SQL + Objets --- -# Protips - ---- -## Les dépendances circulaires entre paquets +### Les dépendances circulaires entre paquets Problème : @@ -582,13 +621,6 @@ doctrine: --- # 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 diff --git a/sflive_paris2018_big_transp.png b/sflive_paris2018_big_transp.png new file mode 100644 index 0000000000000000000000000000000000000000..272d410697dc2834136509760d216eb382a6d28a Binary files /dev/null and b/sflive_paris2018_big_transp.png differ diff --git a/talk.md b/talk.md new file mode 100644 index 0000000000000000000000000000000000000000..8271ff13b03d3a31aa6c06271d9a59e0654c2c34 --- /dev/null +++ b/talk.md @@ -0,0 +1,173 @@ +Bonsoir, bonsoir, +je me présente, je suis Grégoire Paris, Software Engineer chez ManoMano.com +depuis 3 jours, car je viens de quitter Universciné, un fournisseur de vidéo à +la demande. +C'est mon premier talk alors il va falloir être très indulgents avec moi, +heureusement je ne suis pas seul, je suis venu avec Maxime Veber, qui va se +présenter à son tour. + +… + +Nous sommes venu vous parler d'un outil que vous connaissez bien, Doctrine, +conçu par Benjamin Eberlei, maintenu au jour le jour par Marco Pivetta et +Ghuilherme Blanco, et plus récemment par Michael Moravec et Luis Cobucci, +particulièrement en ce qui concerne la prochaine version, Doctrine 3. + +Ce soir, nous allons essayer de vous montrer que Doctrine est un outil qui est +très peu intrusif pour peu qu'on sache comment faire pour s'en découpler. + +Rentrons tout de suite dans le vif du sujet avec une bonne vieille entité telle +que vous la connaissez, telle que n'importe programmeur lisant la documentation +Doctrine ou Symfony la produira. + +Alors, une entité, pour le programmeur lambda, c'est avant tout une suite de +propriétés typées. Chaque propriété vient avec un setter et un getter, à +l'exception notable de l'identifiant, qui est initialisé par Doctrine, qui +demande le prochain identifiant à la base de données. Ces setters et getters +sont dépourvus de toute logique, et si vous les testez, vous aurez +probablement l'impression de perdre votre temps et de faire un travail +répétitif, à tel point que certains vont jusqu'à créer des paquets Composer +pour automatiser ce genre de tests, et que jusqu'à très récemment, il y avait +une commande qui permettait de les générer. +On peut remarquer que Doctrine ne fait pas d'active record et ne vous oblige +pas à étendre quoi que ce soit, ce qui est une très bonne chose, car ce serait +une forme de couplage très forte. +Ce modèle porte un nom ou des noms, il est connu sous le nom de modèle +anémique, car il plein de vide. + +Mais si vous avez écouté le talk de Romain Kuzniak tout à l'heure, vous savez +que ce n'est pas la seule architecture possible. + +Maxime et moi, nous aimons et utilsons le Domain Driven Design dans nos application. + +C'est une architecture dont voici certains points clés, le principal point-clé +étant dans le nom, DDD. Une des choses importantes en DDD, c'est qu'on commence +par concevoir son domaine, et qu'on repousse le choix du framework, de la +méthode de communication (http ou console), de la base de données et au final, +de la plupart des outils tiers au plus tard possible. +On commence par concevoir son modèle, c'est à dire principalement ses entités +et les règles métier qui vont avec. Comme il n'y a pas de dépendances tierces +impliquées à ce moment là, les tests sont faciles à écrire car on maitrise +totalement le code. +Concevoir une entité c'est s'assurer qu'elle est conforme à tout moment, dès sa +construction, aux règles demandées par le client. +Faire du DDD, c'est faire de la bonne POO, et faire de la bonne POO, c'est +respecter l'encapsulation. Et l'entité qu'on a vu précédemment ne le fait pas +du tout: elle vous laisse lire et écrire librement ce que vous voulez dans +chacune des propriétés, sans aucun contrôle. +Une fois qu'on a fait la partie domaine, alors et seulement alors, on +s'intéresse à l'infrastructure, et dans ce talk on va vous parler de la partie +persistence avec Doctrine, et comment l'utiliser sans corrompre notre entité. + +Par exemple, saviez vous que Doctrine n'a pas du tout besoin de setters? +Lorsque je me suis mis au DDD, j'avais ce préjugé, et je me suis rendu compte +que Doctrine n'appelle aucune de vos méthodes. Pour être exact, quasiment +aucune de vos méthodes, et en Doctrine 3, ce sera vraiment aucune. Même pas le +constructeur. Ce que fait Doctrine lors de l'hydratation, c'est qu'il utilise +des techniques de magie noire comme de la réflection, ou de la déserialisation +de string pour initialiser les propriétés. + +Sachant cela, ce que vous pouvez faire, c'est utiliser le constructeur pour +valider les règles métier, des sortes de règles de validation aussi appelées +invariants car ce sont des choses qui doivent être vraies tout le temps, qui +sont données par le client. +De cette façon, il devient impossible d'avoir un objet invalide. Si ce que vous +passez au constructeur est invalide, alors une exception métier est jetée, avec +un message explicite. Pour rendre le debug de votre application facile, il faut +crasher aussitôt et de la façon la plus visible possible. Plutôt que de cacher +la poussière sous le tapis, on jette une exception et on se débarasse de tout +une classe de bug venant du fait que vous pensez quelque chose être vrai alors +qu'il ne l'est pas. Par exemple ici, après l'instanciation, si on fait appel à +getId(). C'est un des problèmes avec les identifiants. L'autre problème, c'est +le couplage qu'on a avec la base de données qui est censée rester un outil +externe et ne devrait pas dicter les ids. + +Comme solution, vous pouvez utiliser les UUIDs, si vous n'avez pas de clé +naturelle. Une clé naturelle, c'est par exemple l'ISBN d'un livre. Si vous avez +la chance d'en avoir une dans votre domaine, utilisez la, sinon utilisez les +UUIDs. + +UUID, ça veut dire universally unique identifier, ce qui signifie que +l'identifiant est unique non seulement au sein de votre application, mais de +l'univers tout entier. Si vous faites la refonte d'une application en Symfony +2, ça peut s'avérer très utile si vous avez besoin de donner la possibilité à +votre client de créer ses propres entités, mais aussi d'en importer depuis +l'ancienne base, car vous ne risquez pas d'avoir les collisions que vous auriez +en cas d'auto-increment. Autre chose, un UUID, c'est dur à deviner, alors que +si vous avez un entier, il est fort probable qu'une entité avec un entier +voisin existe également, information toujours bonne à prendre quand on est +malveillant. Autre information que ça peut laisser transparaitre: le nombre +d'entité. + +Maintenant qu'on a résolu le problème des ids, revenons à nos moutons. On a un +constructeur, avec n propriétés, et chaque propriété a x règles de validation… +ça peut vite devenir très lourd pour le constructeur, surtout que certaines +règles de validation devront aussi être vérifier si vous avez des mutateurs +(des méthodes qui changent vos propriétés). Pour résoudre ce problème, on peut +utiliser des value objects. Voilà à quoi ça peut ressembler. + +On a une classe, sans identités, avec seulement une ou deux propriétés qui vont +ensemble, et des règles métier pour les valider. Le nom de value object vient +du fait que ce genre d'objet est caractérisé par sa valeur, et que deux value +object doivent être considérés comme égaux si et seulement si toutes leurs +propriétés sont égales. Ce qui va se passer si vous partez là dessus, c'est que +vous allez passer au constructeur des value objects. + +Le problème avec cette approche, c'est que l'instanciation peut vite devenir +très pénible, car il faut ajouter un use statement par value object, plus celui +de la classe principale. + +Une bonne solution, c'est d'utiliser des constructeurs nommés, qui sont des +factory statiques. Je dis des parce que vous pouvez en avoir plusieurs. Ces +constructeurs nommés, vous allez leur passer des types primitifs et vous allez +pouvoir les charger de mettre des valeurs par défaut ou non suivant le +constructeur. Vous pouvez même mettre le constructeur en privé si vous voulez +forcer les gens à créer de nouveaux constructeurs nommés si ceux que vous avez +faits ne sont pas assez complets. + +Alors maintenant, on a un obtenu des entités qui ne sont plus des tableaux +améliorés. On n'est pas en train de modéliser une base de données, on a fait +les chose à l'endroit et on a modélisé le problème du client, code first, ou +même model first pour ceux qui commencent par faire de l'UML et génèrent +ensuite du code. Comment persister une structure arborescente de value objects +avec Doctrine? + +Deux solutions: les custom types, et les value objects. Commençons par la plus +simple, les custom types. + +Un custom type, c'est un type à vous, qui va devenir utilisable dans vos +mappings Doctrine au même type que string, json_array ou integer. +Pour en créer un, vous devez pour le moment étendre une classe de base, et +surcharger quelques méthodes. Dans le futur, il y aura probablement un gros +refactoring de cette partie pour que vous n'ayez plus qu'à implémenter une +interface. Il y a également gros à parier que `getName` disparaitra, de même +qu'il avait disparu dans le composant form de Symfony. Pour le moment, il vous +faut utiliser le même nom dans cette méthode et lors de l'enregistrement dans +la registre des types, qui se comme ceci. +Les 2 méthodes qui nous intéressent, `convertToPHPValue` et +`convertToDatabaseValue` doivent permettre de convertir une valeur provenant +d'une colonne de la base de données en value object et vice versa. Avec cette +méthode, le constructeur de vos value object va être appelé lors de +l'hydratation, et vous pourrez voir si vous avez oublié de migrer vos données +dans le cas où une nouvelle règle métier n'est pas respectée. +Le problème, c'est que ça ne gère qu'une seule colonne. Si vous désirez gérer +plusieurs colonnes, tournez vous plutôt vers les embeddables. + +Les embeddables sont des values objects marqué comme tels par du mapping +Doctrine, et embarqués dans votre classe de base. Dans la classe embarquée, +vous pouvez mapper chaque champ comme vous le feriez pour n'importe quelle +autre classe, et Doctrine va simplement stocker ces nouveaux champs dans la +même table que l'entité qui embarque l'embeddable. +L'annotation `Embedded` vous permet de spécifier un préfixe pour les colonnes +correspondant à la classe embarquée, ce qui vous permet d'éviter les +collisions. Notez que le constructeur de la classe embarquée n'est pas appelée +lors de l'hydratation. + +Le seul problème avec cette implémentation, c'est la nullabilité. Si votre +embeddable est optionnel, plutôt que de mettre null, doctrine va l'hydrater +avec un value object dont les propriétés seront null. + +Pour pallier ce problème un paquet est disponible, ce paquet utilise un +listener qui va agir en postLoad pour vérifier si l'embeddable est considéré +null. Si oui, il utilisera le composant property access pour mettre ce champ à +`null`. La conséquence, c'est qu'il vous faut un setter.