feat: migrer vers Symfony 8, PHP 8.4 et les dépendances majeures associées

- PHP 8.3 → 8.4 (Dockerfile + composer.json)
- Symfony 7.0 → 8.0 (tous les composants symfony/*)
- API Platform 3.x → 4.x : migration openapiContext → openapi: new Operation(...)
- Doctrine DBAL 3 → 4 : suppression use_savepoints, replace prepare/executeQuery
- Doctrine ORM 2.x → 3.x : ClassMetadataInfo → ClassMetadata, setParameters → setParameter
- Doctrine Bundle 2.x → 3.x, Fixtures Bundle 3.x → 4.x
- zenstruck/foundry 1.x → 2.x : ModelFactory → PersistentObjectFactory, getDefaults → defaults
- phpmd/phpmd 2.x → 3.x-dev (seule version supportant Symfony 8)
- phparkitect 0.3 → 0.8 : NotDependsOnTheseNamespaces prend un array
- symfony/mercure-bundle 0.3 → 0.4, symfony/monolog-bundle 3 → 4
- Suppression de runtime/frankenphp-symfony (intégré nativement dans symfony/runtime 8)
- worker.Caddyfile : suppression de APP_RUNTIME (détection automatique Symfony 8)
- Routes errors.xml/wdt.xml/profiler.xml → .php (Symfony 8 supprime le XML)
- Types::ARRAY → Types::JSON dans Entity/Manga.php (DBAL 4 retire array type)
- Suppression de src/Schedule.php (doublon vide avec MonitoringSchedule)
- Tests : hydra:Collection → Collection, hydra:member → member (API Platform 4)
This commit is contained in:
ext.jeremy.guillot@maxicoffee.domains
2026-03-26 17:55:12 +01:00
parent 5a0888eb28
commit 5ed303612a
371 changed files with 6194 additions and 4160 deletions

View File

@@ -7,7 +7,7 @@ final readonly class ConvertFileCommand
public function __construct(
public string $filePath,
public string $originalFilename,
public int $fileSize
public int $fileSize,
) {
}
}

View File

@@ -13,7 +13,7 @@ final readonly class ConvertFileCommandHandler
private const MAX_FILE_SIZE = 150 * 1024 * 1024; // 150MB
public function __construct(
private ConversionServiceInterface $conversionService
private ConversionServiceInterface $conversionService,
) {
}

View File

@@ -10,7 +10,7 @@ final readonly class ConversionResponse
public string $convertedFilePath,
public string $outputFilename,
public int $originalFileSize,
public int $convertedFileSize
public int $convertedFileSize,
) {
}

View File

@@ -2,9 +2,7 @@
namespace App\Domain\Conversion\Domain\Exception;
use RuntimeException;
class ConversionException extends RuntimeException
class ConversionException extends \RuntimeException
{
public static function fileNotFound(string $filePath): self
{

View File

@@ -7,7 +7,7 @@ final readonly class ConversionRequest
public function __construct(
private string $filePath,
private string $originalFilename,
private int $fileSize
private int $fileSize,
) {
}
@@ -29,6 +29,7 @@ final readonly class ConversionRequest
public function getOutputFilename(): string
{
$pathInfo = pathinfo($this->originalFilename, PATHINFO_FILENAME);
return $pathInfo . '.cbz';
return $pathInfo.'.cbz';
}
}

View File

@@ -8,7 +8,7 @@ final readonly class ConversionResult
private string $convertedFilePath,
private string $outputFilename,
private int $originalFileSize,
private int $convertedFileSize
private int $convertedFileSize,
) {
}

View File

@@ -5,18 +5,16 @@ namespace App\Domain\Conversion\Infrastructure\ApiPlatform\Controller;
use App\Domain\Conversion\Application\Command\ConvertFileCommand;
use App\Domain\Conversion\Application\CommandHandler\ConvertFileCommandHandler;
use App\Domain\Conversion\Domain\Exception\ConversionException;
use App\Domain\Conversion\Infrastructure\ApiPlatform\Resource\ConvertFileResource;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
#[AsController]
final class ConvertFileController extends AbstractController
{
public function __construct(
private readonly ConvertFileCommandHandler $commandHandler
private readonly ConvertFileCommandHandler $commandHandler,
) {
}
@@ -25,7 +23,7 @@ final class ConvertFileController extends AbstractController
$uploadedFile = $request->files->get('file');
if (!$uploadedFile) {
return $this->json([
['propertyPath' => 'file', 'message' => 'Please upload a file']
['propertyPath' => 'file', 'message' => 'Please upload a file'],
], 422);
}
@@ -58,7 +56,6 @@ final class ConvertFileController extends AbstractController
'Content-Disposition' => sprintf('attachment; filename=%s', $response->outputFilename),
]
);
} catch (ConversionException $e) {
return $this->json(['error' => $e->getMessage()], 400);
}
@@ -72,8 +69,9 @@ final class ConvertFileController extends AbstractController
if (!$uploadedFile->isValid()) {
$errors[] = [
'propertyPath' => 'file',
'message' => 'The uploaded file is not valid: ' . $uploadedFile->getErrorMessage()
'message' => 'The uploaded file is not valid: '.$uploadedFile->getErrorMessage(),
];
return $errors;
}
@@ -82,7 +80,7 @@ final class ConvertFileController extends AbstractController
if ($uploadedFile->getSize() > $maxSize) {
$errors[] = [
'propertyPath' => 'file',
'message' => 'The uploaded file is too large. Allowed size is 150MB.'
'message' => 'The uploaded file is too large. Allowed size is 150MB.',
];
}
@@ -93,7 +91,7 @@ final class ConvertFileController extends AbstractController
if (!in_array($extension, $allowedExtensions)) {
$errors[] = [
'propertyPath' => 'file',
'message' => 'Please upload a valid CBR or CBZ file'
'message' => 'Please upload a valid CBR or CBZ file',
];
}

View File

@@ -4,10 +4,10 @@ namespace App\Domain\Conversion\Infrastructure\ApiPlatform\Resource;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Post;
use ApiPlatform\OpenApi\Model;
use ApiPlatform\OpenApi\Model\Operation;
use ApiPlatform\OpenApi\Model\RequestBody;
use App\Domain\Conversion\Infrastructure\ApiPlatform\Controller\ConvertFileController;
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\Validator\Constraints as Assert;
#[ApiResource(
shortName: 'Conversion',
@@ -16,11 +16,11 @@ use Symfony\Component\Validator\Constraints as Assert;
uriTemplate: '/conversions/convert',
controller: ConvertFileController::class,
deserialize: false,
openapiContext: [
'summary' => 'Convert comic book file to CBZ',
'description' => 'Converts a CBR or CBZ file to CBZ format and returns the converted file for download',
'requestBody' => [
'content' => [
openapi: new Operation(
summary: 'Convert comic book file to CBZ',
description: 'Converts a CBR or CBZ file to CBZ format and returns the converted file for download',
requestBody: new RequestBody(
content: new \ArrayObject([
'multipart/form-data' => [
'schema' => [
'type' => 'object',
@@ -29,28 +29,28 @@ use Symfony\Component\Validator\Constraints as Assert;
'file' => [
'type' => 'string',
'format' => 'binary',
'description' => 'Comic book file to convert (CBR, CBZ, max 150MB)'
]
]
]
]
]
],
'responses' => [
'description' => 'Comic book file to convert (CBR, CBZ, max 150MB)',
],
],
],
],
])
),
responses: [
'200' => [
'description' => 'File converted successfully',
'content' => [
'application/x-cbz' => [
'schema' => [
'type' => 'string',
'format' => 'binary'
]
]
]
]
'format' => 'binary',
],
],
],
],
]
]
)
)
),
]
)]
class ConvertFileResource

View File

@@ -16,7 +16,7 @@ final class ConversionService implements ConversionServiceInterface
public function __construct(string $projectDir)
{
$this->tempDir = $projectDir . '/public/tmp';
$this->tempDir = $projectDir.'/public/tmp';
$this->filesystem = new Filesystem();
}
@@ -40,10 +40,10 @@ final class ConversionService implements ConversionServiceInterface
private function convertCbrToCbz(string $cbrPath): string
{
$tempDir = $this->tempDir . '/' . uniqid('cbr_conversion_');
$tempDir = $this->tempDir.'/'.uniqid('cbr_conversion_');
$this->filesystem->mkdir($tempDir);
$extractDir = $tempDir . '/extract';
$extractDir = $tempDir.'/extract';
$this->filesystem->mkdir($extractDir);
// Essayer d'extraire avec unrar-free
@@ -56,16 +56,16 @@ final class ConversionService implements ConversionServiceInterface
$process->run();
if (!$process->isSuccessful()) {
throw new \RuntimeException("Extraction failed: " . $process->getErrorOutput());
throw new \RuntimeException('Extraction failed: '.$process->getErrorOutput());
}
}
// Créer le CBZ
$cbzFileName = pathinfo($cbrPath, PATHINFO_FILENAME) . '.cbz';
$cbzPath = $this->tempDir . '/' . $cbzFileName;
$cbzFileName = pathinfo($cbrPath, PATHINFO_FILENAME).'.cbz';
$cbzPath = $this->tempDir.'/'.$cbzFileName;
$zip = new \ZipArchive();
if ($zip->open($cbzPath, \ZipArchive::CREATE) !== true) {
throw new \RuntimeException("Cannot create ZIP file");
if (true !== $zip->open($cbzPath, \ZipArchive::CREATE)) {
throw new \RuntimeException('Cannot create ZIP file');
}
$files = new \RecursiveIteratorIterator(

View File

@@ -7,7 +7,7 @@ readonly class ChapterEditData
public function __construct(
public string $id,
public ?string $title = null,
public ?int $volume = null
public ?int $volume = null,
) {
}
}

View File

@@ -2,12 +2,10 @@
namespace App\Domain\Manga\Application\Command;
use DateTimeImmutable;
readonly class CheckMonitoredMangas
{
public function __construct(
public ?DateTimeImmutable $since = null
public ?\DateTimeImmutable $since = null,
) {
}
}

View File

@@ -14,7 +14,7 @@ readonly class CreateManga
public string $status,
public ?string $externalId,
public ?string $imageUrl,
public ?float $rating
public ?float $rating,
) {
}
}

View File

@@ -5,7 +5,7 @@ namespace App\Domain\Manga\Application\Command;
readonly class CreateMangaFromMangadex
{
public function __construct(
public string $externalId
public string $externalId,
) {
}
}

View File

@@ -7,7 +7,7 @@ use App\Domain\Shared\Domain\Contract\CommandInterface;
readonly class DeleteCbz implements CommandInterface
{
public function __construct(
public string $chapterId
public string $chapterId,
) {
}
}

View File

@@ -7,7 +7,7 @@ use App\Domain\Shared\Domain\Contract\CommandInterface;
readonly class DeleteChapter implements CommandInterface
{
public function __construct(
public string $chapterId
public string $chapterId,
) {
}
}

View File

@@ -7,7 +7,7 @@ use App\Domain\Shared\Domain\Contract\CommandInterface;
readonly class DeleteManga implements CommandInterface
{
public function __construct(
public string $mangaId
public string $mangaId,
) {
}
}

View File

@@ -13,7 +13,7 @@ readonly class EditManga
public ?array $genres = null,
public ?string $status = null,
public ?float $rating = null,
public ?array $alternativeSlugs = null
public ?array $alternativeSlugs = null,
) {
}
}

View File

@@ -8,7 +8,7 @@ readonly class EditMultipleChapters
* @param array<ChapterEditData> $chapters
*/
public function __construct(
public array $chapters
public array $chapters,
) {
}
}

