Skip to content

Générer une stack entité complète ★★★ Avancé ~10 min

Contexte

Créer une entité Doctrine complète nécessite plusieurs fichiers : l'entité elle-même, son repository, sa factory Foundry pour les tests, sa story pour les fixtures et les tests unitaires. Faire tout ça manuellement est long et source d'oublis.

Objectif

Générer tous les fichiers nécessaires pour une entité en une seule commande :

  • ✅ Entity Doctrine avec principes Elegant Objects
  • ✅ Repository avec interface
  • ✅ Factory Foundry pour tests
  • ✅ Story Foundry pour fixtures
  • ✅ Tests unitaires (Entity + Repository)
  • ✅ Configuration automatique (ORM mapping)

Prérequis

Plugins :

  • framework - Générateurs d'entités
  • qa - Validation PHPStan

Outils :

  • Doctrine ORM configuré
  • Foundry installé (zenstruck/foundry)
  • PHPUnit configuré

Configuration :

yaml
# config/packages/doctrine.yaml
doctrine:
    orm:
        auto_mapping: true
        mappings:
            App:
                is_bundle: false
                dir: '%kernel.project_dir%/src/Entity'
                prefix: 'App\Entity'

Workflow Étape par Étape

Phase 1 : Générer la stack complète

Commande :

bash
/framework:make-all Product

Que se passe-t-il ?

Claude lance un orchestrateur qui exécute 10 tâches en parallèle :

  1. Créer Entity - src/Entity/Product.php avec Elegant Objects
  2. Créer Repository - src/Repository/ProductRepository.php
  3. Créer RepositoryInterface - src/Repository/ProductRepositoryInterface.php
  4. Créer Factory - tests/Factory/ProductFactory.php
  5. Créer Story - tests/Story/ProductStory.php
  6. Créer EntityTest - tests/Entity/ProductTest.php
  7. Créer RepositoryTest - tests/Repository/ProductRepositoryTest.php
  8. Configurer ORM - Ajouter mapping si nécessaire
  9. Générer migration - bin/console make:migration
  10. Lancer tests - Vérifier que tout compile

Output attendu :

✅ Task 1/10 : Entity créée (src/Entity/Product.php)
✅ Task 2/10 : Repository créé (src/Repository/ProductRepository.php)
✅ Task 3/10 : RepositoryInterface créé
✅ Task 4/10 : Factory créée (tests/Factory/ProductFactory.php)
✅ Task 5/10 : Story créée (tests/Story/ProductStory.php)
✅ Task 6/10 : EntityTest créé
✅ Task 7/10 : RepositoryTest créé
✅ Task 8/10 : ORM configuré
✅ Task 9/10 : Migration générée (Version20260201120000.php)
✅ Task 10/10 : Tests passent (8 tests, 24 assertions)

📦 Stack Product complète générée !

Phase 2 : Personnaliser l'entité

Les fichiers générés utilisent des propriétés par défaut. Tu dois les personnaliser :

Éditer src/Entity/Product.php :

php
<?php

namespace App\Entity;

use App\Repository\ProductRepositoryInterface;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity(repositoryClass: ProductRepository::class)]
class Product
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 255)]
    private string $name;

    #[ORM\Column(type: 'decimal', precision: 10, scale: 2)]
    private string $price;

    #[ORM\Column]
    private bool $active = true;

    public function __construct(string $name, string $price)
    {
        $this->name = $name;
        $this->price = $price;
    }

    public function id(): ?int
    {
        return $this->id;
    }

    public function name(): string
    {
        return $this->name;
    }

    public function price(): string
    {
        return $this->price;
    }

    public function isActive(): bool
    {
        return $this->active;
    }

    public function activate(): void
    {
        $this->active = true;
    }

    public function deactivate(): void
    {
        $this->active = false;
    }
}

Éditer tests/Factory/ProductFactory.php :

php
<?php

namespace App\Tests\Factory;

use App\Entity\Product;
use Zenstruck\Foundry\ModelFactory;

final class ProductFactory extends ModelFactory
{
    protected function getDefaults(): array
    {
        return [
            'name' => self::faker()->productName(),
            'price' => self::faker()->randomFloat(2, 1, 1000),
        ];
    }

    protected static function getClass(): string
    {
        return Product::class;
    }
}

Phase 3 : Valider avec PHPStan

Commande :

bash
vendor/bin/phpstan analyse src/ tests/ --level=9

Output attendu :

[OK] No errors

Phase 4 : Lancer les tests

Commande :

bash
vendor/bin/phpunit tests/Entity/ProductTest.php tests/Repository/ProductRepositoryTest.php

Output attendu :

OK (8 tests, 24 assertions)

Exemple Complet

Scénario : Créer entité Order avec relations

Besoin : Entité Order avec relation ManyToOne vers User.

Commande :

bash
/framework:make-all Order

Fichiers générés :

src/Entity/Order.php
src/Repository/OrderRepository.php
src/Repository/OrderRepositoryInterface.php
tests/Factory/OrderFactory.php
tests/Story/OrderStory.php
tests/Entity/OrderTest.php
tests/Repository/OrderRepositoryTest.php
migrations/Version20260201120000.php

