Initial commit
This commit is contained in:
12
.claude-plugin/plugin.json
Normal file
12
.claude-plugin/plugin.json
Normal 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
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# framework
|
||||
|
||||
Plugin pour atournayre/framework avec commandes et intégrations
|
||||
193
plugin.lock.json
Normal file
193
plugin.lock.json
Normal 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
258
skills/make-all/README.md
Normal 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
195
skills/make-all/SKILL.md
Normal 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
|
||||
237
skills/make-collection/README.md
Normal file
237
skills/make-collection/README.md
Normal 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é
|
||||
192
skills/make-collection/SKILL.md
Normal file
192
skills/make-collection/SKILL.md
Normal 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
|
||||
@@ -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.)
|
||||
}
|
||||
55
skills/make-contracts/README.md
Normal file
55
skills/make-contracts/README.md
Normal 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
|
||||
59
skills/make-contracts/SKILL.md
Normal file
59
skills/make-contracts/SKILL.md
Normal 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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Contracts;
|
||||
|
||||
interface HasUrlsInterface
|
||||
{
|
||||
public function urls(): mixed;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Contracts;
|
||||
|
||||
/**
|
||||
* Marker interface for Invalide data objects.
|
||||
*/
|
||||
interface InvalideDataInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Contracts;
|
||||
|
||||
interface InvalideInterface
|
||||
{
|
||||
public function invalide(): mixed;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Contracts;
|
||||
|
||||
/**
|
||||
* Marker interface for Out data objects.
|
||||
*/
|
||||
interface OutDataInterface
|
||||
{
|
||||
}
|
||||
10
skills/make-contracts/templates/Contracts/OutInterface.php
Normal file
10
skills/make-contracts/templates/Contracts/OutInterface.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Contracts;
|
||||
|
||||
interface OutInterface
|
||||
{
|
||||
public function out(): mixed;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Contracts\Story;
|
||||
|
||||
interface StoryInterface
|
||||
{
|
||||
public function build(): void;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Contracts;
|
||||
|
||||
/**
|
||||
* Marker interface for Urls data objects.
|
||||
*/
|
||||
interface UrlsDataInterface
|
||||
{
|
||||
}
|
||||
107
skills/make-entity/README.md
Normal file
107
skills/make-entity/README.md
Normal 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
108
skills/make-entity/SKILL.md
Normal 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
|
||||
85
skills/make-entity/templates/Entity/Utilisateur.php
Normal file
85
skills/make-entity/templates/Entity/Utilisateur.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
interface UtilisateurRepositoryInterface
|
||||
{
|
||||
}
|
||||
275
skills/make-factory/README.md
Normal file
275
skills/make-factory/README.md
Normal file
@@ -0,0 +1,275 @@
|
||||
# Framework Make Factory
|
||||
|
||||
Génère une Factory Foundry pour tests.
|
||||
|
||||
## Vue d'ensemble
|
||||
Cette skill crée une Factory Zenstruck Foundry pour générer facilement des instances d'entités dans les tests selon les principes Elegant Objects.
|
||||
|
||||
## Caractéristiques
|
||||
|
||||
### Classe Factory générée
|
||||
- Extends PersistentObjectFactory
|
||||
- Classe `final`
|
||||
- Méthode `class()` retournant le FQCN
|
||||
- Méthode `defaults()` avec valeurs Faker
|
||||
- Méthode `initialize()` utilisant `instantiateWith()`
|
||||
- Appelle la factory statique `Entity::create()`
|
||||
- Méthode `withSpecificId()` par défaut
|
||||
|
||||
## Utilisation
|
||||
|
||||
```bash
|
||||
Use skill framework:make:factory
|
||||
```
|
||||
|
||||
Vous serez invité à fournir le nom de l'entité.
|
||||
|
||||
## Exemple d'utilisation
|
||||
|
||||
```bash
|
||||
EntityName: Product
|
||||
```
|
||||
|
||||
Génère :
|
||||
```php
|
||||
// src/Factory/ProductFactory.php
|
||||
/**
|
||||
* @extends PersistentObjectFactory<Product>
|
||||
*/
|
||||
final class ProductFactory extends PersistentObjectFactory
|
||||
{
|
||||
public static function class(): string
|
||||
{
|
||||
return Product::class;
|
||||
}
|
||||
|
||||
protected function defaults(): array|callable
|
||||
{
|
||||
return [
|
||||
'id' => Uuid::v4(),
|
||||
'name' => self::faker()->words(3, true),
|
||||
'price' => self::faker()->randomFloat(2, 10, 1000),
|
||||
];
|
||||
}
|
||||
|
||||
protected function initialize(): static
|
||||
{
|
||||
return $this
|
||||
->instantiateWith(function (array $attributes) {
|
||||
return Product::create(
|
||||
id: $attributes['id'],
|
||||
name: $attributes['name'],
|
||||
price: $attributes['price'],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public function withSpecificId(string $uuid): self
|
||||
{
|
||||
return $this->with([
|
||||
'id' => Uuid::fromString($uuid),
|
||||
]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Structure créée
|
||||
|
||||
```
|
||||
src/
|
||||
└── Factory/
|
||||
└── {EntityName}Factory.php
|
||||
```
|
||||
|
||||
## Prérequis
|
||||
- L'entité doit exister dans `src/Entity/{EntityName}.php`
|
||||
- Zenstruck Foundry installé
|
||||
|
||||
## Usage dans les tests
|
||||
|
||||
### Création basique
|
||||
```php
|
||||
use App\Factory\ProductFactory;
|
||||
|
||||
// Créer et persister une instance
|
||||
$product = ProductFactory::createOne();
|
||||
|
||||
// Créer plusieurs instances
|
||||
$products = ProductFactory::createMany(10);
|
||||
|
||||
// Créer sans persister
|
||||
$product = ProductFactory::new()->withoutPersisting()->create();
|
||||
```
|
||||
|
||||
### Personnalisation
|
||||
```php
|
||||
// Override propriétés
|
||||
$product = ProductFactory::createOne([
|
||||
'name' => 'Mon Produit',
|
||||
'price' => 99.99,
|
||||
]);
|
||||
|
||||
// Enchaînement
|
||||
$product = ProductFactory::new()
|
||||
->withSpecificId('018d5e5e-5e5e-7e5e-ae5e-5e5e5e5e5e5e')
|
||||
->with(['name' => 'Custom'])
|
||||
->createOne();
|
||||
```
|
||||
|
||||
### Tests unitaires
|
||||
```php
|
||||
final class ProductTest extends TestCase
|
||||
{
|
||||
public function testProductCanBeCreated(): void
|
||||
{
|
||||
$product = ProductFactory::createOne([
|
||||
'name' => 'Test Product',
|
||||
'price' => 50.0,
|
||||
]);
|
||||
|
||||
self::assertSame('Test Product', $product->name());
|
||||
self::assertSame(50.0, $product->price());
|
||||
}
|
||||
|
||||
public function testCannotHaveNegativePrice(): void
|
||||
{
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
|
||||
ProductFactory::createOne(['price' => -10.0]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Tests fonctionnels
|
||||
```php
|
||||
final class ProductControllerTest extends WebTestCase
|
||||
{
|
||||
public function testListProducts(): void
|
||||
{
|
||||
ProductFactory::createMany(5);
|
||||
|
||||
$client = static::createClient();
|
||||
$client->request('GET', '/products');
|
||||
|
||||
self::assertResponseIsSuccessful();
|
||||
self::assertSelectorTextContains('h1', 'Products');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Enrichissement (principe YAGNI)
|
||||
|
||||
Ajouter méthodes custom **uniquement si demandées** :
|
||||
|
||||
```php
|
||||
// États métier
|
||||
public function active(): self
|
||||
{
|
||||
return $this->with(['isActive' => true]);
|
||||
}
|
||||
|
||||
public function inactive(): self
|
||||
{
|
||||
return $this->with(['isActive' => false]);
|
||||
}
|
||||
|
||||
// Valeurs spécifiques
|
||||
public function expensive(): self
|
||||
{
|
||||
return $this->with([
|
||||
'price' => self::faker()->randomFloat(2, 500, 2000)
|
||||
]);
|
||||
}
|
||||
|
||||
public function outOfStock(): self
|
||||
{
|
||||
return $this->with(['stock' => 0]);
|
||||
}
|
||||
|
||||
// Relations
|
||||
public function withCategory(Category $category): self
|
||||
{
|
||||
return $this->with(['category' => $category]);
|
||||
}
|
||||
```
|
||||
|
||||
## Valeurs par défaut Faker
|
||||
|
||||
```php
|
||||
protected function defaults(): array|callable
|
||||
{
|
||||
return [
|
||||
// UUID
|
||||
'id' => Uuid::v4(),
|
||||
|
||||
// Strings
|
||||
'name' => self::faker()->words(3, true),
|
||||
'email' => self::faker()->email(),
|
||||
'description' => self::faker()->paragraph(),
|
||||
'slug' => self::faker()->slug(),
|
||||
|
||||
// Nombres
|
||||
'price' => self::faker()->randomFloat(2, 10, 1000),
|
||||
'quantity' => self::faker()->numberBetween(1, 100),
|
||||
'stock' => self::faker()->numberBetween(0, 50),
|
||||
|
||||
// Booleans
|
||||
'isActive' => true,
|
||||
'isPublished' => self::faker()->boolean(70), // 70% true
|
||||
|
||||
// Dates
|
||||
'createdAt' => \DateTimeImmutable::createFromMutable(
|
||||
self::faker()->dateTimeBetween('-1 year')
|
||||
),
|
||||
|
||||
// Arrays
|
||||
'tags' => self::faker()->words(5),
|
||||
|
||||
// Relations (avec autre Factory)
|
||||
'category' => CategoryFactory::new(),
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
## Patterns avancés
|
||||
|
||||
### Factory avec relations
|
||||
```php
|
||||
protected function defaults(): array|callable
|
||||
{
|
||||
return [
|
||||
'id' => Uuid::v4(),
|
||||
'name' => self::faker()->words(3, true),
|
||||
'category' => CategoryFactory::new(),
|
||||
];
|
||||
}
|
||||
|
||||
public function inCategory(Category $category): self
|
||||
{
|
||||
return $this->with(['category' => $category]);
|
||||
}
|
||||
```
|
||||
|
||||
### Factory avec états complexes
|
||||
```php
|
||||
public function published(): self
|
||||
{
|
||||
return $this
|
||||
->with(['isPublished' => true])
|
||||
->with(['publishedAt' => new \DateTimeImmutable()]);
|
||||
}
|
||||
|
||||
public function draft(): self
|
||||
{
|
||||
return $this
|
||||
->with(['isPublished' => false])
|
||||
->with(['publishedAt' => null]);
|
||||
}
|
||||
```
|
||||
|
||||
## Principes Elegant Objects appliqués
|
||||
- Classe finale
|
||||
- Utilise `instantiateWith()` pour appeler `Entity::create()`
|
||||
- Pas de `new Entity()` direct
|
||||
- Méthodes custom uniquement si demandées (YAGNI)
|
||||
- Type-safe avec générique PHPDoc
|
||||
256
skills/make-factory/SKILL.md
Normal file
256
skills/make-factory/SKILL.md
Normal file
@@ -0,0 +1,256 @@
|
||||
---
|
||||
name: framework:make:factory
|
||||
description: Génère Factory Foundry pour tests
|
||||
license: MIT
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# Framework Make Factory Skill
|
||||
|
||||
## Description
|
||||
Génère une Factory Foundry pour créer facilement des instances d'entités dans les tests.
|
||||
|
||||
La Factory utilise Zenstruck Foundry et respecte les principes Elegant Objects en utilisant la factory statique `create()` de l'entité.
|
||||
|
||||
## Usage
|
||||
```
|
||||
Use skill framework:make:factory
|
||||
|
||||
Vous serez invité à fournir :
|
||||
- Le nom de l'entité (ex: Product, User, Order)
|
||||
```
|
||||
|
||||
## Templates
|
||||
- `Factory/UtilisateurFactory.php` - Template de factory Foundry
|
||||
|
||||
## Variables requises
|
||||
- **{EntityName}** - Nom de l'entité en PascalCase (ex: Utilisateur, Product)
|
||||
- **{entityName}** - Nom de l'entité en camelCase (ex: utilisateur, product)
|
||||
- **{namespace}** - Namespace du projet (défaut: App)
|
||||
- **{properties}** - Liste des propriétés de l'entité pour `defaults()`
|
||||
|
||||
## Dépendances
|
||||
- Requiert que l'entité existe dans `src/Entity/{EntityName}.php`
|
||||
- Requiert Zenstruck Foundry installé
|
||||
|
||||
## Outputs
|
||||
- `src/Factory/{EntityName}Factory.php`
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Demander le nom de l'entité (EntityName)
|
||||
2. Vérifier que l'entité existe dans `src/Entity/{EntityName}.php`
|
||||
- Si non : arrêter et demander de créer l'entité d'abord
|
||||
3. Lire l'entité pour détecter les propriétés du constructeur `create()`
|
||||
4. Générer la factory depuis le template :
|
||||
- Remplacer `{EntityName}` par le nom fourni
|
||||
- Remplacer `{entityName}` par la version camelCase
|
||||
- Générer `defaults()` avec valeurs par défaut pour chaque propriété
|
||||
- Générer `instantiateWith()` appelant `Entity::create()`
|
||||
5. Afficher le fichier créé
|
||||
|
||||
## Patterns appliqués
|
||||
|
||||
### Classe Factory
|
||||
- Extends PersistentObjectFactory
|
||||
- Classe `final`
|
||||
- Méthode statique `class()` retournant le FQCN de l'entité
|
||||
- Méthode `defaults()` avec valeurs par défaut des propriétés
|
||||
- Méthode `initialize()` avec `instantiateWith()` appelant `Entity::create()`
|
||||
- Méthodes custom (ex: `withSpecificId()`)
|
||||
|
||||
## Exemple
|
||||
|
||||
```bash
|
||||
Use skill framework:make:factory
|
||||
|
||||
# Saisies utilisateur :
|
||||
EntityName: Product
|
||||
|
||||
# Résultat :
|
||||
✓ src/Factory/ProductFactory.php
|
||||
```
|
||||
|
||||
Fichier généré :
|
||||
```php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Factory;
|
||||
|
||||
use App\Entity\Product;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
use Zenstruck\Foundry\Persistence\PersistentObjectFactory;
|
||||
|
||||
/**
|
||||
* @extends PersistentObjectFactory<Product>
|
||||
*/
|
||||
final class ProductFactory extends PersistentObjectFactory
|
||||
{
|
||||
public static function class(): string
|
||||
{
|
||||
return Product::class;
|
||||
}
|
||||
|
||||
protected function defaults(): array|callable
|
||||
{
|
||||
return [
|
||||
'id' => Uuid::v4(),
|
||||
'name' => self::faker()->words(3, true),
|
||||
'price' => self::faker()->randomFloat(2, 10, 1000),
|
||||
'stock' => self::faker()->numberBetween(0, 100),
|
||||
'isActive' => true,
|
||||
];
|
||||
}
|
||||
|
||||
protected function initialize(): static
|
||||
{
|
||||
return $this
|
||||
->instantiateWith(function (array $attributes) {
|
||||
return Product::create(
|
||||
id: $attributes['id'],
|
||||
name: $attributes['name'],
|
||||
price: $attributes['price'],
|
||||
stock: $attributes['stock'],
|
||||
isActive: $attributes['isActive'],
|
||||
);
|
||||
})
|
||||
;
|
||||
}
|
||||
|
||||
public function withSpecificId(string $uuid): self
|
||||
{
|
||||
return $this->with([
|
||||
'id' => Uuid::fromString($uuid),
|
||||
]);
|
||||
}
|
||||
|
||||
public function inactive(): self
|
||||
{
|
||||
return $this->with(['isActive' => false]);
|
||||
}
|
||||
|
||||
public function outOfStock(): self
|
||||
{
|
||||
return $this->with(['stock' => 0]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage dans les tests
|
||||
|
||||
### Création simple
|
||||
```php
|
||||
// Crée et persiste en DB
|
||||
$product = ProductFactory::createOne();
|
||||
|
||||
// Crée plusieurs instances
|
||||
$products = ProductFactory::createMany(10);
|
||||
|
||||
// Crée sans persister
|
||||
$product = ProductFactory::new()->withoutPersisting()->create();
|
||||
```
|
||||
|
||||
### Personnalisation
|
||||
```php
|
||||
// Override propriétés
|
||||
$product = ProductFactory::createOne([
|
||||
'name' => 'Custom Product',
|
||||
'price' => 99.99,
|
||||
]);
|
||||
|
||||
// Méthodes custom
|
||||
$product = ProductFactory::new()
|
||||
->withSpecificId('018d5e5e-5e5e-7e5e-ae5e-5e5e5e5e5e5e')
|
||||
->inactive()
|
||||
->createOne();
|
||||
|
||||
$product = ProductFactory::new()->outOfStock()->createOne();
|
||||
```
|
||||
|
||||
### Dans les tests
|
||||
```php
|
||||
final class ProductTest extends TestCase
|
||||
{
|
||||
public function testCannotDecreaseStockWhenOutOfStock(): void
|
||||
{
|
||||
$product = ProductFactory::new()->outOfStock()->createOne();
|
||||
|
||||
$this->expectException(\DomainException::class);
|
||||
$product->decreaseStock(1);
|
||||
}
|
||||
|
||||
public function testInactiveProductsCannotBePurchased(): void
|
||||
{
|
||||
$product = ProductFactory::new()->inactive()->createOne();
|
||||
|
||||
$this->expectException(\DomainException::class);
|
||||
$product->purchase(1);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Valeurs par défaut recommandées
|
||||
|
||||
### Types courants
|
||||
```php
|
||||
// Strings
|
||||
'name' => self::faker()->words(3, true),
|
||||
'email' => self::faker()->email(),
|
||||
'description' => self::faker()->paragraph(),
|
||||
|
||||
// Nombres
|
||||
'price' => self::faker()->randomFloat(2, 10, 1000),
|
||||
'quantity' => self::faker()->numberBetween(1, 100),
|
||||
'stock' => self::faker()->numberBetween(0, 50),
|
||||
|
||||
// Booleans
|
||||
'isActive' => true,
|
||||
'isPublished' => self::faker()->boolean(),
|
||||
|
||||
// Dates
|
||||
'createdAt' => \DateTimeImmutable::createFromMutable(self::faker()->dateTime()),
|
||||
|
||||
// UUID
|
||||
'id' => Uuid::v4(),
|
||||
```
|
||||
|
||||
## Méthodes custom recommandées
|
||||
|
||||
```php
|
||||
// Par ID spécifique
|
||||
public function withSpecificId(string $uuid): self
|
||||
{
|
||||
return $this->with(['id' => Uuid::fromString($uuid)]);
|
||||
}
|
||||
|
||||
// États métier
|
||||
public function active(): self
|
||||
{
|
||||
return $this->with(['isActive' => true]);
|
||||
}
|
||||
|
||||
public function inactive(): self
|
||||
{
|
||||
return $this->with(['isActive' => false]);
|
||||
}
|
||||
|
||||
// Valeurs spécifiques
|
||||
public function expensive(): self
|
||||
{
|
||||
return $this->with(['price' => self::faker()->randomFloat(2, 500, 2000)]);
|
||||
}
|
||||
|
||||
public function cheap(): self
|
||||
{
|
||||
return $this->with(['price' => self::faker()->randomFloat(2, 1, 50)]);
|
||||
}
|
||||
```
|
||||
|
||||
## Notes
|
||||
- Utilise `instantiateWith()` pour appeler la factory statique `create()` de l'entité
|
||||
- Respecte le principe Elegant Objects (pas de `new Entity()` direct)
|
||||
- Faker disponible via `self::faker()`
|
||||
- Méthodes custom uniquement si demandées explicitement (YAGNI)
|
||||
- Factory persiste par défaut, utiliser `withoutPersisting()` si besoin
|
||||
45
skills/make-factory/templates/Factory/UtilisateurFactory.php
Normal file
45
skills/make-factory/templates/Factory/UtilisateurFactory.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Factory;
|
||||
|
||||
use App\Entity\Utilisateur;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
use Zenstruck\Foundry\Persistence\PersistentObjectFactory;
|
||||
|
||||
/**
|
||||
* @extends PersistentObjectFactory<Utilisateur>
|
||||
*/
|
||||
final class UtilisateurFactory extends PersistentObjectFactory
|
||||
{
|
||||
public static function class(): string
|
||||
{
|
||||
return Utilisateur::class;
|
||||
}
|
||||
|
||||
protected function defaults(): array|callable
|
||||
{
|
||||
return [
|
||||
'id' => Uuid::v4(),
|
||||
];
|
||||
}
|
||||
|
||||
protected function initialize(): static
|
||||
{
|
||||
return $this
|
||||
->instantiateWith(function (array $attributes) {
|
||||
return Utilisateur::create(
|
||||
id: $attributes['id'],
|
||||
);
|
||||
})
|
||||
;
|
||||
}
|
||||
|
||||
public function withSpecificId(string $uuid): self
|
||||
{
|
||||
return $this->with([
|
||||
'id' => Uuid::fromString($uuid),
|
||||
]);
|
||||
}
|
||||
}
|
||||
191
skills/make-invalide/README.md
Normal file
191
skills/make-invalide/README.md
Normal 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
|
||||
185
skills/make-invalide/SKILL.md
Normal file
185
skills/make-invalide/SKILL.md
Normal 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"
|
||||
@@ -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
118
skills/make-out/README.md
Normal 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
112
skills/make-out/SKILL.md
Normal 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
311
skills/make-story/README.md
Normal 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
291
skills/make-story/SKILL.md
Normal 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
|
||||
16
skills/make-story/templates/Story/AppStory.php
Normal file
16
skills/make-story/templates/Story/AppStory.php
Normal 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();
|
||||
}
|
||||
}
|
||||
26
skills/make-story/templates/Story/UtilisateurStory.php
Normal file
26
skills/make-story/templates/Story/UtilisateurStory.php
Normal 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
218
skills/make-urls/README.md
Normal 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
222
skills/make-urls/SKILL.md
Normal 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)
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
27
skills/make-urls/templates/Urls/UtilisateurUrls.php
Normal file
27
skills/make-urls/templates/Urls/UtilisateurUrls.php
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user