View File

@@ -7,7 +7,7 @@ use App\Domain\Manga\Domain\Model\ValueObject\MangaId;
readonly class FetchMangaChapters
{
public function __construct(
public MangaId $mangaId
public MangaId $mangaId,
) {
}
}

View File

@@ -7,7 +7,7 @@ readonly class ImportChapter
public function __construct(
public string $mangaId,
public float $chapterNumber,
public string $fileBinary
public string $fileBinary,
) {
}
}

View File

@@ -7,7 +7,7 @@ readonly class ImportVolume
public function __construct(
public string $mangaId,
public int $volumeNumber,
public string $fileBinary
public string $fileBinary,
) {
}
}

View File

@@ -7,7 +7,7 @@ use App\Domain\Manga\Domain\Model\ValueObject\MangaId;
readonly class RefreshMangaChapters
{
public function __construct(
public MangaId $mangaId
public MangaId $mangaId,
) {
}
}

View File

@@ -8,7 +8,7 @@ readonly class ToggleMangaMonitoring
{
public function __construct(
public MangaId $mangaId,
public bool $enabled
public bool $enabled,
) {
}
}

View File

@@ -6,14 +6,13 @@ use App\Domain\Manga\Application\Command\CheckMonitoredMangas;
use App\Domain\Manga\Application\Command\RefreshMangaChapters;
use App\Domain\Manga\Application\Query\MonitoringCriteria;
use App\Domain\Manga\Domain\Contract\Repository\MangaRepositoryInterface;
use DateTimeImmutable;
use Symfony\Component\Messenger\MessageBusInterface;
readonly class CheckMonitoredMangasHandler
{
public function __construct(
private MangaRepositoryInterface $mangaRepository,
private MessageBusInterface $commandBus
private MessageBusInterface $commandBus,
) {
}
@@ -21,7 +20,7 @@ readonly class CheckMonitoredMangasHandler
{
$criteria = new MonitoringCriteria(
enabled: true,
lastCheckBefore: $command->since ?? new DateTimeImmutable('-1 hour')
lastCheckBefore: $command->since ?? new \DateTimeImmutable('-1 hour')
);
$monitoredMangas = $this->mangaRepository->findByMonitoringCriteria($criteria);

View File

@@ -3,7 +3,6 @@
namespace App\Domain\Manga\Application\CommandHandler;
use App\Domain\Manga\Application\Command\CreateMangaFromMangadex;
use App\Domain\Manga\Application\Response\CreateMangaResponse;
use App\Domain\Manga\Domain\Contract\Provider\MangaProviderInterface;
use App\Domain\Manga\Domain\Contract\Repository\MangaRepositoryInterface;
use App\Domain\Manga\Domain\Contract\Service\ImageProcessorInterface;
@@ -19,7 +18,7 @@ readonly class CreateMangaFromMangadexHandler
private MangaProviderInterface $mangaProvider,
private MangaRepositoryInterface $mangaRepository,
private ImageProcessorInterface $imageProcessor,
private EventDispatcherInterface $eventDispatcher
private EventDispatcherInterface $eventDispatcher,
) {
}
@@ -27,7 +26,7 @@ readonly class CreateMangaFromMangadexHandler
{
$manga = $this->mangaProvider->findByExternalId(new ExternalId($command->externalId));
if ($manga === null) {
if (null === $manga) {
throw new MangaNotFoundException('Manga not found on Mangadex');
}
@@ -41,7 +40,7 @@ readonly class CreateMangaFromMangadexHandler
// Met à jour le manga avec les nouveaux chemins d'images
$manga->updateImageUrls(new ImageUrls($fullImagePath, $thumbnailPath));
} catch (\Exception $e) {
throw new \RuntimeException('Erreur lors du traitement de l\'image : ' . $e->getMessage());
throw new \RuntimeException('Erreur lors du traitement de l\'image : '.$e->getMessage());
}
$this->mangaRepository->save($manga);

View File

@@ -20,7 +20,7 @@ readonly class CreateMangaHandler
public function __construct(
private MangaRepositoryInterface $mangaRepository,
private ImageProcessorInterface $imageProcessor,
private MessageBusInterface $messageBus
private MessageBusInterface $messageBus,
) {
}
@@ -48,7 +48,7 @@ readonly class CreateMangaHandler
$thumbnailPath = $this->imageProcessor->createThumbnail($fullImagePath);
$manga->updateImageUrls(new ImageUrls($fullImagePath, $thumbnailPath));
} catch (\Exception $e) {
throw new \RuntimeException('Erreur lors du traitement de l\'image : ' . $e->getMessage());
throw new \RuntimeException('Erreur lors du traitement de l\'image : '.$e->getMessage());
}
}

