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] =?UTF-8?q?refactor(manga):=20Chapter=20entit=C3=A9=20DDD?= =?UTF-8?q?=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(