Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 17:58:52 +08:00
commit eac17f89fa
40 changed files with 4048 additions and 0 deletions

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

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

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

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

View File

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

View File

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

View File

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