Initial commit
This commit is contained in:
275
skills/make-factory/README.md
Normal file
275
skills/make-factory/README.md
Normal 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
|
||||
256
skills/make-factory/SKILL.md
Normal file
256
skills/make-factory/SKILL.md
Normal 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
|
||||
45
skills/make-factory/templates/Factory/UtilisateurFactory.php
Normal file
45
skills/make-factory/templates/Factory/UtilisateurFactory.php
Normal 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),
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user