View File

@@ -5,8 +5,8 @@ namespace App\Domain\Manga\Application\CommandHandler;
use App\Domain\Manga\Application\Command\DeleteCbz;
use App\Domain\Manga\Domain\Contract\Repository\MangaRepositoryInterface;
use App\Domain\Manga\Domain\Contract\Service\FileServiceInterface;
use App\Domain\Manga\Domain\Exception\ChapterNotFoundException;
use App\Domain\Manga\Domain\Exception\CbzFileNotFoundException;
use App\Domain\Manga\Domain\Exception\ChapterNotFoundException;
use App\Domain\Shared\Domain\Contract\CommandHandlerInterface;
use App\Domain\Shared\Domain\Contract\CommandInterface;
@@ -14,7 +14,7 @@ readonly class DeleteCbzHandler implements CommandHandlerInterface
{
public function __construct(
private MangaRepositoryInterface $mangaRepository,
private FileServiceInterface $fileService
private FileServiceInterface $fileService,
) {
}

View File

@@ -11,7 +11,7 @@ use App\Domain\Shared\Domain\Contract\CommandInterface;
readonly class DeleteChapterHandler implements CommandHandlerInterface
{
public function __construct(
private MangaRepositoryInterface $mangaRepository
private MangaRepositoryInterface $mangaRepository,
) {
}

View File

@@ -11,7 +11,7 @@ use App\Domain\Shared\Domain\Contract\CommandInterface;
readonly class DeleteMangaHandler implements CommandHandlerInterface
{
public function __construct(
private MangaRepositoryInterface $mangaRepository
private MangaRepositoryInterface $mangaRepository,
) {
}

View File

@@ -10,7 +10,7 @@ use App\Domain\Manga\Domain\Model\ValueObject\MangaTitle;
readonly class EditMangaHandler
{
public function __construct(
private MangaRepositoryInterface $mangaRepository
private MangaRepositoryInterface $mangaRepository,
) {
}
@@ -23,35 +23,35 @@ readonly class EditMangaHandler
}
// Update only provided fields (partial update)
if ($command->title !== null) {
if (null !== $command->title) {
$manga->updateTitle(new MangaTitle($command->title));
}
if ($command->description !== null) {
if (null !== $command->description) {
$manga->updateDescription($command->description);
}
if ($command->author !== null) {
if (null !== $command->author) {
$manga->updateAuthor($command->author);
}
if ($command->publicationYear !== null) {
if (null !== $command->publicationYear) {
$manga->updatePublicationYear($command->publicationYear);
}
if ($command->genres !== null) {
if (null !== $command->genres) {
$manga->updateGenres($command->genres);
}
if ($command->status !== null) {
if (null !== $command->status) {
$manga->updateStatus($command->status);
}
if ($command->rating !== null) {
if (null !== $command->rating) {
$manga->setRating($command->rating);
}
if ($command->alternativeSlugs !== null) {
if (null !== $command->alternativeSlugs) {
$manga->updateAlternativeSlugs($command->alternativeSlugs);
}

View File

@@ -9,7 +9,7 @@ use App\Domain\Manga\Domain\Exception\ChapterNotFoundException;
readonly class EditMultipleChaptersHandler
{
public function __construct(
private MangaRepositoryInterface $mangaRepository
private MangaRepositoryInterface $mangaRepository,
) {
}
@@ -24,11 +24,11 @@ readonly class EditMultipleChaptersHandler
$manga = $this->mangaRepository->findById($chapter->getMangaId()->getValue());
if ($chapterData->title !== null) {
if (null !== $chapterData->title) {
$manga->updateChapterTitle($chapter, $chapterData->title);
}
if ($chapterData->volume !== null) {
if (null !== $chapterData->volume) {
$manga->updateChapterVolume($chapter, $chapterData->volume);
}

View File

@@ -12,7 +12,7 @@ readonly class FetchMangaChaptersHandler
{
public function __construct(
private MangaRepositoryInterface $mangaRepository,
private ChapterSynchronizationServiceInterface $chapterSynchronizationService
private ChapterSynchronizationServiceInterface $chapterSynchronizationService,
) {
}
@@ -20,12 +20,12 @@ readonly class FetchMangaChaptersHandler
{
$manga = $this->mangaRepository->findById($command->mangaId->getValue());
if ($manga === null) {
if (null === $manga) {
throw new MangaNotFoundException();
}
if ($manga->getExternalId() === null) {
throw new MangadexApiException("Manga has no external_id");
if (null === $manga->getExternalId()) {
throw new MangadexApiException('Manga has no external_id');
}
// Synchronisation initiale (pas d'événements)

View File

@@ -4,15 +4,15 @@ namespace App\Domain\Manga\Application\CommandHandler;
use App\Domain\Manga\Application\Command\ImportChapter;
use App\Domain\Manga\Domain\Contract\Repository\MangaRepositoryInterface;
use App\Domain\Manga\Domain\Exception\MangaNotFoundException;
use App\Domain\Manga\Domain\Exception\ChapterNotFoundException;
use App\Domain\Manga\Domain\Exception\MangaNotFoundException;
use App\Domain\Shared\Domain\Contract\ImageStorageInterface;
readonly class ImportChapterHandler
{
public function __construct(
private MangaRepositoryInterface $mangaRepository,
private ImageStorageInterface $imageStorage
private ImageStorageInterface $imageStorage,
) {
}
@@ -55,6 +55,6 @@ readonly class ImportChapterHandler
{
$zipMagicNumber = "\x50\x4b\x03\x04"; // PK\x03\x04
return strpos($fileBinary, $zipMagicNumber) === 0;
return 0 === strpos($fileBinary, $zipMagicNumber);
}
}

View File

@@ -11,7 +11,7 @@ readonly class ImportVolumeHandler
{
public function __construct(
private MangaRepositoryInterface $mangaRepository,
private ImageStorageInterface $imageStorage
private ImageStorageInterface $imageStorage,
) {
}
@@ -35,9 +35,7 @@ readonly class ImportVolumeHandler
);
if (empty($chapters)) {
throw new \InvalidArgumentException(
"No chapters found for manga {$command->mangaId} in volume {$command->volumeNumber}"
);
throw new \InvalidArgumentException("No chapters found for manga {$command->mangaId} in volume {$command->volumeNumber}");
}
// 4. Extract CBZ into individual images storage (shared directory for all volume chapters)
@@ -56,6 +54,6 @@ readonly class ImportVolumeHandler
{
$zipMagicNumber = "\x50\x4b\x03\x04"; // PK\x03\x04
return strpos($fileBinary, $zipMagicNumber) === 0;
return 0 === strpos($fileBinary, $zipMagicNumber);
}
}

View File

@@ -7,7 +7,6 @@ use App\Domain\Manga\Domain\Contract\Repository\MangaRepositoryInterface;
use App\Domain\Manga\Domain\Contract\Service\ChapterSynchronizationServiceInterface;
use App\Domain\Manga\Domain\Event\ChapterReadyForScraping;
use App\Domain\Manga\Domain\Model\ValueObject\ChapterId;
use DateTimeImmutable;
use Symfony\Component\Messenger\MessageBusInterface;
readonly class RefreshMangaChaptersHandler
@@ -15,7 +14,7 @@ readonly class RefreshMangaChaptersHandler
public function __construct(
private MangaRepositoryInterface $mangaRepository,
private ChapterSynchronizationServiceInterface $chapterSynchronizationService,
private MessageBusInterface $eventBus
private MessageBusInterface $eventBus,
) {
}
@@ -23,7 +22,7 @@ readonly class RefreshMangaChaptersHandler
{
$manga = $this->mangaRepository->findById($command->mangaId->getValue());
if ($manga === null) {
if (null === $manga) {
throw new \RuntimeException('Manga not found');
}
@@ -31,7 +30,7 @@ readonly class RefreshMangaChaptersHandler
$newChapterIds = $this->chapterSynchronizationService->synchronizeChapters($manga);
// Mise à jour de la date de monitoring
$manga->updateLastMonitoringCheck(new DateTimeImmutable());
$manga->updateLastMonitoringCheck(new \DateTimeImmutable());
$this->mangaRepository->save($manga);
// Événement de scraping pour chaque nouveau chapitre

View File

@@ -9,7 +9,7 @@ use App\Domain\Manga\Domain\Exception\MangaNotFoundException;
readonly class ToggleMangaMonitoringHandler
{
public function __construct(
private MangaRepositoryInterface $mangaRepository
private MangaRepositoryInterface $mangaRepository,
) {
}

View File

@@ -10,7 +10,7 @@ use App\Domain\Manga\Domain\Model\ValueObject\MangaId;
readonly class MangaCreatedEventListener
{
public function __construct(
private FetchMangaChaptersHandler $fetchMangaChaptersHandler
private FetchMangaChaptersHandler $fetchMangaChaptersHandler,
) {
}

View File

@@ -23,7 +23,7 @@ readonly class VolumeImportedEventListener
}
$chapters = $this->mangaRepository->findChaptersByMangaIdAndVolume($manga->getId()->getValue(), (int) $event->volume);
if ($chapters === []) {
if ([] === $chapters) {
return;
}

View File

@@ -7,7 +7,7 @@ use App\Domain\Shared\Domain\Contract\QueryInterface;
readonly class DownloadCbz implements QueryInterface
{
public function __construct(
public string $chapterId
public string $chapterId,
) {
}
}

View File

@@ -8,7 +8,7 @@ readonly class DownloadVolume implements QueryInterface
{
public function __construct(
public string $mangaId,
public int $volume
public int $volume,
) {
}
}

View File

@@ -7,7 +7,7 @@ namespace App\Domain\Manga\Application\Query;
readonly class FindMangaMatchByFilename
{
public function __construct(
public string $filename
public string $filename,
) {
}
}

View File

@@ -5,7 +5,7 @@ namespace App\Domain\Manga\Application\Query;
readonly class GetMangaById
{
public function __construct(
public string $id
public string $id,
) {
}
}

View File

@@ -5,7 +5,7 @@ namespace App\Domain\Manga\Application\Query;
readonly class GetMangaBySlug
{
public function __construct(
public string $slug
public string $slug,
) {
}
}

View File

@@ -8,7 +8,7 @@ readonly class GetMangaChapters
public string $mangaId,
public ?int $page = 1,
public ?int $limit = 20,
public ?string $sortOrder = 'desc'
public ?string $sortOrder = 'desc',
) {
}
}

View File

@@ -8,7 +8,7 @@ readonly class GetMangaList
public ?int $page = 1,
public ?int $limit = 20,
public ?string $sortBy = 'title',
public ?string $sortOrder = 'asc'
public ?string $sortOrder = 'asc',
) {
}
}

View File

@@ -2,13 +2,11 @@
namespace App\Domain\Manga\Application\Query;
use DateTimeImmutable;
readonly class MonitoringCriteria
{
public function __construct(
public bool $enabled,
public ?DateTimeImmutable $lastCheckBefore = null
public ?\DateTimeImmutable $lastCheckBefore = null,
) {
}
}

View File

@@ -7,7 +7,7 @@ readonly class SearchLocalManga
public function __construct(
public string $query,
public int $page = 1,
public int $limit = 20
public int $limit = 20,
) {
}
}

View File

@@ -5,7 +5,7 @@ namespace App\Domain\Manga\Application\Query;
readonly class SearchManga
{
public function __construct(
public string $title
public string $title,
) {
}
}

View File

@@ -13,7 +13,7 @@ readonly class DiscoverMangaHandler
{
public function __construct(
private MangaRepositoryInterface $mangaRepository,
private MangaProviderInterface $mangaProvider
private MangaProviderInterface $mangaProvider,
) {
}
@@ -41,7 +41,7 @@ readonly class DiscoverMangaHandler
$recommendations = array_values(array_filter(
$collection->getItems(),
fn (Manga $m) => $m->getExternalId() === null
fn (Manga $m) => null === $m->getExternalId()
|| !in_array($m->getExternalId()->getValue(), $ownedExternalIds, true)
));

View File

@@ -7,8 +7,8 @@ use App\Domain\Manga\Application\Response\DownloadResponse;
use App\Domain\Manga\Domain\Contract\Repository\MangaRepositoryInterface;
use App\Domain\Manga\Domain\Contract\Service\FileServiceInterface;
use App\Domain\Manga\Domain\Exception\CbzFileNotFoundException;
use App\Domain\Manga\Domain\Exception\ChapterNotFoundException;
use App\Domain\Manga\Domain\Exception\ChapterNotAvailableException;
use App\Domain\Manga\Domain\Exception\ChapterNotFoundException;
use App\Domain\Shared\Domain\Contract\QueryHandlerInterface;
use App\Domain\Shared\Domain\Contract\QueryInterface;
use App\Domain\Shared\Domain\Contract\ResponseInterface;
@@ -17,7 +17,7 @@ readonly class DownloadCbzHandler implements QueryHandlerInterface
{
public function __construct(
private MangaRepositoryInterface $mangaRepository,
private FileServiceInterface $fileService
private FileServiceInterface $fileService,
) {
}

View File

@@ -16,7 +16,7 @@ readonly class DownloadVolumeHandler implements QueryHandlerInterface
{
public function __construct(
private MangaRepositoryInterface $mangaRepository,
private FileServiceInterface $fileService
private FileServiceInterface $fileService,
) {
}

View File

@@ -15,7 +15,7 @@ readonly class FindMangaMatchByFilenameHandler
{
public function __construct(
private FilenameAnalyzerInterface $filenameAnalyzer,
private MangaRepositoryInterface $mangaRepository
private MangaRepositoryInterface $mangaRepository,
) {
}
@@ -70,7 +70,7 @@ readonly class FindMangaMatchByFilenameHandler
/**
* Calcule un score de correspondance entre le manga et le titre recherché
* Score plus élevé = meilleure correspondance
* Score plus élevé = meilleure correspondance.
*/
private function calculateMatchScore(Manga $manga, string $searchedTitle): int
{
@@ -97,12 +97,12 @@ readonly class FindMangaMatchByFilenameHandler
}
// Le titre du manga contient le terme recherché
if (stripos($mangaTitle, $searchedTitle) !== false) {
if (false !== stripos($mangaTitle, $searchedTitle)) {
$score += 50;
}
// Le terme recherché contient le titre du manga
if (stripos($searchedTitle, $mangaTitle) !== false) {
if (false !== stripos($searchedTitle, $mangaTitle)) {
$score += 40;
}

View File

@@ -10,7 +10,7 @@ use App\Domain\Manga\Domain\Exception\MangaNotFoundException;
readonly class GetMangaByIdHandler
{
public function __construct(
private MangaRepositoryInterface $mangaRepository
private MangaRepositoryInterface $mangaRepository,
) {
}

View File

@@ -11,7 +11,7 @@ use App\Domain\Manga\Domain\Model\ValueObject\MangaSlug;
readonly class GetMangaBySlugHandler
{
public function __construct(
private MangaRepositoryInterface $mangaRepository
private MangaRepositoryInterface $mangaRepository,
) {
}

View File

@@ -12,7 +12,7 @@ use App\Domain\Manga\Domain\Model\Chapter;
readonly class GetMangaChaptersHandler
{
public function __construct(
private MangaRepositoryInterface $mangaRepository
private MangaRepositoryInterface $mangaRepository,
) {
}
@@ -30,7 +30,7 @@ readonly class GetMangaChaptersHandler
$grouped = $this->groupChapters($allChapters);
if ($query->sortOrder === 'desc') {
if ('desc' === $query->sortOrder) {
usort($grouped, fn (ChapterResponse $a, ChapterResponse $b) => $b->number <=> $a->number);
}
@@ -58,7 +58,7 @@ readonly class GetMangaChaptersHandler
$pagesDir = $chapter->getPagesDirectory();
$volume = $chapter->getVolume();
if ($pagesDir !== null && $volume !== null) {
if (null !== $pagesDir && null !== $volume) {
if ($pagesDir === $currentPagesDir && $volume === $currentVolume) {
$currentGroup[] = $chapter;
} else {
@@ -104,7 +104,7 @@ readonly class GetMangaChaptersHandler
$max = max($numbers);
$fmt = fn (float $n) => $n == (int) $n ? (string) (int) $n : (string) $n;
$range = count($group) > 1 ? $fmt($min) . '-' . $fmt($max) : $fmt($min);
$range = count($group) > 1 ? $fmt($min).'-'.$fmt($max) : $fmt($min);
return new ChapterResponse(
id: $first->getId(),

View File

@@ -3,13 +3,13 @@
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;
use App\Domain\Manga\Domain\Contract\Repository\MangaRepositoryInterface;
readonly class GetMangaListHandler
{
public function __construct(
private MangaRepositoryInterface $mangaRepository
private MangaRepositoryInterface $mangaRepository,
) {
}
@@ -28,7 +28,7 @@ readonly class GetMangaListHandler
foreach ($mangas as $manga) {
$id = $manga->getId()->getValue();
$chapterCounts[$id] = [
'total' => $this->mangaRepository->countChapters($id),
'total' => $this->mangaRepository->countChapters($id),
'scraped' => $this->mangaRepository->countAvailableChapters($id),
];
}

View File

@@ -11,7 +11,7 @@ use App\Domain\Manga\Domain\Model\Manga;
readonly class SearchLocalMangaHandler
{
public function __construct(
private MangaRepositoryInterface $repository
private MangaRepositoryInterface $repository,
) {
}

View File

@@ -11,7 +11,7 @@ use App\Domain\Manga\Domain\Model\Manga;
readonly class SearchMangaHandler
{
public function __construct(
private MangaProviderInterface $mangaProvider
private MangaProviderInterface $mangaProvider,
) {
}
@@ -19,7 +19,6 @@ readonly class SearchMangaHandler
{
$mangaCollection = $this->mangaProvider->search($query->title);
return new MangaSearchResponse(
array_map(
fn (Manga $manga, int $index) => new MangaSearchItem(

View File

@@ -8,7 +8,7 @@ readonly class ChapterListResponse
public array $chapters,
public int $total,
public int $page,
public int $limit
public int $limit,
) {
}

View File

@@ -8,7 +8,7 @@ use Symfony\Component\HttpFoundation\Response;
readonly class DownloadResponse implements ResponseInterface
{
public function __construct(
public Response $httpResponse
public Response $httpResponse,
) {
}
}

View File

@@ -9,7 +9,7 @@ readonly class MangaListResponse
public int $total,
public int $page,
public int $limit,
public array $chapterCounts = []
public array $chapterCounts = [],
) {
}

View File

@@ -14,7 +14,7 @@ readonly class MangaMatchItem
public ?string $thumbnailUrl,
public int $matchScore,
public ?float $chapterNumber = null,
public ?float $volumeNumber = null
public ?float $volumeNumber = null,
) {
}
}

View File

@@ -13,7 +13,7 @@ readonly class MangaMatchResponse
public array $matches,
public ?float $chapterNumber,
public ?float $volumeNumber,
public array $possibleTitles
public array $possibleTitles,
) {
}

View File

@@ -18,7 +18,7 @@ readonly class MangaResponse
public ?string $imageUrl,
public ?string $thumbnailUrl,
public ?float $rating,
public bool $monitored
public bool $monitored,
) {
}
}

View File

@@ -16,7 +16,7 @@ readonly class MangaSearchItem
public string $status,
public ?string $imageUrl,
public ?string $thumbnailUrl,
public ?float $rating
public ?float $rating,
) {
}
}

View File

@@ -11,7 +11,7 @@ readonly class SearchLocalMangaResponse
public array $items,
public int $total,
public int $page,
public int $limit
public int $limit,
) {
}

View File

@@ -7,12 +7,12 @@ use App\Domain\Manga\Domain\Exception\MangadexAuthenticationException;
interface MangadexClientInterface
{
/**
* @throws \App\Domain\Manga\Domain\Exception\MangadexAuthenticationException
* @throws MangadexAuthenticationException
*/
public function authenticate(): void;
/**
* @throws \App\Domain\Manga\Domain\Exception\MangadexAuthenticationException
* @throws MangadexAuthenticationException
*/
public function refreshToken(): void;
@@ -39,6 +39,7 @@ interface MangadexClientInterface
/**
* @param string[] $mangaIds
*
* @return array{
* statistics: array<string, array{
* rating: array{average: float}

View File

@@ -3,8 +3,8 @@
namespace App\Domain\Manga\Domain\Contract\Repository;
use App\Domain\Manga\Application\Query\MonitoringCriteria;
use App\Domain\Manga\Domain\Model\Manga;
use App\Domain\Manga\Domain\Model\Chapter;
use App\Domain\Manga\Domain\Model\Manga;
use App\Domain\Manga\Domain\Model\ValueObject\ExternalId;
use App\Domain\Manga\Domain\Model\ValueObject\MangaSlug;
@@ -13,13 +13,21 @@ interface MangaRepositoryInterface
// --- Manga ---
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 findBySlug(MangaSlug $slug): ?Manga;
public function findByExternalId(ExternalId $externalId): ?Manga;
public function save(Manga $manga): void;
public function delete(Manga $manga): void;
public function search(string $query, int $page = 1, int $limit = 20): array;
public function countSearch(string $query): int;
/**
@@ -35,14 +43,20 @@ interface MangaRepositoryInterface
* @return Chapter[]
*/
public function findAllChapters(string $mangaId, string $sortOrder = 'desc'): array;
public function countChapters(string $mangaId): int;
public function countAvailableChapters(string $mangaId): int;
public function findChapterById(string $id): ?Chapter;
public function findVisibleChapterById(string $id): ?Chapter;
public function findChapterByMangaIdAndNumber(string $mangaId, float $chapterNumber): ?Chapter;
/**
* @param float[] $chapterNumbers
*
* @return array<float, Chapter>
*/
public function findExistingChaptersByNumbers(string $mangaId, array $chapterNumbers): array;
@@ -61,5 +75,4 @@ interface MangaRepositoryInterface
* @return Chapter[]
*/
public function findVisibleChaptersWithPagesByMangaIdAndVolume(string $mangaId, int $volume): array;
}

View File

@@ -7,7 +7,8 @@ use App\Domain\Manga\Domain\Model\Manga;
interface ChapterSynchronizationServiceInterface
{
/**
* Synchronise les chapitres d'un manga depuis la source externe
* Synchronise les chapitres d'un manga depuis la source externe.
*
* @return string[] IDs des nouveaux chapitres ajoutés
*/
public function synchronizeChapters(Manga $manga): array;

View File

@@ -7,23 +7,24 @@ use Symfony\Component\HttpFoundation\Response;
interface FileServiceInterface
{
/**
* Télécharge un fichier CBZ
* Télécharge un fichier CBZ.
*/
public function downloadCbz(string $filePath, string $filename): Response;
/**
* Crée un fichier ZIP contenant plusieurs CBZ
* Crée un fichier ZIP contenant plusieurs CBZ.
*
* @param array<string> $cbzPaths
*/
public function createVolumeCbz(array $cbzPaths, string $volumeName): Response;
/**
* Supprime un fichier CBZ du système de fichiers
* Supprime un fichier CBZ du système de fichiers.
*/
public function deleteCbzFile(string $filePath): bool;
/**
* Vérifie si un fichier CBZ existe
* Vérifie si un fichier CBZ existe.
*/
public function cbzExists(string $filePath): bool;
}

View File

@@ -7,7 +7,7 @@ use App\Domain\Manga\Domain\Model\ValueObject\ChapterId;
readonly class ChapterReadyForScraping
{
public function __construct(
public ChapterId $chapterId
public ChapterId $chapterId,
) {
}
}

View File

@@ -6,7 +6,7 @@ readonly class MangaCreated
{
public function __construct(
public string $mangaId,
public string $externalId
public string $externalId,
) {
}
}

View File

@@ -2,9 +2,7 @@
namespace App\Domain\Manga\Domain\Exception;
use DomainException;
class CbzFileNotFoundException extends DomainException
class CbzFileNotFoundException extends \DomainException
{
public function __construct(string $filePath)
{

View File

@@ -2,9 +2,7 @@
namespace App\Domain\Manga\Domain\Exception;
use DomainException;
class ChapterNotAvailableException extends DomainException
class ChapterNotAvailableException extends \DomainException
{
public function __construct(int $chapterId)
{

View File

@@ -2,9 +2,7 @@
namespace App\Domain\Manga\Domain\Exception;
use DomainException;
class ChapterNotFoundException extends DomainException
class ChapterNotFoundException extends \DomainException
{
public function __construct(string $chapterId)
{

View File

@@ -2,9 +2,7 @@
namespace App\Domain\Manga\Domain\Exception;
use DomainException;
class VolumeNotFoundException extends DomainException
class VolumeNotFoundException extends \DomainException
{
public function __construct(string $mangaId, int $volume)
{

View File

@@ -13,7 +13,7 @@ readonly class AnalyzedFilename
public function __construct(
private MangaTitle $title,
private ?ChapterNumber $chapterNumber = null,
private ?VolumeNumber $volumeNumber = null
private ?VolumeNumber $volumeNumber = null,
) {
}
@@ -34,11 +34,11 @@ readonly class AnalyzedFilename
public function hasChapterNumber(): bool
{
return $this->chapterNumber !== null;
return null !== $this->chapterNumber;
}
public function hasVolumeNumber(): bool
{
return $this->volumeNumber !== null;
return null !== $this->volumeNumber;
}
}

View File

@@ -16,7 +16,7 @@ class Chapter
private bool $isVisible,
private ?string $pagesDirectory = null,
private int $pageCount = 0,
private \DateTimeImmutable $createdAt = new \DateTimeImmutable()
private \DateTimeImmutable $createdAt = new \DateTimeImmutable(),
) {
}
@@ -52,7 +52,7 @@ class Chapter
public function isAvailable(): bool
{
return $this->pagesDirectory !== null;
return null !== $this->pagesDirectory;
}
public function getPagesDirectory(): ?string

View File

@@ -9,7 +9,6 @@ use App\Domain\Manga\Domain\Model\ValueObject\MangaSlug;
use App\Domain\Manga\Domain\Model\ValueObject\MangaTitle;
use App\Domain\Manga\Domain\Model\ValueObject\MonitoringStatus;
use App\Domain\Shared\Domain\Model\AggregateRoot;
use DateTimeImmutable;
final class Manga extends AggregateRoot
{
@@ -23,22 +22,22 @@ final class Manga extends AggregateRoot
private array $chaptersToDelete = [];
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 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,
private ?ImageUrls $imageUrls = null,
private array $alternativeSlugs = [],
private ?DateTimeImmutable $createdAt = null,
private ?string $imageUrl = null,
private ?float $rating = null,
private ?ImageUrls $imageUrls = null,
private array $alternativeSlugs = [],
private ?\DateTimeImmutable $createdAt = null,
private ?MonitoringStatus $monitoringStatus = null,
private ?DateTimeImmutable $lastMonitoringCheck = null,
private ?\DateTimeImmutable $lastMonitoringCheck = null,
) {
$this->monitoringStatus = $this->monitoringStatus ?? MonitoringStatus::disabled();
}
@@ -158,7 +157,7 @@ final class Manga extends AggregateRoot
$this->alternativeSlugs = $alternativeSlugs;
}
public function getCreatedAt(): ?DateTimeImmutable
public function getCreatedAt(): ?\DateTimeImmutable
{
return $this->createdAt;
}
@@ -181,7 +180,7 @@ final class Manga extends AggregateRoot
public function enableMonitoring(): void
{
$this->monitoringStatus = MonitoringStatus::enabled();
$this->lastMonitoringCheck = new DateTimeImmutable();
$this->lastMonitoringCheck = new \DateTimeImmutable();
}
public function disableMonitoring(): void
@@ -190,12 +189,12 @@ final class Manga extends AggregateRoot
$this->lastMonitoringCheck = null;
}
public function getLastMonitoringCheck(): ?DateTimeImmutable
public function getLastMonitoringCheck(): ?\DateTimeImmutable
{
return $this->lastMonitoringCheck;
}
public function updateLastMonitoringCheck(DateTimeImmutable $lastMonitoringCheck): void
public function updateLastMonitoringCheck(\DateTimeImmutable $lastMonitoringCheck): void
{
$this->lastMonitoringCheck = $lastMonitoringCheck;
}

View File

@@ -5,7 +5,7 @@ namespace App\Domain\Manga\Domain\Model\ValueObject;
readonly class ChapterId
{
public function __construct(
private string $value
private string $value,
) {
}

View File

@@ -4,15 +4,13 @@ declare(strict_types=1);
namespace App\Domain\Manga\Domain\Model\ValueObject;
use InvalidArgumentException;
readonly class ChapterNumber
{
public function __construct(
private float $value
private float $value,
) {
if ($value < 0) {
throw new InvalidArgumentException('Chapter number cannot be negative');
throw new \InvalidArgumentException('Chapter number cannot be negative');
}
}

View File

@@ -7,7 +7,7 @@ use App\Domain\Manga\Domain\Exception\InvalidExternalIdException;
readonly class ExternalId
{
public function __construct(
private string $value
private string $value,
) {
if (empty($value)) {
throw new InvalidExternalIdException('External ID cannot be empty');

View File

@@ -6,7 +6,7 @@ readonly class ImageUrls
{
public function __construct(
private string $full,
private string $thumbnail
private string $thumbnail,
) {
}

View File

@@ -7,7 +7,7 @@ use App\Domain\Manga\Domain\Exception\InvalidMangaIdException;
readonly class MangaId
{
public function __construct(
private string $value
private string $value,
) {
if (empty($value)) {
throw new InvalidMangaIdException('Manga ID cannot be empty');

View File

@@ -7,7 +7,7 @@ use App\Domain\Manga\Domain\Exception\InvalidMangaSlugException;
readonly class MangaSlug
{
public function __construct(
private string $value
private string $value,
) {
if (empty(trim($value))) {
throw new InvalidMangaSlugException('Manga slug cannot be empty');

View File

@@ -7,7 +7,7 @@ use App\Domain\Manga\Domain\Exception\InvalidMangaTitleException;
readonly class MangaTitle
{
public function __construct(
private string $value
private string $value,
) {
if (empty(trim($value))) {
throw new InvalidMangaTitleException('Manga title cannot be empty');

View File

@@ -5,7 +5,7 @@ namespace App\Domain\Manga\Domain\Model\ValueObject;
readonly class MonitoringStatus
{
public function __construct(
private bool $enabled
private bool $enabled,
) {
}

View File

@@ -4,15 +4,13 @@ declare(strict_types=1);
namespace App\Domain\Manga\Domain\Model\ValueObject;
use InvalidArgumentException;
readonly class VolumeNumber
{
public function __construct(
private float $value
private float $value,
) {
if ($value < 0) {
throw new InvalidArgumentException('Volume number cannot be negative');
throw new \InvalidArgumentException('Volume number cannot be negative');
}
}

View File

@@ -6,18 +6,16 @@ use App\Domain\Manga\Application\Command\ImportChapter;
use App\Domain\Manga\Application\CommandHandler\ImportChapterHandler;
use App\Domain\Manga\Domain\Exception\ChapterNotFoundException;
use App\Domain\Manga\Domain\Exception\MangaNotFoundException;
use App\Domain\Manga\Infrastructure\ApiPlatform\Resource\ImportChapterResource;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
#[AsController]
final class ImportChapterController extends AbstractController
{
public function __construct(
private readonly ImportChapterHandler $commandHandler
private readonly ImportChapterHandler $commandHandler,
) {
}
@@ -31,19 +29,19 @@ final class ImportChapterController extends AbstractController
// Validate required fields
if (!$mangaId) {
return $this->json([
['propertyPath' => 'mangaId', 'message' => 'mangaId is required']
['propertyPath' => 'mangaId', 'message' => 'mangaId is required'],
], 422);
}
if (!$chapterNumber) {
return $this->json([
['propertyPath' => 'chapterNumber', 'message' => 'chapterNumber is required']
['propertyPath' => 'chapterNumber', 'message' => 'chapterNumber is required'],
], 422);
}
if (!$uploadedFile) {
return $this->json([
['propertyPath' => 'file', 'message' => 'Please upload a file']
['propertyPath' => 'file', 'message' => 'Please upload a file'],
], 422);
}
@@ -56,9 +54,9 @@ final class ImportChapterController extends AbstractController
try {
// Read file binary content
$fileBinary = file_get_contents($uploadedFile->getPathname());
if ($fileBinary === false) {
if (false === $fileBinary) {
return $this->json([
['propertyPath' => 'file', 'message' => 'Failed to read the uploaded file']
['propertyPath' => 'file', 'message' => 'Failed to read the uploaded file'],
], 400);
}
@@ -75,31 +73,27 @@ final class ImportChapterController extends AbstractController
return $this->json([
'message' => 'Chapter imported successfully',
'mangaId' => $mangaId,
'chapterNumber' => $chapterNumber
'chapterNumber' => $chapterNumber,
], 200);
} catch (MangaNotFoundException $e) {
return $this->json([
'error' => 'Manga not found',
'details' => $e->getMessage()
'details' => $e->getMessage(),
], 404);
} catch (ChapterNotFoundException $e) {
return $this->json([
'error' => 'Chapter not found',
'details' => $e->getMessage()
'details' => $e->getMessage(),
], 404);
} catch (\InvalidArgumentException $e) {
return $this->json([
'error' => 'Invalid file',
'details' => $e->getMessage()
'details' => $e->getMessage(),
], 400);
} catch (\Exception $e) {
return $this->json([
'error' => 'Import failed',
'details' => $e->getMessage()
'details' => $e->getMessage(),
], 500);
}
}
@@ -112,8 +106,9 @@ final class ImportChapterController extends AbstractController
if (!$uploadedFile->isValid()) {
$errors[] = [
'propertyPath' => 'file',
'message' => 'The uploaded file is not valid: ' . $uploadedFile->getErrorMessage()
'message' => 'The uploaded file is not valid: '.$uploadedFile->getErrorMessage(),
];
return $errors;
}
@@ -122,7 +117,7 @@ final class ImportChapterController extends AbstractController
if ($uploadedFile->getSize() > $maxSize) {
$errors[] = [
'propertyPath' => 'file',
'message' => 'The uploaded file is too large. Allowed size is 500MB.'
'message' => 'The uploaded file is too large. Allowed size is 500MB.',
];
}
@@ -133,7 +128,7 @@ final class ImportChapterController extends AbstractController
if (!in_array($extension, $allowedExtensions)) {
$errors[] = [
'propertyPath' => 'file',
'message' => 'Please upload a valid CBZ file'
'message' => 'Please upload a valid CBZ file',
];
}

View File

@@ -5,7 +5,6 @@ namespace App\Domain\Manga\Infrastructure\ApiPlatform\Controller;
use App\Domain\Manga\Application\Command\ImportVolume;
use App\Domain\Manga\Application\CommandHandler\ImportVolumeHandler;
use App\Domain\Manga\Domain\Exception\MangaNotFoundException;
use App\Domain\Manga\Infrastructure\ApiPlatform\Resource\ImportVolumeResource;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
@@ -15,7 +14,7 @@ use Symfony\Component\HttpKernel\Attribute\AsController;
final class ImportVolumeController extends AbstractController
{
public function __construct(
private readonly ImportVolumeHandler $commandHandler
private readonly ImportVolumeHandler $commandHandler,
) {
}
@@ -29,19 +28,19 @@ final class ImportVolumeController extends AbstractController
// Validate required fields
if (!$mangaId) {
return $this->json([
['propertyPath' => 'mangaId', 'message' => 'mangaId is required']
['propertyPath' => 'mangaId', 'message' => 'mangaId is required'],
], 422);
}
if (!$volumeNumber) {
return $this->json([
['propertyPath' => 'volumeNumber', 'message' => 'volumeNumber is required']
['propertyPath' => 'volumeNumber', 'message' => 'volumeNumber is required'],
], 422);
}
if (!$uploadedFile) {
return $this->json([
['propertyPath' => 'file', 'message' => 'Please upload a file']
['propertyPath' => 'file', 'message' => 'Please upload a file'],
], 422);
}
@@ -54,9 +53,9 @@ final class ImportVolumeController extends AbstractController
try {
// Read file binary content
$fileBinary = file_get_contents($uploadedFile->getPathname());
if ($fileBinary === false) {
if (false === $fileBinary) {
return $this->json([
['propertyPath' => 'file', 'message' => 'Failed to read the uploaded file']
['propertyPath' => 'file', 'message' => 'Failed to read the uploaded file'],
], 400);
}
@@ -73,26 +72,24 @@ final class ImportVolumeController extends AbstractController
return $this->json([
'message' => 'Volume imported successfully',
'mangaId' => $mangaId,
'volumeNumber' => (int) $volumeNumber
'volumeNumber' => (int) $volumeNumber,
], 200);
} catch (MangaNotFoundException $e) {
return $this->json([
'error' => 'Manga not found',
'details' => $e->getMessage()
'details' => $e->getMessage(),
], 404);
} catch (\InvalidArgumentException $e) {
$statusCode = str_contains($e->getMessage(), 'not found') ? 404 : 400;
return $this->json([
'error' => 'Invalid request',
'details' => $e->getMessage()
'details' => $e->getMessage(),
], $statusCode);
} catch (\Exception $e) {
return $this->json([
'error' => 'Import failed',
'details' => $e->getMessage()
'details' => $e->getMessage(),
], 500);
}
}
@@ -105,8 +102,9 @@ final class ImportVolumeController extends AbstractController
if (!$uploadedFile->isValid()) {
$errors[] = [
'propertyPath' => 'file',
'message' => 'The uploaded file is not valid: ' . $uploadedFile->getErrorMessage()
'message' => 'The uploaded file is not valid: '.$uploadedFile->getErrorMessage(),
];
return $errors;
}
@@ -115,7 +113,7 @@ final class ImportVolumeController extends AbstractController
if ($uploadedFile->getSize() > $maxSize) {
$errors[] = [
'propertyPath' => 'file',
'message' => 'The uploaded file is too large. Allowed size is 500MB.'
'message' => 'The uploaded file is too large. Allowed size is 500MB.',
];
}
@@ -126,7 +124,7 @@ final class ImportVolumeController extends AbstractController
if (!in_array($extension, $allowedExtensions)) {
$errors[] = [
'propertyPath' => 'file',
'message' => 'Please upload a valid CBZ file'
'message' => 'Please upload a valid CBZ file',
];
}

View File

@@ -11,7 +11,7 @@ readonly class ChapterCollection
public int $page,
public int $limit,
public bool $hasNextPage,
public bool $hasPreviousPage
public bool $hasPreviousPage,
) {
}
}

View File

@@ -8,13 +8,13 @@ readonly class FilenameMatchCollection
{
/**
* @param FilenameMatchItem[] $matches
* @param string[] $possibleTitles
* @param string[] $possibleTitles
*/
public function __construct(
public array $matches,
public ?float $chapterNumber,
public ?int $volumeNumber,
public array $possibleTitles
public array $possibleTitles,
) {
}
}

View File

@@ -14,7 +14,7 @@ readonly class FilenameMatchItem
public ?string $thumbnailUrl,
public int $matchScore,
public ?float $chapterNumber,
public ?float $volumeNumber
public ?float $volumeNumber,
) {
}
}

View File

@@ -2,8 +2,6 @@
namespace App\Domain\Manga\Infrastructure\ApiPlatform\Dto;
use ApiPlatform\Metadata\ApiProperty;
readonly class MangaCollection
{
public function __construct(
@@ -13,7 +11,7 @@ readonly class MangaCollection
public int $page,
public int $limit,
public bool $hasNextPage,
public bool $hasPreviousPage
public bool $hasPreviousPage,
) {
}
}

View File

@@ -21,7 +21,7 @@ readonly class MangaDetail
public ?string $imageUrl,
public ?string $thumbnailUrl,
public ?float $rating,
public bool $monitored
public bool $monitored,
) {
}
}

View File

@@ -3,7 +3,6 @@
namespace App\Domain\Manga\Infrastructure\ApiPlatform\Dto;
use ApiPlatform\Metadata\ApiProperty;
use DateTimeImmutable;
readonly class MangaListItem
{
@@ -20,7 +19,7 @@ readonly class MangaListItem
public array $genres,
public string $status,
public ?float $rating,
public DateTimeImmutable $createdAt,
public \DateTimeImmutable $createdAt,
public bool $monitored = false,
public int $chaptersTotal = 0,
public int $chaptersScraped = 0,

View File

@@ -6,7 +6,7 @@ readonly class MangaSearchCollection
{
public function __construct(
/** @var MangaSearchItem[] */
public array $items
public array $items,
) {
}
}

View File

@@ -18,7 +18,7 @@ readonly class MangaSearchItem
public string $status,
public ?string $imageUrl,
public ?string $thumbnailUrl,
public ?float $rating
public ?float $rating,
) {
}
}

View File

@@ -4,6 +4,7 @@ namespace App\Domain\Manga\Infrastructure\ApiPlatform\Resource;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Post;
use ApiPlatform\OpenApi\Model\Operation;
use App\Domain\Manga\Infrastructure\ApiPlatform\State\Processor\CreateMangaDirectlyProcessor;
use Symfony\Component\Validator\Constraints as Assert;
@@ -13,11 +14,11 @@ use Symfony\Component\Validator\Constraints as Assert;
new Post(
uriTemplate: '/mangas/create',
processor: CreateMangaDirectlyProcessor::class,
openapiContext: [
'summary' => 'Create a new manga directly',
'description' => 'Creates a new manga with provided data'
]
)
openapi: new Operation(
summary: 'Create a new manga directly',
description: 'Creates a new manga with provided data'
)
),
]
)]
class CreateMangaDirectlyResource

View File

@@ -4,6 +4,8 @@ namespace App\Domain\Manga\Infrastructure\ApiPlatform\Resource;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Post;
use ApiPlatform\OpenApi\Model\Operation;
use ApiPlatform\OpenApi\Model\RequestBody;
use App\Domain\Manga\Infrastructure\ApiPlatform\State\Processor\CreateMangaProcessor;
use Symfony\Component\Validator\Constraints as Assert;
@@ -13,11 +15,11 @@ use Symfony\Component\Validator\Constraints as Assert;
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',
'requestBody' => [
'content' => [
openapi: new Operation(
summary: 'Create a new manga from Mangadex',
description: 'Creates a new manga by fetching its data from Mangadex using an external ID',
requestBody: new RequestBody(
content: new \ArrayObject([
'application/json' => [
'schema' => [
'type' => 'object',
@@ -25,15 +27,15 @@ use Symfony\Component\Validator\Constraints as Assert;
'properties' => [
'externalId' => [
'type' => 'string',
'description' => 'The Mangadex ID of the manga'
]
]
]
]
]
]
]
)
'description' => 'The Mangadex ID of the manga',
],
],
],
],
])
)
)
),
]
)]
class CreateMangaResource

Some files were not shown because too many files have changed in this diff Show More