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 :
Outils :
- Doctrine ORM configuré
- Foundry installé (
zenstruck/foundry) - PHPUnit configuré
Configuration :
# 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 :
/framework:make-all ProductQue se passe-t-il ?
Claude lance un orchestrateur qui exécute 10 tâches en parallèle :
- Créer Entity -
src/Entity/Product.phpavec Elegant Objects - Créer Repository -
src/Repository/ProductRepository.php - Créer RepositoryInterface -
src/Repository/ProductRepositoryInterface.php - Créer Factory -
tests/Factory/ProductFactory.php - Créer Story -
tests/Story/ProductStory.php - Créer EntityTest -
tests/Entity/ProductTest.php - Créer RepositoryTest -
tests/Repository/ProductRepositoryTest.php - Configurer ORM - Ajouter mapping si nécessaire
- Générer migration -
bin/console make:migration - 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
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
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 :
vendor/bin/phpstan analyse src/ tests/ --level=9Output attendu :
[OK] No errorsPhase 4 : Lancer les tests
Commande :
vendor/bin/phpunit tests/Entity/ProductTest.php tests/Repository/ProductRepositoryTest.phpOutput 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 :
/framework:make-all OrderFichiers 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.phpPersonnaliser l'entité :
#[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 :
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 :
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 :
vendor/bin/phpunitOutput :
OK (15 tests, 42 assertions)Variantes
Générer seulement l'entité
/framework:make-entity ProductGénère uniquement src/Entity/Product.php.
Générer seulement la Factory
/framework:make-factory ProductGénère uniquement tests/Factory/ProductFactory.php.
Générer avec Collection typée
Pour une entité avec collection (ex: Order avec OrderItems) :
/framework:make-all Order
/framework:make-collection OrderItemsVoir Workflow CQRS.
Troubleshooting
Erreur "Entity already exists"
Symptôme : Entity Product already exists at src/Entity/Product.php
Solution :
- Supprimer l'entité existante
- Ou renommer l'entité :
/framework:make-all ProductV2
Erreur migration
Symptôme : bin/console make:migration failed
Solution :
- Vérifier que Doctrine est configuré
- Vérifier que la database existe
- Lancer manuellement :bash
bin/console doctrine:migrations:diff
Tests en échec
Symptôme : ProductTest::testConstructor failed
Solution :
- Vérifier que la Factory est bien configurée
- Vérifier que les propriétés de l'entité sont correctes
- Relancer les tests
PHPStan erreurs
Symptôme : Property Product::$name is never read
Solution :
Ajouter un getter :
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 :
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 :
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