Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 17:58:52 +08:00
commit eac17f89fa
40 changed files with 4048 additions and 0 deletions

View File

@@ -0,0 +1,275 @@
# Framework Make Factory
Génère une Factory Foundry pour tests.
## Vue d'ensemble
Cette skill crée une Factory Zenstruck Foundry pour générer facilement des instances d'entités dans les tests selon les principes Elegant Objects.
## Caractéristiques
### Classe Factory générée
- Extends PersistentObjectFactory
- Classe `final`
- Méthode `class()` retournant le FQCN
- Méthode `defaults()` avec valeurs Faker
- Méthode `initialize()` utilisant `instantiateWith()`
- Appelle la factory statique `Entity::create()`
- Méthode `withSpecificId()` par défaut
## Utilisation
```bash
Use skill framework:make:factory
```
Vous serez invité à fournir le nom de l'entité.
## Exemple d'utilisation
```bash
EntityName: Product
```
Génère :
```php
// src/Factory/ProductFactory.php
/**
* @extends PersistentObjectFactory<Product>
*/
final class ProductFactory extends PersistentObjectFactory
{
public static function class(): string
{
return Product::class;
}
protected function defaults(): array|callable
{
return [
'id' => Uuid::v4(),
'name' => self::faker()->words(3, true),
'price' => self::faker()->randomFloat(2, 10, 1000),
];
}
protected function initialize(): static
{
return $this
->instantiateWith(function (array $attributes) {
return Product::create(
id: $attributes['id'],
name: $attributes['name'],
price: $attributes['price'],
);
});
}
public function withSpecificId(string $uuid): self
{
return $this->with([
'id' => Uuid::fromString($uuid),
]);
}
}
```
## Structure créée
```
src/
└── Factory/
└── {EntityName}Factory.php
```
## Prérequis
- L'entité doit exister dans `src/Entity/{EntityName}.php`
- Zenstruck Foundry installé
## Usage dans les tests
### Création basique
```php
use App\Factory\ProductFactory;
// Créer et persister une instance
$product = ProductFactory::createOne();
// Créer plusieurs instances
$products = ProductFactory::createMany(10);
// Créer sans persister
$product = ProductFactory::new()->withoutPersisting()->create();
```
### Personnalisation
```php
// Override propriétés
$product = ProductFactory::createOne([
'name' => 'Mon Produit',
'price' => 99.99,
]);
// Enchaînement
$product = ProductFactory::new()
->withSpecificId('018d5e5e-5e5e-7e5e-ae5e-5e5e5e5e5e5e')
->with(['name' => 'Custom'])
->createOne();
```
### Tests unitaires
```php
final class ProductTest extends TestCase
{
public function testProductCanBeCreated(): void
{
$product = ProductFactory::createOne([
'name' => 'Test Product',
'price' => 50.0,
]);
self::assertSame('Test Product', $product->name());
self::assertSame(50.0, $product->price());
}
public function testCannotHaveNegativePrice(): void
{
$this->expectException(\InvalidArgumentException::class);
ProductFactory::createOne(['price' => -10.0]);
}
}
```
### Tests fonctionnels
```php
final class ProductControllerTest extends WebTestCase
{
public function testListProducts(): void
{
ProductFactory::createMany(5);
$client = static::createClient();
$client->request('GET', '/products');
self::assertResponseIsSuccessful();
self::assertSelectorTextContains('h1', 'Products');
}
}
```
## Enrichissement (principe YAGNI)
Ajouter méthodes custom **uniquement si demandées** :
```php
// États métier
public function active(): self
{
return $this->with(['isActive' => true]);
}
public function inactive(): self
{
return $this->with(['isActive' => false]);
}
// Valeurs spécifiques
public function expensive(): self
{
return $this->with([
'price' => self::faker()->randomFloat(2, 500, 2000)
]);
}
public function outOfStock(): self
{
return $this->with(['stock' => 0]);
}
// Relations
public function withCategory(Category $category): self
{
return $this->with(['category' => $category]);
}
```
## Valeurs par défaut Faker
```php
protected function defaults(): array|callable
{
return [
// UUID
'id' => Uuid::v4(),
// Strings
'name' => self::faker()->words(3, true),
'email' => self::faker()->email(),
'description' => self::faker()->paragraph(),
'slug' => self::faker()->slug(),
// Nombres
'price' => self::faker()->randomFloat(2, 10, 1000),
'quantity' => self::faker()->numberBetween(1, 100),
'stock' => self::faker()->numberBetween(0, 50),
// Booleans
'isActive' => true,
'isPublished' => self::faker()->boolean(70), // 70% true
// Dates
'createdAt' => \DateTimeImmutable::createFromMutable(
self::faker()->dateTimeBetween('-1 year')
),
// Arrays
'tags' => self::faker()->words(5),
// Relations (avec autre Factory)
'category' => CategoryFactory::new(),
];
}
```
## Patterns avancés
### Factory avec relations
```php
protected function defaults(): array|callable
{
return [
'id' => Uuid::v4(),
'name' => self::faker()->words(3, true),
'category' => CategoryFactory::new(),
];
}
public function inCategory(Category $category): self
{
return $this->with(['category' => $category]);
}
```
### Factory avec états complexes
```php
public function published(): self
{
return $this
->with(['isPublished' => true])
->with(['publishedAt' => new \DateTimeImmutable()]);
}
public function draft(): self
{
return $this
->with(['isPublished' => false])
->with(['publishedAt' => null]);
}
```
## Principes Elegant Objects appliqués
- Classe finale
- Utilise `instantiateWith()` pour appeler `Entity::create()`
- Pas de `new Entity()` direct
- Méthodes custom uniquement si demandées (YAGNI)
- Type-safe avec générique PHPDoc