Personnaliser l'entité :

php
#[ORM\Entity]
class Order
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\ManyToOne(targetEntity: User::class)]
    #[ORM\JoinColumn(nullable: false)]
    private User $user;

    #[ORM\Column(type: 'decimal', precision: 10, scale: 2)]
    private string $total;

    #[ORM\Column(length: 50)]
    private string $status = 'pending';

    public function __construct(User $user, string $total)
    {
        $this->user = $user;
        $this->total = $total;
    }

    // ... getters
}

Mettre à jour la Factory :

php
final class OrderFactory extends ModelFactory
{
    protected function getDefaults(): array
    {
        return [
            'user' => UserFactory::new(),
            'total' => self::faker()->randomFloat(2, 10, 1000),
        ];
    }

    protected static function getClass(): string
    {
        return Order::class;
    }
}

Utiliser la Story dans les tests :

php
class OrderControllerTest extends WebTestCase
{
    public function testListOrders(): void
    {
        OrderStory::load(); // Charge 10 orders

        $client = static::createClient();
        $client->request('GET', '/api/orders');

        $this->assertResponseIsSuccessful();
        $this->assertCount(10, json_decode($client->getResponse()->getContent(), true));
    }
}

Lancer les tests :

bash
vendor/bin/phpunit

Output :

OK (15 tests, 42 assertions)

Variantes

Générer seulement l'entité

bash
/framework:make-entity Product

Génère uniquement src/Entity/Product.php.

Générer seulement la Factory

bash
/framework:make-factory Product

Génère uniquement tests/Factory/ProductFactory.php.

Générer avec Collection typée

Pour une entité avec collection (ex: Order avec OrderItems) :

bash
/framework:make-all Order
/framework:make-collection OrderItems

Voir Workflow CQRS.

Troubleshooting

Erreur "Entity already exists"

Symptôme : Entity Product already exists at src/Entity/Product.php

Solution :

  1. Supprimer l'entité existante
  2. Ou renommer l'entité : /framework:make-all ProductV2

Erreur migration

Symptôme : bin/console make:migration failed

Solution :

  1. Vérifier que Doctrine est configuré
  2. Vérifier que la database existe
  3. Lancer manuellement :
    bash
    bin/console doctrine:migrations:diff

Tests en échec

Symptôme : ProductTest::testConstructor failed

Solution :

  1. Vérifier que la Factory est bien configurée
  2. Vérifier que les propriétés de l'entité sont correctes
  3. Relancer les tests

PHPStan erreurs

Symptôme : Property Product::$name is never read

Solution :

Ajouter un getter :

php
public function name(): string
{
    return $this->name;
}

Ou utiliser /qa:phpstan-resolver pour auto-fix.

Liens Connexes

Use cases :

Plugins :

Documentation externe :

Tips & Best Practices

✅ Bonnes pratiques

  • Elegant Objects : pas de setters, constructeur avec tous les required fields
  • Immutabilité : préférer méthodes with*() plutôt que setters
  • Value Objects : utiliser des VO pour price, email, etc.
  • Repository Interface : toujours injecter l'interface, pas la classe concrète

🔍 Optimisations

Factory avancée avec états :

php
final class ProductFactory extends ModelFactory
{
    public function active(): self
    {
        return $this->addState(['active' => true]);
    }

    public function inactive(): self
    {
        return $this->addState(['active' => false]);
    }

    public function expensive(): self
    {
        return $this->addState(['price' => self::faker()->randomFloat(2, 500, 1000)]);
    }
}

// Usage
ProductFactory::new()->active()->expensive()->create();

Story réutilisable :

php
final class ProductStory extends Story
{
    public function build(): void
    {
        ProductFactory::new()->active()->createMany(5);
        ProductFactory::new()->inactive()->createMany(3);
    }
}

🎯 Métriques de qualité

Une entité de qualité c'est :

  • ✅ PHPStan niveau 9 vert
  • ✅ 100% coverage sur Entity + Repository
  • ✅ Factory avec états métiers
  • ✅ Story pour fixtures réalistes
  • ✅ Pas de setters (Elegant Objects)

Checklist Validation

Avant de générer :

  • [ ] Nom de l'entité en PascalCase (ex: Product, OrderItem)
  • [ ] Doctrine ORM configuré
  • [ ] Foundry installé
  • [ ] Database créée

Après génération :

  • [ ] Entity créée avec constructeur
  • [ ] Repository + Interface créés
  • [ ] Factory créée avec defaults
  • [ ] Story créée
  • [ ] Tests unitaires créés
  • [ ] Migration générée
  • [ ] PHPStan niveau 9 vert
  • [ ] Tests passent

Avant PR :

  • [ ] Factory personnalisée avec vraies données
  • [ ] Story avec scénarios réalistes
  • [ ] Tests couvrant tous les cas métiers
  • [ ] Documentation entité à jour

Publié sous licence MIT