feat: Reader beginning

This commit is contained in:
ext.jeremy.guillot@maxicoffee.domains
2025-02-16 16:15:42 +01:00
parent e90c0a140e
commit 55945adc53
37 changed files with 1057 additions and 47 deletions

View 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;
}
}

View 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;
}
}

View File

@@ -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(),
);
}
}

View File

@@ -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()
);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View 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;
}
}

View File

@@ -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;
}

View File

@@ -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()));
}
}

View 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;
}
}

View 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,
];
}
}

View 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;
}
}

View 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;
}
}

View File

@@ -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()
{
}
}

View File

@@ -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()
{
}
}

View File

@@ -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()
);
}
}

View File

@@ -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
)
);
}
}

View File

@@ -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;
}
}

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -9,5 +9,6 @@ class Chapter
public readonly string $mangaId,
public readonly int $chapterNumber,
public readonly int $volumeNumber,
public ?string $cbzPath = null,
) {}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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,
) {
}
}

View File

@@ -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
)
);
}

View File

@@ -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();
}
}

View File

@@ -48,8 +48,13 @@ final class ChapterFactory extends ModelFactory
{
return [
'manga' => MangaFactory::new(),
'number' => self::faker()->randomNumber(2),
'pages' => [],
'number' => self::faker()->randomFloat(2, 0, 999),
'volume' => self::faker()->optional()->numberBetween(1, 100),
'title' => self::faker()->optional()->sentence(3),
'localPath' => self::faker()->optional()->filePath(),
'externalId' => self::faker()->optional()->uuid(),
'cbzPath' => self::faker()->optional()->filePath(),
'visible' => true,
];
}

View File

@@ -50,12 +50,20 @@ final class MangaFactory extends ModelFactory
$title = self::faker()->words(rand(1, 3), true);
return [
'slug' => $this->slugger->slug($title)->lower(),
'title' => $title,
'description' => self::faker()->text(),
'genres' => self::faker()->words(rand(1, 5)),
'publicationYear' => self::faker()->year(),
'rating' => self::faker()->randomFloat(1, 0, 10),
'slug' => $this->slugger->slug($title)->lower(),
'imageUrl' => self::faker()->optional()->imageUrl(),
'publicationYear' => self::faker()->optional()->year(),
'description' => self::faker()->optional()->text(),
'genres' => self::faker()->optional()->words(rand(1, 5)),
'createdAt' => \DateTimeImmutable::createFromMutable(self::faker()->dateTime()),
'rating' => self::faker()->optional()->randomFloat(1, 0, 10),
'author' => self::faker()->optional()->name(),
'externalId' => self::faker()->optional()->uuid(),
'status' => self::faker()->optional()->randomElement(['ongoing', 'completed', 'hiatus']),
'thumbnailUrl' => self::faker()->optional()->imageUrl(150, 150),
'monitored' => self::faker()->boolean(),
'AlternativeSlugs' => self::faker()->optional()->words(3),
];
}

View File

@@ -47,9 +47,12 @@ final class SourceFactory extends ModelFactory
protected function getDefaults(): array
{
return [
'baseUrl' => self::faker()->text(255),
'createdAt' => \DateTimeImmutable::createFromMutable(self::faker()->dateTime()),
'name' => self::faker()->optional()->company(),
'description' => self::faker()->optional()->text(),
'baseUrl' => self::faker()->url(),
'scrappingParameters' => [],
'isActive' => self::faker()->boolean(),
'createdAt' => \DateTimeImmutable::createFromMutable(self::faker()->dateTime()),
'updatedAt' => \DateTimeImmutable::createFromMutable(self::faker()->dateTime()),
];
}