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

311
skills/make-story/README.md Normal file
View File

@@ -0,0 +1,311 @@
# Framework Make Story
Génère une Story Foundry pour fixtures de tests.
## Vue d'ensemble
Cette skill crée une Story Zenstruck Foundry pour orchestrer la création de fixtures de tests cohérentes et réutilisables.
## Caractéristiques
### Classe Story générée
- Extends Story
- Implements StoryInterface
- Classe `final`
- Méthode `build()` avec scénarios
- Utilise Factories pour créer instances
- Scénarios prédéfinis
### Classe AppStory
- Point d'entrée global
- Attribut #[AsFixture(name: 'main')]
- Charge toutes les stories
- Gère les dépendances entre stories
## Utilisation
```bash
Use skill framework:make:story
```
Vous serez invité à fournir le nom de l'entité.
## Exemple d'utilisation
```bash
EntityName: Product
```
Génère :
```php
// src/Story/ProductStory.php
final class ProductStory extends Story implements StoryInterface
{
public function build(): void
{
// Produit par défaut
ProductFactory::createOne();
// Produits avec IDs spécifiques
ProductFactory::new()
->withSpecificId('01234567-89ab-cdef-0123-456789abcdef')
->create();
// Plusieurs produits
ProductFactory::createMany(10);
}
}
// src/Story/AppStory.php (updated)
#[AsFixture(name: 'main')]
final class AppStory extends Story implements StoryInterface
{
public function build(): void
{
ProductStory::load();
// ... autres stories
}
}
```
## Structure créée
```
src/
└── Story/
├── {EntityName}Story.php
└── AppStory.php
```
## Prérequis
- L'entité doit exister
- La Factory doit exister (créée automatiquement si absente)
- StoryInterface doit exister dans Contracts
- Zenstruck Foundry installé
## Usage dans les tests
### Tests unitaires
```php
use App\Story\ProductStory;
final class ProductServiceTest extends TestCase
{
protected function setUp(): void
{
ProductStory::load();
}
public function testCalculateTotalPrice(): void
{
$products = ProductFactory::repository()->findAll();
$service = new ProductService();
$total = $service->calculateTotal($products);
self::assertGreaterThan(0, $total);
}
}
```
### Tests fonctionnels
```php
use App\Story\AppStory;
final class ProductControllerTest extends WebTestCase
{
protected function setUp(): void
{
AppStory::load();
}
public function testListProducts(): void
{
$client = static::createClient();
$client->request('GET', '/products');
self::assertResponseIsSuccessful();
self::assertSelectorExists('.product-item');
}
}
```
### Fixtures Doctrine
```bash
# Charger toutes les fixtures
php bin/console doctrine:fixtures:load --append
# Ou juste AppStory via attribut #[AsFixture]
```
## Enrichissement (principe YAGNI)
### Scénarios métier
```php
final class ProductStory extends Story implements StoryInterface
{
public function build(): void
{
// Scénario : catalogue actif
ProductFactory::new()
->active()
->createMany(20);
// Scénario : produits en promotion
ProductFactory::new()
->active()
->createMany(5, ['price' => 9.99]);
// Scénario : rupture de stock
ProductFactory::new()
->outOfStock()
->createMany(3);
// Scénario : nouveau produit vedette
ProductFactory::createOne([
'name' => 'Featured Product',
'price' => 199.99,
'stock' => 100,
'isActive' => true,
]);
}
}
```
### Avec relations
```php
final class ProductStory extends Story implements StoryInterface
{
public function build(): void
{
// Charger dépendances
CategoryStory::load();
// Récupérer catégories
$electronics = CategoryFactory::find(['name' => 'Electronics']);
$books = CategoryFactory::find(['name' => 'Books']);
// Produits par catégorie
ProductFactory::new()
->inCategory($electronics)
->createMany(10);
ProductFactory::new()
->inCategory($books)
->createMany(15);
}
}
```
### États nommés
```php
final class ProductStory extends Story implements StoryInterface
{
public function build(): void
{
// États nommés pour réutilisation
$this->addState('premium', ProductFactory::createOne([
'name' => 'Premium Product',
'price' => 999.99,
]));
$this->addState('cheap', ProductFactory::createOne([
'name' => 'Cheap Product',
'price' => 9.99,
]));
$this->addState('test_product', ProductFactory::createOne([
'id' => Uuid::fromString('01234567-89ab-cdef-0123-456789abcdef'),
]));
}
}
// Usage
$premium = ProductStory::load()->get('premium');
$cheap = ProductStory::load()->get('cheap');
```
### Scénarios complexes
```php
final class OrderStory extends Story implements StoryInterface
{
public function build(): void
{
// Charger dépendances
UserStory::load();
ProductStory::load();
$users = UserFactory::repository()->findAll();
$products = ProductFactory::repository()->findAll();
// Commandes par utilisateur
foreach ($users as $user) {
OrderFactory::createOne([
'user' => $user,
'items' => array_slice($products, 0, rand(1, 5)),
]);
}
// Commande de test spécifique
$this->addState('test_order', OrderFactory::createOne([
'user' => UserStory::load()->get('test_user'),
'items' => [ProductStory::load()->get('test_product')],
'status' => 'pending',
]));
}
}
```
## AppStory orchestration
```php
#[AsFixture(name: 'main')]
final class AppStory extends Story implements StoryInterface
{
public function build(): void
{
// Ordre important : dépendances d'abord
CategoryStory::load();
ProductStory::load();
UserStory::load();
OrderStory::load();
ReviewStory::load();
}
}
```
## Configuration Foundry
```yaml
# config/packages/zenstruck_foundry.yaml
when@dev:
zenstruck_foundry:
auto_refresh_proxies: false
when@test:
zenstruck_foundry:
auto_refresh_proxies: false
make_factory:
default_namespace: 'App\Factory'
```
## Commandes utiles
```bash
# Charger fixtures
php bin/console doctrine:fixtures:load --append
# Purger DB puis charger
php bin/console doctrine:fixtures:load
# Dans tests
AppStory::load();
ProductStory::load();
```
## Principes Elegant Objects appliqués
- Classe finale
- Implements StoryInterface
- Méthode `build()` claire et concise
- Scénarios nommés explicitement
- DRY : fixtures centralisées
- États nommés pour réutilisation

