feat: GetPage endpoint

This commit is contained in:
ext.jeremy.guillot@maxicoffee.domains
2025-02-16 18:22:20 +01:00
parent 55945adc53
commit 33f5a5568a
14 changed files with 710 additions and 0 deletions

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace App\Domain\Reader\Application\Query;
final readonly class GetChapterPage
{
public function __construct(
private string $chapterId,
private int $pageNumber
) {
}
public function getChapterId(): string
{
return $this->chapterId;
}
public function getPageNumber(): int
{
return $this->pageNumber;
}
}

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace App\Domain\Reader\Application\QueryHandler;
use App\Domain\Reader\Application\Query\GetChapterPage;
use App\Domain\Reader\Application\Response\ChapterPageResponse;
use App\Domain\Reader\Domain\Contract\Repository\ChapterRepositoryInterface;
use App\Domain\Reader\Domain\Exception\ChapterNotFoundException;
use App\Domain\Reader\Domain\Exception\PageNotFoundException;
use App\Domain\Reader\Domain\ValueObject\ChapterId;
use App\Domain\Reader\Domain\ValueObject\PageNumber;
final readonly class GetChapterPageHandler
{
public function __construct(
private ChapterRepositoryInterface $chapterRepository
) {
}
public function handle(GetChapterPage $query): ChapterPageResponse
{
$chapterId = new ChapterId($query->getChapterId());
$pageNumber = new PageNumber($query->getPageNumber());
$totalPages = $this->chapterRepository->getTotalPagesForChapter($chapterId);
if ($totalPages === 0) {
throw ChapterNotFoundException::forChapter($chapterId);
}
if ($pageNumber->getValue() > $totalPages) {
throw PageNotFoundException::forPage($chapterId, $pageNumber);
}
$page = $this->chapterRepository->getPageContent($chapterId, $pageNumber);
return new ChapterPageResponse(
$page->getId(),
$page->getPageNumber()->getValue(),
$page->getBase64Content(),
$page->getMimeType(),
$page->getDimensions()
);
}
}

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace App\Domain\Reader\Application\Response;
final readonly class ChapterPageResponse
{
public function __construct(
private string $id,
private int $pageNumber,
private string $base64Content,
private string $mimeType,
private array $dimensions
) {
}
public function getId(): string
{
return $this->id;
}
public function getPageNumber(): int
{
return $this->pageNumber;
}
public function getBase64Content(): string
{
return $this->base64Content;
}
public function getMimeType(): string
{
return $this->mimeType;
}
public function getDimensions(): array
{
return $this->dimensions;
}
}

View File

