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,12 @@
{
"name": "framework",
"description": "Plugin pour atournayre/framework avec commandes et intégrations",
"version": "1.0.0",
"author": {
"name": "Aurélien Tournayre",
"email": "aurelien.tournayre@gmail.com"
},
"skills": [
"./skills"
]
}

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
# framework
Plugin pour atournayre/framework avec commandes et intégrations

193
plugin.lock.json Normal file
View File

@@ -0,0 +1,193 @@
{
"$schema": "internal://schemas/plugin.lock.v1.json",
"pluginId": "gh:atournayre/claude-marketplace:framework",
"normalized": {
"repo": null,
"ref": "refs/tags/v20251128.0",
"commit": "cc79b16cece49758a37985f2926bf3305bb3b987",
"treeHash": "8e414effa99b116b6952629a2697a26291b9e09f48eb4ec1f378578b1a823be3",
"generatedAt": "2025-11-28T10:14:01.106931Z",
"toolVersion": "publish_plugins.py@0.2.0"
},
"origin": {
"remote": "git@github.com:zhongweili/42plugin-data.git",
"branch": "master",
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
},
"manifest": {
"name": "framework",
"description": "Plugin pour atournayre/framework avec commandes et intégrations",
"version": "1.0.0"
},
"content": {
"files": [
{
"path": "README.md",
"sha256": "c81d4d93521b16f2bbf322413ca8ce8af64802b6adbeed3ce098cb067e662044"
},
{
"path": ".claude-plugin/plugin.json",
"sha256": "02c38379c0442351d5cbd6b2dfd743105f991d3e4af2ba5c724c16cefe0ad236"
},
{
"path": "skills/make-urls/README.md",
"sha256": "7dac36005d6a4ffb651f4bf3dc6bf96ae2c9e64c87f0f601c83fceb6adda0773"
},
{
"path": "skills/make-urls/SKILL.md",
"sha256": "c0b8375416853121e12994d8eadaa8462562e18f59a2a30e66963308e54429d5"
},
{
"path": "skills/make-urls/templates/MessageHandler/UtilisateurUrlsMessageHandler.php",
"sha256": "7d8e2c3ce5c0d4c92b8bb857922b3233ca40dbd5be721ac74e6a1b7a022ec273"
},
{
"path": "skills/make-urls/templates/MessageHandler/UtilisateurUrlsMessage.php",
"sha256": "a45c8716a38371a6a152b7bf79f89cbe5660a63d2c0387ff844e30650f758559"
},
{
"path": "skills/make-urls/templates/Urls/UtilisateurUrls.php",
"sha256": "786dfd7d90d79102c8d58b8fc6d25a4d67387941680c9fe615969291e0a1bd57"
},
{
"path": "skills/make-entity/README.md",
"sha256": "7e8b2b898fa045a425e70879e09eb381967141adada567435a128b1fce6f9f0a"
},
{
"path": "skills/make-entity/SKILL.md",
"sha256": "1526a6cd6500f9c176c7c9035d9d708616e79b67a3c2fd64ef77771316a49ac8"
},
{
"path": "skills/make-entity/templates/Repository/UtilisateurRepositoryInterface.php",
"sha256": "40d3b44c07a6d8b1064edf6770d5a3300619c0ab2ea5055155877d3135f710a8"
},
{
"path": "skills/make-entity/templates/Repository/UtilisateurRepository.php",
"sha256": "5235da516cbb958e93836feade807fdb43f36fd6bb528c4d4bf542f98c8c4805"
},
{
"path": "skills/make-entity/templates/Entity/Utilisateur.php",
"sha256": "02b52ed35279f1eefbfd46245c26796e0be954197801b1340d787b5837afb13a"
},
{
"path": "skills/make-contracts/README.md",
"sha256": "0c9503a709718effc61015620abfac908520f98bebca3836838b99bea4512787"
},
{
"path": "skills/make-contracts/SKILL.md",
"sha256": "9ad070e55b24cf672d8b2512e0246f5cadc5df4b41fa0ae314deef912ad9ded4"
},
{
"path": "skills/make-contracts/templates/Contracts/OutInterface.php",
"sha256": "a5a4b73c5978168f2a56fc857c1e9145bf4d6d22f6181719d87d416d2a3ae480"
},
{
"path": "skills/make-contracts/templates/Contracts/HasUrlsInterface.php",
"sha256": "26aca7de55a3caf234ff7f748cc6ace7ec0cb937657f3af67b68773c4d635115"
},
{
"path": "skills/make-contracts/templates/Contracts/UrlsDataInterface.php",
"sha256": "533abfbb7abbe30ea037a1966b2bd2ba2efcbca041013799caceba7c17367cbf"
},
{
"path": "skills/make-contracts/templates/Contracts/InvalideDataInterface.php",
"sha256": "cd284a22f72e6e410b7462de52487cecbe3842d08f971b4dd62832345300d2b7"
},
{
"path": "skills/make-contracts/templates/Contracts/OutDataInterface.php",
"sha256": "fbb2d3e219243201ea00b6a1cb2e2d3c127829ecf64dbc7c0b2347dda4109679"
},
{
"path": "skills/make-contracts/templates/Contracts/InvalideInterface.php",
"sha256": "48ddb50e0c1a23ef93f2ea5793878f6bfd87727f413769e37df46229d37700a7"
},
{
"path": "skills/make-contracts/templates/Contracts/Doctrine/DoctrineMigrationInterface.php",
"sha256": "508aa5b0c26ab241abdf715d270df7b9da10ec06a95dd39303ce9c125b1f4962"
},
{
"path": "skills/make-contracts/templates/Contracts/Story/StoryInterface.php",
"sha256": "6b7d8cd6fe341a52e07796617def4db72d4b54df8df82a1ff50574421d0ceb95"
},
{
"path": "skills/make-out/README.md",
"sha256": "e4b1b6b62cea8c4c26f4920d4d30a0e29ea5c296eabfd0229e7d9b92144ff18d"
},
{
"path": "skills/make-out/SKILL.md",
"sha256": "c8f759d5e496c88806305b054113f1e42974bce06b088b4b56801b2a14d9a9d4"
},
{
"path": "skills/make-out/templates/Out/UtilisateurOut.php",
"sha256": "e5a82265834e301de7f163407215789914e14ac72878198ed19d28890bdd4e6e"
},
{
"path": "skills/make-invalide/README.md",
"sha256": "3ec4caf68128a722857a4166be4caf6bff1dc7980d2f5e0c15d8dd1bb53bb56e"
},
{
"path": "skills/make-invalide/SKILL.md",
"sha256": "725666ddd9b829608f5d41ae4dbe641da4beb1f9b2bed32fde08cc899a617224"
},
{
"path": "skills/make-invalide/templates/Invalide/UtilisateurInvalide.php",
"sha256": "3fea86c0007e2ac53d9582a1fffd31ccc43f54d42d8ee384edd1ea95404c2f9e"
},
{
"path": "skills/make-story/README.md",
"sha256": "d48c7051af564de41571fc7184c4d32144b2fcb2c9552062d07db78b7fe9827e"
},
{
"path": "skills/make-story/SKILL.md",
"sha256": "97194fe8ae895ad13dd1d46531174b26b4b5b9f07d1f18360e0491f3bb742d65"
},
{
"path": "skills/make-story/templates/Story/UtilisateurStory.php",
"sha256": "2f7078db14764b3caa3046448ffdf2bfe5f8a51e0817dab476a85e6495d8b66b"
},
{
"path": "skills/make-story/templates/Story/AppStory.php",
"sha256": "2141beb4b0657a7a3ef0925311e8801f33e2d6a891e4505f01e9d0a68c060f2e"
},
{
"path": "skills/make-all/README.md",
"sha256": "2f8baedb0c5e2ccb7de7244cc2a346800d5d1cc8a807c015ea23fcc31e0d7b00"
},
{
"path": "skills/make-all/SKILL.md",
"sha256": "6e3b3fe815ccec8fa69f9ab16e8c98f5908309d8c5350cabbee9dd21077cc863"
},
{
"path": "skills/make-collection/README.md",
"sha256": "906fbcb32c4018d9105f2fe3c043d9c0758e97e25b5890953fb2690c8498ad61"
},
{
"path": "skills/make-collection/SKILL.md",
"sha256": "a6cf7028a1022c748c240766cef1e36d11a93769a52fa84169087d55e47247be"
},
{
"path": "skills/make-collection/templates/Collection/UtilisateurCollection.php",
"sha256": "114ce58b7f1b1665902d7f9312a6df67390306a2be0d502d9df28ab1508bcb74"
},
{
"path": "skills/make-factory/README.md",
"sha256": "5973f214706811bb83acb31851e6a83c3f6c183795a50680e30a8229e705533b"
},
{
"path": "skills/make-factory/SKILL.md",
"sha256": "dd2e502de98b7ccc75a4e027e84ba147cbd96bcf6f6ce6838a5a3b5cf2e1fc40"
},
{
"path": "skills/make-factory/templates/Factory/UtilisateurFactory.php",
"sha256": "95a85202d958c1a303ba722ab06c541668cf905084c7e4a9e1a13061c7942c89"
}
],
"dirSha256": "8e414effa99b116b6952629a2697a26291b9e09f48eb4ec1f378578b1a823be3"
},
"security": {
"scannedAt": null,
"scannerVersion": null,
"flags": []
}
}

