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