From 19a697c7127238114b9b0f72a3b4e810f85bea8e Mon Sep 17 00:00:00 2001 From: "ext.jeremy.guillot@maxicoffee.domains" Date: Sun, 23 Mar 2025 17:35:27 +0100 Subject: [PATCH] feat: ajout de rules pour cursor --- .cursor/rules/api_platform.mdc | 213 +++++++++++++++++++++++++++++++++ .cursor/rules/architecture.mdc | 210 ++++++++++++++++++++++++++++++++ .cursor/rules/business.mdc | 54 +++++++++ .cursor/rules/tests.mdc | 202 +++++++++++++++++++++++++++++++ 4 files changed, 679 insertions(+) create mode 100644 .cursor/rules/api_platform.mdc create mode 100644 .cursor/rules/architecture.mdc create mode 100644 .cursor/rules/business.mdc create mode 100644 .cursor/rules/tests.mdc diff --git a/.cursor/rules/api_platform.mdc b/.cursor/rules/api_platform.mdc new file mode 100644 index 0000000..7093d9b --- /dev/null +++ b/.cursor/rules/api_platform.mdc @@ -0,0 +1,213 @@ +--- +description: +globs: +alwaysApply: true +--- +# API Platform dans Mangarr + +## Structure de l'API +L'API est organisée dans la couche Infrastructure de chaque domaine : + +``` +Domain/Manga/Infrastructure/ApiPlatform/ +├── Resource/ # Configuration des ressources API +│ └── MangaResource.php +├── State/ # Providers et Processors + ├── Provider/ # State Providers + └── Processor/ # State Processors + +``` + +## Règles d'Organisation + +### 1. Resources +- Localisation : `Infrastructure/ApiPlatform/Resource/` +- Principes : + - Une classe = une ressource API + - Documentation exhaustive avec les attributs PHP 8 + - Validation contraintes avec les attributs Symfony + - Nommage : `{Nom}Resource` + +### 2. State Providers/Processors +- Localisation : `Infrastructure/ApiPlatform/State/` +- Principes : + - Utiliser les cas d'utilisation du domaine (Commands/Queries) + - Ne pas contenir de logique métier + - Conversion Resource ↔ Command/Query + - Nommage : `{Action}{Resource}StateProvider/Processor` + +## Exemples de Code + +### 1. Resource API +```php +namespace App\Domain\Manga\Infrastructure\ApiPlatform\Resource; + +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\Post; +use App\Domain\Manga\Infrastructure\ApiPlatform\State\Provider\GetMangaStateProvider; +use App\Domain\Manga\Infrastructure\ApiPlatform\State\Processor\CreateMangaStateProcessor; +use Symfony\Component\Validator\Constraints as Assert; + +#[ApiResource( + shortName: 'Manga', + operations: [ + new Get( + uriTemplate: '/mangas/{id}', + provider: GetMangaStateProvider::class, + description: 'Récupère un manga par son identifiant', + openapi: [ + 'summary' => 'Récupère un manga', + '200' => [ + 'description' => 'Manga trouvé', + 'content' => [ + 'application/json' => [ + 'schema' => [ + 'type' => 'object', + 'properties' => [ + 'id' => ['type' => 'string', 'format' => 'uuid'], + 'title' => ['type' => 'string'], + 'description' => ['type' => 'string', 'nullable' => true], + 'authors' => [ + 'type' => 'array', + 'items' => ['type' => 'string'] + ], + 'coverUrl' => ['type' => 'string', 'format' => 'uri', 'nullable' => true] + ], + 'required' => ['id', 'title', 'authors'] + ] + ] + ] + ], + '404' => [ + 'description' => 'Manga non trouvé' + ] + ] + ), + new Post( + uriTemplate: '/mangas', + processor: CreateMangaStateProcessor::class, + description: 'Crée un nouveau manga', + openapi: [ + 'requestBody' => [ + 'content' => [ + 'application/json' => [ + 'schema' => [ + 'type' => 'object', + 'properties' => [ + 'title' => ['type' => 'string'], + 'description' => ['type' => 'string', 'nullable' => true], + 'authors' => [ + 'type' => 'array', + 'items' => ['type' => 'string'] + ], + 'coverUrl' => ['type' => 'string', 'format' => 'uri', 'nullable' => true] + ], + 'required' => ['title'] + ] + ] + ] + ], + 'responses' => [ + '201' => [ + 'description' => 'Manga créé' + ], + '400' => [ + 'description' => 'Données invalides' + ] + ] + ] + ) + ] +)] +class MangaResource +{ +} +``` + +### 2. State Provider +```php +namespace App\Domain\Manga\Infrastructure\ApiPlatform\State\Provider; + +use ApiPlatform\Metadata\Operation; +use ApiPlatform\State\ProviderInterface; +use App\Domain\Manga\Application\Query\GetMangaByIdQuery; +use App\Domain\Shared\Contract\Response; +use Symfony\Component\Messenger\MessageBusInterface; + +class GetMangaStateProvider implements ProviderInterface +{ + public function __construct( + private readonly MessageBusInterface $queryBus + ) {} + + public function provide(Operation $operation, array $uriVariables = [], array $context = []): ?Response + { + $query = new GetMangaByIdQuery($uriVariables['id']); + $response = $this->queryBus->dispatch($query); + + if (null === $response) { + return null; + } + + return $response; + } +} +``` + +### 3. State Processor +```php +namespace App\Domain\Manga\Infrastructure\ApiPlatform\State\Processor; + +use ApiPlatform\Metadata\Operation; +use ApiPlatform\State\ProcessorInterface; +use App\Domain\Manga\Application\Command\CreateMangaCommand; +use Symfony\Component\Messenger\MessageBusInterface; + +class CreateMangaStateProcessor implements ProcessorInterface +{ + public function __construct( + private readonly MessageBusInterface $commandBus + ) {} + + public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): void + { + assert($data instanceof MangaResource); + + $command = new CreateMangaCommand( + title: $data->title, + description: $data->description, + authors: $data->authors, + coverUrl: $data->coverUrl + ); + + $this->commandBus->dispatch($command); + } +} +``` + +## Bonnes Pratiques + +### 1. Documentation +- Documentation exhaustive des endpoints +- Description claire des paramètres +- Exemples de requêtes/réponses +- Documentation des codes d'erreur + +### 2. Validation +- Validation stricte des entrées +- Groupes de validation par contexte +- Messages d'erreur explicites +- Validation des types et formats + +### 3. Sécurité +- Définition claire des accès +- Validation des permissions +- Sanitization des entrées +- Gestion des erreurs sécurisée + +### 4. Performance +- Pagination par défaut +- Sélection des champs (sparse fieldsets) +- Gestion des includes (relationships) +- Cache approprié \ No newline at end of file diff --git a/.cursor/rules/architecture.mdc b/.cursor/rules/architecture.mdc new file mode 100644 index 0000000..c86b0f1 --- /dev/null +++ b/.cursor/rules/architecture.mdc @@ -0,0 +1,210 @@ +--- +description: +globs: +alwaysApply: true +--- +# Architecture Hexagonale de Mangarr + +## Structure Générale +L'application suit une architecture hexagonale (ports & adapters) avec une séparation claire des responsabilités. Le code métier est organisé en domaines distincts dans le dossier `src/Domain/`. + +``` +src/ +└── Domain/ + ├── Shared/ # Code partagé entre les domaines + ├── Manga/ # Domaine de gestion des mangas + ├── Reader/ # Domaine de lecture + └── Scraping/ # Domaine de scraping +``` + +## Organisation des Domaines +Chaque domaine suit la même structure hexagonale : + +``` +Domain/Manga/ +├── Domain/ # Cœur métier +│ ├── Entity/ # Entités du domaine +│ ├── ValueObject/ # Objets de valeur +│ ├── Event/ # Événements du domaine +│ └── Exception/ # Exceptions métier +├── Application/ # Cas d'utilisation +│ ├── Command/ # Commandes (DTO) +│ ├── CommandHandler/# Gestionnaires de commandes +│ ├── Query/ # Requêtes (DTO) +│ ├── QueryHandler/ # Gestionnaires de requêtes +│ └── Response/ # Objets de réponse (DTO) +└── Infrastructure/ # Adaptateurs + ├── Repository/ # Implémentation des repositories + ├── Service/ # Services techniques + └── Persistence/ # Persistence des données +``` + +## Règles d'Architecture + +### 1. Règles Générales +- Tout le code métier doit résider dans le namespace `App\Domain` +- Les dépendances externes doivent être limitées et explicitement autorisées +- Les exceptions standards et utilitaires autorisés : + - `DateTimeImmutable` + - `RuntimeException` + - `Exception` + - `DomainException` + - `Symfony\Component\HttpKernel\Exception` + - `InvalidArgumentException` + +### 2. Domaine Shared +- Le domaine `Shared` ne doit dépendre d'aucun autre domaine +- Il contient les contrats et les types partagés entre les domaines +- Exemple : `App\Domain\Shared\Contract\UuidInterface` + +### 3. Couche Domain +- Ne doit dépendre que d'elle-même et du domaine Shared +- Contient la logique métier pure +- Ne doit pas avoir de dépendances externes +- Structure des composants : + - Les `Entity` sont les objets métier principaux + - Les `ValueObject` sont immuables et s'auto-valident + - Les `Event` représentent les changements d'état du domaine + - Les `Exception` définissent les erreurs métier spécifiques + +### 4. Couche Application +- Peut dépendre de son propre domaine et du domaine Shared +- Peut utiliser les dépendances externes autorisées : + - `Symfony\Component\Messenger` + - `Ramsey\Uuid` +- Ne doit JAMAIS dépendre de la couche Infrastructure +- Structure des composants : + - Les `Query` sont des DTO (Data Transfer Objects) en lecture seule + - Les `Command` sont des DTO pour les modifications + - Les `QueryHandler` doivent : + - Implémenter `QueryHandlerInterface` + - Prendre une seule `Query` en paramètre + - Retourner une `Response` + - Les `CommandHandler` doivent : + - Implémenter `CommandHandlerInterface` + - Prendre une seule `Command` en paramètre + - Ne pas retourner de valeur (void) + - Les `Response` sont des DTO immuables pour les résultats de requêtes + +### 5. Couche Infrastructure +- Implémente les interfaces définies dans le domaine +- Peut dépendre de toutes les couches de son domaine +- Contient les adaptateurs pour les services externes +- Structure des composants : + - Les `Repository` implémentent les interfaces du domaine + - Les `Service` fournissent des fonctionnalités techniques + - La `Persistence` gère le stockage des données + +## Flux de Dépendances +``` +Infrastructure → Application → Domain + ↓ ↓ ↓ + External Shared Shared +``` + +## Validation +Les règles d'architecture sont validées par phparkitect. Les violations de ces règles entraîneront une erreur lors de la validation. + +## Exemples de Code + +### Domain Layer +```php +namespace App\Domain\Manga\Domain\Entity; + +class Manga +{ + private MangaId $id; + private Title $title; + private Description $description; + + public function __construct(MangaId $id, Title $title) + { + $this->id = $id; + $this->title = $title; + } +} +``` + +### Application Layer +```php +namespace App\Domain\Manga\Application\Query; + +readonly class GetMangaByIdQuery +{ + public function __construct( + public string $id + ) {} +} + +namespace App\Domain\Manga\Application\QueryHandler; + +class GetMangaByIdQueryHandler implements QueryHandlerInterface +{ + public function __construct( + private MangaRepositoryInterface $mangaRepository + ) {} + + public function __invoke(GetMangaByIdQuery $query): MangaResponse + { + $manga = $this->mangaRepository->get($query->id); + return new MangaResponse($manga); + } +} + +namespace App\Domain\Manga\Application\Command; + +readonly class CreateMangaCommand +{ + public function __construct( + public string $title, + public ?string $description = null, + ) {} +} + +namespace App\Domain\Manga\Application\CommandHandler; + +class CreateMangaCommandHandler implements CommandHandlerInterface +{ + public function __construct( + private MangaRepositoryInterface $mangaRepository + ) {} + + public function __invoke(CreateMangaCommand $command): void + { + $manga = Manga::create($command->title, $command->description); + $this->mangaRepository->save($manga); + } +} + +namespace App\Domain\Manga\Application\Response; + +readonly class MangaResponse +{ + public function __construct( + public string $id, + public string $title, + public ?string $description + ) {} + + public static function fromEntity(Manga $manga): self + { + return new self( + $manga->getId()->toString(), + $manga->getTitle()->value(), + $manga->getDescription()?->value() + ); + } +} +``` + +### Infrastructure Layer +```php +namespace App\Domain\Manga\Infrastructure\Repository; + +use App\Domain\Manga\Domain\Repository\MangaRepositoryInterface; + +class DoctrineMangaRepository implements MangaRepositoryInterface +{ + // Implementation +} +``` \ No newline at end of file diff --git a/.cursor/rules/business.mdc b/.cursor/rules/business.mdc new file mode 100644 index 0000000..d214861 --- /dev/null +++ b/.cursor/rules/business.mdc @@ -0,0 +1,54 @@ +--- +description: +globs: +alwaysApply: true +--- + +# Contexte Métier de Mangarr + +## Objectif Principal +Mangarr est une application de gestion et d'automatisation pour la collection de mangas, inspirée par Sonarr. Elle permet aux utilisateurs de suivre, télécharger et organiser automatiquement leurs mangas depuis différentes sources en ligne. + +## Fonctionnalités Principales + +### 1. Gestion de la Bibliothèque +- Suivi des séries de mangas +- Organisation automatique des chapitres +- Gestion des métadonnées (titres, auteurs, descriptions, couvertures) + +### 2. Automatisation +- Scraping automatique des nouvelles sorties +- Téléchargement automatique des nouveaux chapitres +- Notifications lors de nouvelles sorties + +### 3. Sources et Scraping +- Support de multiples sources de mangas en ligne +- Système de scraping modulaire et extensible +- Gestion des priorités des sources + +### 4. Interface Utilisateur +- Téléchargement des chapitres en .cbz pour l'utilisateur +- Calendrier des sorties +- État des téléchargements +- Configuration des préférences +- Recherche et découverte de nouveaux mangas + +### 5. Intégration +- API RESTful pour l'intégration avec d'autres services +- Support des lecteurs de manga externes +- Export/Import de la bibliothèque + +## Règles Métier Importantes +1. Un manga peut avoir plusieurs sources disponibles +2. Les chapitres doivent être uniques (pas de doublons) +3. Les métadonnées doivent être cohérentes entre les sources +4. Le système doit respecter les limitations des sites sources +5. La qualité des scans doit être vérifiée avant l'archivage + +## Architecture +L'application suit une architecture modulaire avec : +- Backend en PHP, Symfony pour le scraping et la gestion +- Frontend moderne pour l'interface utilisateur +- Base de données pour le stockage des métadonnées +- Système de files d'attente pour les téléchargements +- Cache pour optimiser les performances diff --git a/.cursor/rules/tests.mdc b/.cursor/rules/tests.mdc new file mode 100644 index 0000000..3fb51d0 --- /dev/null +++ b/.cursor/rules/tests.mdc @@ -0,0 +1,202 @@ +--- +description: +globs: +alwaysApply: true +--- +# Tests de Mangarr + +## Structure des Tests +L'application suit une organisation stricte des tests reflétant l'architecture hexagonale : + +``` +tests/ +├── Domain/ # Tests unitaires par domaine +│ ├── Manga/ +│ │ ├── Application/ # Tests des cas d'utilisation +│ │ ├── Domain/ # Tests du cœur métier +│ │ └── Adapter/ # Implémentations InMemory des ports +│ ├── Reader/ +│ │ └── ... +│ └── Scraping/ +│ └── ... +├── Feature/ # Tests fonctionnels par domaine +│ ├── Manga/ +│ ├── Reader/ +│ └── Scraping/ +├── Shared/ # Tests et adapters partagés +│ └── Adapter/ # Adapters partagés entre domaines +└── Fixtures/ # Fixtures de test partagées +``` + +## Règles de Test + +### 1. Tests Unitaires (Domain) +- Localisation : `tests/Domain/NomDuDomain/` +- Principes : + - Tester chaque composant de manière isolée + - Éviter l'utilisation de mocks + - Utiliser des adapters InMemory + - Nommer les classes de test avec le suffixe `Test` + +### 2. Adapters de Test +- Localisation : `tests/Domain/NomDuDomain/Adapter/` +- Principes : + - Implémenter les interfaces du domaine + - Stocker les données dans des tableaux + - Préfixer les classes avec `InMemory` + - Si utilisé par plusieurs domaines → déplacer dans `tests/Shared/Adapter/` + +### 3. Tests Fonctionnels (Feature) +- Localisation : `tests/Feature/NomDuDomain/` +- Principes : + - Tester les endpoints HTTP + - Utiliser le trait `ResetDatabase` + - Tester le flux complet + - Nommer les classes avec le suffixe `Test` + +## Exemples de Code + +### 1. Adapter InMemory +```php +namespace Tests\Domain\Manga\Adapter; + +use App\Domain\Manga\Domain\Entity\Manga; +use App\Domain\Manga\Domain\Repository\MangaRepositoryInterface; + +class InMemoryMangaRepository implements MangaRepositoryInterface +{ + /** @var array */ + private array $mangas = []; + + public function save(Manga $manga): void + { + $this->mangas[$manga->getId()->toString()] = $manga; + } + + public function get(string $id): ?Manga + { + return $this->mangas[$id] ?? null; + } + + public function clear(): void + { + $this->mangas = []; + } +} +``` + +### 2. Test Unitaire +```php +namespace Tests\Domain\Manga\Application; + +use App\Domain\Manga\Application\Command\CreateMangaCommand; +use App\Domain\Manga\Application\CommandHandler\CreateMangaCommandHandler; +use Tests\Domain\Manga\Adapter\InMemoryMangaRepository; +use PHPUnit\Framework\TestCase; + +class CreateMangaCommandHandlerTest extends TestCase +{ + private InMemoryMangaRepository $repository; + private CreateMangaCommandHandler $handler; + + protected function setUp(): void + { + $this->repository = new InMemoryMangaRepository(); + $this->handler = new CreateMangaCommandHandler($this->repository); + } + + public function test_it_creates_manga(): void + { + // Given + $command = new CreateMangaCommand('One Piece'); + + // When + $this->handler->__invoke($command); + + // Then + $mangas = $this->repository->findAll(); + $this->assertCount(1, $mangas); + $this->assertEquals('One Piece', $mangas[0]->getTitle()->value()); + } +} +``` + +### 3. Test Fonctionnel +```php +namespace Tests\Feature\Manga; + +use Tests\Shared\ResetDatabase; +use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; + +class CreateMangaTest extends WebTestCase +{ + use ResetDatabase; + + public function test_it_creates_manga_through_api(): void + { + // Given + $client = static::createClient(); + $data = ['title' => 'One Piece']; + + // When + $client->request('POST', '/api/mangas', [], [], [], json_encode($data)); + + // Then + $this->assertResponseIsSuccessful(); + $this->assertJsonContains(['title' => 'One Piece']); + } +} +``` + +### 4. Adapter Partagé +```php +namespace Tests\Shared\Adapter; + +use App\Domain\Shared\Contract\MessageBusInterface; + +class InMemoryMessageBus implements MessageBusInterface +{ + /** @var array */ + private array $messages = []; + + public function dispatch(object $message): void + { + $this->messages[] = $message; + } + + public function getDispatchedMessages(): array + { + return $this->messages; + } + + public function clear(): void + { + $this->messages = []; + } +} +``` + +## Bonnes Pratiques + +### 1. Organisation des Tests +- Un test par classe +- Regrouper les tests par fonctionnalité +- Suivre la même structure que le code source +- Utiliser des données de test explicites + +### 2. Nommage +- Classes de test : `{ClassTestée}Test` +- Méthodes de test : `test_it_{comportement_testé}` +- Adapters : `InMemory{Interface}` + +### 3. Assertions +- Utiliser des assertions spécifiques +- Vérifier les états plutôt que les interactions +- Tester les cas d'erreur +- Tester les cas limites + +### 4. Données de Test +- Utiliser des fixtures pour les données complexes +- Créer des données spécifiques au test quand possible +- Éviter les dépendances entre tests +- Nettoyer l'état après chaque test \ No newline at end of file