From 4017cabff2b343a9704dd621d7b2d121656146ef Mon Sep 17 00:00:00 2001 From: "ext.jeremy.guillot@maxicoffee.domains" Date: Tue, 11 Feb 2025 00:40:47 +0100 Subject: [PATCH] feat: Image saving for manga creation --- config/services.yaml | 5 + .../CreateMangaFromMangadexHandler.php | 18 +++- .../Service/ImageProcessorInterface.php | 16 ++++ src/Domain/Manga/Domain/Model/Manga.php | 12 +++ .../Domain/Model/ValueObject/ImageUrls.php | 21 +++++ .../Persistence/LegacyMangaRepository.php | 3 +- .../Infrastructure/Service/ImageProcessor.php | 91 +++++++++++++++++++ .../Manga/Adapter/InMemoryImageProcessor.php | 23 +++++ .../CreateMangaFromMangadexHandlerTest.php | 54 +++++------ 9 files changed, 212 insertions(+), 31 deletions(-) create mode 100644 src/Domain/Manga/Domain/Contract/Service/ImageProcessorInterface.php create mode 100644 src/Domain/Manga/Domain/Model/ValueObject/ImageUrls.php create mode 100644 src/Domain/Manga/Infrastructure/Service/ImageProcessor.php create mode 100644 tests/Domain/Manga/Adapter/InMemoryImageProcessor.php diff --git a/config/services.yaml b/config/services.yaml index 74c1048..5fdc806 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -107,3 +107,8 @@ services: $clientSecret: '%env(MANGADEX_CLIENT_SECRET)%' $username: '%env(MANGADEX_USERNAME)%' $password: '%env(MANGADEX_PASSWORD)%' + + App\Domain\Manga\Infrastructure\Service\ImageProcessor: + arguments: + $publicDir: '%kernel.project_dir%/public' + $httpClient: '@GuzzleHttp\Client' diff --git a/src/Domain/Manga/Application/CommandHandler/CreateMangaFromMangadexHandler.php b/src/Domain/Manga/Application/CommandHandler/CreateMangaFromMangadexHandler.php index e3a48b6..149bc42 100644 --- a/src/Domain/Manga/Application/CommandHandler/CreateMangaFromMangadexHandler.php +++ b/src/Domain/Manga/Application/CommandHandler/CreateMangaFromMangadexHandler.php @@ -5,14 +5,17 @@ namespace App\Domain\Manga\Application\CommandHandler; use App\Domain\Manga\Application\Command\CreateMangaFromMangadex; use App\Domain\Manga\Domain\Contract\Provider\MangaProviderInterface; use App\Domain\Manga\Domain\Contract\Repository\MangaRepositoryInterface; +use App\Domain\Manga\Domain\Contract\Service\ImageProcessorInterface; use App\Domain\Manga\Domain\Exception\MangaNotFoundException; use App\Domain\Manga\Domain\Model\ValueObject\ExternalId; +use App\Domain\Manga\Domain\Model\ValueObject\ImageUrls; readonly class CreateMangaFromMangadexHandler { public function __construct( private MangaProviderInterface $mangaProvider, - private MangaRepositoryInterface $mangaRepository + private MangaRepositoryInterface $mangaRepository, + private ImageProcessorInterface $imageProcessor ) {} public function handle(CreateMangaFromMangadex $command): void @@ -23,6 +26,19 @@ readonly class CreateMangaFromMangadexHandler throw new MangaNotFoundException('Manga not found on Mangadex'); } + try { + // Télécharge l'image originale + $fullImagePath = $this->imageProcessor->downloadImage($manga->getImageUrl()); + + // Crée la miniature à partir de l'image originale + $thumbnailPath = $this->imageProcessor->createThumbnail($fullImagePath); + + // 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()); + } + $this->mangaRepository->save($manga); } } \ No newline at end of file diff --git a/src/Domain/Manga/Domain/Contract/Service/ImageProcessorInterface.php b/src/Domain/Manga/Domain/Contract/Service/ImageProcessorInterface.php new file mode 100644 index 0000000..ee6b080 --- /dev/null +++ b/src/Domain/Manga/Domain/Contract/Service/ImageProcessorInterface.php @@ -0,0 +1,16 @@ +rating; } + public function getImageUrls(): ?ImageUrls + { + return $this->imageUrls; + } + public function setRating(float $rating): void { $this->rating = $rating; @@ -87,4 +94,9 @@ final class Manga { $this->id = $id; } + + public function updateImageUrls(ImageUrls $imageUrls): void + { + $this->imageUrls = $imageUrls; + } } \ No newline at end of file diff --git a/src/Domain/Manga/Domain/Model/ValueObject/ImageUrls.php b/src/Domain/Manga/Domain/Model/ValueObject/ImageUrls.php new file mode 100644 index 0000000..3bf1087 --- /dev/null +++ b/src/Domain/Manga/Domain/Model/ValueObject/ImageUrls.php @@ -0,0 +1,21 @@ +full; + } + + public function getThumbnail(): string + { + return $this->thumbnail; + } +} \ No newline at end of file diff --git a/src/Domain/Manga/Infrastructure/Persistence/LegacyMangaRepository.php b/src/Domain/Manga/Infrastructure/Persistence/LegacyMangaRepository.php index 04d3e16..c771664 100644 --- a/src/Domain/Manga/Infrastructure/Persistence/LegacyMangaRepository.php +++ b/src/Domain/Manga/Infrastructure/Persistence/LegacyMangaRepository.php @@ -65,7 +65,8 @@ readonly class LegacyMangaRepository implements MangaRepositoryInterface ->setGenres($manga->getGenres()) ->setStatus($manga->getStatus()) ->setExternalId($manga->getExternalId()->getValue()) - ->setImageUrl($manga->getImageUrl()) + ->setImageUrl($manga->getImageUrls()->getFull()) + ->setThumbnailUrl($manga->getImageUrls()->getThumbnail()) ->setMonitored(false); if ($manga->getRating() !== null) { diff --git a/src/Domain/Manga/Infrastructure/Service/ImageProcessor.php b/src/Domain/Manga/Infrastructure/Service/ImageProcessor.php new file mode 100644 index 0000000..75e2eea --- /dev/null +++ b/src/Domain/Manga/Infrastructure/Service/ImageProcessor.php @@ -0,0 +1,91 @@ +imageManager = new ImageManager(new Driver()); + $this->ensureDirectoriesExist(); + } + + public function downloadImage(string $imageUrl): string + { + try { + $response = $this->httpClient->get($imageUrl); + + if ($response->getStatusCode() !== 200) { + throw new \RuntimeException('Échec du téléchargement de l\'image'); + } + + $uuid = Uuid::uuid4()->toString(); + $extension = pathinfo($imageUrl, PATHINFO_EXTENSION) ?: 'jpg'; + $filename = sprintf('%s.%s', $uuid, $extension); + $fullPath = $this->publicDir . self::FULL_IMAGES_DIR . '/' . $filename; + + $image = $this->imageManager->read($response->getBody()->getContents()); + $image->save($fullPath, quality: self::FULL_IMAGE_QUALITY); + + return self::FULL_IMAGES_DIR . '/' . $filename; + } catch (\Exception $e) { + throw new \RuntimeException('Erreur lors du téléchargement de l\'image : ' . $e->getMessage()); + } + } + + public function createThumbnail(string $originalImagePath): string + { + try { + $fullPath = $this->publicDir . $originalImagePath; + if (!file_exists($fullPath)) { + throw new \RuntimeException('Image originale non trouvée'); + } + + $filename = pathinfo($originalImagePath, PATHINFO_BASENAME); + $thumbnailPath = $this->publicDir . self::THUMBNAILS_DIR . '/' . $filename; + + $thumbnail = $this->imageManager->read($fullPath); + $thumbnail->cover(self::THUMBNAIL_WIDTH, self::THUMBNAIL_HEIGHT); + $thumbnail->save($thumbnailPath, quality: self::THUMBNAIL_QUALITY); + + return self::THUMBNAILS_DIR . '/' . $filename; + } catch (\Exception $e) { + throw new \RuntimeException('Erreur lors de la création de la miniature : ' . $e->getMessage()); + } + } + + private function ensureDirectoriesExist(): void + { + $directories = [ + $this->publicDir . self::FULL_IMAGES_DIR, + $this->publicDir . self::THUMBNAILS_DIR, + ]; + + foreach ($directories as $directory) { + if (!is_dir($directory)) { + if (!mkdir($directory, 0755, true) && !is_dir($directory)) { + throw new \RuntimeException(sprintf('Le répertoire "%s" n\'a pas pu être créé', $directory)); + } + } + } + } +} \ No newline at end of file diff --git a/tests/Domain/Manga/Adapter/InMemoryImageProcessor.php b/tests/Domain/Manga/Adapter/InMemoryImageProcessor.php new file mode 100644 index 0000000..41e3eea --- /dev/null +++ b/tests/Domain/Manga/Adapter/InMemoryImageProcessor.php @@ -0,0 +1,23 @@ +provider = new InMemoryMangaProvider(); + $manga = new Manga( + new MangaId('123'), + new MangaTitle('One Piece'), + new MangaSlug('one-piece'), + 'Description test', + 'Eiichiro Oda', + 1997, + ['action', 'adventure'], + 'ongoing', + new ExternalId('external-123'), + 'http://example.com/image.jpg', + 4.5 + ); + $this->provider = new InMemoryMangaProvider([$manga]); $this->repository = new InMemoryMangaRepository(); + $this->imageProcessor = new InMemoryImageProcessor(); $this->handler = new CreateMangaFromMangadexHandler( $this->provider, - $this->repository + $this->repository, + $this->imageProcessor ); } public function testHandleSuccess(): void { - // Arrange - $externalId = 'manga-123'; - $manga = new Manga( - new MangaId('123'), - new MangaTitle('Test Manga'), - new MangaSlug('test-manga'), - 'Description', - 'Author', - 2020, - ['action'], - 'ongoing', - new ExternalId($externalId), - 'http://example.com/cover.jpg', - 4.5 - ); - - $this->provider = new InMemoryMangaProvider([$manga]); - $this->handler = new CreateMangaFromMangadexHandler( - $this->provider, - $this->repository - ); - - // Act - $command = new CreateMangaFromMangadex($externalId); + $command = new CreateMangaFromMangadex('external-123'); $this->handler->handle($command); // Assert $savedManga = $this->repository->findById('123'); $this->assertNotNull($savedManga); - $this->assertEquals($externalId, $savedManga->getExternalId()->getValue()); - $this->assertEquals('Test Manga', $savedManga->getTitle()->getValue()); + $this->assertEquals('One Piece', $savedManga->getTitle()->getValue()); + $this->assertNotNull($savedManga->getImageUrls()); + $this->assertStringStartsWith('/images/full/', $savedManga->getImageUrls()->getFull()); + $this->assertStringStartsWith('/images/thumbnails/', $savedManga->getImageUrls()->getThumbnail()); } public function testHandleThrowsExceptionWhenMangaNotFound(): void { // Arrange - $command = new CreateMangaFromMangadex('non-existent-manga'); + $command = new CreateMangaFromMangadex('non-existent-id'); // Assert $this->expectException(MangaNotFoundException::class);