258
skills/make-all/README.md Normal file
View File

@@ -0,0 +1,258 @@
# Framework Make All
Orchestrateur générant une stack complète pour une entité.
## Vue d'ensemble
Cette skill orchestre l'appel de toutes les skills du framework pour générer une entité complète avec tous ses composants selon les principes Elegant Objects et DDD.
## Caractéristiques
### Stack complète générée
- **Contracts** - Interfaces de base (si absentes)
- **Entity** - Entité Doctrine + Repository
- **Out** - DTO immuable pour output
- **Invalide** - Exceptions métier
- **Urls** - Génération d'URLs + CQRS
- **Collection** - Collection typée
- **Factory** - Factory Foundry pour tests
- **Story** - Story Foundry pour fixtures
## Utilisation
```bash
Use skill framework:make:all
```
Vous serez invité à fournir :
1. Nom de l'entité
2. Propriétés avec types (optionnel)
## Exemple d'utilisation
```bash
EntityName: Product
Properties:
- name: string
- description: string
- price: float
- stock: int
- isActive: bool
```
Génère 15+ fichiers en une seule commande.
## Ordre d'exécution
### Phase 1 - Fondation
```
framework:make:contracts (si besoin)
```
### Phase 2 - Core
```
framework:make:entity
```
### Phase 3 - Patterns
```
framework:make:out
framework:make:invalide
```
### Phase 4 - Avancé
```
framework:make:urls
framework:make:collection
```
### Phase 5 - Tests
```
framework:make:factory
framework:make:story
```
## Fichiers générés
### Contracts (si absents)
```
src/Contracts/
├── OutInterface.php
├── InvalideInterface.php
├── HasUrlsInterface.php
├── OutDataInterface.php
├── InvalideDataInterface.php
├── UrlsDataInterface.php
├── Story/
│ └── StoryInterface.php
└── Doctrine/
└── DoctrineMigrationInterface.php
```
### Core
```
src/
├── Entity/
│ └── Product.php
└── Repository/
├── ProductRepository.php
└── ProductRepositoryInterface.php
```
### Patterns
```
src/
├── Out/
│ └── ProductOut.php
└── Invalide/
└── ProductInvalide.php
```
### Avancé
```
src/
├── Urls/
│ └── ProductUrls.php
├── MessageHandler/
│ ├── ProductUrlsMessage.php
│ └── ProductUrlsMessageHandler.php
└── Collection/
└── ProductCollection.php
```
### Tests
```
src/
├── Factory/
│ └── ProductFactory.php
└── Story/
├── ProductStory.php
└── AppStory.php (updated)
```
## Output exemple
```
✓ Phase 1 - Fondation
✓ Contracts déjà présents
✓ Phase 2 - Core
✓ src/Entity/Product.php
✓ src/Repository/ProductRepository.php
✓ src/Repository/ProductRepositoryInterface.php
✓ Phase 3 - Patterns
✓ src/Out/ProductOut.php
✓ src/Invalide/ProductInvalide.php
✓ Phase 4 - Avancé
✓ src/Urls/ProductUrls.php
✓ src/MessageHandler/ProductUrlsMessage.php
✓ src/MessageHandler/ProductUrlsMessageHandler.php
✓ src/Collection/ProductCollection.php
✓ Phase 5 - Tests
✓ src/Factory/ProductFactory.php
✓ src/Story/ProductStory.php
✓ src/Story/AppStory.php (updated)
📊 Total: 15 fichiers créés
```
## Prochaines étapes
Après génération, suivre ces étapes :
1. **Migration Doctrine**
```bash
php bin/console make:migration
php bin/console doctrine:migrations:migrate
```
2. **Enrichir les classes**
- ProductInvalide : ajouter exceptions métier
- ProductUrls : ajouter méthodes d'URLs
- ProductOut : ajouter propriétés exposées
- ProductCollection : ajouter méthodes métier (YAGNI)
- ProductFactory : ajouter méthodes custom (YAGNI)
- ProductStory : ajouter scénarios de test
3. **Tests**
```bash
php bin/phpunit
```
4. **Validation PHPStan**
```bash
vendor/bin/phpstan analyse
```
## Prérequis
- Framework `atournayre/framework` installé avec ses dépendances
- Projet Symfony avec Doctrine ORM configuré
- Zenstruck Foundry pour les tests (optionnel)
## Avantages
### Rapidité
- Une seule commande pour générer toute la stack
- Pas besoin d'appeler 8 skills manuellement
- Gain de temps considérable
### Cohérence
- Ordre d'exécution garanti
- Dépendances gérées automatiquement
- Pas de risque d'oublier un composant
### Best practices
- Principes Elegant Objects appliqués partout
- DDD respecté
- Architecture cohérente
## Cas d'usage
### Nouveau projet
```bash
# Créer première entité complète
Use skill framework:make:all
EntityName: User
```
### Ajout feature
```bash
# Ajouter nouvelle entité au projet existant
Use skill framework:make:all
EntityName: Order
```
### Prototypage rapide
```bash
# Générer rapidement plusieurs entités
Use skill framework:make:all (Product)
Use skill framework:make:all (Category)
Use skill framework:make:all (Review)
```
## Gestion d'erreurs
Si une skill échoue :
1. Affichage clair de l'erreur
2. Indication de la skill en erreur
3. Arrêt du processus
4. Fichiers déjà créés conservés
## Options futures
Possibles extensions :
- `--skip-tests` : sans Factory/Story
- `--skip-urls` : sans Urls/CQRS
- `--minimal` : Entity + Repository + Out uniquement
- `--api-only` : Stack pour API (Entity + Repository + Out + Collection)
## Principes Elegant Objects appliqués
- Toutes les classes finales
- Constructeurs privés
- Factory statiques
- Immutabilité encouragée
- Interfaces pour tous les contrats
- Tests first-class citizens

195
skills/make-all/SKILL.md Normal file
View File