View File

@@ -0,0 +1,256 @@
---
name: framework:make:factory
description: Génère Factory Foundry pour tests
license: MIT
version: 1.0.0
---
# Framework Make Factory Skill
## Description
Génère une Factory Foundry pour créer facilement des instances d'entités dans les tests.
La Factory utilise Zenstruck Foundry et respecte les principes Elegant Objects en utilisant la factory statique `create()` de l'entité.
## Usage
```
Use skill framework:make:factory
Vous serez invité à fournir :
- Le nom de l'entité (ex: Product, User, Order)
```
## Templates
- `Factory/UtilisateurFactory.php` - Template de factory Foundry
## Variables requises
- **{EntityName}** - Nom de l'entité en PascalCase (ex: Utilisateur, Product)
- **{entityName}** - Nom de l'entité en camelCase (ex: utilisateur, product)
- **{namespace}** - Namespace du projet (défaut: App)
- **{properties}** - Liste des propriétés de l'entité pour `defaults()`
## Dépendances
- Requiert que l'entité existe dans `src/Entity/{EntityName}.php`
- Requiert Zenstruck Foundry installé
## Outputs
- `src/Factory/{EntityName}Factory.php`
## Workflow
1. Demander le nom de l'entité (EntityName)
2. Vérifier que l'entité existe dans `src/Entity/{EntityName}.php`
- Si non : arrêter et demander de créer l'entité d'abord
3. Lire l'entité pour détecter les propriétés du constructeur `create()`
4. Générer la factory depuis le template :
- Remplacer `{EntityName}` par le nom fourni
- Remplacer `{entityName}` par la version camelCase
- Générer `defaults()` avec valeurs par défaut pour chaque propriété
- Générer `instantiateWith()` appelant `Entity::create()`
5. Afficher le fichier créé
## Patterns appliqués
### Classe Factory
- Extends PersistentObjectFactory
- Classe `final`
- Méthode statique `class()` retournant le FQCN de l'entité
- Méthode `defaults()` avec valeurs par défaut des propriétés
- Méthode `initialize()` avec `instantiateWith()` appelant `Entity::create()`
- Méthodes custom (ex: `withSpecificId()`)
## Exemple
```bash
Use skill framework:make:factory
# Saisies utilisateur :
EntityName: Product
# Résultat :
✓ src/Factory/ProductFactory.php
```
Fichier généré :
```php
<?php
declare(strict_types=1);
namespace App\Factory;
use App\Entity\Product;
use Symfony\Component\Uid\Uuid;
use Zenstruck\Foundry\Persistence\PersistentObjectFactory;
/**
* @extends PersistentObjectFactory<Product>
*/
final class ProductFactory extends PersistentObjectFactory
{
public static function class(): string
{
return Product::class;
}
protected function defaults(): array|callable
{
return [
'id' => Uuid::v4(),
'name' => self::faker()->words(3, true),
'price' => self::faker()->randomFloat(2, 10, 1000),
'stock' => self::faker()->numberBetween(0, 100),
'isActive' => true,
];
}
protected function initialize(): static
{
return $this
->instantiateWith(function (array $attributes) {
return Product::create(
id: $attributes['id'],
name: $attributes['name'],
price: $attributes['price'],
stock: $attributes['stock'],
isActive: $attributes['isActive'],
);
})
;
}
public function withSpecificId(string $uuid): self
{
return $this->with([
'id' => Uuid::fromString($uuid),
]);
}
public function inactive(): self
{
return $this->with(['isActive' => false]);
}
public function outOfStock(): self
{
return $this->with(['stock' => 0]);
}
}
```
## Usage dans les tests
### Création simple
```php
// Crée et persiste en DB
$product = ProductFactory::createOne();
// Crée plusieurs instances
$products = ProductFactory::createMany(10);
// Crée sans persister
$product = ProductFactory::new()->withoutPersisting()->create();
```
### Personnalisation
```php
// Override propriétés
$product = ProductFactory::createOne([
'name' => 'Custom Product',
'price' => 99.99,
]);
// Méthodes custom
$product = ProductFactory::new()
->withSpecificId('018d5e5e-5e5e-7e5e-ae5e-5e5e5e5e5e5e')
->inactive()
->createOne();
$product = ProductFactory::new()->outOfStock()->createOne();
```
### Dans les tests
```php
final class ProductTest extends TestCase
{
public function testCannotDecreaseStockWhenOutOfStock(): void
{
$product = ProductFactory::new()->outOfStock()->createOne();
$this->expectException(\DomainException::class);
$product->decreaseStock(1);
}
public function testInactiveProductsCannotBePurchased(): void
{
$product = ProductFactory::new()->inactive()->createOne();
$this->expectException(\DomainException::class);
$product->purchase(1);
}
}
```
## Valeurs par défaut recommandées
### Types courants
```php
// Strings
'name' => self::faker()->words(3, true),
'email' => self::faker()->email(),
'description' => self::faker()->paragraph(),
// Nombres
'price' => self::faker()->randomFloat(2, 10, 1000),
'quantity' => self::faker()->numberBetween(1, 100),
'stock' => self::faker()->numberBetween(0, 50),
// Booleans
'isActive' => true,
'isPublished' => self::faker()->boolean(),
// Dates
'createdAt' => \DateTimeImmutable::createFromMutable(self::faker()->dateTime()),
// UUID
'id' => Uuid::v4(),
```
## Méthodes custom recommandées
```php
// Par ID spécifique
public function withSpecificId(string $uuid): self
{
return $this->with(['id' => Uuid::fromString($uuid)]);
}
// États métier
public function active(): self
{
return $this->with(['isActive' => true]);
}
public function inactive(): self
{
return $this->with(['isActive' => false]);
}
// Valeurs spécifiques
public function expensive(): self
{
return $this->with(['price' => self::faker()->randomFloat(2, 500, 2000)]);
}
public function cheap(): self
{
return $this->with(['price' => self::faker()->randomFloat(2, 1, 50)]);
}
```
## Notes
- Utilise `instantiateWith()` pour appeler la factory statique `create()` de l'entité
- Respecte le principe Elegant Objects (pas de `new Entity()` direct)
- Faker disponible via `self::faker()`
- Méthodes custom uniquement si demandées explicitement (YAGNI)
- Factory persiste par défaut, utiliser `withoutPersisting()` si besoin

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace App\Factory;
use App\Entity\Utilisateur;
use Symfony\Component\Uid\Uuid;
use Zenstruck\Foundry\Persistence\PersistentObjectFactory;
/**
* @extends PersistentObjectFactory<Utilisateur>
*/
final class UtilisateurFactory extends PersistentObjectFactory
{
public static function class(): string
{
return Utilisateur::class;
}
protected function defaults(): array|callable
{
return [
'id' => Uuid::v4(),
];
}
protected function initialize(): static
{
return $this
->instantiateWith(function (array $attributes) {
return Utilisateur::create(
id: $attributes['id'],
);
})
;
}
public function withSpecificId(string $uuid): self
{
return $this->with([
'id' => Uuid::fromString($uuid),
]);
}
}