From 3f08e1c8997bf60f4fca85a5e60ae10f3d766c25 Mon Sep 17 00:00:00 2001 From: "ext.jeremy.guillot@maxicoffee.domains" Date: Fri, 27 Mar 2026 12:08:06 +0100 Subject: [PATCH 1/2] =?UTF-8?q?fix(monitoring):=20corriger=20le=20schedule?= =?UTF-8?q?r=20qui=20ne=20d=C3=A9tectait=20plus=20les=20nouveaux=20chapitr?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MonitoringSchedule : supprimer la date passée au message (était évaluée une seule fois au démarrage du container, rendant la requête caduque après le premier cycle) - CheckMonitoredMangasHandler : calculer `since` dynamiquement à l'exécution (`new \DateTimeImmutable('-2 hours')`) plutôt que de dépendre du message - AutoScrapingListener : corriger le TypeError silencieux — créer un ScrapingJob avant d'appeler ScrapeChapterHandler (paramètre jobId manquant) Ajoute les tests unitaires CheckMonitoredMangasHandlerTest et AutoScrapingListenerTest. --- .../CheckMonitoredMangasHandler.php | 2 +- .../Scheduler/MonitoringSchedule.php | 4 +- .../EventListener/AutoScrapingListener.php | 11 +- .../CheckMonitoredMangasHandlerTest.php | 110 ++++++++++++++++++ .../AutoScrapingListenerTest.php | 104 +++++++++++++++++ 5 files changed, 226 insertions(+), 5 deletions(-) create mode 100644 tests/Domain/Manga/Application/CommandHandler/CheckMonitoredMangasHandlerTest.php create mode 100644 tests/Domain/Scraping/Infrastructure/EventListener/AutoScrapingListenerTest.php diff --git a/src/Domain/Manga/Application/CommandHandler/CheckMonitoredMangasHandler.php b/src/Domain/Manga/Application/CommandHandler/CheckMonitoredMangasHandler.php index abb1b04..2b41f6f 100644 --- a/src/Domain/Manga/Application/CommandHandler/CheckMonitoredMangasHandler.php +++ b/src/Domain/Manga/Application/CommandHandler/CheckMonitoredMangasHandler.php @@ -20,7 +20,7 @@ readonly class CheckMonitoredMangasHandler { $criteria = new MonitoringCriteria( enabled: true, - lastCheckBefore: $command->since ?? new \DateTimeImmutable('-1 hour') + lastCheckBefore: new \DateTimeImmutable('-2 hours') ); $monitoredMangas = $this->mangaRepository->findByMonitoringCriteria($criteria); diff --git a/src/Domain/Manga/Infrastructure/Scheduler/MonitoringSchedule.php b/src/Domain/Manga/Infrastructure/Scheduler/MonitoringSchedule.php index 6081e51..b809cc8 100644 --- a/src/Domain/Manga/Infrastructure/Scheduler/MonitoringSchedule.php +++ b/src/Domain/Manga/Infrastructure/Scheduler/MonitoringSchedule.php @@ -21,9 +21,7 @@ class MonitoringSchedule implements ScheduleProviderInterface { return (new Schedule())->add( // Toutes les 2 heures, vérifie les mangas qui n'ont pas été vérifiés depuis 2 heures - RecurringMessage::every('2 hours', new CheckMonitoredMangas( - new \DateTimeImmutable('-2 hours') - )) + RecurringMessage::every('2 hours', new CheckMonitoredMangas()) )->stateful($this->cache); } } diff --git a/src/Domain/Scraping/Infrastructure/EventListener/AutoScrapingListener.php b/src/Domain/Scraping/Infrastructure/EventListener/AutoScrapingListener.php index bd4210b..829a349 100644 --- a/src/Domain/Scraping/Infrastructure/EventListener/AutoScrapingListener.php +++ b/src/Domain/Scraping/Infrastructure/EventListener/AutoScrapingListener.php @@ -7,6 +7,9 @@ use App\Domain\Scraping\Application\Command\ScrapeChapter; use App\Domain\Scraping\Application\CommandHandler\ScrapeChapterHandler; use App\Domain\Scraping\Domain\Contract\Repository\ChapterRepositoryInterface; use App\Domain\Scraping\Domain\Contract\Repository\MangaRepositoryInterface; +use App\Domain\Scraping\Domain\Model\ScrapingJob; +use App\Domain\Shared\Domain\Contract\JobRepositoryInterface; +use Ramsey\Uuid\Uuid; use Symfony\Component\Messenger\Attribute\AsMessageHandler; class AutoScrapingListener @@ -15,6 +18,7 @@ class AutoScrapingListener private readonly ScrapeChapterHandler $scrapeChapterHandler, private readonly ChapterRepositoryInterface $chapterRepository, private readonly MangaRepositoryInterface $mangaRepository, + private readonly JobRepositoryInterface $jobRepository, ) { } @@ -25,7 +29,12 @@ class AutoScrapingListener $manga = $this->mangaRepository->getById($chapter->mangaId); if ($manga->isMonitored()) { - $this->scrapeChapterHandler->handle(new ScrapeChapter($event->chapterId->getValue())); + $jobId = Uuid::uuid4()->toString(); + $job = new ScrapingJob($jobId); + $job->context['chapterId'] = $event->chapterId->getValue(); + $this->jobRepository->save($job); + + $this->scrapeChapterHandler->handle(new ScrapeChapter($event->chapterId->getValue(), $jobId)); } } } diff --git a/tests/Domain/Manga/Application/CommandHandler/CheckMonitoredMangasHandlerTest.php b/tests/Domain/Manga/Application/CommandHandler/CheckMonitoredMangasHandlerTest.php new file mode 100644 index 0000000..53bd168 --- /dev/null +++ b/tests/Domain/Manga/Application/CommandHandler/CheckMonitoredMangasHandlerTest.php @@ -0,0 +1,110 @@ +mangaRepository = new InMemoryMangaRepository(); + $this->commandBus = new InMemoryMessageBus(); + $this->commandBus->clear(); + $this->handler = new CheckMonitoredMangasHandler($this->mangaRepository, $this->commandBus); + } + + private function createManga(string $id): Manga + { + return new Manga( + new MangaId($id), + new MangaTitle('Manga ' . $id), + new MangaSlug('manga-' . $id), + 'Description', + 'Author', + 2024, + [], + 'ongoing', + new ExternalId('ext-' . $id) + ); + } + + public function testDispatchesRefreshForMonitoredMangaWithOldCheck(): void + { + $manga = $this->createManga('manga-1'); + $manga->enableMonitoring(); + $manga->updateLastMonitoringCheck(new \DateTimeImmutable('-3 hours')); + $this->mangaRepository->save($manga); + + $this->handler->handle(new CheckMonitoredMangas()); + + $this->assertTrue($this->commandBus->hasMessageOfType(RefreshMangaChapters::class)); + $dispatched = array_filter( + $this->commandBus->getDispatchedMessages(), + fn ($m) => $m instanceof RefreshMangaChapters + ); + $this->assertCount(1, $dispatched); + $this->assertSame('manga-1', array_values($dispatched)[0]->mangaId->getValue()); + } + + public function testDoesNotDispatchForNonMonitoredManga(): void + { + $manga = $this->createManga('manga-2'); + $this->mangaRepository->save($manga); + + $this->handler->handle(new CheckMonitoredMangas()); + + $this->assertFalse($this->commandBus->hasMessageOfType(RefreshMangaChapters::class)); + } + + public function testDoesNotDispatchForMangaWithRecentCheck(): void + { + $manga = $this->createManga('manga-3'); + $manga->enableMonitoring(); + $manga->updateLastMonitoringCheck(new \DateTimeImmutable('-30 minutes')); + $this->mangaRepository->save($manga); + + $this->handler->handle(new CheckMonitoredMangas()); + + $this->assertFalse($this->commandBus->hasMessageOfType(RefreshMangaChapters::class)); + } + + public function testDispatchesOnlyMangasWithOldCheck(): void + { + $mangaOld = $this->createManga('manga-old'); + $mangaOld->enableMonitoring(); + $mangaOld->updateLastMonitoringCheck(new \DateTimeImmutable('-3 hours')); + $this->mangaRepository->save($mangaOld); + + $mangaRecent = $this->createManga('manga-recent'); + $mangaRecent->enableMonitoring(); + $mangaRecent->updateLastMonitoringCheck(new \DateTimeImmutable('-30 minutes')); + $this->mangaRepository->save($mangaRecent); + + $mangaDisabled = $this->createManga('manga-disabled'); + $this->mangaRepository->save($mangaDisabled); + + $this->handler->handle(new CheckMonitoredMangas()); + + $dispatched = array_filter( + $this->commandBus->getDispatchedMessages(), + fn ($m) => $m instanceof RefreshMangaChapters + ); + $this->assertCount(1, $dispatched); + $this->assertSame('manga-old', array_values($dispatched)[0]->mangaId->getValue()); + } +} diff --git a/tests/Domain/Scraping/Infrastructure/EventListener/AutoScrapingListenerTest.php b/tests/Domain/Scraping/Infrastructure/EventListener/AutoScrapingListenerTest.php new file mode 100644 index 0000000..7d798d2 --- /dev/null +++ b/tests/Domain/Scraping/Infrastructure/EventListener/AutoScrapingListenerTest.php @@ -0,0 +1,104 @@ +chapterRepository = new InMemoryChapterRepository(); + $this->mangaRepository = new InMemoryMangaRepository(); + $this->mangaRepository->clear(); + $this->jobRepository = new InMemoryJobRepository(); + $this->eventBus = new InMemoryEventBus(); + + $handler = new ScrapeChapterHandler( + new InMemoryScraperFactory(), + new InMemoryImageDownloader(), + new InMemoryImageStorage(), + $this->jobRepository, + $this->chapterRepository, + $this->mangaRepository, + new InMemorySourceRepository(), + $this->eventBus, + ); + + $this->listener = new AutoScrapingListener( + $handler, + $this->chapterRepository, + $this->mangaRepository, + $this->jobRepository, + ); + } + + public function testCreatesJobAndScrapesWhenMangaIsMonitored(): void + { + $chapterId = 'chapter-uuid-1'; + $mangaId = 'manga-monitored'; + + $this->chapterRepository->save(new Chapter($chapterId, $mangaId, 1177.0, null)); + $this->mangaRepository->save(new Manga( + $mangaId, 'One Piece', 'one-piece', 'Desc', 'Oda', '1997', true + )); + + $this->listener->onChapterReadyForScraping( + new ChapterReadyForScraping(new ChapterId($chapterId)) + ); + + $jobs = $this->jobRepository->findByType('scraping_job'); + $this->assertCount(1, $jobs); + + $job = array_values($jobs)[0]; + $this->assertSame($chapterId, $job->context['chapterId']); + $this->assertInstanceOf(ScrapingJob::class, $job); + + $hasChapterScraped = count(array_filter( + $this->eventBus->getDispatchedMessages(), + fn ($m) => $m instanceof ChapterScraped + )) > 0; + $this->assertTrue($hasChapterScraped); + } + + public function testDoesNothingWhenMangaIsNotMonitored(): void + { + $chapterId = 'chapter-uuid-2'; + $mangaId = 'manga-not-monitored'; + + $this->chapterRepository->save(new Chapter($chapterId, $mangaId, 1176.0, null)); + $this->mangaRepository->save(new Manga( + $mangaId, 'One Piece', 'one-piece', 'Desc', 'Oda', '1997', false + )); + + $this->listener->onChapterReadyForScraping( + new ChapterReadyForScraping(new ChapterId($chapterId)) + ); + + $this->assertEmpty($this->jobRepository->findByType('scraping_job')); + $this->assertEmpty($this->eventBus->getDispatchedMessages()); + } +} From f42b5a9cf57a7a215ea58958c5da5d425102c83a Mon Sep 17 00:00:00 2001 From: "ext.jeremy.guillot@maxicoffee.domains" Date: Fri, 27 Mar 2026 14:21:05 +0100 Subject: [PATCH 2/2] =?UTF-8?q?feat(monitoring):=20ajouter=20une=20command?= =?UTF-8?q?e=20console=20pour=20d=C3=A9clencher=20le=20monitoring=20manuel?= =?UTF-8?q?lement?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Permet de tester le scheduler en prod sans attendre le cycle de 2h : make sf c="app:monitoring:run" --- src/Command/RunMonitoringCommand.php | 36 ++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 src/Command/RunMonitoringCommand.php diff --git a/src/Command/RunMonitoringCommand.php b/src/Command/RunMonitoringCommand.php new file mode 100644 index 0000000..80c7ce0 --- /dev/null +++ b/src/Command/RunMonitoringCommand.php @@ -0,0 +1,36 @@ +writeln('Déclenchement du monitoring des mangas...'); + + $this->commandBus->dispatch(new CheckMonitoredMangas()); + + $output->writeln('Vérification lancée. Les nouveaux chapitres détectés seront scrappés via le worker commands.'); + + return Command::SUCCESS; + } +}