@@ -0,0 +1,195 @@
---
name: framework:make:all
description: Génère tous les fichiers pour une entité complète (orchestrateur)
license: MIT
version: 1.0.0
---
# Framework Make All Skill
## Description
Orchestrateur qui génère tous les fichiers nécessaires pour une entité complète en appelant séquentiellement toutes les skills du framework.
Cette skill crée une stack complète respectant les principes Elegant Objects et DDD pour une entité donnée.
## Usage
```
Use skill framework:make:all
Vous serez invité à fournir :
- Le nom de l'entité (ex: Product, User, Order)
- Les propriétés avec leurs types (optionnel)
```
## Dépendances
Cette skill orchestre l'appel de toutes les autres skills :
1. `framework:make:contracts` (si pas déjà présents)
2. `framework:make:entity`
3. `framework:make:out`
4. `framework:make:invalide`
5. `framework:make:urls`
6. `framework:make:collection`
7. `framework:make:factory`
8. `framework:make:story`
## Variables requises
- **{EntityName}** - Nom de l'entité en PascalCase (ex: Utilisateur, Product)
- **{properties}** - Liste des propriétés avec types (optionnel, array)
## Outputs
Tous les fichiers générés par les 8 skills :
**Contracts** (si non existants)
- `src/Contracts/OutInterface.php`
- `src/Contracts/InvalideInterface.php`
- `src/Contracts/HasUrlsInterface.php`
- `src/Contracts/OutDataInterface.php`
- `src/Contracts/InvalideDataInterface.php`
- `src/Contracts/UrlsDataInterface.php`
- `src/Contracts/Story/StoryInterface.php`
- `src/Contracts/Doctrine/DoctrineMigrationInterface.php`
**Core**
- `src/Entity/{EntityName}.php`
- `src/Repository/{EntityName}Repository.php`
- `src/Repository/{EntityName}RepositoryInterface.php`
**Patterns**
- `src/Out/{EntityName}Out.php`
- `src/Invalide/{EntityName}Invalide.php`
**Avancé**
- `src/Urls/{EntityName}Urls.php`
- `src/MessageHandler/{EntityName}UrlsMessage.php`
- `src/MessageHandler/{EntityName}UrlsMessageHandler.php`
- `src/Collection/{EntityName}Collection.php`
**Tests**
- `src/Factory/{EntityName}Factory.php`
- `src/Story/{EntityName}Story.php`
- `src/Story/AppStory.php` (updated)
## Workflow
1. Demander le nom de l'entité (EntityName)
2. Demander les propriétés (optionnel)
3. Vérifier si `src/Contracts/` existe
- Si non : exécuter `framework:make:contracts`
4. Exécuter séquentiellement :
1. `framework:make:entity` (avec EntityName et properties)
2. `framework:make:out` (avec EntityName)
3. `framework:make:invalide` (avec EntityName)
4. `framework:make:urls` (avec EntityName)
5. `framework:make:collection` (avec EntityName)
6. `framework:make:factory` (avec EntityName et properties)
7. `framework:make:story` (avec EntityName)
5. Afficher le résumé de tous les fichiers créés
6. Afficher les prochaines étapes recommandées
## Ordre d'exécution (critique)
L'ordre d'appel des skills est important car il respecte les dépendances :
```
Phase 1 - Fondation
└── make:contracts (si besoin)
Phase 2 - Core
└── make:entity (dépend de: contracts)
Phase 3 - Patterns (parallélisables mais dépendent de entity)
├── make:out (dépend de: entity)
└── make:invalide (dépend de: entity)
Phase 4 - Avancé (dépendent de entity + repository)
├── make:urls (dépend de: entity, repository)
└── make:collection (dépend de: entity)
Phase 5 - Tests (dépendent de entity)
├── make:factory (dépend de: entity)
└── make:story (dépend de: entity, factory)
```
## Exemple
```bash
Use skill framework:make:all
# Saisies utilisateur :
EntityName: Product
Properties:
- name: string
- description: string
- price: float
- stock: int
- isActive: bool
# Résultat :
✓ Phase 1 - Fondation
✓ Contracts déjà présents
✓ Phase 2 - Core
✓ src/Entity/Product.php
✓ src/Repository/ProductRepository.php
✓ src/Repository/ProductRepositoryInterface.php
✓ Phase 3 - Patterns
✓ src/Out/ProductOut.php
✓ src/Invalide/ProductInvalide.php
✓ Phase 4 - Avancé
✓ src/Urls/ProductUrls.php
✓ src/MessageHandler/ProductUrlsMessage.php
✓ src/MessageHandler/ProductUrlsMessageHandler.php
✓ src/Collection/ProductCollection.php
✓ Phase 5 - Tests
✓ src/Factory/ProductFactory.php
✓ src/Story/ProductStory.php
✓ src/Story/AppStory.php (updated)
📊 Total: 15 fichiers créés
📝 Prochaines étapes recommandées:
1. Créer la migration Doctrine: php bin/console make:migration
2. Exécuter la migration: php bin/console doctrine:migrations:migrate
3. Enrichir ProductInvalide avec exceptions métier
4. Enrichir ProductUrls avec méthodes d'URLs
5. Enrichir ProductOut avec propriétés exposées
6. Enrichir ProductCollection avec méthodes métier (si besoin)
7. Enrichir ProductFactory avec méthodes custom (si besoin)
8. Enrichir ProductStory avec scénarios de test
9. Lancer les tests: php bin/phpunit
```
## Validation après génération
La skill doit vérifier que tous les fichiers ont été créés correctement :
1. Vérifier existence de tous les fichiers
2. Vérifier que les imports sont corrects
3. Vérifier que les namespaces sont cohérents
4. Vérifier que AppStory a été mis à jour
## Gestion des erreurs
Si une skill échoue :
1. Afficher l'erreur clairement
2. Indiquer quelle skill a échoué
3. Proposer de corriger manuellement
4. Ne pas continuer avec les skills suivantes si une skill critique échoue
## Options avancées (futur)
Potentiellement ajouter des flags :
- `--skip-tests` : ne pas générer Factory et Story
- `--skip-urls` : ne pas générer Urls + handlers
- `--minimal` : générer uniquement Entity + Repository + Out
- `--api-only` : générer pour API (Entity + Repository + Out + Collection)
## Notes
- Cette skill est un orchestrateur, elle ne contient pas de templates
- Elle appelle séquentiellement toutes les autres skills
- L'ordre d'exécution est critique et respecte les dépendances
- Idéale pour démarrer rapidement avec une nouvelle entité
- Génère une stack complète Elegant Objects + DDD

View File

