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] 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; } } - - - -