From c50f1638eeb303944f4756dcf42c8c0185ea4a34 Mon Sep 17 00:00:00 2001 From: "ext.jeremy.guillot@maxicoffee.domains" Date: Mon, 9 Mar 2026 17:54:35 +0100 Subject: [PATCH 1/6] refactor(manga): merge ChapterRepositoryInterface into MangaRepositoryInterface + pagesDirectory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Supprime ChapterRepositoryInterface du domaine Manga (et ses implémentations LegacyChapterRepository et InMemoryChapterRepository) - Déplace toutes les méthodes chapter vers MangaRepositoryInterface avec nommage explicite (findChapterById, findVisibleChapterById, updateChapter, deleteChapter, etc.) - Remplace cbzPath par pagesDirectory + pageCount dans le modèle Chapter (transition : toChapterDomain conserve un fallback cbzPath pour les données existantes, updateChapter synchronise les deux colonnes jusqu'à la Phase 4) - Ajoute la migration Doctrine (pages_directory, page_count sur la table chapter) - Met à jour tous les handlers, listeners, query handlers et state providers du domaine Manga pour injecter uniquement MangaRepositoryInterface - Adapte les tests unitaires et InMemoryMangaRepository avec les nouvelles méthodes Co-Authored-By: Claude Sonnet 4.6 --- config/services.yaml | 4 - migrations/Version20260309165048.php | 42 ++++++ .../CommandHandler/DeleteCbzHandler.php | 15 +- .../CommandHandler/DeleteChapterHandler.php | 11 +- .../EditMultipleChaptersHandler.php | 8 +- .../CommandHandler/ImportChapterHandler.php | 22 +-- .../CommandHandler/ImportVolumeHandler.php | 25 +--- .../ChapterImportedEventListener.php | 11 +- .../VolumeImportedEventListener.php | 9 +- .../QueryHandler/DownloadCbzHandler.php | 15 +- .../QueryHandler/DownloadVolumeHandler.php | 7 +- .../QueryHandler/GetMangaChaptersHandler.php | 2 +- .../Application/Response/ChapterResponse.php | 4 +- .../Repository/ChapterRepositoryInterface.php | 29 ---- .../Repository/MangaRepositoryInterface.php | 47 ++++++- src/Domain/Manga/Domain/Model/Chapter.php | 20 ++- .../State/Provider/DeleteCbzProvider.php | 6 +- .../State/Provider/DeleteChapterProvider.php | 6 +- .../GetMangaChaptersStateProvider.php | 2 +- .../Persistence/LegacyMangaRepository.php | 111 ++++++++++++++- .../Repository/LegacyChapterRepository.php | 128 ------------------ .../MangadxChapterSynchronizationService.php | 13 +- src/Entity/Chapter.php | 30 ++++ .../Adapter/InMemoryChapterRepository.php | 95 ------------- .../Manga/Adapter/InMemoryMangaRepository.php | 116 ++++++++++++++-- .../ImportChapterHandlerTest.php | 32 ++--- .../ImportVolumeHandlerTest.php | 19 +-- 27 files changed, 410 insertions(+), 419 deletions(-) create mode 100644 migrations/Version20260309165048.php delete mode 100644 src/Domain/Manga/Domain/Contract/Repository/ChapterRepositoryInterface.php delete mode 100644 src/Domain/Manga/Infrastructure/Persistence/Repository/LegacyChapterRepository.php delete mode 100644 tests/Domain/Manga/Adapter/InMemoryChapterRepository.php diff --git a/config/services.yaml b/config/services.yaml index 66c2c77..dc75239 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -148,10 +148,6 @@ services: $publicDir: '%kernel.project_dir%/public' $httpClient: '@GuzzleHttp\Client' - # Chapter Repository - App\Domain\Manga\Domain\Contract\Repository\ChapterRepositoryInterface: - alias: App\Domain\Manga\Infrastructure\Persistence\Repository\LegacyChapterRepository - # File Service App\Domain\Manga\Domain\Contract\Service\FileServiceInterface: alias: App\Domain\Manga\Infrastructure\Service\FileService diff --git a/migrations/Version20260309165048.php b/migrations/Version20260309165048.php new file mode 100644 index 0000000..8573102 --- /dev/null +++ b/migrations/Version20260309165048.php @@ -0,0 +1,42 @@ +addSql('ALTER TABLE chapter ADD pages_directory VARCHAR(255) DEFAULT NULL'); + $this->addSql('ALTER TABLE chapter ADD page_count INT DEFAULT 0 NOT NULL'); + $this->addSql('DROP INDEX idx_available_at'); + $this->addSql('DROP INDEX idx_delivered_at'); + $this->addSql('DROP INDEX idx_queue_available'); + $this->addSql('DROP INDEX idx_queue_name'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE SCHEMA public'); + $this->addSql('CREATE INDEX idx_available_at ON messenger_messages (available_at)'); + $this->addSql('CREATE INDEX idx_delivered_at ON messenger_messages (delivered_at)'); + $this->addSql('CREATE INDEX idx_queue_available ON messenger_messages (queue_name, available_at)'); + $this->addSql('CREATE INDEX idx_queue_name ON messenger_messages (queue_name)'); + $this->addSql('ALTER TABLE chapter DROP pages_directory'); + $this->addSql('ALTER TABLE chapter DROP page_count'); + } +} diff --git a/src/Domain/Manga/Application/CommandHandler/DeleteCbzHandler.php b/src/Domain/Manga/Application/CommandHandler/DeleteCbzHandler.php index cf656ec..4e4dc7e 100644 --- a/src/Domain/Manga/Application/CommandHandler/DeleteCbzHandler.php +++ b/src/Domain/Manga/Application/CommandHandler/DeleteCbzHandler.php @@ -3,7 +3,7 @@ namespace App\Domain\Manga\Application\CommandHandler; use App\Domain\Manga\Application\Command\DeleteCbz; -use App\Domain\Manga\Domain\Contract\Repository\ChapterRepositoryInterface; +use App\Domain\Manga\Domain\Contract\Repository\MangaRepositoryInterface; use App\Domain\Manga\Domain\Contract\Service\FileServiceInterface; use App\Domain\Manga\Domain\Exception\ChapterNotFoundException; use App\Domain\Manga\Domain\Exception\CbzFileNotFoundException; @@ -15,7 +15,7 @@ use App\Domain\Shared\Domain\Contract\CommandInterface; readonly class DeleteCbzHandler implements CommandHandlerInterface { public function __construct( - private ChapterRepositoryInterface $chapterRepository, + private MangaRepositoryInterface $mangaRepository, private FileServiceInterface $fileService ) {} @@ -23,22 +23,16 @@ readonly class DeleteCbzHandler implements CommandHandlerInterface { assert($command instanceof DeleteCbz); - $chapter = $this->chapterRepository->findVisibleById($command->chapterId); + $chapter = $this->mangaRepository->findVisibleChapterById($command->chapterId); if (!$chapter) { throw new ChapterNotFoundException($command->chapterId); } - // Check if chapter has a CBZ file if (!$chapter->isAvailable()) { throw new CbzFileNotFoundException($command->chapterId); } - // Delete the physical CBZ file - // Note: We'll need to get the CBZ path from somewhere, likely from a legacy repository - // For now, we'll just mark the chapter as not available - - // Update chapter to mark CBZ as not available $updatedChapter = new Chapter( new ChapterId($chapter->getId()), $chapter->getMangaId(), @@ -47,9 +41,10 @@ readonly class DeleteCbzHandler implements CommandHandlerInterface $chapter->getVolume(), $chapter->isVisible(), null, + 0, $chapter->getCreatedAt() ); - $this->chapterRepository->save($updatedChapter); + $this->mangaRepository->updateChapter($updatedChapter); } } diff --git a/src/Domain/Manga/Application/CommandHandler/DeleteChapterHandler.php b/src/Domain/Manga/Application/CommandHandler/DeleteChapterHandler.php index 4a96ea7..6cc8682 100644 --- a/src/Domain/Manga/Application/CommandHandler/DeleteChapterHandler.php +++ b/src/Domain/Manga/Application/CommandHandler/DeleteChapterHandler.php @@ -3,7 +3,7 @@ namespace App\Domain\Manga\Application\CommandHandler; use App\Domain\Manga\Application\Command\DeleteChapter; -use App\Domain\Manga\Domain\Contract\Repository\ChapterRepositoryInterface; +use App\Domain\Manga\Domain\Contract\Repository\MangaRepositoryInterface; use App\Domain\Manga\Domain\Exception\ChapterNotFoundException; use App\Domain\Manga\Domain\Model\Chapter; use App\Domain\Manga\Domain\Model\ValueObject\ChapterId; @@ -13,14 +13,14 @@ use App\Domain\Shared\Domain\Contract\CommandInterface; readonly class DeleteChapterHandler implements CommandHandlerInterface { public function __construct( - private ChapterRepositoryInterface $chapterRepository + private MangaRepositoryInterface $mangaRepository ) {} public function handle(CommandInterface $command): void { assert($command instanceof DeleteChapter); - $chapter = $this->chapterRepository->findVisibleById($command->chapterId); + $chapter = $this->mangaRepository->findVisibleChapterById($command->chapterId); if (!$chapter) { throw new ChapterNotFoundException($command->chapterId); @@ -33,10 +33,11 @@ readonly class DeleteChapterHandler implements CommandHandlerInterface title: $chapter->getTitle(), volume: $chapter->getVolume(), isVisible: false, - cbzPath: $chapter->getCbzPath(), + pagesDirectory: $chapter->getPagesDirectory(), + pageCount: $chapter->getPageCount(), createdAt: $chapter->getCreatedAt() ); - $this->chapterRepository->save($updatedChapter); + $this->mangaRepository->updateChapter($updatedChapter); } } diff --git a/src/Domain/Manga/Application/CommandHandler/EditMultipleChaptersHandler.php b/src/Domain/Manga/Application/CommandHandler/EditMultipleChaptersHandler.php index 5812b15..ea825e9 100644 --- a/src/Domain/Manga/Application/CommandHandler/EditMultipleChaptersHandler.php +++ b/src/Domain/Manga/Application/CommandHandler/EditMultipleChaptersHandler.php @@ -3,19 +3,19 @@ namespace App\Domain\Manga\Application\CommandHandler; use App\Domain\Manga\Application\Command\EditMultipleChapters; -use App\Domain\Manga\Domain\Contract\Repository\ChapterRepositoryInterface; +use App\Domain\Manga\Domain\Contract\Repository\MangaRepositoryInterface; use App\Domain\Manga\Domain\Exception\ChapterNotFoundException; readonly class EditMultipleChaptersHandler { public function __construct( - private ChapterRepositoryInterface $chapterRepository + private MangaRepositoryInterface $mangaRepository ) {} public function handle(EditMultipleChapters $command): void { foreach ($command->chapters as $chapterData) { - $chapter = $this->chapterRepository->findById($chapterData->id); + $chapter = $this->mangaRepository->findChapterById($chapterData->id); if (!$chapter) { throw new ChapterNotFoundException($chapterData->id); @@ -31,7 +31,7 @@ readonly class EditMultipleChaptersHandler $updatedChapter = $updatedChapter->updateVolume($chapterData->volume); } - $this->chapterRepository->save($updatedChapter); + $this->mangaRepository->updateChapter($updatedChapter); } } } diff --git a/src/Domain/Manga/Application/CommandHandler/ImportChapterHandler.php b/src/Domain/Manga/Application/CommandHandler/ImportChapterHandler.php index 08e2be8..07e9461 100644 --- a/src/Domain/Manga/Application/CommandHandler/ImportChapterHandler.php +++ b/src/Domain/Manga/Application/CommandHandler/ImportChapterHandler.php @@ -3,20 +3,17 @@ namespace App\Domain\Manga\Application\CommandHandler; use App\Domain\Manga\Application\Command\ImportChapter; -use App\Domain\Manga\Domain\Contract\Repository\ChapterRepositoryInterface; use App\Domain\Manga\Domain\Contract\Repository\MangaRepositoryInterface; use App\Domain\Manga\Domain\Exception\MangaNotFoundException; use App\Domain\Manga\Domain\Exception\ChapterNotFoundException; use App\Domain\Manga\Domain\Model\Chapter; use App\Domain\Manga\Domain\Model\ValueObject\ChapterId; use App\Domain\Shared\Domain\Contract\MangaPathManagerInterface; -use Ramsey\Uuid\Uuid; readonly class ImportChapterHandler { public function __construct( private MangaRepositoryInterface $mangaRepository, - private ChapterRepositoryInterface $chapterRepository, private MangaPathManagerInterface $pathManager ) {} @@ -34,7 +31,7 @@ readonly class ImportChapterHandler } // 3. Check if chapter exists - $existingChapter = $this->chapterRepository->findByMangaIdAndChapterNumber( + $existingChapter = $this->mangaRepository->findChapterByMangaIdAndNumber( $command->mangaId, $command->chapterNumber ); @@ -46,7 +43,8 @@ readonly class ImportChapterHandler // 4. Save the CBZ file to storage using the path manager $cbzPath = $this->saveCbzFile($command, $manga, $existingChapter); - // 5. Update existing chapter with new CBZ path + // 5. Update existing chapter with new path + // Note: pagesDirectory holds CBZ path during transition; Phase 3 will store individual images $updatedChapter = new Chapter( id: new ChapterId($existingChapter->getId()), mangaId: $existingChapter->getMangaId(), @@ -54,29 +52,22 @@ readonly class ImportChapterHandler title: $existingChapter->getTitle(), volume: $existingChapter->getVolume(), isVisible: $existingChapter->isVisible(), - cbzPath: $cbzPath, + pagesDirectory: $cbzPath, + pageCount: $existingChapter->getPageCount(), createdAt: $existingChapter->getCreatedAt() ); - $this->chapterRepository->save($updatedChapter); + $this->mangaRepository->updateChapter($updatedChapter); } - /** - * Validate that the binary data is a valid CBZ (ZIP) file - */ private function isValidCbzFile(string $fileBinary): bool { - // CBZ files are ZIP archives, check for ZIP magic number $zipMagicNumber = "\x50\x4b\x03\x04"; // PK\x03\x04 return strpos($fileBinary, $zipMagicNumber) === 0; } - /** - * Save the CBZ file to storage and return the path - */ private function saveCbzFile(ImportChapter $command, \App\Domain\Manga\Domain\Model\Manga $manga, Chapter $chapter): string { - // Build the final CBZ path using the path manager (creates directories) $volumeNumber = $chapter->getVolume() ?? 0; $cbzPath = $this->pathManager->buildChapterCbzPath( $manga->getTitle()->getValue(), @@ -85,7 +76,6 @@ readonly class ImportChapterHandler (string)$command->chapterNumber ); - // Write the binary content directly to the CBZ path if (!file_put_contents($cbzPath, $command->fileBinary)) { throw new \RuntimeException('Failed to save CBZ file'); } diff --git a/src/Domain/Manga/Application/CommandHandler/ImportVolumeHandler.php b/src/Domain/Manga/Application/CommandHandler/ImportVolumeHandler.php index 404755d..882397d 100644 --- a/src/Domain/Manga/Application/CommandHandler/ImportVolumeHandler.php +++ b/src/Domain/Manga/Application/CommandHandler/ImportVolumeHandler.php @@ -3,7 +3,6 @@ namespace App\Domain\Manga\Application\CommandHandler; use App\Domain\Manga\Application\Command\ImportVolume; -use App\Domain\Manga\Domain\Contract\Repository\ChapterRepositoryInterface; use App\Domain\Manga\Domain\Contract\Repository\MangaRepositoryInterface; use App\Domain\Manga\Domain\Exception\MangaNotFoundException; use App\Domain\Manga\Domain\Model\Chapter; @@ -14,7 +13,6 @@ readonly class ImportVolumeHandler { public function __construct( private MangaRepositoryInterface $mangaRepository, - private ChapterRepositoryInterface $chapterRepository, private MangaPathManagerInterface $pathManager ) {} @@ -32,7 +30,7 @@ readonly class ImportVolumeHandler } // 3. Get all chapters for this volume - $chapters = $this->chapterRepository->findByMangaIdAndVolume( + $chapters = $this->mangaRepository->findChaptersByMangaIdAndVolume( $command->mangaId, $command->volumeNumber ); @@ -46,7 +44,8 @@ readonly class ImportVolumeHandler // 4. Save the CBZ file to storage using the path manager $cbzPath = $this->saveCbzFile($command, $manga); - // 5. Update all chapters with the volume CBZ path + // 5. Update all chapters with the volume path + // Note: pagesDirectory holds CBZ path during transition; Phase 3 will store individual images foreach ($chapters as $chapter) { $updatedChapter = new Chapter( id: new ChapterId($chapter->getId()), @@ -55,37 +54,29 @@ readonly class ImportVolumeHandler title: $chapter->getTitle(), volume: $chapter->getVolume(), isVisible: $chapter->isVisible(), - cbzPath: $cbzPath, + pagesDirectory: $cbzPath, + pageCount: $chapter->getPageCount(), createdAt: $chapter->getCreatedAt() ); - $this->chapterRepository->save($updatedChapter); + $this->mangaRepository->updateChapter($updatedChapter); } } - /** - * Validate that the binary data is a valid CBZ (ZIP) file - */ private function isValidCbzFile(string $fileBinary): bool { - // CBZ files are ZIP archives, check for ZIP magic number $zipMagicNumber = "\x50\x4b\x03\x04"; // PK\x03\x04 return strpos($fileBinary, $zipMagicNumber) === 0; } - /** - * Save the CBZ file to storage and return the path - */ private function saveCbzFile(ImportVolume $command, \App\Domain\Manga\Domain\Model\Manga $manga): string { - // Build the final CBZ path using the path manager (creates directories) $cbzPath = $this->pathManager->buildVolumeCbzPath( $manga->getTitle()->getValue(), (string)$manga->getPublicationYear(), $command->volumeNumber ); - // Write the binary content directly to the CBZ path if (!file_put_contents($cbzPath, $command->fileBinary)) { throw new \RuntimeException('Failed to save CBZ file'); } @@ -93,7 +84,3 @@ readonly class ImportVolumeHandler return $cbzPath; } } - - - - diff --git a/src/Domain/Manga/Application/EventListener/ChapterImportedEventListener.php b/src/Domain/Manga/Application/EventListener/ChapterImportedEventListener.php index 39e2d22..3b43b10 100644 --- a/src/Domain/Manga/Application/EventListener/ChapterImportedEventListener.php +++ b/src/Domain/Manga/Application/EventListener/ChapterImportedEventListener.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace App\Domain\Manga\Application\EventListener; -use App\Domain\Manga\Domain\Contract\Repository\ChapterRepositoryInterface; use App\Domain\Manga\Domain\Contract\Repository\MangaRepositoryInterface; use App\Domain\Manga\Domain\Model\Chapter; use App\Domain\Manga\Domain\Model\ValueObject\ChapterId; @@ -15,17 +14,16 @@ readonly class ChapterImportedEventListener { public function __construct( private MangaRepositoryInterface $mangaRepository, - private ChapterRepositoryInterface $chapterRepository, ) {} public function __invoke(ChapterImported $event): void { $manga = $this->mangaRepository->findBySlug(new MangaSlug($event->mangaSlug)); if (!$manga) { - return; // Manga introuvable, on ignore + return; } - $chapters = $this->chapterRepository->findVisibleByMangaIdAndVolume($manga->getId()->getValue(), (int) $event->volume); + $chapters = $this->mangaRepository->findVisibleChaptersByMangaIdAndVolume($manga->getId()->getValue(), (int) $event->volume); foreach ($chapters as $chapter) { if ($chapter->getNumber() === (float) $event->chapterNumber) { $updated = new Chapter( @@ -36,13 +34,12 @@ readonly class ChapterImportedEventListener $chapter->getVolume(), $chapter->isVisible(), $event->cbzPath, + $chapter->getPageCount(), $chapter->getCreatedAt(), ); - $this->chapterRepository->save($updated); + $this->mangaRepository->updateChapter($updated); break; } } } } - - diff --git a/src/Domain/Manga/Application/EventListener/VolumeImportedEventListener.php b/src/Domain/Manga/Application/EventListener/VolumeImportedEventListener.php index 6ed8923..e3bbe68 100644 --- a/src/Domain/Manga/Application/EventListener/VolumeImportedEventListener.php +++ b/src/Domain/Manga/Application/EventListener/VolumeImportedEventListener.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace App\Domain\Manga\Application\EventListener; -use App\Domain\Manga\Domain\Contract\Repository\ChapterRepositoryInterface; use App\Domain\Manga\Domain\Contract\Repository\MangaRepositoryInterface; use App\Domain\Manga\Domain\Model\Chapter; use App\Domain\Manga\Domain\Model\ValueObject\ChapterId; @@ -15,7 +14,6 @@ readonly class VolumeImportedEventListener { public function __construct( private MangaRepositoryInterface $mangaRepository, - private ChapterRepositoryInterface $chapterRepository, ) {} public function __invoke(VolumeImported $event): void @@ -25,7 +23,7 @@ readonly class VolumeImportedEventListener return; } - $chapters = $this->chapterRepository->findByMangaIdAndVolume($manga->getId()->getValue(), (int) $event->volume); + $chapters = $this->mangaRepository->findChaptersByMangaIdAndVolume($manga->getId()->getValue(), (int) $event->volume); if ($chapters === []) { return; } @@ -39,11 +37,10 @@ readonly class VolumeImportedEventListener $chapter->getVolume(), $chapter->isVisible(), $event->cbzPath, + $chapter->getPageCount(), $chapter->getCreatedAt(), ); - $this->chapterRepository->save($updated); + $this->mangaRepository->updateChapter($updated); } } } - - diff --git a/src/Domain/Manga/Application/QueryHandler/DownloadCbzHandler.php b/src/Domain/Manga/Application/QueryHandler/DownloadCbzHandler.php index a4f92e2..0133f16 100644 --- a/src/Domain/Manga/Application/QueryHandler/DownloadCbzHandler.php +++ b/src/Domain/Manga/Application/QueryHandler/DownloadCbzHandler.php @@ -4,7 +4,7 @@ namespace App\Domain\Manga\Application\QueryHandler; use App\Domain\Manga\Application\Query\DownloadCbz; use App\Domain\Manga\Application\Response\DownloadResponse; -use App\Domain\Manga\Domain\Contract\Repository\ChapterRepositoryInterface; +use App\Domain\Manga\Domain\Contract\Repository\MangaRepositoryInterface; use App\Domain\Manga\Domain\Contract\Service\FileServiceInterface; use App\Domain\Manga\Domain\Exception\CbzFileNotFoundException; use App\Domain\Manga\Domain\Exception\ChapterNotFoundException; @@ -16,7 +16,7 @@ use App\Domain\Shared\Domain\Contract\ResponseInterface; readonly class DownloadCbzHandler implements QueryHandlerInterface { public function __construct( - private ChapterRepositoryInterface $chapterRepository, + private MangaRepositoryInterface $mangaRepository, private FileServiceInterface $fileService ) {} @@ -24,7 +24,7 @@ readonly class DownloadCbzHandler implements QueryHandlerInterface { assert($query instanceof DownloadCbz); - $chapter = $this->chapterRepository->findVisibleById($query->chapterId); + $chapter = $this->mangaRepository->findVisibleChapterById($query->chapterId); if (!$chapter) { throw new ChapterNotFoundException($query->chapterId); @@ -34,14 +34,11 @@ readonly class DownloadCbzHandler implements QueryHandlerInterface throw new ChapterNotAvailableException($query->chapterId); } - // Use the actual CBZ path from the chapter - $cbzPath = $chapter->getCbzPath(); - - // Extract the existing filename from the path - $filename = basename($cbzPath); + $pagesDirectory = $chapter->getPagesDirectory(); + $filename = basename($pagesDirectory); try { - $httpResponse = $this->fileService->downloadCbz($cbzPath, $filename); + $httpResponse = $this->fileService->downloadCbz($pagesDirectory, $filename); } catch (CbzFileNotFoundException $e) { throw new ChapterNotAvailableException($query->chapterId); } diff --git a/src/Domain/Manga/Application/QueryHandler/DownloadVolumeHandler.php b/src/Domain/Manga/Application/QueryHandler/DownloadVolumeHandler.php index 47f236e..f9a06b5 100644 --- a/src/Domain/Manga/Application/QueryHandler/DownloadVolumeHandler.php +++ b/src/Domain/Manga/Application/QueryHandler/DownloadVolumeHandler.php @@ -4,7 +4,6 @@ namespace App\Domain\Manga\Application\QueryHandler; use App\Domain\Manga\Application\Query\DownloadVolume; use App\Domain\Manga\Application\Response\DownloadResponse; -use App\Domain\Manga\Domain\Contract\Repository\ChapterRepositoryInterface; use App\Domain\Manga\Domain\Contract\Repository\MangaRepositoryInterface; use App\Domain\Manga\Domain\Contract\Service\FileServiceInterface; use App\Domain\Manga\Domain\Exception\MangaNotFoundException; @@ -16,7 +15,6 @@ use App\Domain\Shared\Domain\Contract\ResponseInterface; readonly class DownloadVolumeHandler implements QueryHandlerInterface { public function __construct( - private ChapterRepositoryInterface $chapterRepository, private MangaRepositoryInterface $mangaRepository, private FileServiceInterface $fileService ) {} @@ -31,7 +29,7 @@ readonly class DownloadVolumeHandler implements QueryHandlerInterface throw new MangaNotFoundException($query->mangaId); } - $chapters = $this->chapterRepository->findVisibleWithCbzByMangaIdAndVolume( + $chapters = $this->mangaRepository->findVisibleChaptersWithPagesByMangaIdAndVolume( $query->mangaId, $query->volume ); @@ -40,10 +38,9 @@ readonly class DownloadVolumeHandler implements QueryHandlerInterface throw new VolumeNotFoundException($query->mangaId, $query->volume); } - // Collect CBZ paths for all chapters $cbzPaths = []; foreach ($chapters as $chapter) { - $cbzPaths[] = $chapter->getCbzPath(); + $cbzPaths[] = $chapter->getPagesDirectory(); } $volumeName = sprintf('%s_vol%d', diff --git a/src/Domain/Manga/Application/QueryHandler/GetMangaChaptersHandler.php b/src/Domain/Manga/Application/QueryHandler/GetMangaChaptersHandler.php index b9e1ed4..b335a7b 100644 --- a/src/Domain/Manga/Application/QueryHandler/GetMangaChaptersHandler.php +++ b/src/Domain/Manga/Application/QueryHandler/GetMangaChaptersHandler.php @@ -38,7 +38,7 @@ readonly class GetMangaChaptersHandler title: $chapter->getTitle(), volume: $chapter->getVolume(), isVisible: $chapter->isVisible(), - cbzPath: $chapter->getCbzPath(), + pagesDirectory: $chapter->getPagesDirectory(), createdAt: $chapter->getCreatedAt() ), $chapters diff --git a/src/Domain/Manga/Application/Response/ChapterResponse.php b/src/Domain/Manga/Application/Response/ChapterResponse.php index 3fad1eb..364a414 100644 --- a/src/Domain/Manga/Application/Response/ChapterResponse.php +++ b/src/Domain/Manga/Application/Response/ChapterResponse.php @@ -10,7 +10,7 @@ readonly class ChapterResponse public ?string $title, public ?int $volume, public bool $isVisible, - public ?string $cbzPath, + public ?string $pagesDirectory, public \DateTimeImmutable $createdAt ) {} -} \ No newline at end of file +} diff --git a/src/Domain/Manga/Domain/Contract/Repository/ChapterRepositoryInterface.php b/src/Domain/Manga/Domain/Contract/Repository/ChapterRepositoryInterface.php deleted file mode 100644 index e2accf8..0000000 --- a/src/Domain/Manga/Domain/Contract/Repository/ChapterRepositoryInterface.php +++ /dev/null @@ -1,29 +0,0 @@ - @@ -30,7 +43,27 @@ interface MangaRepositoryInterface public function findExistingChaptersByNumbers(string $mangaId, array $chapterNumbers): array; /** - * @return Manga[] + * @return Chapter[] */ - public function findByMonitoringCriteria(MonitoringCriteria $criteria): array; + public function findChaptersByMangaIdAndVolume(string $mangaId, int $volume): array; + + /** + * @return Chapter[] + */ + public function findVisibleChaptersByMangaIdAndVolume(string $mangaId, int $volume): array; + + /** + * @return Chapter[] + */ + public function findVisibleChaptersWithPagesByMangaIdAndVolume(string $mangaId, int $volume): array; + + // --- Chapters (write) --- + + /** Create a new chapter and return its generated ID. */ + public function saveChapter(Chapter $chapter): ChapterId; + + /** Update an existing chapter. */ + public function updateChapter(Chapter $chapter): void; + + public function deleteChapter(Chapter $chapter): void; } diff --git a/src/Domain/Manga/Domain/Model/Chapter.php b/src/Domain/Manga/Domain/Model/Chapter.php index a5b9e60..c44fa75 100644 --- a/src/Domain/Manga/Domain/Model/Chapter.php +++ b/src/Domain/Manga/Domain/Model/Chapter.php @@ -13,7 +13,8 @@ readonly class Chapter private ?string $title, private ?int $volume, private bool $isVisible, - private ?string $cbzPath = null, + private ?string $pagesDirectory = null, + private int $pageCount = 0, private \DateTimeImmutable $createdAt = new \DateTimeImmutable() ) {} @@ -49,12 +50,17 @@ readonly class Chapter public function isAvailable(): bool { - return $this->cbzPath !== null; + return $this->pagesDirectory !== null; } - public function getCbzPath(): ?string + public function getPagesDirectory(): ?string { - return $this->cbzPath; + return $this->pagesDirectory; + } + + public function getPageCount(): int + { + return $this->pageCount; } public function getCreatedAt(): \DateTimeImmutable @@ -71,7 +77,8 @@ readonly class Chapter $title, $this->volume, $this->isVisible, - $this->cbzPath, + $this->pagesDirectory, + $this->pageCount, $this->createdAt ); } @@ -85,7 +92,8 @@ readonly class Chapter $this->title, $volume, $this->isVisible, - $this->cbzPath, + $this->pagesDirectory, + $this->pageCount, $this->createdAt ); } diff --git a/src/Domain/Manga/Infrastructure/ApiPlatform/State/Provider/DeleteCbzProvider.php b/src/Domain/Manga/Infrastructure/ApiPlatform/State/Provider/DeleteCbzProvider.php index c7b68cf..8c3d7ae 100644 --- a/src/Domain/Manga/Infrastructure/ApiPlatform/State/Provider/DeleteCbzProvider.php +++ b/src/Domain/Manga/Infrastructure/ApiPlatform/State/Provider/DeleteCbzProvider.php @@ -4,7 +4,7 @@ namespace App\Domain\Manga\Infrastructure\ApiPlatform\State\Provider; use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ProviderInterface; -use App\Domain\Manga\Domain\Contract\Repository\ChapterRepositoryInterface; +use App\Domain\Manga\Domain\Contract\Repository\MangaRepositoryInterface; use App\Domain\Manga\Domain\Exception\ChapterNotFoundException; use App\Domain\Manga\Domain\Exception\CbzFileNotFoundException; use App\Domain\Manga\Infrastructure\ApiPlatform\Resource\DeleteCbzResource; @@ -13,7 +13,7 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; readonly class DeleteCbzProvider implements ProviderInterface { public function __construct( - private ChapterRepositoryInterface $chapterRepository + private MangaRepositoryInterface $mangaRepository ) {} public function provide(Operation $operation, array $uriVariables = [], array $context = []): DeleteCbzResource @@ -25,7 +25,7 @@ readonly class DeleteCbzProvider implements ProviderInterface $chapterId = $uriVariables['id']; try { - $chapter = $this->chapterRepository->findVisibleById($chapterId); + $chapter = $this->mangaRepository->findVisibleChapterById($chapterId); if (!$chapter) { throw new ChapterNotFoundException($chapterId); diff --git a/src/Domain/Manga/Infrastructure/ApiPlatform/State/Provider/DeleteChapterProvider.php b/src/Domain/Manga/Infrastructure/ApiPlatform/State/Provider/DeleteChapterProvider.php index 5609daa..91ea080 100644 --- a/src/Domain/Manga/Infrastructure/ApiPlatform/State/Provider/DeleteChapterProvider.php +++ b/src/Domain/Manga/Infrastructure/ApiPlatform/State/Provider/DeleteChapterProvider.php @@ -4,7 +4,7 @@ namespace App\Domain\Manga\Infrastructure\ApiPlatform\State\Provider; use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ProviderInterface; -use App\Domain\Manga\Domain\Contract\Repository\ChapterRepositoryInterface; +use App\Domain\Manga\Domain\Contract\Repository\MangaRepositoryInterface; use App\Domain\Manga\Domain\Exception\ChapterNotFoundException; use App\Domain\Manga\Infrastructure\ApiPlatform\Resource\DeleteChapterResource; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -12,7 +12,7 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; readonly class DeleteChapterProvider implements ProviderInterface { public function __construct( - private ChapterRepositoryInterface $chapterRepository + private MangaRepositoryInterface $mangaRepository ) {} public function provide(Operation $operation, array $uriVariables = [], array $context = []): DeleteChapterResource @@ -24,7 +24,7 @@ readonly class DeleteChapterProvider implements ProviderInterface $chapterId = $uriVariables['id']; try { - $chapter = $this->chapterRepository->findVisibleById($chapterId); + $chapter = $this->mangaRepository->findVisibleChapterById($chapterId); if (!$chapter) { throw new ChapterNotFoundException($chapterId); diff --git a/src/Domain/Manga/Infrastructure/ApiPlatform/State/Provider/GetMangaChaptersStateProvider.php b/src/Domain/Manga/Infrastructure/ApiPlatform/State/Provider/GetMangaChaptersStateProvider.php index 9f88617..3987fa3 100644 --- a/src/Domain/Manga/Infrastructure/ApiPlatform/State/Provider/GetMangaChaptersStateProvider.php +++ b/src/Domain/Manga/Infrastructure/ApiPlatform/State/Provider/GetMangaChaptersStateProvider.php @@ -52,7 +52,7 @@ readonly class GetMangaChaptersStateProvider implements ProviderInterface title: $chapter->title, volume: $chapter->volume, isVisible: $chapter->isVisible, - isAvailable: $chapter->cbzPath !== null, + isAvailable: $chapter->pagesDirectory !== null, createdAt: $chapter->createdAt->format(\DateTimeInterface::RFC3339) ); } diff --git a/src/Domain/Manga/Infrastructure/Persistence/LegacyMangaRepository.php b/src/Domain/Manga/Infrastructure/Persistence/LegacyMangaRepository.php index 8c01282..44c5710 100644 --- a/src/Domain/Manga/Infrastructure/Persistence/LegacyMangaRepository.php +++ b/src/Domain/Manga/Infrastructure/Persistence/LegacyMangaRepository.php @@ -179,7 +179,9 @@ readonly class LegacyMangaRepository implements MangaRepositoryInterface ->setNumber($chapter->getNumber()) ->setTitle($chapter->getTitle()) ->setVolume($chapter->getVolume()) - ->setVisible($chapter->isVisible()); + ->setVisible($chapter->isVisible()) + ->setPagesDirectory($chapter->getPagesDirectory()) + ->setPageCount($chapter->getPageCount()); $this->entityManager->persist($entity); $this->entityManager->flush(); @@ -187,6 +189,107 @@ readonly class LegacyMangaRepository implements MangaRepositoryInterface return new ChapterId((string) $entity->getId()); } + public function findChapterById(string $id): ?Chapter + { + $entity = $this->entityManager->find(EntityChapter::class, $id); + + return $entity ? $this->toChapterDomain($entity) : null; + } + + public function findVisibleChapterById(string $id): ?Chapter + { + $entity = $this->entityManager->createQueryBuilder() + ->select('c') + ->from(EntityChapter::class, 'c') + ->where('c.id = :id') + ->andWhere('c.visible = :visible') + ->setParameter('id', $id) + ->setParameter('visible', 1) + ->getQuery() + ->getOneOrNullResult(); + + return $entity ? $this->toChapterDomain($entity) : null; + } + + public function findChapterByMangaIdAndNumber(string $mangaId, float $chapterNumber): ?Chapter + { + $entity = $this->entityManager->createQueryBuilder() + ->select('c') + ->from(EntityChapter::class, 'c') + ->where('c.manga = :mangaId') + ->andWhere('c.number = :chapterNumber') + ->setParameter('mangaId', $mangaId) + ->setParameter('chapterNumber', $chapterNumber) + ->getQuery() + ->getOneOrNullResult(); + + return $entity ? $this->toChapterDomain($entity) : null; + } + + public function updateChapter(Chapter $chapter): void + { + $entity = $this->entityManager->find(EntityChapter::class, $chapter->getId()); + + if (!$entity) { + throw new \RuntimeException(sprintf('Chapter with id %s not found', $chapter->getId())); + } + + $entity->setVisible($chapter->isVisible()) + ->setPagesDirectory($chapter->getPagesDirectory()) + ->setPageCount($chapter->getPageCount()) + ->setTitle($chapter->getTitle()) + ->setVolume($chapter->getVolume()) + // Keep cbzPath in sync during transition (Phase 4 will drop this column) + ->setCbzPath($chapter->getPagesDirectory()); + + $this->entityManager->persist($entity); + $this->entityManager->flush(); + } + + public function deleteChapter(Chapter $chapter): void + { + $entity = $this->entityManager->find(EntityChapter::class, $chapter->getId()); + + if ($entity) { + $this->entityManager->remove($entity); + $this->entityManager->flush(); + } + } + + public function findChaptersByMangaIdAndVolume(string $mangaId, int $volume): array + { + $entities = $this->entityManager->getRepository(EntityChapter::class) + ->findBy(['manga' => $mangaId, 'volume' => $volume]); + + return array_map([$this, 'toChapterDomain'], $entities); + } + + public function findVisibleChaptersByMangaIdAndVolume(string $mangaId, int $volume): array + { + $entities = $this->entityManager->getRepository(EntityChapter::class) + ->findBy(['manga' => $mangaId, 'volume' => $volume, 'visible' => true]); + + return array_map([$this, 'toChapterDomain'], $entities); + } + + public function findVisibleChaptersWithPagesByMangaIdAndVolume(string $mangaId, int $volume): array + { + $entities = $this->entityManager->createQueryBuilder() + ->select('c') + ->from(EntityChapter::class, 'c') + ->where('c.manga = :mangaId') + ->andWhere('c.volume = :volume') + ->andWhere('c.visible = true') + ->andWhere('c.pagesDirectory IS NOT NULL OR c.cbzPath IS NOT NULL') + ->setParameter('mangaId', $mangaId) + ->setParameter('volume', $volume) + ->orderBy('c.number', 'ASC') + ->getQuery() + ->getResult(); + + return array_map([$this, 'toChapterDomain'], $entities); + } + public function search(string $query, int $page = 1, int $limit = 20): array { $offset = ($page - 1) * $limit; @@ -314,12 +417,14 @@ readonly class LegacyMangaRepository implements MangaRepositoryInterface { return new Chapter( id: new ChapterId((string) $entity->getId()), - mangaId: $entity->getManga()->getId(), + mangaId: (string) $entity->getManga()->getId(), number: $entity->getNumber(), title: $entity->getTitle(), volume: $entity->getVolume(), isVisible: $entity->isVisible(), - cbzPath: $entity->getCbzPath() + // Fallback to cbzPath during transition (Phase 4 will drop cbzPath column) + pagesDirectory: $entity->getPagesDirectory() ?? $entity->getCbzPath(), + pageCount: $entity->getPageCount(), ); } } diff --git a/src/Domain/Manga/Infrastructure/Persistence/Repository/LegacyChapterRepository.php b/src/Domain/Manga/Infrastructure/Persistence/Repository/LegacyChapterRepository.php deleted file mode 100644 index 211eb26..0000000 --- a/src/Domain/Manga/Infrastructure/Persistence/Repository/LegacyChapterRepository.php +++ /dev/null @@ -1,128 +0,0 @@ -entityManager->find(ChapterEntity::class, $id); - - return $entity ? $this->toDomainModel($entity) : null; - } - - public function findVisibleById(string $id): ?Chapter - { - $qb = $this->entityManager->createQueryBuilder() - ->select('c') - ->from(ChapterEntity::class, 'c') - ->where('c.id = :id') - ->andWhere('c.visible = :visible') - ->setParameter('id', $id) - ->setParameter('visible', 1); - - $entity = $qb->getQuery()->getOneOrNullResult(); - - return $entity ? $this->toDomainModel($entity) : null; - } - - public function findByMangaIdAndChapterNumber(string $mangaId, float $chapterNumber): ?Chapter - { - $qb = $this->entityManager->createQueryBuilder() - ->select('c') - ->from(ChapterEntity::class, 'c') - ->where('c.manga = :mangaId') - ->andWhere('c.number = :chapterNumber') - ->setParameter('mangaId', $mangaId) - ->setParameter('chapterNumber', $chapterNumber); - - $entity = $qb->getQuery()->getOneOrNullResult(); - - return $entity ? $this->toDomainModel($entity) : null; - } - - public function save(Chapter $chapter): void - { - $entity = $this->entityManager->find(ChapterEntity::class, $chapter->getId()); - - if (!$entity) { - throw new \RuntimeException(sprintf('Chapter with id %s not found', $chapter->getId())); - } - - $entity->setVisible($chapter->isVisible()); - $entity->setCbzPath($chapter->getCbzPath()); - $entity->setTitle($chapter->getTitle()); - $entity->setVolume($chapter->getVolume()); - - $this->entityManager->persist($entity); - $this->entityManager->flush(); - } - - public function delete(Chapter $chapter): void - { - $entity = $this->entityManager->find(ChapterEntity::class, $chapter->getId()); - - if ($entity) { - $this->entityManager->remove($entity); - $this->entityManager->flush(); - } - } - - public function findByMangaIdAndVolume(string $mangaId, int $volume): array - { - $entities = $this->entityManager->getRepository(ChapterEntity::class) - ->findBy(['manga' => $mangaId, 'volume' => $volume]); - - return array_map([$this, 'toDomainModel'], $entities); - } - - public function findVisibleByMangaIdAndVolume(string $mangaId, int $volume): array - { - $entities = $this->entityManager->getRepository(ChapterEntity::class) - ->findBy(['manga' => $mangaId, 'volume' => $volume, 'visible' => true]); - - return array_map([$this, 'toDomainModel'], $entities); - } - - public function findVisibleWithCbzByMangaIdAndVolume(string $mangaId, int $volume): array - { - $qb = $this->entityManager->createQueryBuilder() - ->select('c') - ->from(ChapterEntity::class, 'c') - ->where('c.manga = :mangaId') - ->andWhere('c.volume = :volume') - ->andWhere('c.visible = true') - ->andWhere('c.cbzPath IS NOT NULL') - ->setParameter('mangaId', $mangaId) - ->setParameter('volume', $volume) - ->orderBy('c.number', 'ASC'); - - $entities = $qb->getQuery()->getResult(); - - return array_map([$this, 'toDomainModel'], $entities); - } - - private function toDomainModel(ChapterEntity $entity): Chapter - { - return new Chapter( - new ChapterId((string) $entity->getId()), - (string) $entity->getManga()->getId(), - $entity->getNumber(), - $entity->getTitle(), - $entity->getVolume(), - $entity->isVisible(), - $entity->getCbzPath(), - new \DateTimeImmutable() - ); - } -} diff --git a/src/Domain/Manga/Infrastructure/Service/MangadxChapterSynchronizationService.php b/src/Domain/Manga/Infrastructure/Service/MangadxChapterSynchronizationService.php index 184dcfe..0748239 100644 --- a/src/Domain/Manga/Infrastructure/Service/MangadxChapterSynchronizationService.php +++ b/src/Domain/Manga/Infrastructure/Service/MangadxChapterSynchronizationService.php @@ -73,6 +73,7 @@ readonly class MangadxChapterSynchronizationService implements ChapterSynchroniz isset($chapterData['attributes']['volume']) ? (int) $chapterData['attributes']['volume'] : null, true, null, + 0, new \DateTimeImmutable() ); $chapterLanguages[(string) $chapterNumber] = $language; @@ -142,7 +143,8 @@ readonly class MangadxChapterSynchronizationService implements ChapterSynchroniz $currentChapter->getTitle(), null, // volume = null $currentChapter->isVisible(), - $currentChapter->getCbzPath(), + $currentChapter->getPagesDirectory(), + $currentChapter->getPageCount(), $currentChapter->getCreatedAt() ); } @@ -155,7 +157,8 @@ readonly class MangadxChapterSynchronizationService implements ChapterSynchroniz $currentChapter->getTitle(), $prevVolume, // prend le volume des adjacents $currentChapter->isVisible(), - $currentChapter->getCbzPath(), + $currentChapter->getPagesDirectory(), + $currentChapter->getPageCount(), $currentChapter->getCreatedAt() ); } @@ -209,7 +212,8 @@ readonly class MangadxChapterSynchronizationService implements ChapterSynchroniz $currentChapter->getTitle(), $prevVolume, $currentChapter->isVisible(), - $currentChapter->getCbzPath(), + $currentChapter->getPagesDirectory(), + $currentChapter->getPageCount(), $currentChapter->getCreatedAt() ); } @@ -222,7 +226,8 @@ readonly class MangadxChapterSynchronizationService implements ChapterSynchroniz $currentChapter->getTitle(), $nextVolume, $currentChapter->isVisible(), - $currentChapter->getCbzPath(), + $currentChapter->getPagesDirectory(), + $currentChapter->getPageCount(), $currentChapter->getCreatedAt() ); } diff --git a/src/Entity/Chapter.php b/src/Entity/Chapter.php index 9ea05c5..de4cfef 100644 --- a/src/Entity/Chapter.php +++ b/src/Entity/Chapter.php @@ -39,6 +39,12 @@ class Chapter #[ORM\Column(length: 255, nullable: true)] private ?string $cbzPath = null; + #[ORM\Column(length: 255, nullable: true)] + private ?string $pagesDirectory = null; + + #[ORM\Column(options: ['default' => 0])] + private int $pageCount = 0; + #[ORM\Column(type: 'boolean', options: ['default' => true])] private ?bool $visible = true; @@ -146,4 +152,28 @@ class Chapter return $this; } + + public function getPagesDirectory(): ?string + { + return $this->pagesDirectory; + } + + public function setPagesDirectory(?string $pagesDirectory): static + { + $this->pagesDirectory = $pagesDirectory; + + return $this; + } + + public function getPageCount(): int + { + return $this->pageCount; + } + + public function setPageCount(int $pageCount): static + { + $this->pageCount = $pageCount; + + return $this; + } } diff --git a/tests/Domain/Manga/Adapter/InMemoryChapterRepository.php b/tests/Domain/Manga/Adapter/InMemoryChapterRepository.php deleted file mode 100644 index 0674415..0000000 --- a/tests/Domain/Manga/Adapter/InMemoryChapterRepository.php +++ /dev/null @@ -1,95 +0,0 @@ - */ - private array $chapters = []; - - public function findById(string $id): ?Chapter - { - return $this->chapters[$id] ?? null; - } - - public function findVisibleById(string $id): ?Chapter - { - $chapter = $this->chapters[$id] ?? null; - if ($chapter && $chapter->isVisible()) { - return $chapter; - } - - return null; - } - - public function findByMangaIdAndChapterNumber(string $mangaId, float $chapterNumber): ?Chapter - { - foreach ($this->chapters as $chapter) { - if ($chapter->getMangaId() === $mangaId && $chapter->getNumber() === $chapterNumber) { - return $chapter; - } - } - - return null; - } - - public function save(Chapter $chapter): void - { - $this->chapters[$chapter->getId()] = $chapter; - } - - public function delete(Chapter $chapter): void - { - unset($this->chapters[$chapter->getId()]); - } - - public function findByMangaIdAndVolume(string $mangaId, int $volume): array - { - return array_filter( - $this->chapters, - fn (Chapter $chapter) => $chapter->getMangaId() === $mangaId && $chapter->getVolume() === $volume - ); - } - - public function findVisibleByMangaIdAndVolume(string $mangaId, int $volume): array - { - return array_filter( - $this->chapters, - fn (Chapter $chapter) => - $chapter->getMangaId() === $mangaId && - $chapter->getVolume() === $volume && - $chapter->isVisible() - ); - } - - public function findVisibleWithCbzByMangaIdAndVolume(string $mangaId, int $volume): array - { - return array_filter( - $this->chapters, - fn (Chapter $chapter) => - $chapter->getMangaId() === $mangaId && - $chapter->getVolume() === $volume && - $chapter->isVisible() && - $chapter->isAvailable() - ); - } - - /** - * Get all chapters - */ - public function getAll(): array - { - return array_values($this->chapters); - } - - /** - * Clear all chapters - */ - public function clear(): void - { - $this->chapters = []; - } -} diff --git a/tests/Domain/Manga/Adapter/InMemoryMangaRepository.php b/tests/Domain/Manga/Adapter/InMemoryMangaRepository.php index 8f31db7..aadc349 100644 --- a/tests/Domain/Manga/Adapter/InMemoryMangaRepository.php +++ b/tests/Domain/Manga/Adapter/InMemoryMangaRepository.php @@ -18,6 +18,9 @@ class InMemoryMangaRepository implements MangaRepositoryInterface /** @var array> */ private array $chapters = []; + /** @var array */ + private array $chaptersById = []; + /** @var array */ private array $savedChapters = []; @@ -101,12 +104,106 @@ class InMemoryMangaRepository implements MangaRepositoryInterface return count($this->chapters[$mangaId] ?? []); } + public function findChapterById(string $id): ?Chapter + { + return $this->chaptersById[$id] ?? null; + } + + public function findVisibleChapterById(string $id): ?Chapter + { + $chapter = $this->chaptersById[$id] ?? null; + if ($chapter && $chapter->isVisible()) { + return $chapter; + } + return null; + } + + public function findChapterByMangaIdAndNumber(string $mangaId, float $chapterNumber): ?Chapter + { + foreach ($this->chaptersById as $chapter) { + if ($chapter->getMangaId() === $mangaId && $chapter->getNumber() === $chapterNumber) { + return $chapter; + } + } + return null; + } + + public function saveChapter(Chapter $chapter): ChapterId + { + $this->savedChapters[] = $chapter; + if (!isset($this->chapters[$chapter->getMangaId()])) { + $this->chapters[$chapter->getMangaId()] = []; + } + $this->chapters[$chapter->getMangaId()][] = $chapter; + $this->chaptersById[$chapter->getId()] = $chapter; + return new ChapterId($chapter->getId()); + } + + public function updateChapter(Chapter $chapter): void + { + $this->chaptersById[$chapter->getId()] = $chapter; + + if (isset($this->chapters[$chapter->getMangaId()])) { + foreach ($this->chapters[$chapter->getMangaId()] as $key => $existing) { + if ($existing->getId() === $chapter->getId()) { + $this->chapters[$chapter->getMangaId()][$key] = $chapter; + return; + } + } + } + } + + public function deleteChapter(Chapter $chapter): void + { + unset($this->chaptersById[$chapter->getId()]); + + if (isset($this->chapters[$chapter->getMangaId()])) { + $this->chapters[$chapter->getMangaId()] = array_values( + array_filter( + $this->chapters[$chapter->getMangaId()], + fn (Chapter $c) => $c->getId() !== $chapter->getId() + ) + ); + } + } + + public function findChaptersByMangaIdAndVolume(string $mangaId, int $volume): array + { + return array_values(array_filter( + $this->chaptersById, + fn (Chapter $chapter) => $chapter->getMangaId() === $mangaId && $chapter->getVolume() === $volume + )); + } + + public function findVisibleChaptersByMangaIdAndVolume(string $mangaId, int $volume): array + { + return array_values(array_filter( + $this->chaptersById, + fn (Chapter $chapter) => + $chapter->getMangaId() === $mangaId && + $chapter->getVolume() === $volume && + $chapter->isVisible() + )); + } + + public function findVisibleChaptersWithPagesByMangaIdAndVolume(string $mangaId, int $volume): array + { + return array_values(array_filter( + $this->chaptersById, + fn (Chapter $chapter) => + $chapter->getMangaId() === $mangaId && + $chapter->getVolume() === $volume && + $chapter->isVisible() && + $chapter->isAvailable() + )); + } + public function addChaptersToManga(string $mangaId, int $count): void { $this->chapters[$mangaId] = []; for ($i = 1; $i <= $count; $i++) { - $this->chapters[$mangaId][] = new Chapter( + $chapter = new Chapter( id: new ChapterId((string)$i), mangaId: $mangaId, number: (float)$i, @@ -115,6 +212,8 @@ class InMemoryMangaRepository implements MangaRepositoryInterface isVisible: true, createdAt: new \DateTimeImmutable() ); + $this->chapters[$mangaId][] = $chapter; + $this->chaptersById[$chapter->getId()] = $chapter; } } @@ -128,16 +227,6 @@ class InMemoryMangaRepository implements MangaRepositoryInterface return null; } - public function saveChapter(Chapter $chapter): ChapterId - { - $this->savedChapters[] = $chapter; - if (!isset($this->chapters[$chapter->getMangaId()])) { - $this->chapters[$chapter->getMangaId()] = []; - } - $this->chapters[$chapter->getMangaId()][] = $chapter; - return new ChapterId($chapter->getId()); - } - /** @return array */ public function getSavedChapters(): array { @@ -148,6 +237,7 @@ class InMemoryMangaRepository implements MangaRepositoryInterface { $this->mangas = []; $this->chapters = []; + $this->chaptersById = []; $this->savedChapters = []; } @@ -161,7 +251,6 @@ class InMemoryMangaRepository implements MangaRepositoryInterface $manga->getDescription() ]; - // Ajouter les slugs alternatifs aux champs de recherche foreach ($manga->getAlternativeSlugs() as $altSlug) { $searchableFields[] = $altSlug; } @@ -186,6 +275,7 @@ class InMemoryMangaRepository implements MangaRepositoryInterface { return count($this->search($query, 1, PHP_INT_MAX)); } + public function findExistingChaptersByNumbers(string $mangaId, array $chapterNumbers): array { if (!isset($this->chapters[$mangaId])) { @@ -203,12 +293,10 @@ class InMemoryMangaRepository implements MangaRepositoryInterface return array_filter( array_values($this->mangas), function (Manga $manga) use ($criteria) { - // Vérifier si le monitoring est activé selon le critère if ($manga->getMonitoringStatus()->isEnabled() !== $criteria->enabled) { return false; } - // Vérifier la date de dernière vérification si spécifiée if ($criteria->lastCheckBefore !== null) { $lastCheck = $manga->getLastMonitoringCheck(); if ($lastCheck === null || $lastCheck >= $criteria->lastCheckBefore) { diff --git a/tests/Domain/Manga/Application/CommandHandler/ImportChapterHandlerTest.php b/tests/Domain/Manga/Application/CommandHandler/ImportChapterHandlerTest.php index 1e80133..1b0f169 100644 --- a/tests/Domain/Manga/Application/CommandHandler/ImportChapterHandlerTest.php +++ b/tests/Domain/Manga/Application/CommandHandler/ImportChapterHandlerTest.php @@ -12,7 +12,6 @@ use App\Domain\Manga\Domain\Model\ValueObject\ChapterId; use App\Domain\Manga\Domain\Model\ValueObject\MangaId; use App\Domain\Manga\Domain\Model\ValueObject\MangaSlug; use App\Domain\Manga\Domain\Model\ValueObject\MangaTitle; -use App\Tests\Domain\Manga\Adapter\InMemoryChapterRepository; use App\Tests\Domain\Manga\Adapter\InMemoryMangaRepository; use App\Tests\Domain\Manga\Adapter\InMemoryPathManager; use PHPUnit\Framework\TestCase; @@ -20,18 +19,15 @@ use PHPUnit\Framework\TestCase; class ImportChapterHandlerTest extends TestCase { private InMemoryMangaRepository $mangaRepository; - private InMemoryChapterRepository $chapterRepository; private InMemoryPathManager $pathManager; private ImportChapterHandler $handler; protected function setUp(): void { $this->mangaRepository = new InMemoryMangaRepository(); - $this->chapterRepository = new InMemoryChapterRepository(); $this->pathManager = new InMemoryPathManager(); $this->handler = new ImportChapterHandler( $this->mangaRepository, - $this->chapterRepository, $this->pathManager ); } @@ -66,7 +62,7 @@ class ImportChapterHandlerTest extends TestCase $this->handler->handle($command); } - public function test_it_updates_existing_chapter_with_new_cbz(): void + public function test_it_updates_existing_chapter_with_new_path(): void { // Arrange $mangaId = 'manga-123'; @@ -82,7 +78,7 @@ class ImportChapterHandlerTest extends TestCase ); $this->mangaRepository->save($manga); - // Create an existing chapter without CBZ + // Create an existing chapter without pages $existingChapter = new Chapter( new ChapterId('chapter-123'), $mangaId, @@ -92,7 +88,7 @@ class ImportChapterHandlerTest extends TestCase true, null ); - $this->chapterRepository->save($existingChapter); + $this->mangaRepository->saveChapter($existingChapter); // Import the same chapter with CBZ $cbzBinary = $this->createValidCbzBinary(); @@ -106,18 +102,16 @@ class ImportChapterHandlerTest extends TestCase $this->handler->handle($command); // Assert - $chapters = $this->chapterRepository->getAll(); - $this->assertCount(1, $chapters); // Still only one chapter - - $updatedChapter = $chapters[0]; + $updatedChapter = $this->mangaRepository->findChapterById('chapter-123'); + $this->assertNotNull($updatedChapter); $this->assertEquals('chapter-123', $updatedChapter->getId()); $this->assertEquals($mangaId, $updatedChapter->getMangaId()); $this->assertEquals(1.5, $updatedChapter->getNumber()); - $this->assertEquals('Chapter 1.5', $updatedChapter->getTitle()); // Title preserved - $this->assertEquals(1, $updatedChapter->getVolume()); // Volume preserved + $this->assertEquals('Chapter 1.5', $updatedChapter->getTitle()); + $this->assertEquals(1, $updatedChapter->getVolume()); $this->assertTrue($updatedChapter->isVisible()); - $this->assertTrue($updatedChapter->isAvailable()); // Now has CBZ - $this->assertStringContainsString('_vol1_ch1.5.cbz', $updatedChapter->getCbzPath()); + $this->assertTrue($updatedChapter->isAvailable()); + $this->assertNotNull($updatedChapter->getPagesDirectory()); } public function test_it_throws_exception_when_manga_not_found(): void @@ -168,24 +162,16 @@ class ImportChapterHandlerTest extends TestCase $this->handler->handle($command); } - /** - * Create a minimal valid CBZ (ZIP) binary for testing - */ private function createValidCbzBinary(): string { $tmpFile = tempnam(sys_get_temp_dir(), 'cbz'); - - // Delete the empty file created by tempnam unlink($tmpFile); $zip = new \ZipArchive(); - - // Create a new ZIP archive (avoid opening empty file) if ($zip->open($tmpFile, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) { throw new \RuntimeException('Cannot create test CBZ file'); } - // Add a dummy image file to the ZIP $zip->addFromString('image1.jpg', 'fake-image-data'); $zip->close(); diff --git a/tests/Domain/Manga/Application/CommandHandler/ImportVolumeHandlerTest.php b/tests/Domain/Manga/Application/CommandHandler/ImportVolumeHandlerTest.php index 4cf32f3..e86173d 100644 --- a/tests/Domain/Manga/Application/CommandHandler/ImportVolumeHandlerTest.php +++ b/tests/Domain/Manga/Application/CommandHandler/ImportVolumeHandlerTest.php @@ -11,7 +11,6 @@ use App\Domain\Manga\Domain\Model\ValueObject\ChapterId; use App\Domain\Manga\Domain\Model\ValueObject\MangaId; use App\Domain\Manga\Domain\Model\ValueObject\MangaSlug; use App\Domain\Manga\Domain\Model\ValueObject\MangaTitle; -use App\Tests\Domain\Manga\Adapter\InMemoryChapterRepository; use App\Tests\Domain\Manga\Adapter\InMemoryMangaRepository; use App\Tests\Domain\Manga\Adapter\InMemoryPathManager; use PHPUnit\Framework\TestCase; @@ -19,18 +18,15 @@ use PHPUnit\Framework\TestCase; class ImportVolumeHandlerTest extends TestCase { private InMemoryMangaRepository $mangaRepository; - private InMemoryChapterRepository $chapterRepository; private InMemoryPathManager $pathManager; private ImportVolumeHandler $handler; protected function setUp(): void { $this->mangaRepository = new InMemoryMangaRepository(); - $this->chapterRepository = new InMemoryChapterRepository(); $this->pathManager = new InMemoryPathManager(); $this->handler = new ImportVolumeHandler( $this->mangaRepository, - $this->chapterRepository, $this->pathManager ); } @@ -63,7 +59,7 @@ class ImportVolumeHandlerTest extends TestCase true, null ); - $this->chapterRepository->save($chapter); + $this->mangaRepository->saveChapter($chapter); } $cbzBinary = $this->createValidCbzBinary(); @@ -77,12 +73,12 @@ class ImportVolumeHandlerTest extends TestCase $this->handler->handle($command); // Assert - $chapters = $this->chapterRepository->findByMangaIdAndVolume($mangaId, $volumeNumber); + $chapters = $this->mangaRepository->findChaptersByMangaIdAndVolume($mangaId, $volumeNumber); $this->assertCount(3, $chapters); foreach ($chapters as $chapter) { $this->assertTrue($chapter->isAvailable()); - $this->assertStringContainsString('_vol' . $volumeNumber . '.cbz', $chapter->getCbzPath()); + $this->assertNotNull($chapter->getPagesDirectory()); } } @@ -153,7 +149,7 @@ class ImportVolumeHandlerTest extends TestCase $cbzBinary = $this->createValidCbzBinary(); $command = new ImportVolume( mangaId: $mangaId, - volumeNumber: 999, // Non-existent volume + volumeNumber: 999, fileBinary: $cbzBinary ); @@ -165,9 +161,6 @@ class ImportVolumeHandlerTest extends TestCase $this->handler->handle($command); } - /** - * Create a minimal valid CBZ (ZIP) binary for testing - */ private function createValidCbzBinary(): string { $tmpFile = tempnam(sys_get_temp_dir(), 'cbz_'); @@ -187,7 +180,3 @@ class ImportVolumeHandlerTest extends TestCase return $binaryContent; } } - - - - From a4b3d8a5f1be440ea1348e77c8a59647afcdf7ca Mon Sep 17 00:00:00 2001 From: "ext.jeremy.guillot@maxicoffee.domains" Date: Mon, 9 Mar 2026 18:07:34 +0100 Subject: [PATCH 2/6] test(manga): ajout test regression GET /api/mangas avec chapitres MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Détecte le crash EAGER loading Doctrine si la colonne pages_directory est absente de la table chapter (SQLSTATE 42703). Co-Authored-By: Claude Sonnet 4.6 --- tests/Feature/Manga/GetMangaListTest.php | 33 +++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/tests/Feature/Manga/GetMangaListTest.php b/tests/Feature/Manga/GetMangaListTest.php index 7acc48f..ad88458 100644 --- a/tests/Feature/Manga/GetMangaListTest.php +++ b/tests/Feature/Manga/GetMangaListTest.php @@ -3,12 +3,15 @@ namespace App\Tests\Feature\Manga; use App\Entity\Manga; +use App\Factory\ChapterFactory; +use App\Factory\MangaFactory; use App\Tests\Feature\AbstractApiTestCase; +use Zenstruck\Foundry\Test\Factories; use Zenstruck\Foundry\Test\ResetDatabase; class GetMangaListTest extends AbstractApiTestCase { - use ResetDatabase; + use ResetDatabase, Factories; public function testGetEmptyMangaList(): void { @@ -80,6 +83,34 @@ class GetMangaListTest extends AbstractApiTestCase $this->assertEquals('Manga C', $data['items'][2]['title']); } + public function testGetMangaListWithChapters(): void + { + // Given — manga with chapters triggers Doctrine EAGER loading of chapters collection + // (regression: colonne pages_directory doit exister sinon 500) + $manga = MangaFactory::createOne([ + 'title' => 'One Piece', + 'slug' => 'one-piece', + 'imageUrl' => 'https://example.com/image.jpg', + 'thumbnailUrl' => 'https://example.com/thumb.jpg', + ]); + ChapterFactory::createOne([ + 'manga' => $manga, + 'number' => 1.0, + 'title' => 'Romance Dawn', + 'volume' => 1, + 'visible' => true, + ]); + + // When + $client = static::createClient(); + $client->request('GET', '/api/mangas'); + + // Then — doit retourner 200 même avec des chapitres (eager loading OK) + $this->assertResponseIsSuccessful(); + $data = $client->getResponse()->toArray(); + $this->assertEquals(1, $data['total']); + } + private function createMangas(int $count): void { for ($i = 1; $i <= $count; $i++) { From 2c051351a8be3a01c5de2ec31a1a3e49a30d245a Mon Sep 17 00:00:00 2001 From: "ext.jeremy.guillot@maxicoffee.domains" Date: Mon, 9 Mar 2026 19:15:11 +0100 Subject: [PATCH 3/6] =?UTF-8?q?refactor(manga):=20Chapter=20entit=C3=A9=20?= =?UTF-8?q?DDD=20de=20Manga=20+=20AggregateRoot?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Ajoute AggregateRoot dans Shared (domain events + pull pattern) - Manga extends AggregateRoot, devient vrai aggregate root DDD - Chapter passe de readonly à entité mutable avec MangaId VO - Manga expose les méthodes domaine pour toute mutation de chapitre : addChapter, updateChapterTitle/Volume/Pages, hideChapter, removeChapterPages - Supprime saveChapter/updateChapter/deleteChapter de MangaRepositoryInterface - save(Manga) gère désormais la persistance des chapitres via pull pattern - Tous les handlers/listeners passent par l'agrégat (plus d'accès direct) - phparkitect autorise AggregateRoot dans les couches Domain Co-Authored-By: Claude Sonnet 4.6 --- phparkitect.php | 1 + .../CommandHandler/DeleteCbzHandler.php | 18 +--- .../CommandHandler/DeleteChapterHandler.php | 18 +--- .../EditMultipleChaptersHandler.php | 8 +- .../FetchMangaChaptersHandler.php | 1 + .../CommandHandler/ImportChapterHandler.php | 21 +---- .../CommandHandler/ImportVolumeHandler.php | 19 +--- .../ChapterImportedEventListener.php | 16 +--- .../VolumeImportedEventListener.php | 16 +--- .../Repository/MangaRepositoryInterface.php | 10 -- src/Domain/Manga/Domain/Model/Chapter.php | 46 ++++----- src/Domain/Manga/Domain/Model/Manga.php | 74 ++++++++++++++- .../Persistence/LegacyMangaRepository.php | 93 ++++++++----------- .../MangadxChapterSynchronizationService.php | 6 +- .../Shared/Domain/Model/AggregateRoot.php | 21 +++++ .../Manga/Adapter/InMemoryMangaRepository.php | 93 ++++++++----------- .../ImportChapterHandlerTest.php | 11 +-- .../ImportVolumeHandlerTest.php | 9 +- 18 files changed, 226 insertions(+), 255 deletions(-) create mode 100644 src/Domain/Shared/Domain/Model/AggregateRoot.php diff --git a/phparkitect.php b/phparkitect.php index f111c7c..6a696e4 100644 --- a/phparkitect.php +++ b/phparkitect.php @@ -23,6 +23,7 @@ return static function (Config $config): void { 'Symfony\Component\HttpKernel\Exception', 'Throwable', 'InvalidArgumentException', + 'App\Domain\Shared\Domain\Model\AggregateRoot', ]; // Dépendances externes autorisées diff --git a/src/Domain/Manga/Application/CommandHandler/DeleteCbzHandler.php b/src/Domain/Manga/Application/CommandHandler/DeleteCbzHandler.php index 4e4dc7e..5571fb6 100644 --- a/src/Domain/Manga/Application/CommandHandler/DeleteCbzHandler.php +++ b/src/Domain/Manga/Application/CommandHandler/DeleteCbzHandler.php @@ -7,8 +7,6 @@ use App\Domain\Manga\Domain\Contract\Repository\MangaRepositoryInterface; use App\Domain\Manga\Domain\Contract\Service\FileServiceInterface; use App\Domain\Manga\Domain\Exception\ChapterNotFoundException; use App\Domain\Manga\Domain\Exception\CbzFileNotFoundException; -use App\Domain\Manga\Domain\Model\Chapter; -use App\Domain\Manga\Domain\Model\ValueObject\ChapterId; use App\Domain\Shared\Domain\Contract\CommandHandlerInterface; use App\Domain\Shared\Domain\Contract\CommandInterface; @@ -33,18 +31,8 @@ readonly class DeleteCbzHandler implements CommandHandlerInterface throw new CbzFileNotFoundException($command->chapterId); } - $updatedChapter = new Chapter( - new ChapterId($chapter->getId()), - $chapter->getMangaId(), - $chapter->getNumber(), - $chapter->getTitle(), - $chapter->getVolume(), - $chapter->isVisible(), - null, - 0, - $chapter->getCreatedAt() - ); - - $this->mangaRepository->updateChapter($updatedChapter); + $manga = $this->mangaRepository->findById($chapter->getMangaId()->getValue()); + $manga->removeChapterPages($chapter); + $this->mangaRepository->save($manga); } } diff --git a/src/Domain/Manga/Application/CommandHandler/DeleteChapterHandler.php b/src/Domain/Manga/Application/CommandHandler/DeleteChapterHandler.php index 6cc8682..b67fa0c 100644 --- a/src/Domain/Manga/Application/CommandHandler/DeleteChapterHandler.php +++ b/src/Domain/Manga/Application/CommandHandler/DeleteChapterHandler.php @@ -5,8 +5,6 @@ namespace App\Domain\Manga\Application\CommandHandler; use App\Domain\Manga\Application\Command\DeleteChapter; use App\Domain\Manga\Domain\Contract\Repository\MangaRepositoryInterface; use App\Domain\Manga\Domain\Exception\ChapterNotFoundException; -use App\Domain\Manga\Domain\Model\Chapter; -use App\Domain\Manga\Domain\Model\ValueObject\ChapterId; use App\Domain\Shared\Domain\Contract\CommandHandlerInterface; use App\Domain\Shared\Domain\Contract\CommandInterface; @@ -26,18 +24,8 @@ readonly class DeleteChapterHandler implements CommandHandlerInterface throw new ChapterNotFoundException($command->chapterId); } - $updatedChapter = new Chapter( - id: new ChapterId($chapter->getId()), - mangaId: $chapter->getMangaId(), - number: $chapter->getNumber(), - title: $chapter->getTitle(), - volume: $chapter->getVolume(), - isVisible: false, - pagesDirectory: $chapter->getPagesDirectory(), - pageCount: $chapter->getPageCount(), - createdAt: $chapter->getCreatedAt() - ); - - $this->mangaRepository->updateChapter($updatedChapter); + $manga = $this->mangaRepository->findById($chapter->getMangaId()->getValue()); + $manga->hideChapter($chapter); + $this->mangaRepository->save($manga); } } diff --git a/src/Domain/Manga/Application/CommandHandler/EditMultipleChaptersHandler.php b/src/Domain/Manga/Application/CommandHandler/EditMultipleChaptersHandler.php index ea825e9..790e6ca 100644 --- a/src/Domain/Manga/Application/CommandHandler/EditMultipleChaptersHandler.php +++ b/src/Domain/Manga/Application/CommandHandler/EditMultipleChaptersHandler.php @@ -21,17 +21,17 @@ readonly class EditMultipleChaptersHandler throw new ChapterNotFoundException($chapterData->id); } - $updatedChapter = $chapter; + $manga = $this->mangaRepository->findById($chapter->getMangaId()->getValue()); if ($chapterData->title !== null) { - $updatedChapter = $updatedChapter->updateTitle($chapterData->title); + $manga->updateChapterTitle($chapter, $chapterData->title); } if ($chapterData->volume !== null) { - $updatedChapter = $updatedChapter->updateVolume($chapterData->volume); + $manga->updateChapterVolume($chapter, $chapterData->volume); } - $this->mangaRepository->updateChapter($updatedChapter); + $this->mangaRepository->save($manga); } } } diff --git a/src/Domain/Manga/Application/CommandHandler/FetchMangaChaptersHandler.php b/src/Domain/Manga/Application/CommandHandler/FetchMangaChaptersHandler.php index 2a12cc1..13d18e0 100644 --- a/src/Domain/Manga/Application/CommandHandler/FetchMangaChaptersHandler.php +++ b/src/Domain/Manga/Application/CommandHandler/FetchMangaChaptersHandler.php @@ -29,5 +29,6 @@ readonly class FetchMangaChaptersHandler // Synchronisation initiale (pas d'événements) $this->chapterSynchronizationService->synchronizeChapters($manga); + $this->mangaRepository->save($manga); } } diff --git a/src/Domain/Manga/Application/CommandHandler/ImportChapterHandler.php b/src/Domain/Manga/Application/CommandHandler/ImportChapterHandler.php index 07e9461..e75a0de 100644 --- a/src/Domain/Manga/Application/CommandHandler/ImportChapterHandler.php +++ b/src/Domain/Manga/Application/CommandHandler/ImportChapterHandler.php @@ -6,8 +6,6 @@ use App\Domain\Manga\Application\Command\ImportChapter; use App\Domain\Manga\Domain\Contract\Repository\MangaRepositoryInterface; use App\Domain\Manga\Domain\Exception\MangaNotFoundException; use App\Domain\Manga\Domain\Exception\ChapterNotFoundException; -use App\Domain\Manga\Domain\Model\Chapter; -use App\Domain\Manga\Domain\Model\ValueObject\ChapterId; use App\Domain\Shared\Domain\Contract\MangaPathManagerInterface; readonly class ImportChapterHandler @@ -43,20 +41,9 @@ readonly class ImportChapterHandler // 4. Save the CBZ file to storage using the path manager $cbzPath = $this->saveCbzFile($command, $manga, $existingChapter); - // 5. Update existing chapter with new path - // Note: pagesDirectory holds CBZ path during transition; Phase 3 will store individual images - $updatedChapter = new Chapter( - id: new ChapterId($existingChapter->getId()), - mangaId: $existingChapter->getMangaId(), - number: $existingChapter->getNumber(), - title: $existingChapter->getTitle(), - volume: $existingChapter->getVolume(), - isVisible: $existingChapter->isVisible(), - pagesDirectory: $cbzPath, - pageCount: $existingChapter->getPageCount(), - createdAt: $existingChapter->getCreatedAt() - ); - $this->mangaRepository->updateChapter($updatedChapter); + // 5. Update existing chapter with new path through the aggregate + $manga->updateChapterPages($existingChapter, $cbzPath, $existingChapter->getPageCount()); + $this->mangaRepository->save($manga); } private function isValidCbzFile(string $fileBinary): bool @@ -66,7 +53,7 @@ readonly class ImportChapterHandler return strpos($fileBinary, $zipMagicNumber) === 0; } - private function saveCbzFile(ImportChapter $command, \App\Domain\Manga\Domain\Model\Manga $manga, Chapter $chapter): string + private function saveCbzFile(ImportChapter $command, \App\Domain\Manga\Domain\Model\Manga $manga, \App\Domain\Manga\Domain\Model\Chapter $chapter): string { $volumeNumber = $chapter->getVolume() ?? 0; $cbzPath = $this->pathManager->buildChapterCbzPath( diff --git a/src/Domain/Manga/Application/CommandHandler/ImportVolumeHandler.php b/src/Domain/Manga/Application/CommandHandler/ImportVolumeHandler.php index 882397d..65cf3c5 100644 --- a/src/Domain/Manga/Application/CommandHandler/ImportVolumeHandler.php +++ b/src/Domain/Manga/Application/CommandHandler/ImportVolumeHandler.php @@ -5,8 +5,6 @@ namespace App\Domain\Manga\Application\CommandHandler; use App\Domain\Manga\Application\Command\ImportVolume; use App\Domain\Manga\Domain\Contract\Repository\MangaRepositoryInterface; use App\Domain\Manga\Domain\Exception\MangaNotFoundException; -use App\Domain\Manga\Domain\Model\Chapter; -use App\Domain\Manga\Domain\Model\ValueObject\ChapterId; use App\Domain\Shared\Domain\Contract\MangaPathManagerInterface; readonly class ImportVolumeHandler @@ -44,22 +42,11 @@ readonly class ImportVolumeHandler // 4. Save the CBZ file to storage using the path manager $cbzPath = $this->saveCbzFile($command, $manga); - // 5. Update all chapters with the volume path - // Note: pagesDirectory holds CBZ path during transition; Phase 3 will store individual images + // 5. Update all chapters with the volume path through the aggregate foreach ($chapters as $chapter) { - $updatedChapter = new Chapter( - id: new ChapterId($chapter->getId()), - mangaId: $chapter->getMangaId(), - number: $chapter->getNumber(), - title: $chapter->getTitle(), - volume: $chapter->getVolume(), - isVisible: $chapter->isVisible(), - pagesDirectory: $cbzPath, - pageCount: $chapter->getPageCount(), - createdAt: $chapter->getCreatedAt() - ); - $this->mangaRepository->updateChapter($updatedChapter); + $manga->updateChapterPages($chapter, $cbzPath, $chapter->getPageCount()); } + $this->mangaRepository->save($manga); } private function isValidCbzFile(string $fileBinary): bool diff --git a/src/Domain/Manga/Application/EventListener/ChapterImportedEventListener.php b/src/Domain/Manga/Application/EventListener/ChapterImportedEventListener.php index 3b43b10..e76c4da 100644 --- a/src/Domain/Manga/Application/EventListener/ChapterImportedEventListener.php +++ b/src/Domain/Manga/Application/EventListener/ChapterImportedEventListener.php @@ -5,8 +5,6 @@ declare(strict_types=1); namespace App\Domain\Manga\Application\EventListener; use App\Domain\Manga\Domain\Contract\Repository\MangaRepositoryInterface; -use App\Domain\Manga\Domain\Model\Chapter; -use App\Domain\Manga\Domain\Model\ValueObject\ChapterId; use App\Domain\Manga\Domain\Model\ValueObject\MangaSlug; use App\Domain\Shared\Domain\Event\ChapterImported; @@ -26,18 +24,8 @@ readonly class ChapterImportedEventListener $chapters = $this->mangaRepository->findVisibleChaptersByMangaIdAndVolume($manga->getId()->getValue(), (int) $event->volume); foreach ($chapters as $chapter) { if ($chapter->getNumber() === (float) $event->chapterNumber) { - $updated = new Chapter( - new ChapterId($chapter->getId()), - $chapter->getMangaId(), - $chapter->getNumber(), - $chapter->getTitle(), - $chapter->getVolume(), - $chapter->isVisible(), - $event->cbzPath, - $chapter->getPageCount(), - $chapter->getCreatedAt(), - ); - $this->mangaRepository->updateChapter($updated); + $manga->updateChapterPages($chapter, $event->cbzPath, $chapter->getPageCount()); + $this->mangaRepository->save($manga); break; } } diff --git a/src/Domain/Manga/Application/EventListener/VolumeImportedEventListener.php b/src/Domain/Manga/Application/EventListener/VolumeImportedEventListener.php index e3bbe68..79bb1f4 100644 --- a/src/Domain/Manga/Application/EventListener/VolumeImportedEventListener.php +++ b/src/Domain/Manga/Application/EventListener/VolumeImportedEventListener.php @@ -5,8 +5,6 @@ declare(strict_types=1); namespace App\Domain\Manga\Application\EventListener; use App\Domain\Manga\Domain\Contract\Repository\MangaRepositoryInterface; -use App\Domain\Manga\Domain\Model\Chapter; -use App\Domain\Manga\Domain\Model\ValueObject\ChapterId; use App\Domain\Manga\Domain\Model\ValueObject\MangaSlug; use App\Domain\Shared\Domain\Event\VolumeImported; @@ -29,18 +27,8 @@ readonly class VolumeImportedEventListener } foreach ($chapters as $chapter) { - $updated = new Chapter( - new ChapterId($chapter->getId()), - $chapter->getMangaId(), - $chapter->getNumber(), - $chapter->getTitle(), - $chapter->getVolume(), - $chapter->isVisible(), - $event->cbzPath, - $chapter->getPageCount(), - $chapter->getCreatedAt(), - ); - $this->mangaRepository->updateChapter($updated); + $manga->updateChapterPages($chapter, $event->cbzPath, $chapter->getPageCount()); } + $this->mangaRepository->save($manga); } } diff --git a/src/Domain/Manga/Domain/Contract/Repository/MangaRepositoryInterface.php b/src/Domain/Manga/Domain/Contract/Repository/MangaRepositoryInterface.php index 216342a..e404043 100644 --- a/src/Domain/Manga/Domain/Contract/Repository/MangaRepositoryInterface.php +++ b/src/Domain/Manga/Domain/Contract/Repository/MangaRepositoryInterface.php @@ -6,7 +6,6 @@ use App\Domain\Manga\Application\Query\MonitoringCriteria; use App\Domain\Manga\Domain\Model\Manga; use App\Domain\Manga\Domain\Model\Chapter; use App\Domain\Manga\Domain\Model\ValueObject\ExternalId; -use App\Domain\Manga\Domain\Model\ValueObject\ChapterId; use App\Domain\Manga\Domain\Model\ValueObject\MangaSlug; interface MangaRepositoryInterface @@ -57,13 +56,4 @@ interface MangaRepositoryInterface */ public function findVisibleChaptersWithPagesByMangaIdAndVolume(string $mangaId, int $volume): array; - // --- Chapters (write) --- - - /** Create a new chapter and return its generated ID. */ - public function saveChapter(Chapter $chapter): ChapterId; - - /** Update an existing chapter. */ - public function updateChapter(Chapter $chapter): void; - - public function deleteChapter(Chapter $chapter): void; } diff --git a/src/Domain/Manga/Domain/Model/Chapter.php b/src/Domain/Manga/Domain/Model/Chapter.php index c44fa75..57aed25 100644 --- a/src/Domain/Manga/Domain/Model/Chapter.php +++ b/src/Domain/Manga/Domain/Model/Chapter.php @@ -3,12 +3,13 @@ namespace App\Domain\Manga\Domain\Model; use App\Domain\Manga\Domain\Model\ValueObject\ChapterId; +use App\Domain\Manga\Domain\Model\ValueObject\MangaId; -readonly class Chapter +class Chapter { public function __construct( private ChapterId $id, - private string $mangaId, + private MangaId $mangaId, private float $number, private ?string $title, private ?int $volume, @@ -23,7 +24,7 @@ readonly class Chapter return $this->id->getValue(); } - public function getMangaId(): string + public function getMangaId(): MangaId { return $this->mangaId; } @@ -68,33 +69,24 @@ readonly class Chapter return $this->createdAt; } - public function updateTitle(?string $title): self + public function updateTitle(?string $title): void { - return new self( - $this->id, - $this->mangaId, - $this->number, - $title, - $this->volume, - $this->isVisible, - $this->pagesDirectory, - $this->pageCount, - $this->createdAt - ); + $this->title = $title; } - public function updateVolume(?int $volume): self + public function updateVolume(?int $volume): void { - return new self( - $this->id, - $this->mangaId, - $this->number, - $this->title, - $volume, - $this->isVisible, - $this->pagesDirectory, - $this->pageCount, - $this->createdAt - ); + $this->volume = $volume; + } + + public function updatePagesDirectory(?string $pagesDirectory, int $pageCount = 0): void + { + $this->pagesDirectory = $pagesDirectory; + $this->pageCount = $pageCount; + } + + public function hide(): void + { + $this->isVisible = false; } } diff --git a/src/Domain/Manga/Domain/Model/Manga.php b/src/Domain/Manga/Domain/Model/Manga.php index 169c0ec..00a6cd5 100644 --- a/src/Domain/Manga/Domain/Model/Manga.php +++ b/src/Domain/Manga/Domain/Model/Manga.php @@ -8,10 +8,20 @@ use App\Domain\Manga\Domain\Model\ValueObject\MangaId; use App\Domain\Manga\Domain\Model\ValueObject\MangaSlug; use App\Domain\Manga\Domain\Model\ValueObject\MangaTitle; use App\Domain\Manga\Domain\Model\ValueObject\MonitoringStatus; +use App\Domain\Shared\Domain\Model\AggregateRoot; use DateTimeImmutable; -final class Manga +final class Manga extends AggregateRoot { + /** @var Chapter[] */ + private array $newChapters = []; + + /** @var array */ + private array $modifiedChapters = []; + + /** @var Chapter[] */ + private array $chaptersToDelete = []; + public function __construct( private MangaId $id, private MangaTitle $title, @@ -189,4 +199,66 @@ final class Manga { $this->lastMonitoringCheck = $lastMonitoringCheck; } + + public function addChapter(Chapter $chapter): void + { + $this->newChapters[] = $chapter; + } + + public function updateChapterTitle(Chapter $chapter, ?string $title): void + { + $chapter->updateTitle($title); + $this->modifiedChapters[$chapter->getId()] = $chapter; + } + + public function updateChapterVolume(Chapter $chapter, ?int $volume): void + { + $chapter->updateVolume($volume); + $this->modifiedChapters[$chapter->getId()] = $chapter; + } + + public function updateChapterPages(Chapter $chapter, ?string $pagesDirectory, int $pageCount = 0): void + { + $chapter->updatePagesDirectory($pagesDirectory, $pageCount); + $this->modifiedChapters[$chapter->getId()] = $chapter; + } + + public function hideChapter(Chapter $chapter): void + { + $chapter->hide(); + $this->modifiedChapters[$chapter->getId()] = $chapter; + } + + public function removeChapterPages(Chapter $chapter): void + { + $chapter->updatePagesDirectory(null, 0); + $this->modifiedChapters[$chapter->getId()] = $chapter; + } + + /** @return Chapter[] */ + public function pullNewChapters(): array + { + $chapters = $this->newChapters; + $this->newChapters = []; + + return $chapters; + } + + /** @return Chapter[] */ + public function pullModifiedChapters(): array + { + $chapters = array_values($this->modifiedChapters); + $this->modifiedChapters = []; + + return $chapters; + } + + /** @return Chapter[] */ + public function pullChaptersToDelete(): array + { + $chapters = $this->chaptersToDelete; + $this->chaptersToDelete = []; + + return $chapters; + } } diff --git a/src/Domain/Manga/Infrastructure/Persistence/LegacyMangaRepository.php b/src/Domain/Manga/Infrastructure/Persistence/LegacyMangaRepository.php index 44c5710..0023bed 100644 --- a/src/Domain/Manga/Infrastructure/Persistence/LegacyMangaRepository.php +++ b/src/Domain/Manga/Infrastructure/Persistence/LegacyMangaRepository.php @@ -115,6 +115,44 @@ readonly class LegacyMangaRepository implements MangaRepositoryInterface if ($entity->getId()) { $manga->updateId(new MangaId((string) $entity->getId())); } + + // Handle new chapters added through the aggregate + foreach ($manga->pullNewChapters() as $chapter) { + $mangaEntity = $this->entityManager->find(EntityManga::class, (int) $manga->getId()->getValue()); + $chapterEntity = new EntityChapter(); + $chapterEntity->setManga($mangaEntity) + ->setNumber($chapter->getNumber()) + ->setTitle($chapter->getTitle()) + ->setVolume($chapter->getVolume()) + ->setVisible($chapter->isVisible()) + ->setPagesDirectory($chapter->getPagesDirectory()) + ->setPageCount($chapter->getPageCount()); + $this->entityManager->persist($chapterEntity); + } + + // Handle chapters modified through the aggregate + foreach ($manga->pullModifiedChapters() as $chapter) { + $chapterEntity = $this->entityManager->find(EntityChapter::class, $chapter->getId()); + if ($chapterEntity) { + $chapterEntity->setVisible($chapter->isVisible()) + ->setPagesDirectory($chapter->getPagesDirectory()) + ->setPageCount($chapter->getPageCount()) + ->setTitle($chapter->getTitle()) + ->setVolume($chapter->getVolume()) + ->setCbzPath($chapter->getPagesDirectory()); + $this->entityManager->persist($chapterEntity); + } + } + + // Handle chapters deleted through the aggregate + foreach ($manga->pullChaptersToDelete() as $chapter) { + $chapterEntity = $this->entityManager->find(EntityChapter::class, $chapter->getId()); + if ($chapterEntity) { + $this->entityManager->remove($chapterEntity); + } + } + + $this->entityManager->flush(); } public function delete(DomainManga $manga): void @@ -166,29 +204,6 @@ readonly class LegacyMangaRepository implements MangaRepositoryInterface return $entity ? $this->toDomain($entity) : null; } - public function saveChapter(Chapter $chapter): ChapterId - { - $manga = $this->entityManager->find(EntityManga::class, $chapter->getMangaId()); - - if (!$manga) { - throw new \RuntimeException('Manga not found'); - } - - $entity = new EntityChapter(); - $entity->setManga($manga) - ->setNumber($chapter->getNumber()) - ->setTitle($chapter->getTitle()) - ->setVolume($chapter->getVolume()) - ->setVisible($chapter->isVisible()) - ->setPagesDirectory($chapter->getPagesDirectory()) - ->setPageCount($chapter->getPageCount()); - - $this->entityManager->persist($entity); - $this->entityManager->flush(); - - return new ChapterId((string) $entity->getId()); - } - public function findChapterById(string $id): ?Chapter { $entity = $this->entityManager->find(EntityChapter::class, $id); @@ -226,36 +241,6 @@ readonly class LegacyMangaRepository implements MangaRepositoryInterface return $entity ? $this->toChapterDomain($entity) : null; } - public function updateChapter(Chapter $chapter): void - { - $entity = $this->entityManager->find(EntityChapter::class, $chapter->getId()); - - if (!$entity) { - throw new \RuntimeException(sprintf('Chapter with id %s not found', $chapter->getId())); - } - - $entity->setVisible($chapter->isVisible()) - ->setPagesDirectory($chapter->getPagesDirectory()) - ->setPageCount($chapter->getPageCount()) - ->setTitle($chapter->getTitle()) - ->setVolume($chapter->getVolume()) - // Keep cbzPath in sync during transition (Phase 4 will drop this column) - ->setCbzPath($chapter->getPagesDirectory()); - - $this->entityManager->persist($entity); - $this->entityManager->flush(); - } - - public function deleteChapter(Chapter $chapter): void - { - $entity = $this->entityManager->find(EntityChapter::class, $chapter->getId()); - - if ($entity) { - $this->entityManager->remove($entity); - $this->entityManager->flush(); - } - } - public function findChaptersByMangaIdAndVolume(string $mangaId, int $volume): array { $entities = $this->entityManager->getRepository(EntityChapter::class) @@ -417,7 +402,7 @@ readonly class LegacyMangaRepository implements MangaRepositoryInterface { return new Chapter( id: new ChapterId((string) $entity->getId()), - mangaId: (string) $entity->getManga()->getId(), + mangaId: new MangaId((string) $entity->getManga()->getId()), number: $entity->getNumber(), title: $entity->getTitle(), volume: $entity->getVolume(), diff --git a/src/Domain/Manga/Infrastructure/Service/MangadxChapterSynchronizationService.php b/src/Domain/Manga/Infrastructure/Service/MangadxChapterSynchronizationService.php index 0748239..6a496f3 100644 --- a/src/Domain/Manga/Infrastructure/Service/MangadxChapterSynchronizationService.php +++ b/src/Domain/Manga/Infrastructure/Service/MangadxChapterSynchronizationService.php @@ -67,7 +67,7 @@ readonly class MangadxChapterSynchronizationService implements ChapterSynchroniz if ($shouldReplaceChapter) { $chaptersByNumber[(string) $chapterNumber] = new Chapter( new ChapterId((string) Uuid::uuid4()), - $manga->getId()->getValue(), + $manga->getId(), $chapterNumber, $title, isset($chapterData['attributes']['volume']) ? (int) $chapterData['attributes']['volume'] : null, @@ -98,8 +98,8 @@ readonly class MangadxChapterSynchronizationService implements ChapterSynchroniz // Sauvegarde uniquement les nouveaux chapitres et collecte leurs IDs foreach ($chaptersByNumber as $chapterNumber => $chapter) { if (!isset($existingChapters[(float) $chapterNumber])) { - $newChapterId = $this->mangaRepository->saveChapter($chapter); - $newChapterIds[] = $newChapterId->getValue(); // ✨ Collecte des IDs + $manga->addChapter($chapter); + $newChapterIds[] = $chapter->getId(); } } diff --git a/src/Domain/Shared/Domain/Model/AggregateRoot.php b/src/Domain/Shared/Domain/Model/AggregateRoot.php new file mode 100644 index 0000000..24ddf97 --- /dev/null +++ b/src/Domain/Shared/Domain/Model/AggregateRoot.php @@ -0,0 +1,21 @@ +domainEvents[] = $event; + } + + public function pullDomainEvents(): array + { + $events = $this->domainEvents; + $this->domainEvents = []; + + return $events; + } +} diff --git a/tests/Domain/Manga/Adapter/InMemoryMangaRepository.php b/tests/Domain/Manga/Adapter/InMemoryMangaRepository.php index aadc349..0745ef2 100644 --- a/tests/Domain/Manga/Adapter/InMemoryMangaRepository.php +++ b/tests/Domain/Manga/Adapter/InMemoryMangaRepository.php @@ -8,6 +8,7 @@ use App\Domain\Manga\Domain\Model\Chapter; use App\Domain\Manga\Domain\Model\Manga; use App\Domain\Manga\Domain\Model\ValueObject\ChapterId; use App\Domain\Manga\Domain\Model\ValueObject\ExternalId; +use App\Domain\Manga\Domain\Model\ValueObject\MangaId; use App\Domain\Manga\Domain\Model\ValueObject\MangaSlug; class InMemoryMangaRepository implements MangaRepositoryInterface @@ -21,9 +22,6 @@ class InMemoryMangaRepository implements MangaRepositoryInterface /** @var array */ private array $chaptersById = []; - /** @var array */ - private array $savedChapters = []; - public function findAll(int $page = 1, int $limit = 20, string $sortBy = 'title', string $sortOrder = 'asc'): array { $sortedMangas = array_values($this->mangas); @@ -65,6 +63,39 @@ class InMemoryMangaRepository implements MangaRepositoryInterface public function save(Manga $manga): void { $this->mangas[$manga->getId()->getValue()] = $manga; + + foreach ($manga->pullNewChapters() as $chapter) { + $mangaIdValue = $chapter->getMangaId()->getValue(); + if (!isset($this->chapters[$mangaIdValue])) { + $this->chapters[$mangaIdValue] = []; + } + $this->chapters[$mangaIdValue][] = $chapter; + $this->chaptersById[$chapter->getId()] = $chapter; + } + + foreach ($manga->pullModifiedChapters() as $chapter) { + $this->chaptersById[$chapter->getId()] = $chapter; + $mangaIdValue = $chapter->getMangaId()->getValue(); + if (isset($this->chapters[$mangaIdValue])) { + foreach ($this->chapters[$mangaIdValue] as $key => $existing) { + if ($existing->getId() === $chapter->getId()) { + $this->chapters[$mangaIdValue][$key] = $chapter; + break; + } + } + } + } + + foreach ($manga->pullChaptersToDelete() as $chapter) { + unset($this->chaptersById[$chapter->getId()]); + $mangaIdValue = $chapter->getMangaId()->getValue(); + if (isset($this->chapters[$mangaIdValue])) { + $this->chapters[$mangaIdValue] = array_values(array_filter( + $this->chapters[$mangaIdValue], + fn (Chapter $c) => $c->getId() !== $chapter->getId() + )); + } + } } public function delete(Manga $manga): void @@ -121,57 +152,18 @@ class InMemoryMangaRepository implements MangaRepositoryInterface public function findChapterByMangaIdAndNumber(string $mangaId, float $chapterNumber): ?Chapter { foreach ($this->chaptersById as $chapter) { - if ($chapter->getMangaId() === $mangaId && $chapter->getNumber() === $chapterNumber) { + if ($chapter->getMangaId()->getValue() === $mangaId && $chapter->getNumber() === $chapterNumber) { return $chapter; } } return null; } - public function saveChapter(Chapter $chapter): ChapterId - { - $this->savedChapters[] = $chapter; - if (!isset($this->chapters[$chapter->getMangaId()])) { - $this->chapters[$chapter->getMangaId()] = []; - } - $this->chapters[$chapter->getMangaId()][] = $chapter; - $this->chaptersById[$chapter->getId()] = $chapter; - return new ChapterId($chapter->getId()); - } - - public function updateChapter(Chapter $chapter): void - { - $this->chaptersById[$chapter->getId()] = $chapter; - - if (isset($this->chapters[$chapter->getMangaId()])) { - foreach ($this->chapters[$chapter->getMangaId()] as $key => $existing) { - if ($existing->getId() === $chapter->getId()) { - $this->chapters[$chapter->getMangaId()][$key] = $chapter; - return; - } - } - } - } - - public function deleteChapter(Chapter $chapter): void - { - unset($this->chaptersById[$chapter->getId()]); - - if (isset($this->chapters[$chapter->getMangaId()])) { - $this->chapters[$chapter->getMangaId()] = array_values( - array_filter( - $this->chapters[$chapter->getMangaId()], - fn (Chapter $c) => $c->getId() !== $chapter->getId() - ) - ); - } - } - public function findChaptersByMangaIdAndVolume(string $mangaId, int $volume): array { return array_values(array_filter( $this->chaptersById, - fn (Chapter $chapter) => $chapter->getMangaId() === $mangaId && $chapter->getVolume() === $volume + fn (Chapter $chapter) => $chapter->getMangaId()->getValue() === $mangaId && $chapter->getVolume() === $volume )); } @@ -180,7 +172,7 @@ class InMemoryMangaRepository implements MangaRepositoryInterface return array_values(array_filter( $this->chaptersById, fn (Chapter $chapter) => - $chapter->getMangaId() === $mangaId && + $chapter->getMangaId()->getValue() === $mangaId && $chapter->getVolume() === $volume && $chapter->isVisible() )); @@ -191,7 +183,7 @@ class InMemoryMangaRepository implements MangaRepositoryInterface return array_values(array_filter( $this->chaptersById, fn (Chapter $chapter) => - $chapter->getMangaId() === $mangaId && + $chapter->getMangaId()->getValue() === $mangaId && $chapter->getVolume() === $volume && $chapter->isVisible() && $chapter->isAvailable() @@ -205,7 +197,7 @@ class InMemoryMangaRepository implements MangaRepositoryInterface for ($i = 1; $i <= $count; $i++) { $chapter = new Chapter( id: new ChapterId((string)$i), - mangaId: $mangaId, + mangaId: new MangaId($mangaId), number: (float)$i, title: "Chapter $i", volume: (int)ceil($i / 10), @@ -227,18 +219,11 @@ class InMemoryMangaRepository implements MangaRepositoryInterface return null; } - /** @return array */ - public function getSavedChapters(): array - { - return $this->savedChapters; - } - public function clear(): void { $this->mangas = []; $this->chapters = []; $this->chaptersById = []; - $this->savedChapters = []; } public function search(string $query, int $page = 1, int $limit = 20): array diff --git a/tests/Domain/Manga/Application/CommandHandler/ImportChapterHandlerTest.php b/tests/Domain/Manga/Application/CommandHandler/ImportChapterHandlerTest.php index 1b0f169..0dd29bf 100644 --- a/tests/Domain/Manga/Application/CommandHandler/ImportChapterHandlerTest.php +++ b/tests/Domain/Manga/Application/CommandHandler/ImportChapterHandlerTest.php @@ -76,19 +76,18 @@ class ImportChapterHandlerTest extends TestCase ['action', 'adventure'], 'ongoing' ); - $this->mangaRepository->save($manga); - - // Create an existing chapter without pages + // Create an existing chapter without pages and add through the aggregate $existingChapter = new Chapter( new ChapterId('chapter-123'), - $mangaId, + new MangaId($mangaId), 1.5, 'Chapter 1.5', 1, true, null ); - $this->mangaRepository->saveChapter($existingChapter); + $manga->addChapter($existingChapter); + $this->mangaRepository->save($manga); // Import the same chapter with CBZ $cbzBinary = $this->createValidCbzBinary(); @@ -105,7 +104,7 @@ class ImportChapterHandlerTest extends TestCase $updatedChapter = $this->mangaRepository->findChapterById('chapter-123'); $this->assertNotNull($updatedChapter); $this->assertEquals('chapter-123', $updatedChapter->getId()); - $this->assertEquals($mangaId, $updatedChapter->getMangaId()); + $this->assertEquals($mangaId, $updatedChapter->getMangaId()->getValue()); $this->assertEquals(1.5, $updatedChapter->getNumber()); $this->assertEquals('Chapter 1.5', $updatedChapter->getTitle()); $this->assertEquals(1, $updatedChapter->getVolume()); diff --git a/tests/Domain/Manga/Application/CommandHandler/ImportVolumeHandlerTest.php b/tests/Domain/Manga/Application/CommandHandler/ImportVolumeHandlerTest.php index e86173d..fd86b7e 100644 --- a/tests/Domain/Manga/Application/CommandHandler/ImportVolumeHandlerTest.php +++ b/tests/Domain/Manga/Application/CommandHandler/ImportVolumeHandlerTest.php @@ -46,21 +46,20 @@ class ImportVolumeHandlerTest extends TestCase ['action', 'adventure'], 'ongoing' ); - $this->mangaRepository->save($manga); - - // Create chapters in volume 1 + // Create chapters in volume 1 and add through the aggregate for ($i = 1; $i <= 3; $i++) { $chapter = new Chapter( new ChapterId("chapter-$i"), - $mangaId, + new MangaId($mangaId), (float)$i, "Chapter $i", $volumeNumber, true, null ); - $this->mangaRepository->saveChapter($chapter); + $manga->addChapter($chapter); } + $this->mangaRepository->save($manga); $cbzBinary = $this->createValidCbzBinary(); $command = new ImportVolume( From ff451855a75735b2a0debfb5cda8e488f2bde3e6 Mon Sep 17 00:00:00 2001 From: "ext.jeremy.guillot@maxicoffee.domains" Date: Mon, 9 Mar 2026 19:16:26 +0100 Subject: [PATCH 4/6] fix(manga): ChapterResponse.createdAt en string RFC3339 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ChapterResponse expose createdAt comme string formatée (RFC3339) - GetMangaChaptersHandler formate la date à la construction du DTO - GetMangaChaptersStateProvider adapté en conséquence Co-Authored-By: Claude Sonnet 4.6 --- .../Manga/Application/QueryHandler/GetMangaChaptersHandler.php | 2 +- src/Domain/Manga/Application/Response/ChapterResponse.php | 2 +- .../State/Provider/GetMangaChaptersStateProvider.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Domain/Manga/Application/QueryHandler/GetMangaChaptersHandler.php b/src/Domain/Manga/Application/QueryHandler/GetMangaChaptersHandler.php index b335a7b..b2e9e1d 100644 --- a/src/Domain/Manga/Application/QueryHandler/GetMangaChaptersHandler.php +++ b/src/Domain/Manga/Application/QueryHandler/GetMangaChaptersHandler.php @@ -39,7 +39,7 @@ readonly class GetMangaChaptersHandler volume: $chapter->getVolume(), isVisible: $chapter->isVisible(), pagesDirectory: $chapter->getPagesDirectory(), - createdAt: $chapter->getCreatedAt() + createdAt: $chapter->getCreatedAt()->format(\DateTimeInterface::RFC3339) ), $chapters ), diff --git a/src/Domain/Manga/Application/Response/ChapterResponse.php b/src/Domain/Manga/Application/Response/ChapterResponse.php index 364a414..5ed392c 100644 --- a/src/Domain/Manga/Application/Response/ChapterResponse.php +++ b/src/Domain/Manga/Application/Response/ChapterResponse.php @@ -11,6 +11,6 @@ readonly class ChapterResponse public ?int $volume, public bool $isVisible, public ?string $pagesDirectory, - public \DateTimeImmutable $createdAt + public string $createdAt ) {} } diff --git a/src/Domain/Manga/Infrastructure/ApiPlatform/State/Provider/GetMangaChaptersStateProvider.php b/src/Domain/Manga/Infrastructure/ApiPlatform/State/Provider/GetMangaChaptersStateProvider.php index 3987fa3..94d32cf 100644 --- a/src/Domain/Manga/Infrastructure/ApiPlatform/State/Provider/GetMangaChaptersStateProvider.php +++ b/src/Domain/Manga/Infrastructure/ApiPlatform/State/Provider/GetMangaChaptersStateProvider.php @@ -53,7 +53,7 @@ readonly class GetMangaChaptersStateProvider implements ProviderInterface volume: $chapter->volume, isVisible: $chapter->isVisible, isAvailable: $chapter->pagesDirectory !== null, - createdAt: $chapter->createdAt->format(\DateTimeInterface::RFC3339) + createdAt: $chapter->createdAt ); } } \ No newline at end of file From b52b27189daa7c84d023d99acd2eb4a97922281d Mon Sep 17 00:00:00 2001 From: "ext.jeremy.guillot@maxicoffee.domains" Date: Mon, 9 Mar 2026 19:17:12 +0100 Subject: [PATCH 5/6] =?UTF-8?q?docs(claude):=20mise=20=C3=A0=20jour=20skil?= =?UTF-8?q?l=20testing-strategy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- .claude/skills/testing-strategy/SKILL.md | 1 - 1 file changed, 1 deletion(-) diff --git a/.claude/skills/testing-strategy/SKILL.md b/.claude/skills/testing-strategy/SKILL.md index 1d5e967..caca9e6 100644 --- a/.claude/skills/testing-strategy/SKILL.md +++ b/.claude/skills/testing-strategy/SKILL.md @@ -178,7 +178,6 @@ class InMemoryMangaRepository implements MangaRepositoryInterface | Adapter | Interface implémentée | |----------------------------------|------------------------------------------| | `InMemoryMangaRepository` | `MangaRepositoryInterface` | -| `InMemoryChapterRepository` | `ChapterRepositoryInterface` | | `InMemoryImageProcessor` | `ImageProcessorInterface` | | `InMemoryMangadexClient` | `MangadexClientInterface` | | `InMemoryMangaProvider` | `MangaProviderInterface` | From 640d1cec8278179d0f58b18b20b5bbd6736d10da Mon Sep 17 00:00:00 2001 From: "ext.jeremy.guillot@maxicoffee.domains" Date: Mon, 9 Mar 2026 19:33:26 +0100 Subject: [PATCH 6/6] fix(migration): DROP INDEX IF EXISTS pour messenger_messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Les index idx_available_at/idx_delivered_at/idx_queue_available/idx_queue_name n'existent pas sur tous les environnements. IF EXISTS évite l'erreur 42704. Co-Authored-By: Claude Sonnet 4.6 --- migrations/Version20260309165048.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/migrations/Version20260309165048.php b/migrations/Version20260309165048.php index 8573102..5a3e267 100644 --- a/migrations/Version20260309165048.php +++ b/migrations/Version20260309165048.php @@ -22,10 +22,10 @@ final class Version20260309165048 extends AbstractMigration // this up() migration is auto-generated, please modify it to your needs $this->addSql('ALTER TABLE chapter ADD pages_directory VARCHAR(255) DEFAULT NULL'); $this->addSql('ALTER TABLE chapter ADD page_count INT DEFAULT 0 NOT NULL'); - $this->addSql('DROP INDEX idx_available_at'); - $this->addSql('DROP INDEX idx_delivered_at'); - $this->addSql('DROP INDEX idx_queue_available'); - $this->addSql('DROP INDEX idx_queue_name'); + $this->addSql('DROP INDEX IF EXISTS idx_available_at'); + $this->addSql('DROP INDEX IF EXISTS idx_delivered_at'); + $this->addSql('DROP INDEX IF EXISTS idx_queue_available'); + $this->addSql('DROP INDEX IF EXISTS idx_queue_name'); } public function down(Schema $schema): void