@@ -0,0 +1,237 @@
# Framework Make Collection
Génère une classe Collection typée avec traits Atournayre.
## Vue d'ensemble
Cette skill crée une classe Collection type-safe pour gérer des ensembles d'entités selon les principes Elegant Objects.
## Caractéristiques
### Classe Collection générée
- Classe `final`
- Type-safe (collection d'objets typés)
- Interfaces Atournayre complètes
- Traits pour fonctionnalités de base
- Factory statique `asList()`
- Méthode `toLog()` pour logging
## Utilisation
```bash
Use skill framework:make:collection
```
Vous serez invité à fournir le nom de l'entité.
## Exemple d'utilisation
```bash
EntityName: Product
```
Génère :
```php
// src/Collection/ProductCollection.php
final class ProductCollection implements AsListInterface, ToArrayInterface, CountInterface, ...
{
use Collection;
use Collection\ToArray;
use Collection\Countable;
public static function asList(array $collection): self
{
return new self(PrimitiveCollection::of($collection));
}
public function toLog(): array
{
return [
'count' => $this->count()->value(),
'items' => $this->collection->map(fn (Product $item) => $item->toLog()),
];
}
}
```
## Structure créée
```
src/
└── Collection/
└── {EntityName}Collection.php
```
## Prérequis
- L'entité doit exister dans `src/Entity/{EntityName}.php`
- Framework `atournayre/framework` installé
## Interfaces implémentées
- **AsListInterface** - Factory `asList()`
- **ToArrayInterface** - Conversion en array
- **CountInterface** - Comptage d'éléments
- **CountByInterface** - Comptage conditionnel
- **AtLeastOneElementInterface** - Vérification présence
- **HasSeveralElementsInterface** - Vérification multiple
- **HasNoElementInterface** - Vérification vide
- **HasOneElementInterface** - Vérification unique
- **HasXElementsInterface** - Vérification nombre exact
- **LoggableInterface** - Support logging
## Méthodes disponibles via traits
```php
// Création
$products = ProductCollection::asList([$product1, $product2]);
// Comptage
$products->count(); // Number
$products->hasNoElement(); // bool
$products->hasOneElement(); // bool
$products->hasSeveralElements(); // bool
$products->hasAtLeastOneElement(); // bool
$products->hasXElements(5); // bool
// Comptage conditionnel
$activeCount = $products->countBy(fn (Product $p) => $p->isActive());
// Conversion
$array = $products->toArray();
// Logging
$log = $products->toLog();
```
## Enrichissement (principe YAGNI)
**IMPORTANT** : N'ajouter que les méthodes **explicitement demandées**.
### Exemple : filtrage
```php
public function active(): self
{
return new self(
$this->collection->filter(fn (Product $p) => $p->isActive())
);
}
public function inStock(): self
{
return new self(
$this->collection->filter(fn (Product $p) => $p->stock() > 0)
);
}
```
### Exemple : calculs
```php
public function totalPrice(): float
{
return $this->collection
->map(fn (Product $p) => $p->price())
->reduce(fn (float $sum, float $price) => $sum + $price, 0.0);
}
public function averagePrice(): float
{
if ($this->hasNoElement()) {
return 0.0;
}
return $this->totalPrice() / $this->count()->value();
}
```
### Exemple : tri
```php
public function sortedByName(): self
{
return new self(
$this->collection->sort(fn (Product $a, Product $b) =>
$a->name() <=> $b->name()
)
);
}
public function sortedByPriceDesc(): self
{
return new self(
$this->collection->sort(fn (Product $a, Product $b) =>
$b->price() <=> $a->price()
)
);
}
```
### Exemple : recherche
```php
public function findById(Uuid $id): ?Product
{
return $this->collection
->filter(fn (Product $p) => $p->id()->equals($id))
->first();
}
public function findByName(string $name): self
{
return new self(
$this->collection->filter(fn (Product $p) => $p->name() === $name)
);
}
```
## Usage dans le code
### Depuis un repository
```php
final class ProductRepository extends ServiceEntityRepository
{
public function findAllAsCollection(): ProductCollection
{
return ProductCollection::asList($this->findAll());
}
public function findActiveAsCollection(): ProductCollection
{
return ProductCollection::asList(
$this->createQueryBuilder('p')
->where('p.isActive = true')
->getQuery()
->getResult()
);
}
}
```
### Dans un service
```php
final readonly class ProductService
{
public function calculateTotalStock(ProductCollection $products): int
{
return $products->collection
->map(fn (Product $p) => $p->stock())
->reduce(fn (int $sum, int $stock) => $sum + $stock, 0);
}
}
```
### Dans un contrôleur
```php
public function index(ProductRepository $repository): Response
{
$products = $repository->findAllAsCollection();
return $this->render('product/index.html.twig', [
'products' => $products->active()->sortedByName(),
'total' => $products->count()->value(),
]);
}
```
## Principes Elegant Objects appliqués
- Classe finale
- Factory statique
- Type-safety
- Immutabilité (nouvelles instances pour transformations)
- Pas de méthodes génériques anticipées (YAGNI)
- LoggableInterface pour observabilité

View File

@@ -0,0 +1,192 @@
---
name: framework:make:collection
description: Génère classe Collection typée avec traits Atournayre
license: MIT
version: 1.0.0
---
# Framework Make Collection Skill
## Description
Génère une classe Collection typée pour gérer des ensembles d'entités avec les traits et interfaces Atournayre.
La Collection offre des méthodes pour manipuler des ensembles d'objets de manière type-safe et respectant les principes Elegant Objects.
## Usage
```
Use skill framework:make:collection
Vous serez invité à fournir :
- Le nom de l'entité (ex: Product, User, Order)
```
## Templates
- `Collection/UtilisateurCollection.php` - Template de classe Collection
## 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`
- Requiert framework `atournayre/framework`
## Outputs
- `src/Collection/{EntityName}Collection.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. Générer la classe Collection depuis le template :
- Remplacer `{EntityName}` par le nom fourni
- Remplacer `{entityName}` par la version camelCase
4. Afficher le fichier créé
## Patterns appliqués
### Classe Collection
- Classe `final`
- Implémente interfaces Atournayre :
- AsListInterface
- ToArrayInterface
- CountInterface
- CountByInterface
- AtLeastOneElementInterface
- HasSeveralElementsInterface
- HasNoElementInterface
- HasOneElementInterface
- HasXElementsInterface
- LoggableInterface
- Utilise traits Atournayre :
- Collection
- Collection\ToArray
- Collection\Countable
- Méthode statique `asList(array $collection)`
- Méthode `toLog()` pour logging
## Exemple
```bash
Use skill framework:make:collection
# Saisies utilisateur :
EntityName: Product
# Résultat :
✓ src/Collection/ProductCollection.php
```
Fichier généré :
```php
<?php
declare(strict_types=1);
namespace App\Collection;
use App\Entity\Product;
use Atournayre\Contracts\Collection\AsListInterface;
use Atournayre\Contracts\Collection\AtLeastOneElementInterface;
use Atournayre\Contracts\Collection\CountByInterface;
use Atournayre\Contracts\Collection\CountInterface;
use Atournayre\Contracts\Collection\HasNoElementInterface;
use Atournayre\Contracts\Collection\HasOneElementInterface;
use Atournayre\Contracts\Collection\HasSeveralElementsInterface;
use Atournayre\Contracts\Collection\HasXElementsInterface;
use Atournayre\Contracts\Collection\ToArrayInterface;
use Atournayre\Contracts\Log\LoggableInterface;
use Atournayre\Primitives\Collection as PrimitiveCollection;
use Atournayre\Primitives\Traits\Collection;
final class ProductCollection implements AsListInterface, ToArrayInterface, CountInterface, CountByInterface, AtLeastOneElementInterface, HasSeveralElementsInterface, HasNoElementInterface, HasOneElementInterface, HasXElementsInterface, LoggableInterface
{
use Collection;
use Collection\ToArray;
use Collection\Countable;
public static function asList(array $collection): self
{
return new self(PrimitiveCollection::of($collection));
}
/**
* @return array<string, mixed>
*/
public function toLog(): array
{
return [
'count' => $this->count()->value(),
'items' => $this->collection->map(fn (Product $item) => $item->toLog()),
];
}
// UNIQUEMENT les méthodes EXPLICITEMENT demandées par l'utilisateur
// PAS d'anticipation de besoins futurs
// PAS de méthodes génériques (add, remove, filter, map, etc.)
}
```
## Usage
### Création d'une collection
```php
$products = ProductCollection::asList([
$product1,
$product2,
$product3,
]);
```
### Méthodes disponibles (via traits)
```php
// Comptage
$count = $products->count(); // Atournayre\Primitives\Number
$hasElements = $products->hasNoElement(); // bool
$hasOne = $products->hasOneElement(); // bool
$hasSeveral = $products->hasSeveralElements(); // bool
$hasAtLeastOne = $products->hasAtLeastOneElement(); // bool
$hasX = $products->hasXElements(5); // bool
// Conversion
$array = $products->toArray(); // array
// Comptage personnalisé
$activeCount = $products->countBy(fn (Product $p) => $p->isActive());
```
### Ajout de méthodes métier (YAGNI)
N'ajouter que les méthodes **explicitement demandées** :
```php
final class ProductCollection implements ...
{
// ... traits ...
public function active(): self
{
return new self(
$this->collection->filter(fn (Product $p) => $p->isActive())
);
}
public function totalPrice(): float
{
return $this->collection
->map(fn (Product $p) => $p->price())
->reduce(fn (float $sum, float $price) => $sum + $price, 0.0);
}
}
```
## Notes
- Respect du principe YAGNI : pas de méthodes génériques anticipées
- Seules les méthodes explicitement demandées doivent être ajoutées
- Les traits fournissent déjà beaucoup de fonctionnalités
- La collection est type-safe (typage sur l'entité)
- LoggableInterface permet le logging automatique

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace App\Collection;
use App\Entity\Utilisateur;
use Atournayre\Contracts\Collection\AsListInterface;
use Atournayre\Contracts\Collection\AtLeastOneElementInterface;
use Atournayre\Contracts\Collection\CountByInterface;
use Atournayre\Contracts\Collection\CountInterface;
use Atournayre\Contracts\Collection\HasNoElementInterface;
use Atournayre\Contracts\Collection\HasOneElementInterface;
use Atournayre\Contracts\Collection\HasSeveralElementsInterface;
use Atournayre\Contracts\Collection\HasXElementsInterface;
use Atournayre\Contracts\Collection\ToArrayInterface;
use Atournayre\Contracts\Log\LoggableInterface;
use Atournayre\Primitives\Collection as PrimitiveCollection;
use Atournayre\Primitives\Traits\Collection;
final class UtilisateurCollection implements AsListInterface, ToArrayInterface, CountInterface, CountByInterface, AtLeastOneElementInterface, HasSeveralElementsInterface, HasNoElementInterface, HasOneElementInterface, HasXElementsInterface, LoggableInterface
{
use Collection;
use Collection\ToArray;
use Collection\Countable;
public static function asList(array $collection): self
{
return new self(PrimitiveCollection::of($collection));
}
/**
* @return array<string, mixed>
*/
public function toLog(): array
{
return [
'count' => $this->count()->value(),
'items' => $this->collection->map(fn (Utilisateur $item) => $item->toLog()),
];
}
// UNIQUEMENT les méthodes EXPLICITEMENT demandées par l'utilisateur
// PAS d'anticipation de besoins futurs
// PAS de méthodes génériques (add, remove, filter, map, etc.)
}

View File

@@ -0,0 +1,55 @@
# Framework Make Contracts
Génère les interfaces de contrats pour une architecture Elegant Objects.
## Vue d'ensemble
Cette skill crée l'ensemble des interfaces nécessaires pour supporter les principes Elegant Objects et DDD dans un projet Symfony.
## Interfaces générées
### Interfaces principales
- **OutInterface** - Pour objets de sortie (DTO immuables)
- **InvalideInterface** - Pour exceptions métier
- **HasUrlsInterface** - Pour objets ayant des URLs générées
### Interfaces de data
- **OutDataInterface** - Pour data classes de sortie
- **InvalideDataInterface** - Pour data classes d'invalidation
- **UrlsDataInterface** - Pour data classes d'URLs
### Interfaces spécialisées
- **StoryInterface** - Pour stories de tests (Foundry)
- **DoctrineMigrationInterface** - Pour migrations Doctrine
## Utilisation
```bash
# Via skill
Use skill framework:make:contracts
```
## Structure créée
```
src/
└── Contracts/
├── OutInterface.php
├── InvalideInterface.php
├── HasUrlsInterface.php
├── OutDataInterface.php
├── InvalideDataInterface.php
├── UrlsDataInterface.php
├── Story/
│ └── StoryInterface.php
└── Doctrine/
└── DoctrineMigrationInterface.php
```
## Prérequis
Aucun - C'est la première skill à exécuter dans un nouveau projet.
## Principes Elegant Objects appliqués
- Toutes les interfaces définissent des contrats clairs
- Pas de méthodes statiques
- Chaque interface a une responsabilité unique
- Favorise l'immutabilité et l'encapsulation

View File

@@ -0,0 +1,59 @@
---
name: framework:make:contracts
description: Génère les interfaces de contrats pour une architecture Elegant Objects
license: MIT
version: 1.0.0
---
# Framework Make Contracts Skill
## Description
Génère l'ensemble des interfaces de contrats nécessaires pour une architecture respectant les principes Elegant Objects et DDD.
Ces interfaces servent de fondation pour toutes les autres skills du framework.
## Usage
```
Use skill framework:make:contracts
```
## Templates
- `OutInterface.php` - Interface pour objets de sortie (DTO)
- `InvalideInterface.php` - Interface pour exceptions métier
- `HasUrlsInterface.php` - Interface pour objets ayant des URLs
- `OutDataInterface.php` - Interface pour data classes de sortie
- `InvalideDataInterface.php` - Interface pour data classes d'invalidation
- `UrlsDataInterface.php` - Interface pour data classes d'URLs
- `Story/StoryInterface.php` - Interface pour stories de tests
- `Doctrine/DoctrineMigrationInterface.php` - Interface pour migrations Doctrine
## Variables requises
Aucune - Ces interfaces sont génériques et ne nécessitent pas de paramètres.
## Dépendances
Aucune - C'est la première skill à exécuter (Niveau 0 - Fondation).
## Outputs
- `src/Contracts/OutInterface.php`
- `src/Contracts/InvalideInterface.php`
- `src/Contracts/HasUrlsInterface.php`
- `src/Contracts/OutDataInterface.php`
- `src/Contracts/InvalideDataInterface.php`
- `src/Contracts/UrlsDataInterface.php`
- `src/Contracts/Story/StoryInterface.php`
- `src/Contracts/Doctrine/DoctrineMigrationInterface.php`
## Workflow
1. Vérifier si le répertoire `src/Contracts/` existe
2. Créer la structure de répertoires si nécessaire :
- `src/Contracts/`
- `src/Contracts/Story/`
- `src/Contracts/Doctrine/`
3. Copier tous les templates d'interfaces depuis `framework/skills/make-contracts/templates/Contracts/` vers `src/Contracts/`
4. Afficher la liste des fichiers créés
## Notes
- Ces interfaces n'ont pas besoin d'être modifiées pour chaque projet
- Elles doivent être créées une seule fois par projet
- Toutes les autres skills du framework dépendent de ces interfaces

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace App\Contracts\Doctrine;
use Doctrine\DBAL\Schema\Schema;
interface DoctrineMigrationInterface
{
public function up(Schema $schema): void;
public function down(Schema $schema): void;
public function description(): string;
public function isTransactional(): bool;
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace App\Contracts;
interface HasUrlsInterface
{
public function urls(): mixed;
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace App\Contracts;
/**
* Marker interface for Invalide data objects.
*/
interface InvalideDataInterface
{
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace App\Contracts;
interface InvalideInterface
{
public function invalide(): mixed;
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace App\Contracts;
/**
* Marker interface for Out data objects.
*/
interface OutDataInterface
{
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace App\Contracts;
interface OutInterface
{
public function out(): mixed;
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace App\Contracts\Story;
interface StoryInterface
{
public function build(): void;
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace App\Contracts;
/**
* Marker interface for Urls data objects.
*/
interface UrlsDataInterface
{
}

View File

@@ -0,0 +1,107 @@
# Framework Make Entity
Génère une entité Doctrine avec repository selon principes Elegant Objects.
## Vue d'ensemble
Cette skill crée une entité Doctrine complète respectant les principes Elegant Objects, avec son repository et son interface.
## Caractéristiques
### Entité générée
- Constructeur privé + factory statique `create()`
- Classe `final`
- Propriétés privées avec getters uniquement
- Pas de setters (immutabilité encouragée)
- Implémentation des interfaces de contrats
- Traits Atournayre intégrés
- Attributs Doctrine ORM configurés
- Méthode `toLog()` pour LoggableInterface
### Repository généré
- Extends ServiceEntityRepository
- Classe `final`
- Interface dédiée
- Prêt pour méthodes custom
## Utilisation
```bash
Use skill framework:make:entity
```
Vous serez invité à fournir :
1. Nom de l'entité (PascalCase)
2. Liste des propriétés avec types
## Exemple d'utilisation
```bash
EntityName: Product
Properties:
- name: string
- description: string
- price: float
- stock: int
- isActive: bool
```
Génère :
```php
// src/Entity/Product.php
final class Product implements LoggableInterface, DatabaseEntityInterface, ...
{
use DatabaseTrait;
use NullTrait;
use DependencyInjectionTrait;
private function __construct(
#[ORM\Id]
#[ORM\Column(type: 'uuid')]
private Uuid $id,
#[ORM\Column(type: 'string')]
private string $name,
// ...
) {}
public static function create(Uuid $id, string $name, ...): self
{
return new self(id: $id, name: $name, ...);
}
public function id(): Uuid { return $this->id; }
public function name(): string { return $this->name; }
// ...
}
```
## Structure créée
```
src/
├── Entity/
│ └── {EntityName}.php
└── Repository/
├── {EntityName}Repository.php
└── {EntityName}RepositoryInterface.php
```
## Prérequis
- Contracts doivent exister (appelle automatiquement `framework:make:contracts` si absents)
- Projet Symfony avec Doctrine ORM configuré
- Framework `atournayre/framework` installé
## Types de propriétés supportés
- Scalaires : `string`, `int`, `float`, `bool`
- UUID : `Uuid` (Symfony\Component\Uid\Uuid)
- DateTime : `\DateTimeImmutable`
- Arrays : `array`
- Relations Doctrine (à configurer manuellement après génération)
## Principes Elegant Objects appliqués
- Constructeur privé
- Factory statique pour création
- Classe finale (pas d'héritage)
- Pas de setters publics
- Propriétés privées avec getters
- Interfaces pour tous les contrats
- Immutabilité encouragée

108
skills/make-entity/SKILL.md Normal file
View File

@@ -0,0 +1,108 @@
---
name: framework:make:entity
description: Génère une entité Doctrine avec repository selon principes Elegant Objects
license: MIT
version: 1.0.0
---
# Framework Make Entity Skill
## Description
Génère une entité Doctrine complète avec son repository selon les principes Elegant Objects.
L'entité générée inclut :
- Constructeur privé avec factory statique `create()`
- Traits Elegant Objects (DatabaseTrait, NullTrait, DependencyInjectionTrait)
- Implémentation des interfaces de contrats
- Repository avec interface
## Usage
```
Use skill framework:make:entity
Vous serez invité à fournir :
- Le nom de l'entité (ex: Product, User, Order)
- Les propriétés avec leurs types (ex: name:string, email:string, isActive:bool)
```
## Templates
- `Entity/Utilisateur.php` - Template d'entité
- `Repository/UtilisateurRepository.php` - Template de repository
- `Repository/UtilisateurRepositoryInterface.php` - Template d'interface repository
## 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 avec types (array)
## Dépendances
- Requiert que les Contracts soient présents
- Appelle automatiquement `framework:make:contracts` si les interfaces n'existent pas
## Outputs
- `src/Entity/{EntityName}.php`
- `src/Repository/{EntityName}Repository.php`
- `src/Repository/{EntityName}RepositoryInterface.php`
## Workflow
1. Demander le nom de l'entité (EntityName)
2. Demander les propriétés (nom, type, nullable)
3. Vérifier si `src/Contracts/` existe
- Si non : appeler `framework:make:contracts`
4. Générer l'entité depuis le template :
- Remplacer `{EntityName}` par le nom fourni
- Remplacer `{entityName}` par la version camelCase
- Générer les propriétés dans le constructeur
- Générer les méthodes getter pour chaque propriété
5. Générer le repository et son interface
6. Afficher le résumé des fichiers créés
## Patterns appliqués
### Entité
- Classe `final`
- Constructeur privé
- Factory statique `create()` pour instanciation
- Traits : DatabaseTrait, NullTrait, DependencyInjectionTrait
- Attributs Doctrine ORM (#[ORM\Entity], #[ORM\Id], #[ORM\Column])
- Implémentation des interfaces :
- LoggableInterface
- DatabaseEntityInterface
- NullableInterface
- DependencyInjectionAwareInterface
- OutInterface
- HasUrlsInterface
- InvalideInterface
### Repository
- Classe `final`
- Extends ServiceEntityRepository
- Implémente l'interface du repository
- Constructeur avec ManagerRegistry uniquement
## Exemple
```bash
Use skill framework:make:entity
# Saisies utilisateur :
EntityName: Product
Properties:
- id: Uuid
- name: string
- price: float
- isActive: bool
# Résultat :
✓ src/Entity/Product.php
✓ src/Repository/ProductRepository.php
✓ src/Repository/ProductRepositoryInterface.php
```
## Notes
- L'ID de type Uuid est ajouté automatiquement
- Les propriétés sont toujours privées avec getters
- Pas de setters (immutabilité)
- La méthode `toLog()` inclut automatiquement toutes les propriétés

View File

@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use App\Contracts\HasUrlsInterface;
use App\Contracts\InvalideInterface;
use App\Contracts\OutInterface;
use App\Invalide\UtilisateurInvalide;
use App\MessageHandler\UtilisateurUrlsMessage;
use App\Out\UtilisateurOut;
use App\Repository\UtilisateurRepository;
use App\Urls\UtilisateurUrls;
use Atournayre\Common\Persistance\DatabaseTrait;
use Atournayre\Contracts\DependencyInjection\DependencyInjectionAwareInterface;
use Atournayre\Contracts\Log\LoggableInterface;
use Atournayre\Contracts\Null\NullableInterface;
use Atournayre\Contracts\Persistance\DatabaseEntityInterface;
use Atournayre\Null\NullTrait;
use Atournayre\Traits\DependencyInjectionTrait;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Uid\Uuid;
#[ORM\Entity(repositoryClass: UtilisateurRepository::class)]
final class Utilisateur implements LoggableInterface, DatabaseEntityInterface, NullableInterface, DependencyInjectionAwareInterface, OutInterface, HasUrlsInterface, InvalideInterface
{
use DatabaseTrait;
use DependencyInjectionTrait;
use NullTrait;
private function __construct(
#[ORM\Id]
#[ORM\Column(type: 'uuid')]
private Uuid $id,
) {
}
public static function create(
Uuid $id,
): self {
return new self(
id: $id,
);
}
public function id(): Uuid
{
return $this->id;
}
/**
* @return array<string, mixed>
*/
public function toLog(): array
{
return [
'id' => $this->id,
];
}
public function invalide(): UtilisateurInvalide
{
return UtilisateurInvalide::new(
utilisateur: $this,
);
}
public function out(): UtilisateurOut
{
return UtilisateurOut::new(
utilisateur: $this,
);
}
public function urls(): UtilisateurUrls
{
/** @var UtilisateurUrls $urls */
$urls = UtilisateurUrlsMessage::new(
id: $this->id->toRfc4122(),
)->query($this->dependencyInjection()->queryBus());
return $urls;
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Entity\Utilisateur;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Utilisateur>
*/
final class UtilisateurRepository extends ServiceEntityRepository implements UtilisateurRepositoryInterface
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Utilisateur::class);
}
}

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace App\Repository;
interface UtilisateurRepositoryInterface
{
}

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),
]);
}
}

View File

@@ -0,0 +1,191 @@
# Framework Make Invalide
Génère une classe Invalide pour exceptions métier d'une entité.
## Vue d'ensemble
Cette skill crée une classe Invalide qui encapsule les exceptions métier spécifiques à une entité selon les principes Elegant Objects.
## Caractéristiques
### Classe Invalide générée
- Classe `final`
- Constructeur privé
- Factory statique `new()`
- Encapsule l'entité
- Base pour méthodes factory d'exceptions
## Utilisation
```bash
Use skill framework:make:invalide
```
Vous serez invité à fournir le nom de l'entité.
## Exemple d'utilisation
```bash
EntityName: Product
```
Génère :
```php
// src/Invalide/ProductInvalide.php
final class ProductInvalide
{
private function __construct(
private Product $product,
) {}
public static function new(Product $product): self
{
return new self(product: $product);
}
}
```
## Structure créée
```
src/
└── Invalide/
└── {EntityName}Invalide.php
```
## Prérequis
- L'entité doit exister dans `src/Entity/{EntityName}.php`
## Usage recommandé
### Dans l'entité
```php
final class Product implements InvalideInterface
{
public function invalide(): ProductInvalide
{
return ProductInvalide::new(product: $this);
}
}
```
### Enrichissement avec exceptions métier
```php
final class ProductInvalide
{
private function __construct(
private Product $product,
) {}
public static function new(Product $product): self
{
return new self(product: $product);
}
// Exceptions de validation
public static function carNomVide(): \InvalidArgumentException
{
return new \InvalidArgumentException(
'Le nom du produit ne peut pas être vide'
);
}
public static function carPrixNegatif(): \InvalidArgumentException
{
return new \InvalidArgumentException(
'Le prix ne peut pas être négatif'
);
}
// Exceptions métier
public static function carStockInsuffisant(
int $demande,
int $disponible
): \DomainException {
return new \DomainException(
sprintf(
'Stock insuffisant: %d demandé, %d disponible',
$demande,
$disponible
)
);
}
public static function carProduitInactif(string $id): \DomainException
{
return new \DomainException(
sprintf('Le produit %s est inactif', $id)
);
}
}
```
### Utilisation dans le code
```php
// Validation dans factory
public static function create(
Uuid $id,
string $name,
float $price
): self {
if ('' === $name) {
throw ProductInvalide::carNomVide();
}
if ($price < 0) {
throw ProductInvalide::carPrixNegatif();
}
return new self(id: $id, name: $name, price: $price);
}
// Validation métier
public function decreaseStock(int $quantity): void
{
if (!$this->isActive) {
throw ProductInvalide::carProduitInactif($this->id->toRfc4122());
}
if ($this->stock < $quantity) {
throw ProductInvalide::carStockInsuffisant(
demande: $quantity,
disponible: $this->stock
);
}
$this->stock -= $quantity;
}
```
## Conventions de nommage
### Méthodes factory
- Préfixe : `car` (français)
- Format : `carRaisonDeLErreur`
- Exemples :
- `carNomVide()`
- `carEmailInvalide()`
- `carStockInsuffisant()`
- `carProduitInactif()`
### Messages d'exception
- Pas de point final
- Inclure le contexte maximum
- Une seule phrase sans points internes
- Exemples :
-`'Le nom du produit ne peut pas être vide'`
-`'Stock insuffisant: 5 demandé, 2 disponible'`
-`'Erreur.'`
-`'Le nom est vide. Veuillez le renseigner.'`
## Types d'exceptions recommandés
- `\InvalidArgumentException` - Validation d'arguments
- `\DomainException` - Règles métier
- `\LogicException` - État incohérent
- `\RuntimeException` - Erreur runtime
## Principes Elegant Objects appliqués
- Classe finale
- Constructeur privé
- Factory statiques
- Fail fast
- Messages d'erreur avec contexte
- Exceptions spécifiques au domaine

View File

@@ -0,0 +1,185 @@
---
name: framework:make:invalide
description: Génère classe Invalide (exceptions métier)
license: MIT
version: 1.0.0
---
# Framework Make Invalide Skill
## Description
Génère une classe Invalide pour gérer les exceptions métier d'une entité.
La classe Invalide encapsule l'entité et fournit des factory methods pour créer des exceptions spécifiques au contexte métier.
## Usage
```
Use skill framework:make:invalide
Vous serez invité à fournir :
- Le nom de l'entité (ex: Product, User, Order)
```
## Templates
- `Invalide/UtilisateurInvalide.php` - Template de classe Invalide
## 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`
## Outputs
- `src/Invalide/{EntityName}Invalide.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. Générer la classe Invalide depuis le template :
- Remplacer `{EntityName}` par le nom fourni
- Remplacer `{entityName}` par la version camelCase
4. Afficher le fichier créé
## Patterns appliqués
### Classe Invalide
- Classe `final`
- Constructeur privé
- Factory statique `new()` pour instanciation
- Propriété privée de type entité
- Méthodes factory statiques pour exceptions spécifiques
## Exemple
```bash
Use skill framework:make:invalide
# Saisies utilisateur :
EntityName: Product
# Résultat :
✓ src/Invalide/ProductInvalide.php
```
Fichier généré :
```php
<?php
declare(strict_types=1);
namespace App\Invalide;
use App\Entity\Product;
final class ProductInvalide
{
private function __construct(
private Product $product,
) {
}
public static function new(
Product $product,
): self {
return new self(
product: $product,
);
}
}
```
## Usage dans l'entité
L'entité doit implémenter la méthode `invalide()` :
```php
public function invalide(): ProductInvalide
{
return ProductInvalide::new(
product: $this,
);
}
```
## Enrichissement avec exceptions métier
Ajouter des méthodes factory pour les cas d'erreur spécifiques :
```php
final class ProductInvalide
{
private function __construct(
private Product $product,
) {}
public static function new(Product $product): self
{
return new self(product: $product);
}
public static function carPrixNegatif(): \InvalidArgumentException
{
return new \InvalidArgumentException(
'Le prix du produit ne peut pas être négatif'
);
}
public static function carStockInsuffisant(int $demande, int $disponible): \DomainException
{
return new \DomainException(
sprintf(
'Stock insuffisant: %d demandé, %d disponible',
$demande,
$disponible
)
);
}
public static function carNomVide(): \InvalidArgumentException
{
return new \InvalidArgumentException(
'Le nom du produit ne peut pas être vide'
);
}
}
```
## Usage des exceptions
```php
final class Product
{
public static function create(Uuid $id, string $name, float $price): self
{
if ('' === $name) {
throw ProductInvalide::carNomVide();
}
if ($price < 0) {
throw ProductInvalide::carPrixNegatif();
}
return new self(id: $id, name: $name, price: $price);
}
public function decreaseStock(int $quantity): void
{
if ($this->stock < $quantity) {
throw ProductInvalide::carStockInsuffisant(
demande: $quantity,
disponible: $this->stock
);
}
// ...
}
}
```
## Notes
- Les méthodes factory d'exceptions doivent commencer par `car` (convention)
- Les messages d'exception ne doivent pas finir par un point
- Les messages doivent inclure le maximum de contexte
- Privilégier les exceptions standard PHP (\InvalidArgumentException, \DomainException)
- Respecte le principe "fail fast"

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace App\Invalide;
use App\Entity\Utilisateur;
final class UtilisateurInvalide
{
private function __construct(
private Utilisateur $utilisateur,
) {
}
public static function new(
Utilisateur $utilisateur,
): self {
return new self(
utilisateur: $utilisateur,
);
}
}

118
skills/make-out/README.md Normal file
View File

@@ -0,0 +1,118 @@
# Framework Make Out
Génère une classe Out (DTO immuable pour output).
## Vue d'ensemble
Cette skill crée une classe Out qui sert de Data Transfer Object immuable pour exposer les données d'une entité vers l'extérieur.
## Caractéristiques
### Classe Out générée
- Classe `final readonly` (PHP 8.2+)
- Constructeur privé
- Factory statique `new()`
- Encapsule l'entité
- Complètement immuable
- Couche anti-corruption
## Utilisation
```bash
Use skill framework:make:out
```
Vous serez invité à fournir le nom de l'entité.
## Exemple d'utilisation
```bash
EntityName: Product
```
Génère :
```php
// src/Out/ProductOut.php
final readonly class ProductOut
{
private function __construct(
private Product $product,
) {}
public static function new(Product $product): self
{
return new self(product: $product);
}
}
```
## Structure créée
```
src/
└── Out/
└── {EntityName}Out.php
```
## Prérequis
- L'entité doit exister dans `src/Entity/{EntityName}.php`
- PHP 8.2+ pour readonly classes
## Usage recommandé
### Dans l'entité
```php
final class Product implements OutInterface
{
public function out(): ProductOut
{
return ProductOut::new(product: $this);
}
}
```
### Dans un contrôleur
```php
#[Route('/api/products/{id}', methods: ['GET'])]
public function show(Product $product): JsonResponse
{
return $this->json($product->out());
}
```
### Ajout de méthodes exposées
```php
final readonly class ProductOut
{
private function __construct(
private Product $product,
) {}
public static function new(Product $product): self
{
return new self(product: $product);
}
public function id(): string
{
return $this->product->id()->toRfc4122();
}
public function name(): string
{
return $this->product->name();
}
public function formattedPrice(): string
{
return number_format($this->product->price(), 2) . ' €';
}
}
```
## Principes Elegant Objects appliqués
- Classe finale
- Constructeur privé
- Factory statique
- Immutabilité totale (readonly)
- Encapsulation de l'entité
- Pas de getters bruts, méthodes métier

112
skills/make-out/SKILL.md Normal file
View File

@@ -0,0 +1,112 @@
---
name: framework:make:out
description: Génère classe Out (DTO immuable pour output)
license: MIT
version: 1.0.0
---
# Framework Make Out Skill
## Description
Génère une classe Out (Data Transfer Object) immuable pour représenter les données de sortie d'une entité.
La classe Out est un DTO readonly qui encapsule une entité pour l'exposition vers l'extérieur (API, vues, etc.).
## Usage
```
Use skill framework:make:out
Vous serez invité à fournir :
- Le nom de l'entité (ex: Product, User, Order)
```
## Templates
- `Out/UtilisateurOut.php` - Template de classe Out
## 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`
## Outputs
- `src/Out/{EntityName}Out.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. Générer la classe Out depuis le template :
- Remplacer `{EntityName}` par le nom fourni
- Remplacer `{entityName}` par la version camelCase
4. Afficher le fichier créé
## Patterns appliqués
### Classe Out
- Classe `final readonly`
- Constructeur privé
- Factory statique `new()` pour instanciation
- Propriété privée de type entité
- Objet complètement immuable
## Exemple
```bash
Use skill framework:make:out
# Saisies utilisateur :
EntityName: Product
# Résultat :
✓ src/Out/ProductOut.php
```
Fichier généré :
```php
<?php
declare(strict_types=1);
namespace App\Out;
use App\Entity\Product;
final readonly class ProductOut
{
private function __construct(
private Product $product,
) {
}
public static function new(
Product $product,
): self {
return new self(
product: $product,
);
}
}
```
## Usage dans l'entité
L'entité doit implémenter la méthode `out()` :
```php
public function out(): ProductOut
{
return ProductOut::new(
product: $this,
);
}
```
## Notes
- La classe Out peut être enrichie avec des méthodes pour exposer des propriétés calculées
- Elle sert de couche anti-corruption entre le domaine et l'extérieur
- Permet de contrôler finement ce qui est exposé depuis l'entité
- Respecte le principe d'immutabilité (readonly)

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);
}
}

218
skills/make-urls/README.md Normal file
View File

@@ -0,0 +1,218 @@
# Framework Make Urls
Génère classe Urls + Message CQRS + Handler pour génération d'URLs.
## Vue d'ensemble
Cette skill crée un ensemble de classes respectant le pattern CQRS pour gérer la génération d'URLs d'une entité.
## Caractéristiques
### Classes générées
- **Urls** - Classe finale readonly encapsulant la génération d'URLs
- **UrlsMessage** - Query CQRS pour récupérer les URLs
- **UrlsMessageHandler** - Handler orchestrant récupération entité + génération URLs
## Utilisation
```bash
Use skill framework:make:urls
```
Vous serez invité à fournir le nom de l'entité.
## Exemple d'utilisation
```bash
EntityName: Product
```
Génère 3 fichiers :
```php
// src/Urls/ProductUrls.php
final readonly class ProductUrls
{
private function __construct(
private UrlGeneratorInterface $urlGenerator,
private Product $product,
) {}
public static function new(
UrlGeneratorInterface $urlGenerator,
Product $product,
): self {
return new self(
urlGenerator: $urlGenerator,
product: $product,
);
}
}
// src/MessageHandler/ProductUrlsMessage.php
final class ProductUrlsMessage extends AbstractQueryEvent implements QueryInterface
{
private function __construct(
public string $id,
) {}
public static function new(string $id): self
{
return new self(id: $id);
}
public function id(): string
{
return $this->id;
}
}
// src/MessageHandler/ProductUrlsMessageHandler.php
#[AsMessageHandler]
final readonly class ProductUrlsMessageHandler
{
public function __construct(
private ProductRepositoryInterface $productRepository,
private UrlGeneratorInterface $urlGenerator,
) {}
public function __invoke(ProductUrlsMessage $message): ProductUrls
{
$product = $this->productRepository->find($message->id());
return ProductUrls::new(
urlGenerator: $this->urlGenerator,
product: $product,
);
}
}
```
## Structure créée
```
src/
├── Urls/
│ └── {EntityName}Urls.php
└── MessageHandler/
├── {EntityName}UrlsMessage.php
└── {EntityName}UrlsMessageHandler.php
```
## Prérequis
- L'entité doit exister dans `src/Entity/{EntityName}.php`
- Le repository doit exister dans `src/Repository/{EntityName}Repository.php`
- L'interface repository doit exister
- Symfony Messenger configuré
- Atournayre packages installés (AbstractQueryEvent, QueryInterface)
## Usage recommandé
### Dans l'entité
```php
final class Product implements HasUrlsInterface
{
public function urls(): ProductUrls
{
/** @var ProductUrls $urls */
$urls = ProductUrlsMessage::new(
id: $this->id->toRfc4122(),
)->query($this->dependencyInjection()->queryBus());
return $urls;
}
}
```
### Ajout de méthodes d'URLs
```php
final readonly class ProductUrls
{
private function __construct(
private UrlGeneratorInterface $urlGenerator,
private Product $product,
) {}
public static function new(
UrlGeneratorInterface $urlGenerator,
Product $product,
): self {
return new self(
urlGenerator: $urlGenerator,
product: $product,
);
}
public function show(): string
{
return $this->urlGenerator->generate(
'product_show',
['id' => $this->product->id()->toRfc4122()],
UrlGeneratorInterface::ABSOLUTE_URL
);
}
public function edit(): string
{
return $this->urlGenerator->generate(
'product_edit',
['id' => $this->product->id()->toRfc4122()],
UrlGeneratorInterface::ABSOLUTE_URL
);
}
public function api(): string
{
return $this->urlGenerator->generate(
'api_product_get',
['id' => $this->product->id()->toRfc4122()],
UrlGeneratorInterface::ABSOLUTE_URL
);
}
}
```
### Utilisation dans templates Twig
```twig
{# product/show.html.twig #}
<a href="{{ product.urls.edit }}">Modifier</a>
<a href="{{ product.urls.delete }}">Supprimer</a>
{# API link #}
<code>{{ product.urls.api }}</code>
```
### Utilisation dans contrôleurs
```php
public function show(Product $product): Response
{
return $this->render('product/show.html.twig', [
'product' => $product,
'editUrl' => $product->urls()->edit(),
]);
}
```
## Architecture CQRS
### Flow
1. Entité appelle `ProductUrlsMessage::new(id)`
2. Message envoyé au QueryBus
3. Handler intercepte le message
4. Handler récupère l'entité via repository
5. Handler crée et retourne ProductUrls
6. URLs disponibles dans l'entité
### Avantages
- Séparation des responsabilités
- Testabilité
- Injection de dépendances propre
- Pas de service locator dans l'entité
## Principes Elegant Objects appliqués
- Classes finales
- Constructeurs privés
- Factory statiques
- Immutabilité (readonly)
- Encapsulation de la logique d'URLs
- Pas de getters bruts

222
skills/make-urls/SKILL.md Normal file
View File

@@ -0,0 +1,222 @@
---
name: framework:make:urls
description: Génère classe Urls + Message CQRS + Handler
license: MIT
version: 1.0.0
---
# Framework Make Urls Skill
## Description
Génère une classe Urls pour la génération d'URLs d'une entité, avec le pattern CQRS (Message + MessageHandler).
La classe Urls encapsule la logique de génération d'URLs pour une entité, le Message représente la query CQRS, et le Handler orchestre la récupération de l'entité et la création des URLs.
## Usage
```
Use skill framework:make:urls
Vous serez invité à fournir :
- Le nom de l'entité (ex: Product, User, Order)
```
## Templates
- `Urls/UtilisateurUrls.php` - Template de classe Urls
- `MessageHandler/UtilisateurUrlsMessage.php` - Template de message CQRS
- `MessageHandler/UtilisateurUrlsMessageHandler.php` - Template de handler
## 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`
- Requiert que le repository existe dans `src/Repository/{EntityName}Repository.php`
- Requiert que l'interface repository existe dans `src/Repository/{EntityName}RepositoryInterface.php`
## Outputs
- `src/Urls/{EntityName}Urls.php`
- `src/MessageHandler/{EntityName}UrlsMessage.php`
- `src/MessageHandler/{EntityName}UrlsMessageHandler.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. Vérifier que le repository existe
- Si non : arrêter et demander de créer l'entité avec repository d'abord
4. Générer les 3 classes depuis les templates :
- Remplacer `{EntityName}` par le nom fourni
- Remplacer `{entityName}` par la version camelCase
5. Afficher les fichiers créés
## Patterns appliqués
### Classe Urls
- Classe `final readonly`
- Constructeur privé
- Factory statique `new()`
- Propriétés : UrlGeneratorInterface + entité
- Méthodes pour générer URLs spécifiques
### Message CQRS
- Extends AbstractQueryEvent
- Implements QueryInterface
- Classe `final`
- Constructeur privé avec factory `new()`
- Propriété publique `id`
- Méthode getter `id()`
### MessageHandler
- Classe `final readonly`
- Attribut #[AsMessageHandler]
- Constructeur avec repository + UrlGeneratorInterface
- Méthode `__invoke()` retournant Urls
## Exemple
```bash
Use skill framework:make:urls
# Saisies utilisateur :
EntityName: Product
# Résultat :
✓ src/Urls/ProductUrls.php
✓ src/MessageHandler/ProductUrlsMessage.php
✓ src/MessageHandler/ProductUrlsMessageHandler.php
```
Fichiers générés :
```php
// src/Urls/ProductUrls.php
final readonly class ProductUrls
{
private function __construct(
private UrlGeneratorInterface $urlGenerator,
private Product $product,
) {}
public static function new(
UrlGeneratorInterface $urlGenerator,
Product $product,
): self {
return new self(
urlGenerator: $urlGenerator,
product: $product,
);
}
}
// src/MessageHandler/ProductUrlsMessage.php
final class ProductUrlsMessage extends AbstractQueryEvent implements QueryInterface
{
private function __construct(
public string $id,
) {}
public static function new(string $id): self
{
return new self(id: $id);
}
public function id(): string
{
return $this->id;
}
}
// src/MessageHandler/ProductUrlsMessageHandler.php
#[AsMessageHandler]
final readonly class ProductUrlsMessageHandler
{
public function __construct(
private ProductRepositoryInterface $productRepository,
private UrlGeneratorInterface $urlGenerator,
) {}
public function __invoke(ProductUrlsMessage $message): ProductUrls
{
$product = $this->productRepository->find($message->id());
return ProductUrls::new(
urlGenerator: $this->urlGenerator,
product: $product,
);
}
}
```
## Usage dans l'entité
L'entité doit implémenter la méthode `urls()` :
```php
public function urls(): ProductUrls
{
/** @var ProductUrls $urls */
$urls = ProductUrlsMessage::new(
id: $this->id->toRfc4122(),
)->query($this->dependencyInjection()->queryBus());
return $urls;
}
```
## Enrichissement avec URLs spécifiques
```php
final readonly class ProductUrls
{
private function __construct(
private UrlGeneratorInterface $urlGenerator,
private Product $product,
) {}
public static function new(
UrlGeneratorInterface $urlGenerator,
Product $product,
): self {
return new self(
urlGenerator: $urlGenerator,
product: $product,
);
}
public function show(): string
{
return $this->urlGenerator->generate(
'product_show',
['id' => $this->product->id()->toRfc4122()],
UrlGeneratorInterface::ABSOLUTE_URL
);
}
public function edit(): string
{
return $this->urlGenerator->generate(
'product_edit',
['id' => $this->product->id()->toRfc4122()],
UrlGeneratorInterface::ABSOLUTE_URL
);
}
public function delete(): string
{
return $this->urlGenerator->generate(
'product_delete',
['id' => $this->product->id()->toRfc4122()],
UrlGeneratorInterface::ABSOLUTE_URL
);
}
}
```
## Notes
- Pattern CQRS : séparation query (Message) / handler
- UrlGeneratorInterface injecté pour génération d'URLs
- Repository utilisé pour récupérer l'entité par ID
- Classe Urls peut être enrichie avec méthodes spécifiques au besoin
- Respecte le principe d'immutabilité (readonly)

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace App\MessageHandler;
use Atournayre\Common\AbstractQueryEvent;
use Atournayre\Contracts\CommandBus\QueryInterface;
final class UtilisateurUrlsMessage extends AbstractQueryEvent implements QueryInterface
{
private function __construct(
public string $id,
) {
}
public static function new(
string $id,
): self {
return new self(
id: $id,
);
}
public function id(): string
{
return $this->id;
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace App\MessageHandler;
use App\Repository\UtilisateurRepositoryInterface;
use App\Urls\UtilisateurUrls;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
#[AsMessageHandler]
final readonly class UtilisateurUrlsMessageHandler
{
public function __construct(
private UtilisateurRepositoryInterface $utilisateurRepository,
private UrlGeneratorInterface $urlGenerator,
) {
}
public function __invoke(UtilisateurUrlsMessage $message): UtilisateurUrls
{
$utilisateur = $this->utilisateurRepository->find($message->id());
return UtilisateurUrls::new(
urlGenerator: $this->urlGenerator,
utilisateur: $utilisateur,
);
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace App\Urls;
use App\Entity\Utilisateur;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
final readonly class UtilisateurUrls
{
private function __construct(
private UrlGeneratorInterface $urlGenerator,
private Utilisateur $utilisateur,
) {
}
public static function new(
UrlGeneratorInterface $urlGenerator,
Utilisateur $utilisateur,
): self {
return new self(
urlGenerator: $urlGenerator,
utilisateur: $utilisateur,
);
}
}