@@ -6,7 +6,9 @@ namespace App\Domain\Reader\Domain\Contract\Repository;
use App\Domain\Reader\Domain\Model\ChapterContext;
use App\Domain\Reader\Domain\Model\Page;
use App\Domain\Reader\Domain\Model\PageContent;
use App\Domain\Reader\Domain\ValueObject\ChapterId;
use App\Domain\Reader\Domain\ValueObject\PageNumber;
interface ChapterRepositoryInterface
{
@@ -22,4 +24,6 @@ interface ChapterRepositoryInterface
public function getPreviousChapterId(ChapterId $chapterId): ?ChapterId;
public function getNextChapterId(ChapterId $chapterId): ?ChapterId;
public function getPageContent(ChapterId $chapterId, PageNumber $pageNumber): PageContent;
}

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace App\Domain\Reader\Domain\Model;
use App\Domain\Reader\Domain\ValueObject\PageNumber;
final readonly class PageContent
{
public function __construct(
private string $id,
private PageNumber $pageNumber,
private string $base64Content,
private string $mimeType,
private int $width,
private int $height
) {
}
public function getId(): string
{
return $this->id;
}
public function getPageNumber(): PageNumber
{
return $this->pageNumber;
}
public function getBase64Content(): string
{
return $this->base64Content;
}
public function getMimeType(): string
{
return $this->mimeType;
}
public function getDimensions(): array
{
return [
'width' => $this->width,
'height' => $this->height,
];
}
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace App\Domain\Reader\Infrastructure\ApiPlatform\Resource;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use App\Domain\Reader\Infrastructure\ApiPlatform\State\Provider\ChapterPageProvider;
#[ApiResource(
shortName: 'Reader',
operations: [
new Get(
uriTemplate: '/reader/chapter/{chapterId}/page/{pageNumber}',
openapiContext: [
'summary' => 'Récupère une page spécifique d\'un chapitre',
'description' => 'Retourne le contenu d\'une page en base64 avec ses métadonnées',
'parameters' => [
[
'name' => 'chapterId',
'in' => 'path',
'required' => true,
'schema' => ['type' => 'string'],
'description' => 'L\'identifiant du chapitre'
],
[
'name' => 'pageNumber',
'in' => 'path',
'required' => true,
'schema' => ['type' => 'integer', 'minimum' => 1],
'description' => 'Le numéro de la page à récupérer'
],
],
],
provider: ChapterPageProvider::class
),
],
)]
class ChapterPageResource
{
public function __construct()
{
}
}

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace App\Domain\Reader\Infrastructure\ApiPlatform\State\Provider;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use App\Domain\Reader\Application\Query\GetChapterPage;
use App\Domain\Reader\Application\QueryHandler\GetChapterPageHandler;
use App\Domain\Reader\Application\Response\ChapterPageResponse;
final readonly class ChapterPageProvider implements ProviderInterface
{
public function __construct(
private GetChapterPageHandler $handler
) {
}
public function provide(Operation $operation, array $uriVariables = [], array $context = []): ChapterPageResponse
{
return $this->handler->handle(
new GetChapterPage(
$uriVariables['chapterId'],
(int) $uriVariables['pageNumber']
)
);
}
}

View File

@@ -13,6 +13,8 @@ use App\Domain\Reader\Domain\ValueObject\PageNumber;
use App\Entity\Chapter as ChapterEntity;
use Doctrine\ORM\EntityManagerInterface;
use ZipArchive;
use App\Domain\Reader\Domain\Exception\PageNotFoundException;
use App\Domain\Reader\Domain\Model\PageContent;
readonly class LegacyChapterRepository implements ChapterRepositoryInterface
{
@@ -100,6 +102,10 @@ readonly class LegacyChapterRepository implements ChapterRepositoryInterface
'id' => $chapterId->getValue()
]);
if (!$chapter) {
throw ChapterNotFoundException::forChapter($chapterId);
}
$cbzPath = $chapter->getCbzPath();
if (!$cbzPath) {
@@ -156,4 +162,62 @@ readonly class LegacyChapterRepository implements ChapterRepositoryInterface
return $nextChapter ? new ChapterId((string) $nextChapter->getId()) : null;
}
public function getPageContent(ChapterId $chapterId, PageNumber $pageNumber): PageContent
{
$chapter = $this->entityManager->getRepository(ChapterEntity::class)->findOneBy([
'id' => $chapterId->getValue()
]);
if (!$chapter) {
throw ChapterNotFoundException::forChapter($chapterId);
}
$cbzPath = $chapter->getCbzPath();
if (!$cbzPath || !file_exists($cbzPath)) {
throw ChapterNotFoundException::forChapter($chapterId);
}
$zip = new ZipArchive();
$zip->open($cbzPath);
if ($pageNumber->getValue() > $zip->numFiles) {
$zip->close();
throw PageNotFoundException::forPage($chapterId, $pageNumber);
}
$index = $pageNumber->getValue() - 1;
$stat = $zip->statIndex($index);
if ($stat === false) {
$zip->close();
throw PageNotFoundException::forPage($chapterId, $pageNumber);
}
$imageContent = $zip->getFromIndex($index);
if ($imageContent === false) {
$zip->close();
throw PageNotFoundException::forPage($chapterId, $pageNumber);
}
$imageSize = @getimagesizefromstring($imageContent);
if ($imageSize === false) {
$zip->close();
throw PageNotFoundException::forPage($chapterId, $pageNumber);
}
$mimeType = $imageSize['mime'] ?? 'image/jpeg';
$zip->close();
return new PageContent(
$stat['name'],
$pageNumber,
base64_encode($imageContent),
$mimeType,
$imageSize[0],
$imageSize[1]
);
}
}