feat: GetMangaList endpoint + tests + test db
This commit is contained in:
parent
073439163b
commit
e3d380eadd
13
src/Domain/Manga/Application/Query/GetMangaList.php
Normal file
13
src/Domain/Manga/Application/Query/GetMangaList.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Manga\Application\Query;
|
||||
|
||||
readonly class GetMangaList
|
||||
{
|
||||
public function __construct(
|
||||
public ?int $page = 1,
|
||||
public ?int $limit = 20,
|
||||
public ?string $sortBy = 'title',
|
||||
public ?string $sortOrder = 'asc'
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Manga\Application\QueryHandler;
|
||||
|
||||
use App\Domain\Manga\Application\Query\GetMangaList;
|
||||
use App\Domain\Manga\Domain\Contract\Repository\MangaRepositoryInterface;
|
||||
use App\Domain\Manga\Application\Response\MangaListResponse;
|
||||
|
||||
readonly class GetMangaListHandler
|
||||
{
|
||||
public function __construct(
|
||||
private MangaRepositoryInterface $mangaRepository
|
||||
) {}
|
||||
|
||||
public function handle(GetMangaList $query): MangaListResponse
|
||||
{
|
||||
$mangas = $this->mangaRepository->findAll(
|
||||
page: $query->page,
|
||||
limit: $query->limit,
|
||||
sortBy: $query->sortBy,
|
||||
sortOrder: $query->sortOrder
|
||||
);
|
||||
|
||||
$total = $this->mangaRepository->count();
|
||||
|
||||
return new MangaListResponse(
|
||||
mangas: $mangas,
|
||||
total: $total,
|
||||
page: $query->page,
|
||||
limit: $query->limit
|
||||
);
|
||||
}
|
||||
}
|
||||
28
src/Domain/Manga/Application/Response/MangaListResponse.php
Normal file
28
src/Domain/Manga/Application/Response/MangaListResponse.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Manga\Application\Response;
|
||||
|
||||
readonly class MangaListResponse
|
||||
{
|
||||
public function __construct(
|
||||
public array $mangas,
|
||||
public int $total,
|
||||
public int $page,
|
||||
public int $limit
|
||||
) {}
|
||||
|
||||
public function getTotalPages(): int
|
||||
{
|
||||
return (int) ceil($this->total / $this->limit);
|
||||
}
|
||||
|
||||
public function hasNextPage(): bool
|
||||
{
|
||||
return $this->page < $this->getTotalPages();
|
||||
}
|
||||
|
||||
public function hasPreviousPage(): bool
|
||||
{
|
||||
return $this->page > 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Manga\Domain\Contract\Repository;
|
||||
|
||||
use App\Domain\Manga\Domain\Model\Manga;
|
||||
|
||||
interface MangaRepositoryInterface
|
||||
{
|
||||
public function findAll(int $page = 1, int $limit = 20, string $sortBy = 'title', string $sortOrder = 'asc'): array;
|
||||
public function count(): int;
|
||||
public function findById(string $id): ?Manga;
|
||||
public function save(Manga $manga): void;
|
||||
public function delete(Manga $manga): void;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Manga\Domain\Exception;
|
||||
|
||||
class InvalidExternalIdException extends MangaDomainException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Manga\Domain\Exception;
|
||||
|
||||
class InvalidMangaIdException extends MangaDomainException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Manga\Domain\Exception;
|
||||
|
||||
class InvalidMangaSlugException extends MangaDomainException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Manga\Domain\Exception;
|
||||
|
||||
class InvalidMangaTitleException extends MangaDomainException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Manga\Domain\Exception;
|
||||
|
||||
class MangaDomainException extends \DomainException
|
||||
{
|
||||
}
|
||||
80
src/Domain/Manga/Domain/Model/Manga.php
Normal file
80
src/Domain/Manga/Domain/Model/Manga.php
Normal file
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Manga\Domain\Model;
|
||||
|
||||
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;
|
||||
|
||||
readonly class Manga
|
||||
{
|
||||
public function __construct(
|
||||
private MangaId $id,
|
||||
private MangaTitle $title,
|
||||
private MangaSlug $slug,
|
||||
private string $description,
|
||||
private string $author,
|
||||
private int $publicationYear,
|
||||
private array $genres,
|
||||
private string $status,
|
||||
private ?ExternalId $externalId = null,
|
||||
private ?string $imageUrl = null,
|
||||
private ?float $rating = null,
|
||||
) {}
|
||||
|
||||
public function getId(): MangaId
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getTitle(): MangaTitle
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function getSlug(): MangaSlug
|
||||
{
|
||||
return $this->slug;
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
public function getAuthor(): string
|
||||
{
|
||||
return $this->author;
|
||||
}
|
||||
|
||||
public function getPublicationYear(): int
|
||||
{
|
||||
return $this->publicationYear;
|
||||
}
|
||||
|
||||
public function getGenres(): array
|
||||
{
|
||||
return $this->genres;
|
||||
}
|
||||
|
||||
public function getStatus(): string
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
public function getExternalId(): ?ExternalId
|
||||
{
|
||||
return $this->externalId;
|
||||
}
|
||||
|
||||
public function getImageUrl(): ?string
|
||||
{
|
||||
return $this->imageUrl;
|
||||
}
|
||||
|
||||
public function getRating(): ?float
|
||||
{
|
||||
return $this->rating;
|
||||
}
|
||||
}
|
||||
21
src/Domain/Manga/Domain/Model/ValueObject/ExternalId.php
Normal file
21
src/Domain/Manga/Domain/Model/ValueObject/ExternalId.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Manga\Domain\Model\ValueObject;
|
||||
|
||||
use App\Domain\Manga\Domain\Exception\InvalidExternalIdException;
|
||||
|
||||
readonly class ExternalId
|
||||
{
|
||||
public function __construct(
|
||||
private string $value
|
||||
) {
|
||||
if (empty($value)) {
|
||||
throw new InvalidExternalIdException('External ID cannot be empty');
|
||||
}
|
||||
}
|
||||
|
||||
public function getValue(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
}
|
||||
26
src/Domain/Manga/Domain/Model/ValueObject/MangaId.php
Normal file
26
src/Domain/Manga/Domain/Model/ValueObject/MangaId.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Manga\Domain\Model\ValueObject;
|
||||
|
||||
use App\Domain\Manga\Domain\Exception\InvalidMangaIdException;
|
||||
|
||||
readonly class MangaId
|
||||
{
|
||||
public function __construct(
|
||||
private string $value
|
||||
) {
|
||||
if (empty($value)) {
|
||||
throw new InvalidMangaIdException('Manga ID cannot be empty');
|
||||
}
|
||||
}
|
||||
|
||||
public function getValue(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function equals(self $other): bool
|
||||
{
|
||||
return $this->value === $other->value;
|
||||
}
|
||||
}
|
||||
25
src/Domain/Manga/Domain/Model/ValueObject/MangaSlug.php
Normal file
25
src/Domain/Manga/Domain/Model/ValueObject/MangaSlug.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Manga\Domain\Model\ValueObject;
|
||||
|
||||
use App\Domain\Manga\Domain\Exception\InvalidMangaSlugException;
|
||||
|
||||
readonly class MangaSlug
|
||||
{
|
||||
public function __construct(
|
||||
private string $value
|
||||
) {
|
||||
if (empty(trim($value))) {
|
||||
throw new InvalidMangaSlugException('Manga slug cannot be empty');
|
||||
}
|
||||
|
||||
if (!preg_match('/^[a-z0-9]+(?:-[a-z0-9]+)*$/', $value)) {
|
||||
throw new InvalidMangaSlugException('Invalid manga slug format');
|
||||
}
|
||||
}
|
||||
|
||||
public function getValue(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
}
|
||||
21
src/Domain/Manga/Domain/Model/ValueObject/MangaTitle.php
Normal file
21
src/Domain/Manga/Domain/Model/ValueObject/MangaTitle.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Manga\Domain\Model\ValueObject;
|
||||
|
||||
use App\Domain\Manga\Domain\Exception\InvalidMangaTitleException;
|
||||
|
||||
readonly class MangaTitle
|
||||
{
|
||||
public function __construct(
|
||||
private string $value
|
||||
) {
|
||||
if (empty(trim($value))) {
|
||||
throw new InvalidMangaTitleException('Manga title cannot be empty');
|
||||
}
|
||||
}
|
||||
|
||||
public function getValue(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Manga\Infrastructure\ApiPlatform\Dto;
|
||||
|
||||
use ApiPlatform\Metadata\ApiProperty;
|
||||
|
||||
readonly class MangaCollection
|
||||
{
|
||||
public function __construct(
|
||||
/** @var MangaListItem[] */
|
||||
public array $items,
|
||||
public int $total,
|
||||
public int $page,
|
||||
public int $limit,
|
||||
public bool $hasNextPage,
|
||||
public bool $hasPreviousPage
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Manga\Infrastructure\ApiPlatform\Dto;
|
||||
|
||||
use ApiPlatform\Metadata\ApiProperty;
|
||||
|
||||
readonly class MangaListItem
|
||||
{
|
||||
public function __construct(
|
||||
#[ApiProperty(identifier: true)]
|
||||
public string $id,
|
||||
public string $title,
|
||||
public string $slug,
|
||||
public ?string $imageUrl,
|
||||
public string $author,
|
||||
public int $publicationYear,
|
||||
public array $genres,
|
||||
public string $status,
|
||||
public ?float $rating
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Manga\Infrastructure\ApiPlatform\Resource;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use App\Domain\Manga\Infrastructure\ApiPlatform\Dto\MangaCollection;
|
||||
use App\Domain\Manga\Infrastructure\ApiPlatform\State\Provider\GetMangaListStateProvider;
|
||||
|
||||
#[ApiResource(
|
||||
shortName: 'Manga',
|
||||
operations: [
|
||||
new GetCollection(
|
||||
uriTemplate: '/mangas',
|
||||
provider: GetMangaListStateProvider::class,
|
||||
output: MangaCollection::class
|
||||
)
|
||||
]
|
||||
)]
|
||||
class MangaListResource
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Manga\Infrastructure\ApiPlatform\State\Provider;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProviderInterface;
|
||||
use App\Domain\Manga\Application\Query\GetMangaList;
|
||||
use App\Domain\Manga\Application\QueryHandler\GetMangaListHandler;
|
||||
use App\Domain\Manga\Domain\Model\Manga;
|
||||
use App\Domain\Manga\Infrastructure\ApiPlatform\Dto\MangaCollection;
|
||||
use App\Domain\Manga\Infrastructure\ApiPlatform\Dto\MangaListItem;
|
||||
|
||||
readonly class GetMangaListStateProvider implements ProviderInterface
|
||||
{
|
||||
public function __construct(
|
||||
private GetMangaListHandler $handler
|
||||
) {}
|
||||
|
||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): MangaCollection
|
||||
{
|
||||
$page = $context['filters']['page'] ?? 1;
|
||||
$limit = $context['filters']['limit'] ?? 20;
|
||||
$sortBy = $context['filters']['sortBy'] ?? 'title';
|
||||
$sortOrder = $context['filters']['sortOrder'] ?? 'asc';
|
||||
|
||||
$query = new GetMangaList($page, $limit, $sortBy, $sortOrder);
|
||||
$response = $this->handler->handle($query);
|
||||
|
||||
return new MangaCollection(
|
||||
items: array_map(
|
||||
fn (Manga $manga) => $this->createMangaListItem($manga),
|
||||
$response->mangas
|
||||
),
|
||||
total: $response->total,
|
||||
page: $response->page,
|
||||
limit: $response->limit,
|
||||
hasNextPage: $response->hasNextPage(),
|
||||
hasPreviousPage: $response->hasPreviousPage()
|
||||
);
|
||||
}
|
||||
|
||||
private function createMangaListItem(Manga $manga): MangaListItem
|
||||
{
|
||||
return new MangaListItem(
|
||||
id: $manga->getId()->getValue(),
|
||||
title: $manga->getTitle()->getValue(),
|
||||
slug: $manga->getSlug()->getValue(),
|
||||
imageUrl: $manga->getImageUrl(),
|
||||
author: $manga->getAuthor(),
|
||||
publicationYear: $manga->getPublicationYear(),
|
||||
genres: $manga->getGenres(),
|
||||
status: $manga->getStatus(),
|
||||
rating: $manga->getRating()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Manga\Infrastructure\Persistence;
|
||||
|
||||
use App\Domain\Manga\Domain\Contract\Repository\MangaRepositoryInterface;
|
||||
use App\Domain\Manga\Domain\Model\Manga as DomainManga;
|
||||
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\Entity\Manga as EntityManga;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
readonly class LegacyMangaRepository implements MangaRepositoryInterface
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManagerInterface $entityManager
|
||||
) {}
|
||||
|
||||
public function findAll(int $page = 1, int $limit = 20, string $sortBy = 'title', string $sortOrder = 'asc'): array
|
||||
{
|
||||
$offset = ($page - 1) * $limit;
|
||||
|
||||
$queryBuilder = $this->entityManager->createQueryBuilder()
|
||||
->select('m')
|
||||
->from(EntityManga::class, 'm')
|
||||
->orderBy("m.$sortBy", $sortOrder)
|
||||
->setFirstResult($offset)
|
||||
->setMaxResults($limit);
|
||||
|
||||
return array_map(
|
||||
fn (EntityManga $entity) => $this->toDomain($entity),
|
||||
$queryBuilder->getQuery()->getResult()
|
||||
);
|
||||
}
|
||||
|
||||
public function count(): int
|
||||
{
|
||||
return $this->entityManager->createQueryBuilder()
|
||||
->select('COUNT(m.id)')
|
||||
->from(EntityManga::class, 'm')
|
||||
->getQuery()
|
||||
->getSingleScalarResult();
|
||||
}
|
||||
|
||||
public function findById(string $id): ?DomainManga
|
||||
{
|
||||
$entity = $this->entityManager->find(EntityManga::class, $id);
|
||||
|
||||
return $entity ? $this->toDomain($entity) : null;
|
||||
}
|
||||
|
||||
public function save(DomainManga $manga): void
|
||||
{
|
||||
$entity = $this->entityManager->find(EntityManga::class, $manga->getId()->getValue())
|
||||
?? new EntityManga();
|
||||
|
||||
$this->updateEntity($entity, $manga);
|
||||
|
||||
$this->entityManager->persist($entity);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
public function delete(DomainManga $manga): void
|
||||
{
|
||||
$entity = $this->entityManager->find(EntityManga::class, $manga->getId()->getValue());
|
||||
|
||||
if ($entity) {
|
||||
$this->entityManager->remove($entity);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
}
|
||||
|
||||
private function toDomain(EntityManga $entity): DomainManga
|
||||
{
|
||||
return new DomainManga(
|
||||
new MangaId((string) $entity->getId()),
|
||||
new MangaTitle($entity->getTitle()),
|
||||
new MangaSlug($entity->getSlug()),
|
||||
$entity->getDescription(),
|
||||
$entity->getAuthor(),
|
||||
$entity->getPublicationYear(),
|
||||
$entity->getGenres(),
|
||||
$entity->getStatus(),
|
||||
$entity->getExternalId() ? new ExternalId($entity->getExternalId()) : null,
|
||||
$entity->getImageUrl(),
|
||||
$entity->getRating()
|
||||
);
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user