From eac17f89fa10e19165b950fb51a659e7486020c4 Mon Sep 17 00:00:00 2001 From: Zhongwei Li Date: Sat, 29 Nov 2025 17:58:52 +0800 Subject: [PATCH] Initial commit --- .claude-plugin/plugin.json | 12 + README.md | 3 + plugin.lock.json | 193 +++++++++++ skills/make-all/README.md | 258 +++++++++++++++ skills/make-all/SKILL.md | 195 +++++++++++ skills/make-collection/README.md | 237 +++++++++++++ skills/make-collection/SKILL.md | 192 +++++++++++ .../Collection/UtilisateurCollection.php | 46 +++ skills/make-contracts/README.md | 55 ++++ skills/make-contracts/SKILL.md | 59 ++++ .../Doctrine/DoctrineMigrationInterface.php | 18 + .../templates/Contracts/HasUrlsInterface.php | 10 + .../Contracts/InvalideDataInterface.php | 12 + .../templates/Contracts/InvalideInterface.php | 10 + .../templates/Contracts/OutDataInterface.php | 12 + .../templates/Contracts/OutInterface.php | 10 + .../Contracts/Story/StoryInterface.php | 10 + .../templates/Contracts/UrlsDataInterface.php | 12 + skills/make-entity/README.md | 107 ++++++ skills/make-entity/SKILL.md | 108 ++++++ .../templates/Entity/Utilisateur.php | 85 +++++ .../Repository/UtilisateurRepository.php | 20 ++ .../UtilisateurRepositoryInterface.php | 9 + skills/make-factory/README.md | 275 ++++++++++++++++ skills/make-factory/SKILL.md | 256 ++++++++++++++ .../templates/Factory/UtilisateurFactory.php | 45 +++ skills/make-invalide/README.md | 191 +++++++++++ skills/make-invalide/SKILL.md | 185 +++++++++++ .../Invalide/UtilisateurInvalide.php | 23 ++ skills/make-out/README.md | 118 +++++++ skills/make-out/SKILL.md | 112 +++++++ skills/make-story/README.md | 311 ++++++++++++++++++ skills/make-story/SKILL.md | 291 ++++++++++++++++ .../make-story/templates/Story/AppStory.php | 16 + .../templates/Story/UtilisateurStory.php | 26 ++ skills/make-urls/README.md | 218 ++++++++++++ skills/make-urls/SKILL.md | 222 +++++++++++++ .../MessageHandler/UtilisateurUrlsMessage.php | 29 ++ .../UtilisateurUrlsMessageHandler.php | 30 ++ .../templates/Urls/UtilisateurUrls.php | 27 ++ 40 files changed, 4048 insertions(+) create mode 100644 .claude-plugin/plugin.json create mode 100644 README.md create mode 100644 plugin.lock.json create mode 100644 skills/make-all/README.md create mode 100644 skills/make-all/SKILL.md create mode 100644 skills/make-collection/README.md create mode 100644 skills/make-collection/SKILL.md create mode 100644 skills/make-collection/templates/Collection/UtilisateurCollection.php create mode 100644 skills/make-contracts/README.md create mode 100644 skills/make-contracts/SKILL.md create mode 100644 skills/make-contracts/templates/Contracts/Doctrine/DoctrineMigrationInterface.php create mode 100644 skills/make-contracts/templates/Contracts/HasUrlsInterface.php create mode 100644 skills/make-contracts/templates/Contracts/InvalideDataInterface.php create mode 100644 skills/make-contracts/templates/Contracts/InvalideInterface.php create mode 100644 skills/make-contracts/templates/Contracts/OutDataInterface.php create mode 100644 skills/make-contracts/templates/Contracts/OutInterface.php create mode 100644 skills/make-contracts/templates/Contracts/Story/StoryInterface.php create mode 100644 skills/make-contracts/templates/Contracts/UrlsDataInterface.php create mode 100644 skills/make-entity/README.md create mode 100644 skills/make-entity/SKILL.md create mode 100644 skills/make-entity/templates/Entity/Utilisateur.php create mode 100644 skills/make-entity/templates/Repository/UtilisateurRepository.php create mode 100644 skills/make-entity/templates/Repository/UtilisateurRepositoryInterface.php create mode 100644 skills/make-factory/README.md create mode 100644 skills/make-factory/SKILL.md create mode 100644 skills/make-factory/templates/Factory/UtilisateurFactory.php create mode 100644 skills/make-invalide/README.md create mode 100644 skills/make-invalide/SKILL.md create mode 100644 skills/make-invalide/templates/Invalide/UtilisateurInvalide.php create mode 100644 skills/make-out/README.md create mode 100644 skills/make-out/SKILL.md create mode 100644 skills/make-story/README.md create mode 100644 skills/make-story/SKILL.md create mode 100644 skills/make-story/templates/Story/AppStory.php create mode 100644 skills/make-story/templates/Story/UtilisateurStory.php create mode 100644 skills/make-urls/README.md create mode 100644 skills/make-urls/SKILL.md create mode 100644 skills/make-urls/templates/MessageHandler/UtilisateurUrlsMessage.php create mode 100644 skills/make-urls/templates/MessageHandler/UtilisateurUrlsMessageHandler.php create mode 100644 skills/make-urls/templates/Urls/UtilisateurUrls.php diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..8a8447c --- /dev/null +++ b/.claude-plugin/plugin.json @@ -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" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..304fdbb --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# framework + +Plugin pour atournayre/framework avec commandes et intégrations diff --git a/plugin.lock.json b/plugin.lock.json new file mode 100644 index 0000000..d8d13d1 --- /dev/null +++ b/plugin.lock.json @@ -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": [] + } +} \ No newline at end of file diff --git a/skills/make-all/README.md b/skills/make-all/README.md new file mode 100644 index 0000000..f108466 --- /dev/null +++ b/skills/make-all/README.md @@ -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 diff --git a/skills/make-all/SKILL.md b/skills/make-all/SKILL.md new file mode 100644 index 0000000..8de95c1 --- /dev/null +++ b/skills/make-all/SKILL.md @@ -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 diff --git a/skills/make-collection/README.md b/skills/make-collection/README.md new file mode 100644 index 0000000..cbed4fc --- /dev/null +++ b/skills/make-collection/README.md @@ -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é diff --git a/skills/make-collection/SKILL.md b/skills/make-collection/SKILL.md new file mode 100644 index 0000000..0722921 --- /dev/null +++ b/skills/make-collection/SKILL.md @@ -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 + + */ + 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 diff --git a/skills/make-collection/templates/Collection/UtilisateurCollection.php b/skills/make-collection/templates/Collection/UtilisateurCollection.php new file mode 100644 index 0000000..0b3594e --- /dev/null +++ b/skills/make-collection/templates/Collection/UtilisateurCollection.php @@ -0,0 +1,46 @@ + + */ + 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.) +} diff --git a/skills/make-contracts/README.md b/skills/make-contracts/README.md new file mode 100644 index 0000000..8367e39 --- /dev/null +++ b/skills/make-contracts/README.md @@ -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 diff --git a/skills/make-contracts/SKILL.md b/skills/make-contracts/SKILL.md new file mode 100644 index 0000000..cad25c1 --- /dev/null +++ b/skills/make-contracts/SKILL.md @@ -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 diff --git a/skills/make-contracts/templates/Contracts/Doctrine/DoctrineMigrationInterface.php b/skills/make-contracts/templates/Contracts/Doctrine/DoctrineMigrationInterface.php new file mode 100644 index 0000000..6fdf7a4 --- /dev/null +++ b/skills/make-contracts/templates/Contracts/Doctrine/DoctrineMigrationInterface.php @@ -0,0 +1,18 @@ +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 diff --git a/skills/make-entity/SKILL.md b/skills/make-entity/SKILL.md new file mode 100644 index 0000000..9f771bc --- /dev/null +++ b/skills/make-entity/SKILL.md @@ -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 diff --git a/skills/make-entity/templates/Entity/Utilisateur.php b/skills/make-entity/templates/Entity/Utilisateur.php new file mode 100644 index 0000000..d12da63 --- /dev/null +++ b/skills/make-entity/templates/Entity/Utilisateur.php @@ -0,0 +1,85 @@ +id; + } + + /** + * @return array + */ + 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; + } +} diff --git a/skills/make-entity/templates/Repository/UtilisateurRepository.php b/skills/make-entity/templates/Repository/UtilisateurRepository.php new file mode 100644 index 0000000..eae57f8 --- /dev/null +++ b/skills/make-entity/templates/Repository/UtilisateurRepository.php @@ -0,0 +1,20 @@ + + */ +final class UtilisateurRepository extends ServiceEntityRepository implements UtilisateurRepositoryInterface +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Utilisateur::class); + } +} diff --git a/skills/make-entity/templates/Repository/UtilisateurRepositoryInterface.php b/skills/make-entity/templates/Repository/UtilisateurRepositoryInterface.php new file mode 100644 index 0000000..de7f3fc --- /dev/null +++ b/skills/make-entity/templates/Repository/UtilisateurRepositoryInterface.php @@ -0,0 +1,9 @@ + + */ +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 diff --git a/skills/make-factory/SKILL.md b/skills/make-factory/SKILL.md new file mode 100644 index 0000000..0694b9c --- /dev/null +++ b/skills/make-factory/SKILL.md @@ -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 + + */ +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 diff --git a/skills/make-factory/templates/Factory/UtilisateurFactory.php b/skills/make-factory/templates/Factory/UtilisateurFactory.php new file mode 100644 index 0000000..cf43a70 --- /dev/null +++ b/skills/make-factory/templates/Factory/UtilisateurFactory.php @@ -0,0 +1,45 @@ + + */ +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), + ]); + } +} diff --git a/skills/make-invalide/README.md b/skills/make-invalide/README.md new file mode 100644 index 0000000..6c22bcc --- /dev/null +++ b/skills/make-invalide/README.md @@ -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 diff --git a/skills/make-invalide/SKILL.md b/skills/make-invalide/SKILL.md new file mode 100644 index 0000000..8a409da --- /dev/null +++ b/skills/make-invalide/SKILL.md @@ -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 +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" diff --git a/skills/make-invalide/templates/Invalide/UtilisateurInvalide.php b/skills/make-invalide/templates/Invalide/UtilisateurInvalide.php new file mode 100644 index 0000000..3f57781 --- /dev/null +++ b/skills/make-invalide/templates/Invalide/UtilisateurInvalide.php @@ -0,0 +1,23 @@ +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 diff --git a/skills/make-out/SKILL.md b/skills/make-out/SKILL.md new file mode 100644 index 0000000..aa62d1d --- /dev/null +++ b/skills/make-out/SKILL.md @@ -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 +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 diff --git a/skills/make-story/SKILL.md b/skills/make-story/SKILL.md new file mode 100644 index 0000000..4c0014c --- /dev/null +++ b/skills/make-story/SKILL.md @@ -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 +withSpecificId('01234567-89ab-cdef-0123-456789abcdef') + ->create(); + + // Créer plusieurs produits + ProductFactory::createMany(10); + } +} + +// src/Story/AppStory.php +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 diff --git a/skills/make-story/templates/Story/AppStory.php b/skills/make-story/templates/Story/AppStory.php new file mode 100644 index 0000000..ff76b6a --- /dev/null +++ b/skills/make-story/templates/Story/AppStory.php @@ -0,0 +1,16 @@ +withSpecificId('01234567-89ab-cdef-0123-456789abcdef') + ->create(); + + // Créer plusieurs utilisateurs + UtilisateurFactory::createMany(10); + } +} diff --git a/skills/make-urls/README.md b/skills/make-urls/README.md new file mode 100644 index 0000000..9b0b7cc --- /dev/null +++ b/skills/make-urls/README.md @@ -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 #} +Modifier +Supprimer + +{# API link #} +{{ product.urls.api }} +``` + +### 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 diff --git a/skills/make-urls/SKILL.md b/skills/make-urls/SKILL.md new file mode 100644 index 0000000..183d8c0 --- /dev/null +++ b/skills/make-urls/SKILL.md @@ -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) diff --git a/skills/make-urls/templates/MessageHandler/UtilisateurUrlsMessage.php b/skills/make-urls/templates/MessageHandler/UtilisateurUrlsMessage.php new file mode 100644 index 0000000..2a2bcf4 --- /dev/null +++ b/skills/make-urls/templates/MessageHandler/UtilisateurUrlsMessage.php @@ -0,0 +1,29 @@ +id; + } +} diff --git a/skills/make-urls/templates/MessageHandler/UtilisateurUrlsMessageHandler.php b/skills/make-urls/templates/MessageHandler/UtilisateurUrlsMessageHandler.php new file mode 100644 index 0000000..d0d42fb --- /dev/null +++ b/skills/make-urls/templates/MessageHandler/UtilisateurUrlsMessageHandler.php @@ -0,0 +1,30 @@ +utilisateurRepository->find($message->id()); + + return UtilisateurUrls::new( + urlGenerator: $this->urlGenerator, + utilisateur: $utilisateur, + ); + } +} diff --git a/skills/make-urls/templates/Urls/UtilisateurUrls.php b/skills/make-urls/templates/Urls/UtilisateurUrls.php new file mode 100644 index 0000000..548cffd --- /dev/null +++ b/skills/make-urls/templates/Urls/UtilisateurUrls.php @@ -0,0 +1,27 @@ +