291
skills/make-story/SKILL.md Normal file
View File

@@ -0,0 +1,291 @@
---
name: framework:make:story
description: Génère Story Foundry pour fixtures de tests
license: MIT
version: 1.0.0
---
# Framework Make Story Skill
## Description
Génère une Story Foundry pour créer des fixtures de tests complexes avec des scénarios prédéfinis.
La Story orchestre la création de multiples instances d'entités via les Factories et permet de charger des jeux de données cohérents pour les tests.
## Usage
```
Use skill framework:make:story
Vous serez invité à fournir :
- Le nom de l'entité (ex: Product, User, Order)
```
## Templates
- `Story/UtilisateurStory.php` - Template de story pour une entité
- `Story/AppStory.php` - Template de story globale (créé si absent)
## 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)
## Dépendances
- Requiert que l'entité existe dans `src/Entity/{EntityName}.php`
- Appelle automatiquement `framework:make:factory` si la Factory n'existe pas
- Requiert que les Contracts existent (StoryInterface)
## Outputs
- `src/Story/{EntityName}Story.php`
- `src/Story/AppStory.php` (si n'existe pas déjà)
## 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. Vérifier que la Factory existe dans `src/Factory/{EntityName}Factory.php`
- Si non : appeler `framework:make:factory`
4. Générer la Story depuis le template :
- Remplacer `{EntityName}` par le nom fourni
- Remplacer `{entityName}` par la version camelCase
5. Vérifier si `src/Story/AppStory.php` existe
- Si non : créer AppStory avec le template
- Si oui : ajouter `{EntityName}Story::load();` dans la méthode `build()`
6. Afficher les fichiers créés
## Patterns appliqués
### Classe Story
- Extends Story
- Implements StoryInterface
- Classe `final`
- Méthode `build()` créant les fixtures
- Utilise les Factories pour créer les instances
- Scénarios de tests prédéfinis
### Classe AppStory
- Extends Story
- Implements StoryInterface
- Classe `final`
- Attribut #[AsFixture(name: 'main')]
- Méthode `build()` chargeant toutes les stories
- Point d'entrée unique pour charger toutes les fixtures
## Exemple
```bash
Use skill framework:make:story
# Saisies utilisateur :
EntityName: Product
# Résultat :
✓ src/Story/ProductStory.php
✓ src/Story/AppStory.php (updated)
```
Fichiers générés :
```php
// src/Story/ProductStory.php
<?php
declare(strict_types=1);
namespace App\Story;
use App\Contracts\Story\StoryInterface;
use App\Factory\ProductFactory;
use Zenstruck\Foundry\Story;
final class ProductStory extends Story implements StoryInterface
{
public function build(): void
{
// Produit par défaut
ProductFactory::createOne();
// Produits avec IDs spécifiques pour les tests
ProductFactory::new()
->withSpecificId('01234567-89ab-cdef-0123-456789abcdef')
->create();
// Créer plusieurs produits
ProductFactory::createMany(10);
}
}
// src/Story/AppStory.php
<?php
namespace App\Story;
use App\Contracts\Story\StoryInterface;
use Zenstruck\Foundry\Attribute\AsFixture;
use Zenstruck\Foundry\Story;
#[AsFixture(name: 'main')]
final class AppStory extends Story implements StoryInterface
{
public function build(): void
{
ProductStory::load();
}
}
```
## Usage dans les tests
### Charger la story
```php
use App\Story\ProductStory;
final class ProductTest extends TestCase
{
protected function setUp(): void
{
ProductStory::load();
}
public function testProductsAreLoaded(): void
{
$products = ProductFactory::repository()->findAll();
self::assertCount(12, $products); // 1 défaut + 1 spécifique + 10 random
}
}
```
### Charger toutes les stories
```php
use App\Story\AppStory;
final class IntegrationTest extends TestCase
{
protected function setUp(): void
{
AppStory::load(); // Charge ProductStory + autres
}
}
```
### Fixture Doctrine
```bash
# Dans config/packages/zenstruck_foundry.yaml
when@dev:
zenstruck_foundry:
auto_refresh_proxies: false
# Charger les fixtures
php bin/console doctrine:fixtures:load --append
```
## Enrichissement de la Story
### Scénarios complexes
```php
final class ProductStory extends Story implements StoryInterface
{
public function build(): void
{
// Produits actifs
ProductFactory::new()
->active()
->createMany(5);
// Produits inactifs
ProductFactory::new()
->inactive()
->createMany(3);
// Produits en rupture
ProductFactory::new()
->outOfStock()
->createMany(2);
// Produits premium
ProductFactory::new()
->expensive()
->createMany(3);
// Produit spécifique pour tests
ProductFactory::createOne([
'name' => 'Test Product',
'price' => 99.99,
'stock' => 10,
]);
}
}
```
### Avec relations
```php
final class ProductStory extends Story implements StoryInterface
{
public function build(): void
{
// Créer catégories d'abord
CategoryStory::load();
$electronics = CategoryFactory::find(['name' => 'Electronics']);
$books = CategoryFactory::find(['name' => 'Books']);
// Produits électroniques
ProductFactory::new()
->inCategory($electronics)
->createMany(5);
// Livres
ProductFactory::new()
->inCategory($books)
->createMany(10);
}
}
```
### Avec états nommés
```php
final class ProductStory extends Story implements StoryInterface
{
public function build(): void
{
// États nommés pour réutilisation dans tests
$this->addState('premium_product', ProductFactory::createOne([
'name' => 'Premium Product',
'price' => 999.99,
]));
$this->addState('cheap_product', ProductFactory::createOne([
'name' => 'Cheap Product',
'price' => 9.99,
]));
}
}
// Usage dans test
$premium = ProductStory::load()->get('premium_product');
```
## AppStory orchestration
```php
#[AsFixture(name: 'main')]
final class AppStory extends Story implements StoryInterface
{
public function build(): void
{
// Ordre important : dépendances d'abord
CategoryStory::load();
ProductStory::load();
UserStory::load();
OrderStory::load();
}
}
```
## Notes
- AppStory est le point d'entrée pour charger toutes les fixtures
- Attribut #[AsFixture(name: 'main')] permet de charger via Doctrine Fixtures
- Les Stories peuvent avoir des dépendances (charger d'autres Stories)
- Méthode `addState()` permet de nommer des instances pour les tests
- Respecte le principe DRY : scénarios centralisés

View File

@@ -0,0 +1,16 @@
<?php
namespace App\Story;
use App\Contracts\Story\StoryInterface;
use Zenstruck\Foundry\Attribute\AsFixture;
use Zenstruck\Foundry\Story;
#[AsFixture(name: 'main')]
final class AppStory extends Story implements StoryInterface
{
public function build(): void
{
UtilisateurStory::load();
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace App\Story;
use App\Contracts\Story\StoryInterface;
use App\Factory\UtilisateurFactory;
use Zenstruck\Foundry\Story;
final class UtilisateurStory extends Story implements StoryInterface
{
public function build(): void
{
// Utilisateur par défaut
UtilisateurFactory::createOne();
// Utilisateurs avec IDs spécifiques pour les tests
UtilisateurFactory::new()
->withSpecificId('01234567-89ab-cdef-0123-456789abcdef')
->create();
// Créer plusieurs utilisateurs
UtilisateurFactory::createMany(10);
}
}