feat: Reader beginning
This commit is contained in:
parent
e90c0a140e
commit
55945adc53
17
src/Domain/Reader/Application/Query/GetChapterContext.php
Normal file
17
src/Domain/Reader/Application/Query/GetChapterContext.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Reader\Application\Query;
|
||||
|
||||
final class GetChapterContext
|
||||
{
|
||||
public function __construct(
|
||||
private readonly string $chapterId
|
||||
) {
|
||||
}
|
||||
public function getChapterId(): string
|
||||
{
|
||||
return $this->chapterId;
|
||||
}
|
||||
}
|
||||
30
src/Domain/Reader/Application/Query/GetChapterPages.php
Normal file
30
src/Domain/Reader/Application/Query/GetChapterPages.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Reader\Application\Query;
|
||||
|
||||
final readonly class GetChapterPages
|
||||
{
|
||||
public function __construct(
|
||||
private string $chapterId,
|
||||
private int $page = 1,
|
||||
private int $itemsPerPage = 20
|
||||
) {
|
||||
}
|
||||
|
||||
public function getChapterId(): string
|
||||
{
|
||||
return $this->chapterId;
|
||||
}
|
||||
|
||||
public function getPage(): int
|
||||
{
|
||||
return $this->page;
|
||||
}
|
||||
|
||||
public function getItemsPerPage(): int
|
||||
{
|
||||
return $this->itemsPerPage;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Reader\Application\QueryHandler;
|
||||
|
||||
use App\Domain\Reader\Application\Query\GetChapterContext;
|
||||
use App\Domain\Reader\Application\Response\ChapterContextResponse;
|
||||
use App\Domain\Reader\Domain\Contract\Repository\ChapterRepositoryInterface;
|
||||
use App\Domain\Reader\Domain\ValueObject\ChapterId;
|
||||
|
||||
final readonly class GetChapterContextHandler
|
||||
{
|
||||
public function __construct(
|
||||
private ChapterRepositoryInterface $chapterRepository
|
||||
) {
|
||||
}
|
||||
|
||||
public function handle(GetChapterContext $query): ChapterContextResponse
|
||||
{
|
||||
$chapterId = new ChapterId($query->getChapterId());
|
||||
|
||||
$context = $this->chapterRepository->getChapterContext($chapterId);
|
||||
|
||||
return new ChapterContextResponse(
|
||||
$query->getChapterId(),
|
||||
$context->getChapterTitle(),
|
||||
$context->getNumber(),
|
||||
$context->getTotalPages(),
|
||||
$context->getPreviousChapterId()?->getValue(),
|
||||
$context->getNextChapterId()?->getValue(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Reader\Application\QueryHandler;
|
||||
|
||||
use App\Domain\Reader\Application\Query\GetChapterPages;
|
||||
use App\Domain\Reader\Application\Response\ChapterPagesResponse;
|
||||
use App\Domain\Reader\Domain\Contract\Repository\ChapterRepositoryInterface;
|
||||
use App\Domain\Reader\Domain\Exception\ChapterNotFoundException;
|
||||
use App\Domain\Reader\Domain\ValueObject\ChapterId;
|
||||
|
||||
final readonly class GetChapterPagesHandler
|
||||
{
|
||||
public function __construct(
|
||||
private ChapterRepositoryInterface $chapterRepository
|
||||
) {
|
||||
}
|
||||
|
||||
public function handle(GetChapterPages $query): ChapterPagesResponse
|
||||
{
|
||||
$chapterId = new ChapterId($query->getChapterId());
|
||||
|
||||
$totalItems = $this->chapterRepository->getTotalPagesForChapter($chapterId);
|
||||
|
||||
if ($totalItems === 0) {
|
||||
throw ChapterNotFoundException::forChapter($chapterId);
|
||||
}
|
||||
|
||||
$pages = $this->chapterRepository->getPagesForChapter(
|
||||
$chapterId,
|
||||
$query->getPage(),
|
||||
$query->getItemsPerPage()
|
||||
);
|
||||
|
||||
return new ChapterPagesResponse(
|
||||
$pages,
|
||||
$totalItems,
|
||||
$query->getPage(),
|
||||
$query->getItemsPerPage()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Reader\Application\Response;
|
||||
|
||||
final readonly class ChapterContextResponse
|
||||
{
|
||||
public function __construct(
|
||||
private string $id,
|
||||
private string $title,
|
||||
private float $number,
|
||||
private int $totalPages,
|
||||
private ?string $previousChapterId,
|
||||
private ?string $nextChapterId
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function getId(): string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function getNumber(): float
|
||||
{
|
||||
return $this->number;
|
||||
}
|
||||
|
||||
public function getTotalPages(): int
|
||||
{
|
||||
return $this->totalPages;
|
||||
}
|
||||
|
||||
public function getPreviousChapterId(): ?string
|
||||
{
|
||||
return $this->previousChapterId;
|
||||
}
|
||||
|
||||
public function getNextChapterId(): ?string
|
||||
{
|
||||
return $this->nextChapterId;
|
||||
}
|
||||
|
||||
public function getNavigation(): array
|
||||
{
|
||||
$navigation['previousChapter'] = $this->previousChapterId;
|
||||
$navigation['nextChapter'] = $this->nextChapterId;
|
||||
|
||||
return $navigation;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Reader\Application\Response;
|
||||
|
||||
use App\Domain\Reader\Domain\Model\Page;
|
||||
|
||||
final class ChapterPagesResponse
|
||||
{
|
||||
/** @var array<PageResponse> */
|
||||
private array $pages;
|
||||
private int $totalItems;
|
||||
private int $currentPage;
|
||||
private int $itemsPerPage;
|
||||
|
||||
/**
|
||||
* @param array<Page> $pages
|
||||
*/
|
||||
public function __construct(array $pages, int $totalItems, int $currentPage, int $itemsPerPage)
|
||||
{
|
||||
$this->pages = array_map(
|
||||
static fn (Page $page) => new PageResponse($page),
|
||||
$pages
|
||||
);
|
||||
$this->totalItems = $totalItems;
|
||||
$this->currentPage = $currentPage;
|
||||
$this->itemsPerPage = $itemsPerPage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<PageResponse>
|
||||
*/
|
||||
public function getPages(): array
|
||||
{
|
||||
return $this->pages;
|
||||
}
|
||||
|
||||
public function getTotalItems(): int
|
||||
{
|
||||
return $this->totalItems;
|
||||
}
|
||||
|
||||
public function getCurrentPage(): int
|
||||
{
|
||||
return $this->currentPage;
|
||||
}
|
||||
|
||||
public function getItemsPerPage(): int
|
||||
{
|
||||
return $this->itemsPerPage;
|
||||
}
|
||||
|
||||
public function getTotalPages(): int
|
||||
{
|
||||
return (int) ceil($this->totalItems / $this->itemsPerPage);
|
||||
}
|
||||
}
|
||||
43
src/Domain/Reader/Application/Response/PageResponse.php
Normal file
43
src/Domain/Reader/Application/Response/PageResponse.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Reader\Application\Response;
|
||||
|
||||
use App\Domain\Reader\Domain\Model\Page;
|
||||
|
||||
final class PageResponse
|
||||
{
|
||||
private string $id;
|
||||
private int $pageNumber;
|
||||
private string $url;
|
||||
private array $dimensions;
|
||||
|
||||
public function __construct(Page $page)
|
||||
{
|
||||
$this->id = $page->getId();
|
||||
$this->pageNumber = $page->getPageNumber()->getValue();
|
||||
$this->url = $page->getUrl();
|
||||
$this->dimensions = $page->getDimensions();
|
||||
}
|
||||
|
||||
public function getId(): string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getPageNumber(): int
|
||||
{
|
||||
return $this->pageNumber;
|
||||
}
|
||||
|
||||
public function getUrl(): string
|
||||
{
|
||||
return $this->url;
|
||||
}
|
||||
|
||||
public function getDimensions(): array
|
||||
{
|
||||
return $this->dimensions;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
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\ValueObject\ChapterId;
|
||||
|
||||
interface ChapterRepositoryInterface
|
||||
{
|
||||
/**
|
||||
* @return array<Page>
|
||||
*/
|
||||
public function getPagesForChapter(ChapterId $chapterId, int $page = 1, int $itemsPerPage = 20): array;
|
||||
|
||||
public function getChapterContext(ChapterId $chapterId): ChapterContext;
|
||||
|
||||
public function getTotalPagesForChapter(ChapterId $chapterId): int;
|
||||
|
||||
public function getPreviousChapterId(ChapterId $chapterId): ?ChapterId;
|
||||
|
||||
public function getNextChapterId(ChapterId $chapterId): ?ChapterId;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Reader\Domain\Exception;
|
||||
|
||||
use App\Domain\Reader\Domain\ValueObject\ChapterId;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
final class ChapterNotFoundException extends NotFoundHttpException
|
||||
{
|
||||
public static function forChapter(ChapterId $chapterId): self
|
||||
{
|
||||
return new self(sprintf('Le chapitre %s n\'existe pas', $chapterId->getValue()));
|
||||
}
|
||||
}
|
||||
79
src/Domain/Reader/Domain/Model/ChapterContext.php
Normal file
79
src/Domain/Reader/Domain/Model/ChapterContext.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Reader\Domain\Model;
|
||||
|
||||
use App\Domain\Reader\Domain\ValueObject\ChapterId;
|
||||
|
||||
readonly class ChapterContext
|
||||
{
|
||||
public function __construct(
|
||||
private ChapterId $id,
|
||||
private ?ChapterId $previousChapterId,
|
||||
private ?ChapterId $nextChapterId,
|
||||
private string $mangaTitle,
|
||||
private float $number,
|
||||
private ?string $chapterTitle,
|
||||
private ?string $cbzPath,
|
||||
private ?int $volume,
|
||||
private int $totalPages,
|
||||
private bool $isVisible,
|
||||
private \DateTimeImmutable $createdAt
|
||||
) {}
|
||||
|
||||
public function getId(): ChapterId
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getPreviousChapterId(): ?ChapterId
|
||||
{
|
||||
return $this->previousChapterId;
|
||||
}
|
||||
|
||||
public function getNextChapterId(): ?ChapterId
|
||||
{
|
||||
return $this->nextChapterId;
|
||||
}
|
||||
|
||||
public function getMangaTitle(): string
|
||||
{
|
||||
return $this->mangaTitle;
|
||||
}
|
||||
|
||||
public function getNumber(): float
|
||||
{
|
||||
return $this->number;
|
||||
}
|
||||
|
||||
public function getChapterTitle(): ?string
|
||||
{
|
||||
return $this->chapterTitle;
|
||||
}
|
||||
|
||||
public function getCbzPath(): ?string
|
||||
{
|
||||
return $this->cbzPath;
|
||||
}
|
||||
|
||||
public function getVolume(): ?int
|
||||
{
|
||||
return $this->volume;
|
||||
}
|
||||
|
||||
public function getTotalPages(): int
|
||||
{
|
||||
return $this->totalPages;
|
||||
}
|
||||
|
||||
public function isVisible(): bool
|
||||
{
|
||||
return $this->isVisible;
|
||||
}
|
||||
|
||||
public function getCreatedAt(): \DateTimeImmutable
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
}
|
||||
52
src/Domain/Reader/Domain/Model/Page.php
Normal file
52
src/Domain/Reader/Domain/Model/Page.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Reader\Domain\Model;
|
||||
|
||||
use App\Domain\Reader\Domain\ValueObject\PageNumber;
|
||||
|
||||
class Page
|
||||
{
|
||||
public function __construct(
|
||||
private readonly string $id,
|
||||
private readonly PageNumber $pageNumber,
|
||||
private readonly string $url,
|
||||
private readonly int $width,
|
||||
private readonly int $height
|
||||
) {
|
||||
}
|
||||
|
||||
public function getId(): string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getPageNumber(): PageNumber
|
||||
{
|
||||
return $this->pageNumber;
|
||||
}
|
||||
|
||||
public function getUrl(): string
|
||||
{
|
||||
return $this->url;
|
||||
}
|
||||
|
||||
public function getWidth(): int
|
||||
{
|
||||
return $this->width;
|
||||
}
|
||||
|
||||
public function getHeight(): int
|
||||
{
|
||||
return $this->height;
|
||||
}
|
||||
|
||||
public function getDimensions(): array
|
||||
{
|
||||
return [
|
||||
'width' => $this->width,
|
||||
'height' => $this->height,
|
||||
];
|
||||
}
|
||||
}
|
||||
22
src/Domain/Reader/Domain/ValueObject/ChapterId.php
Normal file
22
src/Domain/Reader/Domain/ValueObject/ChapterId.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Reader\Domain\ValueObject;
|
||||
|
||||
readonly class ChapterId
|
||||
{
|
||||
public function __construct(
|
||||
private string $value
|
||||
) {}
|
||||
|
||||
public function getValue(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function equals(self $other): bool
|
||||
{
|
||||
return $this->value === $other->value;
|
||||
}
|
||||
}
|
||||
36
src/Domain/Reader/Domain/ValueObject/PageNumber.php
Normal file
36
src/Domain/Reader/Domain/ValueObject/PageNumber.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Reader\Domain\ValueObject;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
final class PageNumber
|
||||
{
|
||||
private int $value;
|
||||
|
||||
public function __construct(int $value)
|
||||
{
|
||||
if ($value < 1) {
|
||||
throw new InvalidArgumentException('Le numéro de page doit être supérieur à 0');
|
||||
}
|
||||
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
public function getValue(): int
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function equals(self $other): bool
|
||||
{
|
||||
return $this->value === $other->value;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return (string) $this->value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?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\ChapterContextProvider;
|
||||
use Symfony\Component\Serializer\Annotation\Groups;
|
||||
|
||||
#[ApiResource(
|
||||
shortName: 'Reader',
|
||||
operations: [
|
||||
new Get(
|
||||
uriTemplate: '/reader/chapter/{chapterId}',
|
||||
openapiContext: [
|
||||
'summary' => 'Récupère le contexte d\'un chapitre',
|
||||
'description' => 'Retourne les métadonnées du chapitre et sa navigation',
|
||||
'parameters' => [
|
||||
[
|
||||
'name' => 'chapterId',
|
||||
'in' => 'path',
|
||||
'required' => true,
|
||||
'schema' => ['type' => 'string'],
|
||||
],
|
||||
],
|
||||
],
|
||||
provider: ChapterContextProvider::class
|
||||
),
|
||||
],
|
||||
)]
|
||||
class ChapterContextResource
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?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\ChapterPagesProvider;
|
||||
use Symfony\Component\Serializer\Annotation\Groups;
|
||||
|
||||
#[ApiResource(
|
||||
shortName: 'Reader',
|
||||
operations: [
|
||||
new Get(
|
||||
uriTemplate: '/reader/chapter/{chapterId}/pages',
|
||||
openapiContext: [
|
||||
'summary' => 'Récupère les pages d\'un chapitre',
|
||||
'description' => 'Retourne une collection paginée des pages du chapitre',
|
||||
'parameters' => [
|
||||
[
|
||||
'name' => 'chapterId',
|
||||
'in' => 'path',
|
||||
'required' => true,
|
||||
'schema' => ['type' => 'string'],
|
||||
],
|
||||
[
|
||||
'name' => 'page',
|
||||
'in' => 'query',
|
||||
'required' => false,
|
||||
'schema' => ['type' => 'integer', 'default' => 1],
|
||||
],
|
||||
[
|
||||
'name' => 'itemsPerPage',
|
||||
'in' => 'query',
|
||||
'required' => false,
|
||||
'schema' => ['type' => 'integer', 'default' => 20],
|
||||
],
|
||||
],
|
||||
],
|
||||
provider: ChapterPagesProvider::class
|
||||
),
|
||||
],
|
||||
)]
|
||||
class ChapterPagesResource
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?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\GetChapterContext;
|
||||
use App\Domain\Reader\Application\QueryHandler\GetChapterContextHandler;
|
||||
use App\Domain\Reader\Application\Response\ChapterContextResponse;
|
||||
|
||||
|
||||
final readonly class ChapterContextProvider implements ProviderInterface
|
||||
{
|
||||
public function __construct(
|
||||
private GetChapterContextHandler $handler
|
||||
) {
|
||||
}
|
||||
|
||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): ChapterContextResponse
|
||||
{
|
||||
$response = $this->handler->handle(
|
||||
new GetChapterContext(
|
||||
$uriVariables['chapterId']
|
||||
)
|
||||
);
|
||||
|
||||
return new ChapterContextResponse(
|
||||
id: $response->getId(),
|
||||
title: $response->getTitle(),
|
||||
number: $response->getNumber(),
|
||||
totalPages: $response->getTotalPages(),
|
||||
previousChapterId: $response->getPreviousChapterId(),
|
||||
nextChapterId: $response->getNextChapterId()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?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\GetChapterPages;
|
||||
use App\Domain\Reader\Application\QueryHandler\GetChapterPagesHandler;
|
||||
use App\Domain\Reader\Application\Response\ChapterPagesResponse;
|
||||
|
||||
final readonly class ChapterPagesProvider implements ProviderInterface
|
||||
{
|
||||
public function __construct(
|
||||
private GetChapterPagesHandler $handler
|
||||
) {
|
||||
}
|
||||
|
||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): ChapterPagesResponse
|
||||
{
|
||||
$page = $context['filters']['page'] ?? 1;
|
||||
$itemsPerPage = $context['filters']['itemsPerPage'] ?? 20;
|
||||
|
||||
return $this->handler->handle(
|
||||
new GetChapterPages(
|
||||
$uriVariables['chapterId'],
|
||||
(int) $page,
|
||||
(int) $itemsPerPage
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Reader\Infrastructure\Persistence;
|
||||
|
||||
use App\Domain\Reader\Domain\Contract\Repository\ChapterRepositoryInterface;
|
||||
use App\Domain\Reader\Domain\Exception\ChapterNotFoundException;
|
||||
use App\Domain\Reader\Domain\Model\ChapterContext;
|
||||
use App\Domain\Reader\Domain\Model\Page;
|
||||
use App\Domain\Reader\Domain\ValueObject\ChapterId;
|
||||
use App\Domain\Reader\Domain\ValueObject\PageNumber;
|
||||
use App\Entity\Chapter as ChapterEntity;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use ZipArchive;
|
||||
|
||||
readonly class LegacyChapterRepository implements ChapterRepositoryInterface
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManagerInterface $entityManager
|
||||
) {}
|
||||
|
||||
public function getPagesForChapter(ChapterId $chapterId, int $page = 1, int $itemsPerPage = 20): array
|
||||
{
|
||||
$chapter = $this->entityManager->getRepository(ChapterEntity::class)->findOneBy([
|
||||
'id' => $chapterId->getValue()
|
||||
]);
|
||||
|
||||
$cbzPath = $chapter->getCbzPath();
|
||||
|
||||
if (!$cbzPath) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$zip = new ZipArchive();
|
||||
$zip->open($cbzPath);
|
||||
|
||||
$pages = [];
|
||||
$start = ($page - 1) * $itemsPerPage;
|
||||
$end = min($start + $itemsPerPage, $zip->numFiles);
|
||||
|
||||
for ($i = $start; $i < $end; $i++) {
|
||||
$stat = $zip->statIndex($i);
|
||||
if ($stat === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$imageContent = $zip->getFromIndex($i);
|
||||
if ($imageContent === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$imageSize = @getimagesizefromstring($imageContent);
|
||||
if ($imageSize === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$pages[] = new Page(
|
||||
$stat['name'],
|
||||
new PageNumber($i + 1),
|
||||
sprintf('/api/chapters/%s/pages/%d', $chapterId->getValue(), $i + 1),
|
||||
$imageSize[0],
|
||||
$imageSize[1]
|
||||
);
|
||||
}
|
||||
|
||||
$zip->close();
|
||||
return $pages;
|
||||
}
|
||||
|
||||
public function getChapterContext(ChapterId $chapterId): ChapterContext
|
||||
{
|
||||
/** @var ChapterEntity $chapter */
|
||||
$chapter = $this->entityManager->getRepository(ChapterEntity::class)->findOneBy([
|
||||
'id' => $chapterId->getValue()
|
||||
]);
|
||||
|
||||
if (!$chapter) {
|
||||
throw ChapterNotFoundException::forChapter($chapterId);
|
||||
}
|
||||
|
||||
return new ChapterContext(
|
||||
id: $chapterId,
|
||||
previousChapterId: $this->getPreviousChapterId($chapterId),
|
||||
nextChapterId: $this->getNextChapterId($chapterId),
|
||||
mangaTitle: $chapter->getManga()->getTitle(),
|
||||
number: $chapter->getNumber(),
|
||||
chapterTitle: $chapter->getTitle(),
|
||||
cbzPath: $chapter->getCbzPath(),
|
||||
volume: $chapter->getVolume(),
|
||||
totalPages: 0,
|
||||
isVisible: $chapter->isVisible(),
|
||||
createdAt: new \DateTimeImmutable()
|
||||
);
|
||||
}
|
||||
|
||||
public function getTotalPagesForChapter(ChapterId $chapterId): int
|
||||
{
|
||||
$chapter = $this->entityManager->getRepository(ChapterEntity::class)->findOneBy([
|
||||
'id' => $chapterId->getValue()
|
||||
]);
|
||||
|
||||
$cbzPath = $chapter->getCbzPath();
|
||||
|
||||
if (!$cbzPath) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$zip = new ZipArchive();
|
||||
$zip->open($cbzPath);
|
||||
return $zip->numFiles;
|
||||
}
|
||||
|
||||
public function getPreviousChapterId(ChapterId $chapterId): ?ChapterId
|
||||
{
|
||||
$currentChapter = $this->entityManager->getRepository(ChapterEntity::class)->findOneBy([
|
||||
'id' => $chapterId->getValue()
|
||||
]);
|
||||
|
||||
$qb = $this->entityManager->createQueryBuilder();
|
||||
$qb->select('c')
|
||||
->from(ChapterEntity::class, 'c')
|
||||
->where('c.manga = :manga')
|
||||
->andWhere('c.number < :number')
|
||||
->orderBy('c.number', 'DESC')
|
||||
->setMaxResults(1)
|
||||
->setParameters([
|
||||
'manga' => $currentChapter->getManga(),
|
||||
'number' => $currentChapter->getNumber()
|
||||
]);
|
||||
|
||||
$previousChapter = $qb->getQuery()->getOneOrNullResult();
|
||||
|
||||
return $previousChapter ? new ChapterId((string) $previousChapter->getId()) : null;
|
||||
}
|
||||
|
||||
public function getNextChapterId(ChapterId $chapterId): ?ChapterId
|
||||
{
|
||||
$currentChapter = $this->entityManager->getRepository(ChapterEntity::class)->findOneBy([
|
||||
'id' => $chapterId->getValue()
|
||||
]);
|
||||
|
||||
$qb = $this->entityManager->createQueryBuilder();
|
||||
$qb->select('c')
|
||||
->from(ChapterEntity::class, 'c')
|
||||
->where('c.manga = :manga')
|
||||
->andWhere('c.number > :number')
|
||||
->orderBy('c.number', 'ASC')
|
||||
->setMaxResults(1)
|
||||
->setParameters([
|
||||
'manga' => $currentChapter->getManga(),
|
||||
'number' => $currentChapter->getNumber()
|
||||
]);
|
||||
|
||||
$nextChapter = $qb->getQuery()->getOneOrNullResult();
|
||||
|
||||
return $nextChapter ? new ChapterId((string) $nextChapter->getId()) : null;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Domain\Scraping\Application\CommandHandler;
|
||||
|
||||
use App\Domain\Scraping\Application\Command\ScrapeChapter;
|
||||
use App\Domain\Scraping\Domain\Contract\Repository\ChapterRepositoryInterface;
|
||||
use App\Domain\Scraping\Domain\Contract\Repository\ScrapingJobRepositoryInterface;
|
||||
use App\Domain\Scraping\Domain\Contract\Service\ScraperInterface;
|
||||
use App\Domain\Scraping\Domain\Event\ChapterScraped;
|
||||
@@ -18,6 +19,7 @@ readonly class ScrapeChapterHandler
|
||||
public function __construct(
|
||||
private ScraperInterface $scraper,
|
||||
private ScrapingJobRepositoryInterface $scrapingJobRepository,
|
||||
private ChapterRepositoryInterface $chapterRepository,
|
||||
private MessageBusInterface $eventBus
|
||||
) {
|
||||
}
|
||||
@@ -42,6 +44,9 @@ readonly class ScrapeChapterHandler
|
||||
$this->eventBus->dispatch(new ChapterScrapingFailed($command->mangaId, $command->chapterNumber, $job->failureReason));
|
||||
}elseif ($job->status === ScrapingStatus::COMPLETED) {
|
||||
$this->eventBus->dispatch(new ChapterScraped($job->getId()));
|
||||
$chapter = $this->chapterRepository->getByMangaIdAndChapterNumber($command->mangaId, $command->chapterNumber);
|
||||
$chapter->cbzPath = $job->cbzPath->getPath();
|
||||
$this->chapterRepository->save($chapter);
|
||||
}
|
||||
|
||||
$this->scrapingJobRepository->save($job);
|
||||
|
||||
@@ -7,4 +7,5 @@ use App\Domain\Scraping\Domain\Model\Chapter;
|
||||
interface ChapterRepositoryInterface
|
||||
{
|
||||
public function getByMangaIdAndChapterNumber(string $mangaId, int $chapterNumber): Chapter;
|
||||
}
|
||||
public function save(Chapter $chapter): void;
|
||||
}
|
||||
|
||||
@@ -9,5 +9,6 @@ class Chapter
|
||||
public readonly string $mangaId,
|
||||
public readonly int $chapterNumber,
|
||||
public readonly int $volumeNumber,
|
||||
public ?string $cbzPath = null,
|
||||
) {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,17 +2,16 @@
|
||||
|
||||
namespace App\Domain\Scraping\Domain\Model\ValueObject;
|
||||
|
||||
class CbzPath
|
||||
readonly class CbzPath
|
||||
{
|
||||
public function __construct(private readonly string $path)
|
||||
public function __construct(private string $path)
|
||||
{
|
||||
if (empty($path)) {
|
||||
throw new \InvalidArgumentException('Le chemin du fichier CBZ ne peut pas être vide');
|
||||
}
|
||||
}
|
||||
|
||||
public function getPath(): string
|
||||
{
|
||||
return $this->path;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,15 +21,15 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
readonly class ScrapeChapterRequest
|
||||
{
|
||||
public function __construct(
|
||||
#[ApiProperty(description: 'ID du chapitre à scraper')]
|
||||
#[Assert\NotBlank]
|
||||
public string $chapterId,
|
||||
#[ApiProperty(description: 'ID de la source à utiliser')]
|
||||
#[Assert\NotBlank]
|
||||
public string $sourceId,
|
||||
#[ApiProperty(description: 'ID du manga')]
|
||||
#[Assert\NotBlank]
|
||||
public string $mangaId,
|
||||
#[ApiProperty(description: 'Numéro du chapitre')]
|
||||
#[Assert\NotBlank]
|
||||
public string $chapterNumber,
|
||||
#[ApiProperty(description: 'ID de la source')]
|
||||
#[Assert\NotBlank]
|
||||
public string $sourceId,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,9 +22,9 @@ final class ScrapeChapterStateProcessor implements ProcessorInterface
|
||||
{
|
||||
$this->commandBus->dispatch(
|
||||
new ScrapeChapter(
|
||||
$data->chapterId,
|
||||
$data->sourceId,
|
||||
$data->mangaId
|
||||
$data->mangaId,
|
||||
$data->chapterNumber,
|
||||
$data->sourceId
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,12 +7,15 @@ use App\Domain\Scraping\Domain\Exception\ChapterNotFoundException;
|
||||
use App\Domain\Scraping\Domain\Model\Chapter;
|
||||
use App\Entity\Chapter as EntityChapter;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
class LegacyChapterRepository implements ChapterRepositoryInterface
|
||||
readonly class LegacyChapterRepository implements ChapterRepositoryInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private EntityManagerInterface $entityManager,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws ChapterNotFoundException
|
||||
*/
|
||||
public function getByMangaIdAndChapterNumber(string $mangaId, int $chapterNumber): Chapter
|
||||
{
|
||||
$chapterEntity = $this->entityManager->getRepository(EntityChapter::class)->findOneBy([
|
||||
@@ -31,4 +34,23 @@ class LegacyChapterRepository implements ChapterRepositoryInterface
|
||||
volumeNumber: $chapterEntity->getVolume(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ChapterNotFoundException
|
||||
*/
|
||||
public function save(Chapter $chapter): void
|
||||
{
|
||||
$chapterEntity = $this->entityManager->getRepository(EntityChapter::class)->findOneBy([
|
||||
'id' => $chapter->id,
|
||||
]);
|
||||
|
||||
if (!$chapterEntity) {
|
||||
throw new ChapterNotFoundException();
|
||||
}
|
||||
|
||||
$chapterEntity->setCbzPath($chapter->cbzPath);
|
||||
|
||||
$this->entityManager->persist($chapterEntity);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user