feat: endpoint FetchMangaChapters et tests

This commit is contained in:
ext.jeremy.guillot@maxicoffee.domains
2025-02-11 18:00:49 +01:00
parent 3dc0a0b406
commit 879b8fa2dc
20 changed files with 424 additions and 45 deletions

View File

@@ -0,0 +1,10 @@
<?php
namespace App\Domain\Manga\Application\Command;
readonly class FetchMangaChapters
{
public function __construct(
public string $externalId
) {}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace App\Domain\Manga\Application\CommandHandler;
use App\Domain\Manga\Application\Command\FetchMangaChapters;
use App\Domain\Manga\Domain\Contract\Client\MangadexClientInterface;
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\ExternalId;
use Ramsey\Uuid\Uuid;
readonly class FetchMangaChaptersHandler
{
public function __construct(
private MangadexClientInterface $mangadexClient,
private MangaRepositoryInterface $mangaRepository
) {}
public function handle(FetchMangaChapters $command): void
{
$manga = $this->mangaRepository->findByExternalId(new ExternalId($command->externalId));
if ($manga === null) {
throw new \RuntimeException('Manga not found');
}
$offset = 0;
$limit = 500;
$hasMore = true;
while ($hasMore) {
$feed = $this->mangadexClient->getMangaFeed(
$command->externalId,
$offset,
$limit
);
foreach ($feed['data'] as $chapterData) {
$chapter = new Chapter(
new ChapterId((string) Uuid::uuid4()),
$manga->getId()->getValue(),
(float) $chapterData['attributes']['chapter'],
$chapterData['attributes']['title'],
isset($chapterData['attributes']['volume']) ? (int) $chapterData['attributes']['volume'] : null,
true,
new \DateTimeImmutable()
);
$this->mangaRepository->saveChapter($chapter);
}
$offset += $limit;
$hasMore = count($feed['data']) === $limit;
}
}
}

View File

@@ -3,6 +3,8 @@
namespace App\Domain\Manga\Domain\Contract\Repository;
use App\Domain\Manga\Domain\Model\Manga;
use App\Domain\Manga\Domain\Model\Chapter;
use App\Domain\Manga\Domain\Model\ValueObject\ExternalId;
interface MangaRepositoryInterface
{
@@ -13,4 +15,6 @@ interface MangaRepositoryInterface
public function delete(Manga $manga): void;
public function findChapters(string $mangaId, int $page = 1, int $limit = 20, string $sortOrder = 'desc'): array;
public function countChapters(string $mangaId): int;
public function findByExternalId(ExternalId $externalId): ?Manga;
public function saveChapter(Chapter $chapter): void;
}

View File

@@ -21,6 +21,11 @@ readonly class Chapter
return $this->id->getValue();
}
public function getMangaId(): string
{
return $this->mangaId;
}
public function getNumber(): float
{
return $this->number;

View File

@@ -0,0 +1,26 @@
<?php
namespace App\Domain\Manga\Infrastructure\ApiPlatform\Resource;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Post;
use App\Domain\Manga\Infrastructure\ApiPlatform\State\Processor\FetchMangaChaptersProcessor;
use Symfony\Component\Validator\Constraints as Assert;
#[ApiResource(
shortName: 'FetchMangaChapters',
operations: [
new Post(
uriTemplate: '/manga/chapters/fetch',
processor: FetchMangaChaptersProcessor::class,
status: 202
)
]
)]
class FetchMangaChaptersResource
{
public function __construct(
#[Assert\NotBlank]
public string $externalId
) {}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Domain\Manga\Infrastructure\ApiPlatform\State\Processor;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use App\Domain\Manga\Application\Command\FetchMangaChapters;
use App\Domain\Manga\Infrastructure\ApiPlatform\Resource\FetchMangaChaptersResource;
use Symfony\Component\Messenger\MessageBusInterface;
readonly class FetchMangaChaptersProcessor implements ProcessorInterface
{
public function __construct(
private MessageBusInterface $messageBus
) {}
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): void
{
if (!$data instanceof FetchMangaChaptersResource) {
throw new \InvalidArgumentException('Invalid resource type');
}
$this->messageBus->dispatch(
new FetchMangaChapters($data->externalId)
);
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace App\Domain\Manga\Infrastructure\CommandHandler;
use App\Domain\Manga\Application\Command\FetchMangaChapters;
use App\Domain\Manga\Application\CommandHandler\FetchMangaChaptersHandler;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
#[AsMessageHandler]
readonly class SymfonyFetchMangaChaptersHandler
{
public function __construct(
private FetchMangaChaptersHandler $handler
) {}
public function __invoke(FetchMangaChapters $command): void
{
$this->handler->handle($command);
}
}

View File

@@ -126,6 +126,34 @@ readonly class LegacyMangaRepository implements MangaRepositoryInterface
->getSingleScalarResult();
}
public function findByExternalId(ExternalId $externalId): ?DomainManga
{
$entity = $this->entityManager->getRepository(EntityManga::class)->findOneBy([
'externalId' => $externalId->getValue()
]);
return $entity ? $this->toDomain($entity) : null;
}
public function saveChapter(Chapter $chapter): void
{
$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());
$this->entityManager->persist($entity);
$this->entityManager->flush();
}
private function toDomain(EntityManga $entity): DomainManga
{
return new DomainManga(

View File

@@ -1,6 +1,6 @@
<?php
namespace App\Domain\Scraping\Infrastructure\Handler;
namespace App\Domain\Scraping\Infrastructure\CommandHandler;
use App\Domain\Scraping\Application\Command\ScrapeChapter;
use App\Domain\Scraping\Application\CommandHandler\ScrapeChapterHandler;