From 4fff86fd0f755b782226d8ab7d4b42e8a012985a Mon Sep 17 00:00:00 2001 From: Arthurlbc Date: Fri, 21 Mar 2025 11:06:10 +0100 Subject: [PATCH 1/5] fix --- .../src/Application/HTTP/Controller/Course/RemoveMember.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/back/src/Application/HTTP/Controller/Course/RemoveMember.php b/apps/back/src/Application/HTTP/Controller/Course/RemoveMember.php index f69df42..f84522c 100644 --- a/apps/back/src/Application/HTTP/Controller/Course/RemoveMember.php +++ b/apps/back/src/Application/HTTP/Controller/Course/RemoveMember.php @@ -10,7 +10,7 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; -final class RemoveMember extends AbstractController + { public function __construct( private MessageBus $messageBus, From e27c804d1f6cb8a39970be6ed7429668148e4846 Mon Sep 17 00:00:00 2001 From: Arthurlbc Date: Fri, 21 Mar 2025 14:43:42 +0100 Subject: [PATCH 2/5] update article --- .../HTTP/Controller/Course/RemoveMember.php | 2 +- doc/article-2.md | 159 +++++++++++++++ doc/article.md | 190 ++++++++++++++++++ 3 files changed, 350 insertions(+), 1 deletion(-) create mode 100644 doc/article-2.md create mode 100644 doc/article.md diff --git a/apps/back/src/Application/HTTP/Controller/Course/RemoveMember.php b/apps/back/src/Application/HTTP/Controller/Course/RemoveMember.php index f84522c..f69df42 100644 --- a/apps/back/src/Application/HTTP/Controller/Course/RemoveMember.php +++ b/apps/back/src/Application/HTTP/Controller/Course/RemoveMember.php @@ -10,7 +10,7 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; - +final class RemoveMember extends AbstractController { public function __construct( private MessageBus $messageBus, diff --git a/doc/article-2.md b/doc/article-2.md new file mode 100644 index 0000000..aa5450e --- /dev/null +++ b/doc/article-2.md @@ -0,0 +1,159 @@ +# Mapping Doctrine avec PHP : Une Alternative aux Annotations et XML + +## Introduction + +Lorsque l’on travaille avec Doctrine sous PHP/Symfony, on est souvent confronté à la problématique du **mapping des entités**. Les solutions courantes reposent sur les **annotations** ou les fichiers **XML/YAML**, mais elles présentent plusieurs inconvénients : +- Les annotations alourdissent les classes et mêlent logique métier et configuration ORM. +- Le mapping XML/YAML est souvent difficile à maintenir et à tester. + +Dans cet article, je vais vous présenter une alternative méconnue mais très pratique : **le mapping Doctrine avec PHP**. Cette approche, bien que nécessitant un léger apprentissage, apporte de nombreux avantages en termes de **lisibilité, flexibilité et testabilité**. + +## Pourquoi éviter le mapping par annotations ? + +Prenons un exemple classique d'entité avec un mapping par annotations : + +```php +/** + * @ORM\Entity() + * @ORM\Table(name="users") + */ +class User { + /** @ORM\Id @ORM\GeneratedValue @ORM\Column(type="integer") */ + private int $id; + + /** @ORM\Column(type="string", length=255) */ + private string $name; +} +``` + +Bien que cette approche soit simple, elle mélange **logique métier et configuration ORM**, ce qui peut poser des problèmes de lisibilité et de maintenance. + +## Le mapping Doctrine avec PHP + +Avec cette approche, nous séparons totalement le mapping de l’entité : + +```php +class User { + protected int $id; + + public function __construct(protected string $name) {} + + public function getId(): ?int { + return $this->id; + } + + public function getName(): string { + return $this->name; + } +} +``` + +Et voici le mapping correspondant dans une classe dédiée : + +```php +use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\Mapping\ClassMetadataBuilder; +use App\Infrastructure\Doctrine\ORM\Entity; + +class UserMapping { + public static function loadMetadata(ClassMetadata $metadata) { + $builder = new ClassMetadataBuilder($metadata); + $builder->setTable("users"); + + $builder->createField("id", Types::INTEGER) + ->makePrimaryKey() + ->generatedValue() + ->build(); + + $builder->createField("name", Types::STRING) + ->length(255) + ->nullable(false) + ->build(); + } +} +``` + +### ✅ **Avantages de cette approche** + +- **Séparation des responsabilités** : le modèle métier reste indépendant de l’ORM. +- **Meilleure lisibilité** et fichiers plus légers. +- **Testabilité accrue** : couvert par **PHPStan**, **php-cs-fixer**, et autres outils d’analyse. +- **Flexibilité** : plus facile à modifier en cas de changement d’ORM. + +### ❌ **Inconvénients** + +- Nécessite un peu plus de code et d’organisation. +- Moins intuitif au départ pour ceux habitués aux annotations. + +## Création de Types Personnalisés avec Doctrine + +Doctrine permet aussi de créer des types personnalisés, par exemple pour gérer des **Value Objects**. + +### Exemple : un type `Email` + +```php +use Doctrine\DBAL\Platforms\AbstractPlatform; +use Doctrine\DBAL\Types\Type; +use MyApp\Domain\ValueObject\Email; + +class EmailType extends Type { + public const NAME = 'email'; + + public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform) { + return "VARCHAR(255)"; + } + + public function convertToPHPValue($value, AbstractPlatform $platform) { + return new Email($value); + } + + public function convertToDatabaseValue($value, AbstractPlatform $platform) { + return $value instanceof Email ? (string)$value : null; + } + + public function getName() { + return self::NAME; + } +} +``` + +Cela permet d’intégrer proprement un `Email` en tant qu’objet de valeur au sein des entités. +```php + $builder->createField("email", EmailType::NAME) + ->nullable(false) + ->build(); +``` + +## Gestion des Relations entre Entités + +Doctrine facilite la gestion des relations entre entités, mais leur utilisation abusive peut alourdir les requêtes et rendre le projet moins scalable. + +Plutôt que de lier directement les entités avec `ManyToOne` ou `ManyToMany`, nous pouvons stocker les **identifiants** des entités liées sous forme de JSON et les récupérer uniquement lorsque c'est nécessaire. + +### Exemple d’implémentation + +```php +class Order { + private array $productIds = []; + + public function addProduct(int $productId) { + $this->productIds[] = $productId; + } + + public function getProducts(): array { + return $this->productIds; + } +} +``` + +**Pourquoi ?** +- **Moins de requêtes SQL complexes**. +- **Moins de couplage** entre les entités. +- **Plus grande flexibilité** dans l’évolution du modèle de données. + +## Conclusion + +L’utilisation du **mapping Doctrine avec PHP** permet de mieux structurer son code en séparant la logique métier de l’ORM. Associé à une architecture hexagonale et une gestion réfléchie des relations entre entités, cela garantit **une meilleure maintenabilité et une évolutivité accrue**. + +Cependant, cette approche demande un léger effort d’apprentissage et une bonne organisation du projet. Adoptez-la en fonction des besoins spécifiques de votre application ! 🚀 + diff --git a/doc/article.md b/doc/article.md new file mode 100644 index 0000000..0f8e40d --- /dev/null +++ b/doc/article.md @@ -0,0 +1,190 @@ +# Mapping Doctrine avec PHP + +Développeur chez KnpLabs, j’aimerais aujourd’hui vous parler de **mapping**. Cet article fait suite à un projet en PHP/Symfony où nous avons choisi **Doctrine** comme ORM. + +Nous avons tous déjà rencontré la problématique d’entités surchargées d’annotations, que nous avons résolue en déplaçant ce mapping dans du XML, parfois mal formaté et jamais testé. + +J’aimerais vous présenter une autre alternative : + +**Méconnu mais pourtant si pratique**, plein d’avantages, mais ayant certains prérequis, le mapping Doctrine avec PHP est un outil qui m’a personnellement beaucoup plu. C’est dans le cadre d’un projet **from scratch**, avec un découpage en oignon permettant une séparation claire des responsabilités en trois couches distinctes, que nous avons choisi d’utiliser le mapping Doctrine avec PHP. + +L’architecture choisie a pour but de séparer la partie métier pure des dépendances et interactions que peut avoir l’application. Ainsi, notre projet n’est pas fortement dépendant des modifications (non-maintenance ou suppression) des différentes librairies utilisées. + +Pour plus d’informations, je vous invite à lire cet article : [Architecture hexagonal](https://TODO). + +Il est déconseillé d’utiliser directement un modèle comme entité Doctrine, car cela ne respecte pas les standards de l’architecture hexagonale en mélangeant logique métier et configuration ORM, en plus d’alourdir les fichiers, ce qui les rend illisibles. + +## Exemple : Mapping directement dans le modèle + +En plus de la présence de l’ORM dans le domaine, c’est déjà chargé pour une classe pourtant simple ! + +```php +/** + * @ORM\Entity() + * @ORM\Table(name="users") + */ +class User { + /** @ORM\Id @ORM\GeneratedValue @ORM\Column(type="integer") */ + private int $id; + + public function __construct( + /** @ORM\Column(type="string", length=255) */ + private string $name + ){} + + public function getId(): int + { + return $this->id; + } + + public function getName(): string + { + return $this->name; + } +} +``` + +Même classe représentant notre modèle du domaine : +```php +class User { + protected int $id; + + public function __construct(protected string $name) {} + + public function getId(): ?int { + return $this->id; + } + + public function getName(): string { + return $this->name; + } +} +``` + +Et son mapping dans l’infrastructure : + +```php +class UserMapping extends User +{ + public static function loadMetadata(ClassMetadata $metadata) { + + $builder = new ClassMetadataBuilder($metadata); + + $builder->setTable("users"); + + $builder->createField("id", Types::INTEGER) + ->makePrimaryKey() + ->generatedValue() + ->build(); + + $builder->createField("name", Types::STRING) + ->length(255) + ->nullable(false) + ->build(); + } +} +``` +→ **plus de lisibilité et séparation des responsabilités !** + +## Avantages de notre approche : + +- Sépare le modèle du domaine (classe **PHP** "pure") de son son mapping dans l’infrastructure: Pas de logique de dépendances dans notre logique métier. +- Cela permet d’avoir une implémentation plus concise et logique. +- Nous évitons aussi la nécessité d’un troisième fichier dédié au mapping, ce qui simplifie la structure du projet. +- **Testable !** +- Couvert par les différents outils (**php-cs-fixer**, **PHPStan**...). + +## Inconvénients : + +- La prise en main. +- Cela peut paraître verbeux à première vue. + +Comme vu sur les écrans, nous pouvons créer une entité qui étend notre modèle. Ensuite, **Doctrine** nous fournit tous les outils nécessaires pour créer notre mapping. Je conseille tout de même d’abstraire cette logique via des services personnalisé qui seront la seul partie du code a modifier en cas de changement d'ORM. Je vous invite à consulter la classe **ClassMetaData** de Doctrine et la classe **ClassMetaDataBuilder**. L’une permet de charger votre entité et l’autre de construire votre mapping. + +## Création de Types Personnalisés avec Doctrine + +Il est possible de créer des types personnalisés en complément des types fournis par Doctrine. Pour cela, il faut : + +- Créer une classe qui étend **Type** de Doctrine. +- Redéfinir les méthodes **convertToDatabaseValue()** et **convertToPHPValue()**. + +Pour maintenir un faible couplage entre les dépendances et notre code, nous pouvons aussi définir une classe abstraite qui étend **Type** et qui déclare des méthodes **normalize()** et **denormalize()**. Ces méthodes seront redéfinies dans chaque sous-classe en fonction des besoins spécifiques. + +Chaque type personnalisé doit également définir une constante pour le nom de la colonne et implémenter la méthode **getName()** qui retourne cette valeur. + +## Exemple +```php +class EmailType extends Type { + public const NAME = 'email'; + + public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform) { + return "VARCHAR(255)"; + } + + public function convertToPHPValue($value, AbstractPlatform $platform) { + return new Email($value); + } + + public function convertToDatabaseValue($value, AbstractPlatform $platform) { + return $value instanceof Email ? (string) $value : null; + } + + public function getName() { + return self::NAME; + } +} +``` + +Cela permet d’intégrer proprement un `Email` en tant qu’objet de valeur au sein des entités. +```php + $builder->createField("email", EmailType::NAME) + ->nullable(false) + ->build(); +``` + +## Gestion des Relations entre Entités + +Les relations **Many-to-Many** et **Many-to-One** sont bien sûr possibles avec le mapping en PHP, mais elles peuvent rapidement complexifier un projet à grande échelle. De mon point de vue, il est préférable d’éviter ces liens directs entre entités dans le cas de projet amener a évoluer et évoluer. Pour des petits projets la problèmatique n'est pas la même et souvent la rapidité de création sera a privilegié. + +Pour des projets de moyenne à grande envergure, mettre en place ces relations au début du projet peut vite aboutir à des requêtes très complexes, fetchant des données parfois non utiles. + +À la place, j’ai choisi de stocker sous forme de **JSON** en base de données et d'array côté **PHP** les identifiants des modèles liés à mon entité. Ensuite, à l’aide de méthodes **add()**, **remove()**, **has()** et **get()**, je peux interagir avec ces identifiants et les récupérer au besoin. Mes **UseCase** étant wrapés dans une transaction Doctrine via le middleware **doctrine_transaction**, je ne manipule ainsi que des ids et fetch uniquement les objets requis par mon cas d’utilisation. + +```php +class Order { + private array $productIds = []; + + public function addProduct(int $productId) { + $this->productIds[] = $productId; + } + + public function getProducts(): array { + return $this->productIds; + } + + public function hasProduct(string $productId): bool + { + return in_array($productId, $this->productIds); + } + + public function removeProduct(string $productIdToRemove): void + { + if ($this->hasProduct($productIdToRemove)) { + $this->productIds = array_filter($this->productIds, fn ($productId) => $productId !== $productIdToRemove); + } + } +} +``` + +Cette approche permet : + +- Une plus grande flexibilité dans la gestion des relations. +- Une simplification de la gestion des dépendances entre entités. +- Une meilleure évolutivité en cas de changements dans le modèle de données. + +## Conclusion + +L’adoption de l’architecture hexagonale et du découpage en oignon, couplée à Doctrine, nous a permis d’organiser notre code de manière claire et évolutive. En séparant la logique métier des dépendances externes, nous gagnons en maintenabilité et en testabilité. De plus, le mapping PHP permet d’être couvert par des outils d'analyse statique comme **PHPStan**. + +Pour un exemple complet, consultez ce repository GitHub : [Mapping Doctrine avec PHP](https://github.com/Arthurlbc/Mapping-with-php). + From d1f36278b9e5e08c404642c22994917343870781 Mon Sep 17 00:00:00 2001 From: Arthurlbc Date: Fri, 21 Mar 2025 15:58:34 +0100 Subject: [PATCH 3/5] fix: Bobby's review --- doc/article-2.md | 159 ----------------------------------------------- doc/article.md | 40 ++++++------ 2 files changed, 21 insertions(+), 178 deletions(-) delete mode 100644 doc/article-2.md diff --git a/doc/article-2.md b/doc/article-2.md deleted file mode 100644 index aa5450e..0000000 --- a/doc/article-2.md +++ /dev/null @@ -1,159 +0,0 @@ -# Mapping Doctrine avec PHP : Une Alternative aux Annotations et XML - -## Introduction - -Lorsque l’on travaille avec Doctrine sous PHP/Symfony, on est souvent confronté à la problématique du **mapping des entités**. Les solutions courantes reposent sur les **annotations** ou les fichiers **XML/YAML**, mais elles présentent plusieurs inconvénients : -- Les annotations alourdissent les classes et mêlent logique métier et configuration ORM. -- Le mapping XML/YAML est souvent difficile à maintenir et à tester. - -Dans cet article, je vais vous présenter une alternative méconnue mais très pratique : **le mapping Doctrine avec PHP**. Cette approche, bien que nécessitant un léger apprentissage, apporte de nombreux avantages en termes de **lisibilité, flexibilité et testabilité**. - -## Pourquoi éviter le mapping par annotations ? - -Prenons un exemple classique d'entité avec un mapping par annotations : - -```php -/** - * @ORM\Entity() - * @ORM\Table(name="users") - */ -class User { - /** @ORM\Id @ORM\GeneratedValue @ORM\Column(type="integer") */ - private int $id; - - /** @ORM\Column(type="string", length=255) */ - private string $name; -} -``` - -Bien que cette approche soit simple, elle mélange **logique métier et configuration ORM**, ce qui peut poser des problèmes de lisibilité et de maintenance. - -## Le mapping Doctrine avec PHP - -Avec cette approche, nous séparons totalement le mapping de l’entité : - -```php -class User { - protected int $id; - - public function __construct(protected string $name) {} - - public function getId(): ?int { - return $this->id; - } - - public function getName(): string { - return $this->name; - } -} -``` - -Et voici le mapping correspondant dans une classe dédiée : - -```php -use Doctrine\ORM\Mapping\ClassMetadata; -use Doctrine\ORM\Mapping\ClassMetadataBuilder; -use App\Infrastructure\Doctrine\ORM\Entity; - -class UserMapping { - public static function loadMetadata(ClassMetadata $metadata) { - $builder = new ClassMetadataBuilder($metadata); - $builder->setTable("users"); - - $builder->createField("id", Types::INTEGER) - ->makePrimaryKey() - ->generatedValue() - ->build(); - - $builder->createField("name", Types::STRING) - ->length(255) - ->nullable(false) - ->build(); - } -} -``` - -### ✅ **Avantages de cette approche** - -- **Séparation des responsabilités** : le modèle métier reste indépendant de l’ORM. -- **Meilleure lisibilité** et fichiers plus légers. -- **Testabilité accrue** : couvert par **PHPStan**, **php-cs-fixer**, et autres outils d’analyse. -- **Flexibilité** : plus facile à modifier en cas de changement d’ORM. - -### ❌ **Inconvénients** - -- Nécessite un peu plus de code et d’organisation. -- Moins intuitif au départ pour ceux habitués aux annotations. - -## Création de Types Personnalisés avec Doctrine - -Doctrine permet aussi de créer des types personnalisés, par exemple pour gérer des **Value Objects**. - -### Exemple : un type `Email` - -```php -use Doctrine\DBAL\Platforms\AbstractPlatform; -use Doctrine\DBAL\Types\Type; -use MyApp\Domain\ValueObject\Email; - -class EmailType extends Type { - public const NAME = 'email'; - - public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform) { - return "VARCHAR(255)"; - } - - public function convertToPHPValue($value, AbstractPlatform $platform) { - return new Email($value); - } - - public function convertToDatabaseValue($value, AbstractPlatform $platform) { - return $value instanceof Email ? (string)$value : null; - } - - public function getName() { - return self::NAME; - } -} -``` - -Cela permet d’intégrer proprement un `Email` en tant qu’objet de valeur au sein des entités. -```php - $builder->createField("email", EmailType::NAME) - ->nullable(false) - ->build(); -``` - -## Gestion des Relations entre Entités - -Doctrine facilite la gestion des relations entre entités, mais leur utilisation abusive peut alourdir les requêtes et rendre le projet moins scalable. - -Plutôt que de lier directement les entités avec `ManyToOne` ou `ManyToMany`, nous pouvons stocker les **identifiants** des entités liées sous forme de JSON et les récupérer uniquement lorsque c'est nécessaire. - -### Exemple d’implémentation - -```php -class Order { - private array $productIds = []; - - public function addProduct(int $productId) { - $this->productIds[] = $productId; - } - - public function getProducts(): array { - return $this->productIds; - } -} -``` - -**Pourquoi ?** -- **Moins de requêtes SQL complexes**. -- **Moins de couplage** entre les entités. -- **Plus grande flexibilité** dans l’évolution du modèle de données. - -## Conclusion - -L’utilisation du **mapping Doctrine avec PHP** permet de mieux structurer son code en séparant la logique métier de l’ORM. Associé à une architecture hexagonale et une gestion réfléchie des relations entre entités, cela garantit **une meilleure maintenabilité et une évolutivité accrue**. - -Cependant, cette approche demande un léger effort d’apprentissage et une bonne organisation du projet. Adoptez-la en fonction des besoins spécifiques de votre application ! 🚀 - diff --git a/doc/article.md b/doc/article.md index 0f8e40d..d768e4d 100644 --- a/doc/article.md +++ b/doc/article.md @@ -1,14 +1,14 @@ # Mapping Doctrine avec PHP -Développeur chez KnpLabs, j’aimerais aujourd’hui vous parler de **mapping**. Cet article fait suite à un projet en PHP/Symfony où nous avons choisi **Doctrine** comme ORM. +Développeur chez KnpLabs, j’aimerais aujourd’hui vous parler de **mapping**. Cet article fait suite à un projet en PHP/Symfony où nous avons choisi **Doctrine** comme ORM (Object Relational Mapper). -Nous avons tous déjà rencontré la problématique d’entités surchargées d’annotations, que nous avons résolue en déplaçant ce mapping dans du XML, parfois mal formaté et jamais testé. +Nous avons tous déjà été confrontés à la problématique d’entités surchargées d’annotations, que nous avons résolue en déplaçant ce mapping dans du XML, parfois mal formaté et jamais testé. -J’aimerais vous présenter une autre alternative : +J’aimerais vous présenter une alternative : -**Méconnu mais pourtant si pratique**, plein d’avantages, mais ayant certains prérequis, le mapping Doctrine avec PHP est un outil qui m’a personnellement beaucoup plu. C’est dans le cadre d’un projet **from scratch**, avec un découpage en oignon permettant une séparation claire des responsabilités en trois couches distinctes, que nous avons choisi d’utiliser le mapping Doctrine avec PHP. +**Méconnu mais pourtant si pratique**, plein d’avantages, mais avec certains prérequis, le mapping Doctrine avec PHP est une technique qui m’a personnellement beaucoup plu. C’est dans le cadre d’un projet **from scratch**, avec un découpage du code en oignon permettant une séparation claire des responsabilités en trois couches distinctes, que nous avons choisi d’utiliser le mapping Doctrine avec PHP. -L’architecture choisie a pour but de séparer la partie métier pure des dépendances et interactions que peut avoir l’application. Ainsi, notre projet n’est pas fortement dépendant des modifications (non-maintenance ou suppression) des différentes librairies utilisées. +L’architecture choisie a pour but de séparer la partie métier pure des dépendances et interactions que peut avoir l’application. Ainsi, notre projet n’est pas fortement dépendant des modifications (non-maintenance ou suppression des librairies utilisées). Pour plus d’informations, je vous invite à lire cet article : [Architecture hexagonal](https://TODO). @@ -28,8 +28,8 @@ class User { private int $id; public function __construct( - /** @ORM\Column(type="string", length=255) */ - private string $name + /** @ORM\Column(type="string", length=255) */ + private string $name ){} public function getId(): int @@ -51,7 +51,7 @@ class User { public function __construct(protected string $name) {} - public function getId(): ?int { + public function getId(): int { return $this->id; } @@ -75,6 +75,7 @@ class UserMapping extends User $builder->createField("id", Types::INTEGER) ->makePrimaryKey() ->generatedValue() + ->nullable(false) ->build(); $builder->createField("name", Types::STRING) @@ -88,18 +89,18 @@ class UserMapping extends User ## Avantages de notre approche : -- Sépare le modèle du domaine (classe **PHP** "pure") de son son mapping dans l’infrastructure: Pas de logique de dépendances dans notre logique métier. -- Cela permet d’avoir une implémentation plus concise et logique. -- Nous évitons aussi la nécessité d’un troisième fichier dédié au mapping, ce qui simplifie la structure du projet. +- Sépare le modèle du domaine (classe **PHP** "pure") de son mapping dans l’infrastructure: Pas de dépendances dans notre logique métier. +- Cela permet une implémentation plus concise et logique. +- Nous évitons aussi la nécessité d’un troisième fichier de mapping, ce qui simplifie la structure du projet. - **Testable !** - Couvert par les différents outils (**php-cs-fixer**, **PHPStan**...). ## Inconvénients : - La prise en main. -- Cela peut paraître verbeux à première vue. +- Cela peut paraître complexe à première vue. -Comme vu sur les écrans, nous pouvons créer une entité qui étend notre modèle. Ensuite, **Doctrine** nous fournit tous les outils nécessaires pour créer notre mapping. Je conseille tout de même d’abstraire cette logique via des services personnalisé qui seront la seul partie du code a modifier en cas de changement d'ORM. Je vous invite à consulter la classe **ClassMetaData** de Doctrine et la classe **ClassMetaDataBuilder**. L’une permet de charger votre entité et l’autre de construire votre mapping. +Comme vu sur les exemples, nous pouvons créer une entité qui étend notre modèle. Ensuite, **Doctrine** nous fournit tous les outils nécessaires pour créer notre mapping. Je vous invite à consulter la classe **ClassMetaData** de Doctrine et la classe **ClassMetaDataBuilder**. L’une permet de charger votre entité et l’autre de construire votre mapping. ## Création de Types Personnalisés avec Doctrine @@ -108,8 +109,6 @@ Il est possible de créer des types personnalisés en complément des types four - Créer une classe qui étend **Type** de Doctrine. - Redéfinir les méthodes **convertToDatabaseValue()** et **convertToPHPValue()**. -Pour maintenir un faible couplage entre les dépendances et notre code, nous pouvons aussi définir une classe abstraite qui étend **Type** et qui déclare des méthodes **normalize()** et **denormalize()**. Ces méthodes seront redéfinies dans chaque sous-classe en fonction des besoins spécifiques. - Chaque type personnalisé doit également définir une constante pour le nom de la colonne et implémenter la méthode **getName()** qui retourne cette valeur. ## Exemple @@ -142,13 +141,16 @@ Cela permet d’intégrer proprement un `Email` en tant qu’objet de valeur au ->build(); ``` +Je conseille tout de même d’abstraire cette logique via des services personnalisés qui seront la seule partie du code a modifier en cas de changement d'ORM. +Et pour maintenir un faible couplage entre les dépendances et notre code, nous pouvons définir une classe abstraite qui étend **Type** et qui déclare des méthodes **normalize()** et **denormalize()**. Ces méthodes seront redéfinies dans chaque sous-classe en fonction des besoins spécifiques. + ## Gestion des Relations entre Entités -Les relations **Many-to-Many** et **Many-to-One** sont bien sûr possibles avec le mapping en PHP, mais elles peuvent rapidement complexifier un projet à grande échelle. De mon point de vue, il est préférable d’éviter ces liens directs entre entités dans le cas de projet amener a évoluer et évoluer. Pour des petits projets la problèmatique n'est pas la même et souvent la rapidité de création sera a privilegié. +Les relations **Many-to-Many** et **Many-to-One** sont bien sûr possibles avec le mapping en PHP, mais elles peuvent rapidement rendre complexe un projet à grande échelle. De mon point de vue, il est préférable d’éviter ces liens directs entre entités dans le cas de projets amenés à évoluer. Pour des petits projets, la problématique n'est pas la même, et la rapidité de création sera souvent privilégiée -Pour des projets de moyenne à grande envergure, mettre en place ces relations au début du projet peut vite aboutir à des requêtes très complexes, fetchant des données parfois non utiles. +Pour des projets de moyenne à grande envergure, mettre en place ces relations au début du projet peut vite aboutir à des requêtes très complexes, qui récupèrent des données parfois inutiles. -À la place, j’ai choisi de stocker sous forme de **JSON** en base de données et d'array côté **PHP** les identifiants des modèles liés à mon entité. Ensuite, à l’aide de méthodes **add()**, **remove()**, **has()** et **get()**, je peux interagir avec ces identifiants et les récupérer au besoin. Mes **UseCase** étant wrapés dans une transaction Doctrine via le middleware **doctrine_transaction**, je ne manipule ainsi que des ids et fetch uniquement les objets requis par mon cas d’utilisation. +À la place, j’ai choisi de stocker sous forme de **JSON** en base de données et d'array côté **PHP** les identifiants des modèles liés à mon entité. Ensuite, à l’aide des méthodes **add()**, **remove()**, **has()** et **get()**, je peux interagir avec ces identifiants et les récupérer au besoin. Mes **UseCase** étant encapsulés dans une transaction Doctrine via le middleware **doctrine_transaction**, je ne manipule ainsi que des ids et fetch uniquement les objets requis par mon cas d’utilisation. ```php class Order { @@ -186,5 +188,5 @@ Cette approche permet : L’adoption de l’architecture hexagonale et du découpage en oignon, couplée à Doctrine, nous a permis d’organiser notre code de manière claire et évolutive. En séparant la logique métier des dépendances externes, nous gagnons en maintenabilité et en testabilité. De plus, le mapping PHP permet d’être couvert par des outils d'analyse statique comme **PHPStan**. -Pour un exemple complet, consultez ce repository GitHub : [Mapping Doctrine avec PHP](https://github.com/Arthurlbc/Mapping-with-php). +Pour un exemple complet, consultez ce dépôt GitHub : [Mapping Doctrine avec PHP](https://github.com/Arthurlbc/Mapping-with-php). From 514362364bffc9142dfb6eef8c7dc738b6191775 Mon Sep 17 00:00:00 2001 From: Arthurlbc Date: Mon, 7 Apr 2025 08:54:44 +0200 Subject: [PATCH 4/5] fix: Louis's review and update makefile and readme --- README.md | 62 ++++++++++ apps/back/composer.json | 1 + apps/back/composer.lock | 71 +++++++++++- apps/back/public/images/edgar.jpg | Bin 0 -> 5269 bytes .../src/Application/HTTP/Controller/Home.php | 2 +- apps/back/templates/base.html.twig | 3 + doc/article.md | 108 ++++++++++++++---- makefile | 20 +--- 8 files changed, 231 insertions(+), 36 deletions(-) create mode 100644 README.md create mode 100644 apps/back/public/images/edgar.jpg diff --git a/README.md b/README.md new file mode 100644 index 0000000..b3700a4 --- /dev/null +++ b/README.md @@ -0,0 +1,62 @@ +# Projet Symfony avec Docker + +Ce projet utilise **Docker Compose** pour gérer un environnement Symfony avec une base de données. + +## 📌 Prérequis + +Avant de démarrer, assurez-vous d'avoir installé : + +- [Docker](https://www.docker.com/) +- [Docker Compose](https://docs.docker.com/compose/) + +## 🚀 Installation et démarrage du projet + +### 1️⃣ Copier le fichier `.env.dist` vers `.env` +Le fichier `.env` contient les variables d'environnement nécessaires au projet. + +```sh +make copy-env +``` + +### 2️⃣ Démarrer le projet +Pour lancer le projet avec Docker : + +```sh +make start +``` + +Cela effectuera les actions suivantes : +- Lancer les conteneurs en arrière-plan. +- Créer la base de données si elle n'existe pas. +- Appliquer les migrations. +- Charger les données de test (fixtures). + +### 3️⃣ Arrêter le projet +Pour arrêter les conteneurs : + +```sh +make stop +``` + +## 🛠 Commandes utiles + +### 📖 Afficher l'aide des commandes disponibles +```sh +make help +``` + +### 🛡 Vérifications et corrections +- **Analyse du code avec PHPStan** : + ```sh + make phpstan + ``` +- **Correction du code avec PHP-CS-Fixer** : + ```sh + make php-cs-fixer + ``` +- **Exécuter les tests avec PHPSpec** : + ```sh + make phpspec + ``` + +👨‍💻 **Happy coding!** diff --git a/apps/back/composer.json b/apps/back/composer.json index a5b16fb..3b155aa 100644 --- a/apps/back/composer.json +++ b/apps/back/composer.json @@ -11,6 +11,7 @@ "doctrine/doctrine-bundle": "^2.13", "doctrine/doctrine-migrations-bundle": "^3.0", "doctrine/orm": "^3.3", + "symfony/asset": "7.2.*", "symfony/console": "7.2.*", "symfony/dotenv": "7.2.*", "symfony/flex": "^2", diff --git a/apps/back/composer.lock b/apps/back/composer.lock index 597c751..687d014 100644 --- a/apps/back/composer.lock +++ b/apps/back/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "9208ed220cfebac40fbd256c6311a205", + "content-hash": "ce711da6ac88c3862b7167d680d2c93c", "packages": [ { "name": "doctrine/cache", @@ -1470,6 +1470,75 @@ }, "time": "2024-09-11T13:17:53+00:00" }, + { + "name": "symfony/asset", + "version": "v7.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/asset.git", + "reference": "cb926cd59fefa1f9b4900b3695f0f846797ba5c0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/asset/zipball/cb926cd59fefa1f9b4900b3695f0f846797ba5c0", + "reference": "cb926cd59fefa1f9b4900b3695f0f846797ba5c0", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "conflict": { + "symfony/http-foundation": "<6.4" + }, + "require-dev": { + "symfony/http-client": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Asset\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Manages URL generation and versioning of web assets such as CSS stylesheets, JavaScript files and image files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/asset/tree/v7.2.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-10-25T15:15:23+00:00" + }, { "name": "symfony/cache", "version": "v7.2.4", diff --git a/apps/back/public/images/edgar.jpg b/apps/back/public/images/edgar.jpg new file mode 100644 index 0000000000000000000000000000000000000000..db664ba69e5a5c001d36e606e8b1a37963a31ff0 GIT binary patch literal 5269 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 diff --git a/apps/back/templates/base.html.twig b/apps/back/templates/base.html.twig index 3db3fc9..3206937 100644 --- a/apps/back/templates/base.html.twig +++ b/apps/back/templates/base.html.twig @@ -12,6 +12,9 @@ {% endblock %} + + Retour aux cours + {% block body %}{% endblock %} diff --git a/doc/article.md b/doc/article.md index d768e4d..c915628 100644 --- a/doc/article.md +++ b/doc/article.md @@ -1,22 +1,20 @@ # Mapping Doctrine avec PHP -Développeur chez KnpLabs, j’aimerais aujourd’hui vous parler de **mapping**. Cet article fait suite à un projet en PHP/Symfony où nous avons choisi **Doctrine** comme ORM (Object Relational Mapper). +Développeur chez **KnpLabs**, j’aimerais aujourd’hui vous parler de **mapping**. Cet article fait suite à un projet en PHP/Symfony où nous avons choisi **Doctrine** comme ORM (Object Relational Mapper). Nous avons tous déjà été confrontés à la problématique d’entités surchargées d’annotations, que nous avons résolue en déplaçant ce mapping dans du XML, parfois mal formaté et jamais testé. -J’aimerais vous présenter une alternative : +Cet article aborde une alternative : **Méconnu mais pourtant si pratique**, plein d’avantages, mais avec certains prérequis, le mapping Doctrine avec PHP est une technique qui m’a personnellement beaucoup plu. C’est dans le cadre d’un projet **from scratch**, avec un découpage du code en oignon permettant une séparation claire des responsabilités en trois couches distinctes, que nous avons choisi d’utiliser le mapping Doctrine avec PHP. -L’architecture choisie a pour but de séparer la partie métier pure des dépendances et interactions que peut avoir l’application. Ainsi, notre projet n’est pas fortement dépendant des modifications (non-maintenance ou suppression des librairies utilisées). +Pour plus d’informations, je vous invite à lire cet article : [Architecture hexagonale avec Symfony](https://knplabs.com/fr/blog/architecture-hexagonale-comment-ladopter/). -Pour plus d’informations, je vous invite à lire cet article : [Architecture hexagonal](https://TODO). - -Il est déconseillé d’utiliser directement un modèle comme entité Doctrine, car cela ne respecte pas les standards de l’architecture hexagonale en mélangeant logique métier et configuration ORM, en plus d’alourdir les fichiers, ce qui les rend illisibles. +Mais quelle que soit l'architecture que vous choisissez pour votre projet, le mapping avec PHP est possible et permettra d'alléger vos entités, dans le cas où vous utilisiez les annotations Doctrine, et surtout de séparer votre modèle de données de leur implémentation avec l'ORM sans pour autant utiliser des fichiers XML. ## Exemple : Mapping directement dans le modèle -En plus de la présence de l’ORM dans le domaine, c’est déjà chargé pour une classe pourtant simple ! +En plus de la présence de l’ORM dans une classe représentant un modèle métier, c’est déjà chargé pour une classe pourtant simple ! ```php /** @@ -44,7 +42,7 @@ class User { } ``` -Même classe représentant notre modèle du domaine : +Même classe sans annotations (c'est déjà plus léger) : ```php class User { protected int $id; @@ -87,20 +85,20 @@ class UserMapping extends User ``` → **plus de lisibilité et séparation des responsabilités !** -## Avantages de notre approche : +## Avantages de cette approche : -- Sépare le modèle du domaine (classe **PHP** "pure") de son mapping dans l’infrastructure: Pas de dépendances dans notre logique métier. +- Sépare le modèle du domaine (classe **PHP** "pure") de son mapping : Pas de dépendances dans la logique métier. - Cela permet une implémentation plus concise et logique. -- Nous évitons aussi la nécessité d’un troisième fichier de mapping, ce qui simplifie la structure du projet. -- **Testable !** +- Pas de troisième fichier de mapping, ce qui simplifie la structure du projet. +- Simplicité de définition de types personnalisés (comme pour les value objects) -> et **Testable !** avec phpspec par exemple. - Couvert par les différents outils (**php-cs-fixer**, **PHPStan**...). ## Inconvénients : -- La prise en main. -- Cela peut paraître complexe à première vue. +- Changement d'habitude -> Nécessite une prise en main +- La verbosité à première vue, mais qui garantit une totale maîtrise du mapping. -Comme vu sur les exemples, nous pouvons créer une entité qui étend notre modèle. Ensuite, **Doctrine** nous fournit tous les outils nécessaires pour créer notre mapping. Je vous invite à consulter la classe **ClassMetaData** de Doctrine et la classe **ClassMetaDataBuilder**. L’une permet de charger votre entité et l’autre de construire votre mapping. +Comme vu sur les exemples, nous pouvons créer une entité qui étend notre modèle. Ensuite, **Doctrine** nous fournit tous les outils nécessaires pour créer notre mapping. Je vous invite à consulter la classe **[ClassMetadata](https://github.com/andreia/doctrine/blob/master/lib/Doctrine/ORM/Mapping/ClassMetadata.php)** de Doctrine et la classe **[ClassMetadataBuilder](https://github.com/webmozart/doctrine-orm/blob/master/lib/Doctrine/ORM/Mapping/Builder/ClassMetadataBuilder.php)**. L’une permet de charger votre entité et l’autre de construire votre mapping. ## Création de Types Personnalisés avec Doctrine @@ -141,16 +139,85 @@ Cela permet d’intégrer proprement un `Email` en tant qu’objet de valeur au ->build(); ``` -Je conseille tout de même d’abstraire cette logique via des services personnalisés qui seront la seule partie du code a modifier en cas de changement d'ORM. -Et pour maintenir un faible couplage entre les dépendances et notre code, nous pouvons définir une classe abstraite qui étend **Type** et qui déclare des méthodes **normalize()** et **denormalize()**. Ces méthodes seront redéfinies dans chaque sous-classe en fonction des besoins spécifiques. +Il est tout de même préférable d’abstraire cette logique via des services personnalisés qui seront la seule partie du code à modifier en cas de changement d'ORM. +Et pour maintenir un faible couplage entre les dépendances et notre code, nous pouvons définir une classe abstraite qui étend **Type** et qui déclare des méthodes **normalize()** et **denormalize()**. Ces méthodes seront redéfinies dans chaque sous-classe en fonction des besoins spécifiques, comme dans l'exemple ci-dessous : + +```php +getStringTypeDeclarationSQL($column); + } + + /** + * @param ?ValueObject $value + * + * @return ?Normalization + */ + public function convertToDatabaseValue( + mixed $value, + AbstractPlatform $platform, + ): mixed { + if (null === $value) { + return null; + } + + return $this->normalize($value); + } + + /** + * @param ?Normalization $value + * + * @return ?ValueObject + */ + public function convertToPHPValue( + mixed $value, + AbstractPlatform $platform, + ): mixed { + if (null === $value) { + return null; + } + + return $this->denormalize($value); + } + + /** + * @param ValueObject $valueObject + * + * @return Normalization + */ + abstract protected function normalize(mixed $valueObject): mixed; + + /** + * @param Normalization $normalized + * + * @return ValueObject + */ + abstract protected function denormalize(mixed $normalized): mixed; +} +``` ## Gestion des Relations entre Entités -Les relations **Many-to-Many** et **Many-to-One** sont bien sûr possibles avec le mapping en PHP, mais elles peuvent rapidement rendre complexe un projet à grande échelle. De mon point de vue, il est préférable d’éviter ces liens directs entre entités dans le cas de projets amenés à évoluer. Pour des petits projets, la problématique n'est pas la même, et la rapidité de création sera souvent privilégiée +Les relations **One-to-Many** et **Many-to-One** sont bien sûr possibles avec le mapping en PHP, mais elles peuvent rapidement rendre complexe un projet à grande échelle. Il est possible d’éviter ces liens directs entre entités dans le cas de projets amenés à évoluer. Pour des petits projets, la problématique n'est pas la même, et la rapidité de création sera souvent privilégiée Pour des projets de moyenne à grande envergure, mettre en place ces relations au début du projet peut vite aboutir à des requêtes très complexes, qui récupèrent des données parfois inutiles. -À la place, j’ai choisi de stocker sous forme de **JSON** en base de données et d'array côté **PHP** les identifiants des modèles liés à mon entité. Ensuite, à l’aide des méthodes **add()**, **remove()**, **has()** et **get()**, je peux interagir avec ces identifiants et les récupérer au besoin. Mes **UseCase** étant encapsulés dans une transaction Doctrine via le middleware **doctrine_transaction**, je ne manipule ainsi que des ids et fetch uniquement les objets requis par mon cas d’utilisation. +À la place, j’ai choisi de stocker sous forme de **JSON** en base de données et d'array côté **PHP** les identifiants des modèles liés à mon entité. Ensuite, à l’aide des méthodes **add()**, **remove()**, **has()** et **get()**, je peux interagir avec ces identifiants et les récupérer au besoin. Mes **UseCase** étant encapsulés dans une transaction Doctrine via le middleware **doctrine_transaction**, je ne manipule ainsi que des IDs et fetch uniquement les objets requis par mon cas d’utilisation. ```php class Order { @@ -186,7 +253,8 @@ Cette approche permet : ## Conclusion -L’adoption de l’architecture hexagonale et du découpage en oignon, couplée à Doctrine, nous a permis d’organiser notre code de manière claire et évolutive. En séparant la logique métier des dépendances externes, nous gagnons en maintenabilité et en testabilité. De plus, le mapping PHP permet d’être couvert par des outils d'analyse statique comme **PHPStan**. +Merci de votre attention, c’est la fin de cet article sur le mapping doctrine avec PHP. Comme nous avons pu le voir cette méthode apporte avantages et inconvénients, et comme tout en développement ne doit être utilisé qu'après réflexion sur le besoin de l'application. +Dans le cadre d'une application avec un métier complexe, elle trouve tout son intérêt, pour sa maintenabilité, sa séparation du code métier et sa fluidité une fois les mécanismes en place. Pour un exemple complet, consultez ce dépôt GitHub : [Mapping Doctrine avec PHP](https://github.com/Arthurlbc/Mapping-with-php). diff --git a/makefile b/makefile index 693fda0..abf58fc 100644 --- a/makefile +++ b/makefile @@ -3,27 +3,19 @@ help: ## Display this current help @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-25s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) -##@ General - -.PHONY=install-dev copy-env start - copy-env: ## Copy .env.dist to .env cp -n .env.dist .env -start: ## Start project - docker compose up -d +## Start project +start: + docker compose up -d && \ + docker compose run --rm php bin/console doctrine:database:create --if-not-exists && \ + docker compose run --rm php bin/console doctrine:migrations:migrate --no-interaction && \ + docker compose run --rm php bin/console doctrine:fixtures:load --no-interaction stop: ## Stop project docker compose stop -install-dev: ## Install symfony - @echo -n "Are you sure to reinstall Symfony (current Symfony project will be lost?) [y/N] " && read ans && [ $${ans:-N} = y ] - @echo "Installing Symfony version \"${SYMFONY_VERSION}\"" - rm -rf apps/back && mkdir apps/back - docker compose run --rm --no-deps composer-install composer create-project symfony/skeleton\:${SYMFONY_VERSION} back - docker compose stop - make start - phpstan: docker compose exec php vendor/bin/phpstan analyse -c phpstan.neon From b1202f0b67d9b97e14f23f0e90bec70f8bbe1c81 Mon Sep 17 00:00:00 2001 From: Arthurlbc Date: Tue, 8 Apr 2025 10:12:14 +0200 Subject: [PATCH 5/5] doc: add image --- apps/back/public/images/logos_php_dotrine.png | Bin 0 -> 37586 bytes doc/article.md | 1 + 2 files changed, 1 insertion(+) create mode 100644 apps/back/public/images/logos_php_dotrine.png diff --git a/apps/back/public/images/logos_php_dotrine.png b/apps/back/public/images/logos_php_dotrine.png new file mode 100644 index 0000000000000000000000000000000000000000..28ef69db3837652b088eb2ae9493bea1b5175d2f GIT binary patch literal 37586 zcmc$_bypkx*FIXII7N!nqQ%`Eio1Jocc-|txKmo(r8ohCJH=gs1a}GU5S)|y`>fw< zIQx%~Va=M%M`rJR$wa9t%V3}qqrQ3b218C(QvJ=Fw^MQ9vaS89$uzymT#<`oEyV$i-aT$T`?JxXIbs1laflIC#j#l*lEdG&EPVy0zcDA%7z$ zDW>V2b=={T_C+S(?78f)ZlYN4Z+aq=UL(_DE%PwXzyM(lyUFigSiRK8p_vG>;w-~| z9)Gt}tUxKgI$!=?F-ReJ_frTZ4T~I2kT6y+F7BdJuGYbRUbgPc`s}oP{ZZz7E`Q

gr5!7blQ=h@K$BSELlwe|hxH?< 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�AD5K0S4^J+WtO159ws*j|r=*d+I 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 Q|HWFSMwWMK44%gP2WiC}9RL6T literal 0 HcmV?d00001 diff --git a/doc/article.md b/doc/article.md index c915628..2c9fc19 100644 --- a/doc/article.md +++ b/doc/article.md @@ -1,4 +1,5 @@ # Mapping Doctrine avec PHP +![PHP_Doctrine](../apps/back/public/images/logos_php_dotrine.png) Développeur chez **KnpLabs**, j’aimerais aujourd’hui vous parler de **mapping**. Cet article fait suite à un projet en PHP/Symfony où nous avons choisi **Doctrine** comme ORM (Object Relational Mapper).