feat: CreateMangaFromMangadex endpoint + tests, missing image saving
This commit is contained in:
parent
ae0eac3197
commit
50080f9779
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Manga\Application\Command;
|
||||
|
||||
readonly class CreateMangaFromMangadex
|
||||
{
|
||||
public function __construct(
|
||||
public string $externalId
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Manga\Application\CommandHandler;
|
||||
|
||||
use App\Domain\Manga\Application\Command\CreateMangaFromMangadex;
|
||||
use App\Domain\Manga\Domain\Contract\Provider\MangaProviderInterface;
|
||||
use App\Domain\Manga\Domain\Contract\Repository\MangaRepositoryInterface;
|
||||
use App\Domain\Manga\Domain\Exception\MangaNotFoundException;
|
||||
use App\Domain\Manga\Domain\Model\ValueObject\ExternalId;
|
||||
|
||||
readonly class CreateMangaFromMangadexHandler
|
||||
{
|
||||
public function __construct(
|
||||
private MangaProviderInterface $mangaProvider,
|
||||
private MangaRepositoryInterface $mangaRepository
|
||||
) {}
|
||||
|
||||
public function handle(CreateMangaFromMangadex $command): void
|
||||
{
|
||||
$manga = $this->mangaProvider->findByExternalId(new ExternalId($command->externalId));
|
||||
|
||||
if ($manga === null) {
|
||||
throw new MangaNotFoundException('Manga not found on Mangadex');
|
||||
}
|
||||
|
||||
$this->mangaRepository->save($manga);
|
||||
}
|
||||
}
|
||||
@@ -72,4 +72,25 @@ interface MangadexClientInterface
|
||||
* }
|
||||
*/
|
||||
public function getMangaAggregate(string $mangaId): array;
|
||||
|
||||
/**
|
||||
* @return array{
|
||||
* result: string,
|
||||
* data: array{
|
||||
* id: string,
|
||||
* attributes: array{
|
||||
* title: array<string, string>,
|
||||
* description: array<string, string>,
|
||||
* year: ?int,
|
||||
* status: string,
|
||||
* tags: array<array{attributes: array{name: array<string, string>}}>
|
||||
* },
|
||||
* relationships: array<array{
|
||||
* type: string,
|
||||
* attributes: array{name: string|null, fileName: string|null}
|
||||
* }>
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
public function getManga(string $mangaId): array;
|
||||
}
|
||||
@@ -2,9 +2,13 @@
|
||||
|
||||
namespace App\Domain\Manga\Domain\Contract\Provider;
|
||||
|
||||
use App\Domain\Manga\Domain\Model\Manga;
|
||||
use App\Domain\Manga\Domain\Model\MangaCollection;
|
||||
use App\Domain\Manga\Domain\Model\ValueObject\ExternalId;
|
||||
|
||||
interface MangaProviderInterface
|
||||
{
|
||||
public function search(string $title): MangaCollection;
|
||||
|
||||
public function findByExternalId(ExternalId $externalId): ?Manga;
|
||||
}
|
||||
@@ -6,8 +6,8 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
class MangaNotFoundException extends NotFoundHttpException
|
||||
{
|
||||
public function __construct()
|
||||
public function __construct(string $message = 'Manga not found')
|
||||
{
|
||||
parent::__construct('Manga not found');
|
||||
parent::__construct($message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,4 +82,9 @@ final class Manga
|
||||
{
|
||||
$this->rating = $rating;
|
||||
}
|
||||
|
||||
public function updateId(MangaId $id): void
|
||||
{
|
||||
$this->id = $id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Manga\Infrastructure\ApiPlatform\Resource;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use App\Domain\Manga\Infrastructure\ApiPlatform\State\Processor\CreateMangaProcessor;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
#[ApiResource(
|
||||
shortName: 'CreateManga',
|
||||
operations: [
|
||||
new Post(
|
||||
uriTemplate: '/mangas/create-from-mangadex',
|
||||
processor: CreateMangaProcessor::class,
|
||||
openapiContext: [
|
||||
'summary' => 'Create a new manga from Mangadex',
|
||||
'description' => 'Creates a new manga by fetching its data from Mangadex using an external ID'
|
||||
]
|
||||
)
|
||||
]
|
||||
)]
|
||||
class CreateMangaResource
|
||||
{
|
||||
#[Assert\NotBlank]
|
||||
public string $externalId;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Manga\Infrastructure\ApiPlatform\State\Processor;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProcessorInterface;
|
||||
use App\Domain\Manga\Application\Command\CreateMangaFromMangadex;
|
||||
use App\Domain\Manga\Application\CommandHandler\CreateMangaFromMangadexHandler;
|
||||
use App\Domain\Manga\Domain\Exception\MangaNotFoundException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
readonly class CreateMangaProcessor implements ProcessorInterface
|
||||
{
|
||||
public function __construct(
|
||||
private CreateMangaFromMangadexHandler $handler
|
||||
) {}
|
||||
|
||||
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): void
|
||||
{
|
||||
try {
|
||||
$this->handler->handle(new CreateMangaFromMangadex($data->externalId));
|
||||
} catch (MangaNotFoundException $e) {
|
||||
throw new NotFoundHttpException($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -118,6 +118,13 @@ class MangadexClient implements MangadexClientInterface
|
||||
return $this->get('/manga/' . $mangaId . '/aggregate');
|
||||
}
|
||||
|
||||
public function getManga(string $mangaId): array
|
||||
{
|
||||
return $this->get('/manga/' . $mangaId, [
|
||||
'includes' => ['cover_art', 'author']
|
||||
]);
|
||||
}
|
||||
|
||||
private function get(string $endpoint, array $params = []): array
|
||||
{
|
||||
try {
|
||||
|
||||
@@ -55,13 +55,30 @@ readonly class LegacyMangaRepository implements MangaRepositoryInterface
|
||||
|
||||
public function save(DomainManga $manga): void
|
||||
{
|
||||
$entity = $this->entityManager->find(EntityManga::class, $manga->getId()->getValue())
|
||||
?? new EntityManga();
|
||||
|
||||
$this->updateEntity($entity, $manga);
|
||||
$entity = new EntityManga();
|
||||
|
||||
$entity->setTitle($manga->getTitle()->getValue())
|
||||
->setSlug($manga->getSlug()->getValue())
|
||||
->setDescription($manga->getDescription())
|
||||
->setAuthor($manga->getAuthor())
|
||||
->setPublicationYear($manga->getPublicationYear())
|
||||
->setGenres($manga->getGenres())
|
||||
->setStatus($manga->getStatus())
|
||||
->setExternalId($manga->getExternalId()->getValue())
|
||||
->setImageUrl($manga->getImageUrl())
|
||||
->setMonitored(false);
|
||||
|
||||
if ($manga->getRating() !== null) {
|
||||
$entity->setRating($manga->getRating());
|
||||
}
|
||||
|
||||
$this->entityManager->persist($entity);
|
||||
$this->entityManager->flush();
|
||||
|
||||
// Met à jour l'ID du modèle du domaine avec l'ID généré par la BDD
|
||||
if ($entity->getId()) {
|
||||
$manga->updateId(new MangaId((string) $entity->getId()));
|
||||
}
|
||||
}
|
||||
|
||||
public function delete(DomainManga $manga): void
|
||||
@@ -121,29 +138,6 @@ readonly class LegacyMangaRepository implements MangaRepositoryInterface
|
||||
);
|
||||
}
|
||||
|
||||
private function updateEntity(EntityManga $entity, DomainManga $manga): void
|
||||
{
|
||||
$entity->setTitle($manga->getTitle()->getValue())
|
||||
->setSlug($manga->getSlug()->getValue())
|
||||
->setDescription($manga->getDescription())
|
||||
->setAuthor($manga->getAuthor())
|
||||
->setPublicationYear($manga->getPublicationYear())
|
||||
->setGenres($manga->getGenres())
|
||||
->setStatus($manga->getStatus());
|
||||
|
||||
if ($manga->getExternalId()) {
|
||||
$entity->setExternalId($manga->getExternalId()->getValue());
|
||||
}
|
||||
|
||||
if ($manga->getImageUrl()) {
|
||||
$entity->setImageUrl($manga->getImageUrl());
|
||||
}
|
||||
|
||||
if ($manga->getRating()) {
|
||||
$entity->setRating($manga->getRating());
|
||||
}
|
||||
}
|
||||
|
||||
private function toChapterDomain(EntityChapter $entity): Chapter
|
||||
{
|
||||
return new Chapter(
|
||||
|
||||
@@ -123,4 +123,25 @@ readonly class MangadexProvider implements MangaProviderInterface
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function findByExternalId(ExternalId $externalId): ?Manga
|
||||
{
|
||||
try {
|
||||
$result = $this->client->getManga($externalId->getValue());
|
||||
|
||||
if (!isset($result['data'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$manga = $this->createMangaFromResult($result['data']);
|
||||
|
||||
if ($manga) {
|
||||
$this->enrichWithRatings([$manga]);
|
||||
}
|
||||
|
||||
return $manga;
|
||||
} catch (\Exception) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ namespace App\Tests\Domain\Manga\Adapter;
|
||||
use App\Domain\Manga\Domain\Contract\Provider\MangaProviderInterface;
|
||||
use App\Domain\Manga\Domain\Model\Manga;
|
||||
use App\Domain\Manga\Domain\Model\MangaCollection;
|
||||
use App\Domain\Manga\Domain\Model\ValueObject\ExternalId;
|
||||
|
||||
class InMemoryMangaProvider implements MangaProviderInterface
|
||||
{
|
||||
@@ -31,4 +32,15 @@ class InMemoryMangaProvider implements MangaProviderInterface
|
||||
|
||||
return new MangaCollection($results);
|
||||
}
|
||||
|
||||
public function findByExternalId(ExternalId $externalId): ?Manga
|
||||
{
|
||||
foreach ($this->mangas as $manga) {
|
||||
if ($manga->getExternalId() && $manga->getExternalId()->getValue() === $externalId->getValue()) {
|
||||
return $manga;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
123
tests/Domain/Manga/Adapter/InMemoryMangadexClient.php
Normal file
123
tests/Domain/Manga/Adapter/InMemoryMangadexClient.php
Normal file
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Domain\Manga\Adapter;
|
||||
|
||||
use App\Domain\Manga\Domain\Contract\Client\MangadexClientInterface;
|
||||
|
||||
class InMemoryMangadexClient implements MangadexClientInterface
|
||||
{
|
||||
private array $mangas = [];
|
||||
private array $feeds = [];
|
||||
private array $aggregates = [];
|
||||
|
||||
public function __construct(
|
||||
array $mangas = [],
|
||||
array $feeds = [],
|
||||
array $aggregates = []
|
||||
) {
|
||||
$this->mangas = $mangas;
|
||||
$this->feeds = $feeds;
|
||||
$this->aggregates = $aggregates;
|
||||
}
|
||||
|
||||
public function authenticate(): void
|
||||
{
|
||||
// No need to implement for tests
|
||||
}
|
||||
|
||||
public function refreshToken(): void
|
||||
{
|
||||
// No need to implement for tests
|
||||
}
|
||||
|
||||
public function searchManga(string $title): array
|
||||
{
|
||||
$results = [];
|
||||
foreach ($this->mangas as $id => $manga) {
|
||||
if (isset($manga['attributes']['title']['en']) &&
|
||||
str_contains(
|
||||
strtolower($manga['attributes']['title']['en']),
|
||||
strtolower($title)
|
||||
)
|
||||
) {
|
||||
$results[] = array_merge(['id' => $id], $manga);
|
||||
}
|
||||
}
|
||||
|
||||
return ['data' => $results];
|
||||
}
|
||||
|
||||
public function getMangaRatings(array $mangaIds): array
|
||||
{
|
||||
$statistics = [];
|
||||
foreach ($mangaIds as $id) {
|
||||
$statistics[$id] = [
|
||||
'rating' => ['average' => 4.5] // Default rating for tests
|
||||
];
|
||||
}
|
||||
|
||||
return ['statistics' => $statistics];
|
||||
}
|
||||
|
||||
public function getMangaFeed(string $mangaId, int $offset = 0, int $limit = 500, string $order = 'asc'): array
|
||||
{
|
||||
if (!isset($this->feeds[$mangaId])) {
|
||||
return [
|
||||
'data' => [],
|
||||
'total' => 0
|
||||
];
|
||||
}
|
||||
|
||||
$feed = $this->feeds[$mangaId];
|
||||
if ($order === 'desc') {
|
||||
$feed = array_reverse($feed);
|
||||
}
|
||||
|
||||
return [
|
||||
'data' => array_slice($feed, $offset, $limit),
|
||||
'total' => count($feed)
|
||||
];
|
||||
}
|
||||
|
||||
public function getMangaAggregate(string $mangaId): array
|
||||
{
|
||||
if (!isset($this->aggregates[$mangaId])) {
|
||||
return [
|
||||
'result' => 'ok',
|
||||
'volumes' => []
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'result' => 'ok',
|
||||
'volumes' => $this->aggregates[$mangaId]
|
||||
];
|
||||
}
|
||||
|
||||
public function getManga(string $mangaId): array
|
||||
{
|
||||
if (!isset($this->mangas[$mangaId])) {
|
||||
return ['data' => null];
|
||||
}
|
||||
|
||||
return [
|
||||
'result' => 'ok',
|
||||
'data' => array_merge(['id' => $mangaId], $this->mangas[$mangaId])
|
||||
];
|
||||
}
|
||||
|
||||
public function addManga(string $id, array $data): void
|
||||
{
|
||||
$this->mangas[$id] = $data;
|
||||
}
|
||||
|
||||
public function addFeed(string $mangaId, array $feed): void
|
||||
{
|
||||
$this->feeds[$mangaId] = $feed;
|
||||
}
|
||||
|
||||
public function addAggregate(string $mangaId, array $aggregate): void
|
||||
{
|
||||
$this->aggregates[$mangaId] = $aggregate;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Domain\Manga\Application\CommandHandler;
|
||||
|
||||
use App\Domain\Manga\Application\Command\CreateMangaFromMangadex;
|
||||
use App\Domain\Manga\Application\CommandHandler\CreateMangaFromMangadexHandler;
|
||||
use App\Domain\Manga\Domain\Exception\MangaNotFoundException;
|
||||
use App\Domain\Manga\Domain\Model\Manga;
|
||||
use App\Domain\Manga\Domain\Model\ValueObject\ExternalId;
|
||||
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\InMemoryMangaProvider;
|
||||
use App\Tests\Domain\Manga\Adapter\InMemoryMangaRepository;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class CreateMangaFromMangadexHandlerTest extends TestCase
|
||||
{
|
||||
private InMemoryMangaProvider $provider;
|
||||
private InMemoryMangaRepository $repository;
|
||||
private CreateMangaFromMangadexHandler $handler;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->provider = new InMemoryMangaProvider();
|
||||
$this->repository = new InMemoryMangaRepository();
|
||||
$this->handler = new CreateMangaFromMangadexHandler(
|
||||
$this->provider,
|
||||
$this->repository
|
||||
);
|
||||
}
|
||||
|
||||
public function testHandleSuccess(): void
|
||||
{
|
||||
// Arrange
|
||||
$externalId = 'manga-123';
|
||||
$manga = new Manga(
|
||||
new MangaId('123'),
|
||||
new MangaTitle('Test Manga'),
|
||||
new MangaSlug('test-manga'),
|
||||
'Description',
|
||||
'Author',
|
||||
2020,
|
||||
['action'],
|
||||
'ongoing',
|
||||
new ExternalId($externalId),
|
||||
'http://example.com/cover.jpg',
|
||||
4.5
|
||||
);
|
||||
|
||||
$this->provider = new InMemoryMangaProvider([$manga]);
|
||||
$this->handler = new CreateMangaFromMangadexHandler(
|
||||
$this->provider,
|
||||
$this->repository
|
||||
);
|
||||
|
||||
// Act
|
||||
$command = new CreateMangaFromMangadex($externalId);
|
||||
$this->handler->handle($command);
|
||||
|
||||
// Assert
|
||||
$savedManga = $this->repository->findById('123');
|
||||
$this->assertNotNull($savedManga);
|
||||
$this->assertEquals($externalId, $savedManga->getExternalId()->getValue());
|
||||
$this->assertEquals('Test Manga', $savedManga->getTitle()->getValue());
|
||||
}
|
||||
|
||||
public function testHandleThrowsExceptionWhenMangaNotFound(): void
|
||||
{
|
||||
// Arrange
|
||||
$command = new CreateMangaFromMangadex('non-existent-manga');
|
||||
|
||||
// Assert
|
||||
$this->expectException(MangaNotFoundException::class);
|
||||
$this->expectExceptionMessage('Manga not found on Mangadex');
|
||||
|
||||
// Act
|
||||
$this->handler->handle($command);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user