feat: ajout de la fonctionnalité de suppression de mangas, incluant une modale de confirmation pour l'utilisateur, la gestion des erreurs et l'intégration avec l'API pour supprimer les mangas et leurs chapitres associés. Mise à jour des composants Vue et ajout de tests pour valider cette nouvelle fonctionnalité.

This commit is contained in:
ext.jeremy.guillot@maxicoffee.domains
2025-07-23 16:42:54 +02:00
parent 7f9d583c94
commit f09f744a9b
12 changed files with 470 additions and 13 deletions

View File

@@ -0,0 +1,12 @@
<?php
namespace App\Domain\Manga\Application\Command;
use App\Domain\Shared\Domain\Contract\CommandInterface;
readonly class DeleteManga implements CommandInterface
{
public function __construct(
public string $mangaId
) {}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Domain\Manga\Application\CommandHandler;
use App\Domain\Manga\Application\Command\DeleteManga;
use App\Domain\Manga\Domain\Contract\Repository\MangaRepositoryInterface;
use App\Domain\Manga\Domain\Exception\MangaNotFoundException;
use App\Domain\Shared\Domain\Contract\CommandHandlerInterface;
use App\Domain\Shared\Domain\Contract\CommandInterface;
readonly class DeleteMangaHandler implements CommandHandlerInterface
{
public function __construct(
private MangaRepositoryInterface $mangaRepository
) {}
public function handle(CommandInterface $command): void
{
assert($command instanceof DeleteManga);
$manga = $this->mangaRepository->findById($command->mangaId);
if (!$manga) {
throw new MangaNotFoundException(sprintf('Manga with id %s not found', $command->mangaId));
}
$this->mangaRepository->delete($manga);
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace App\Domain\Manga\Infrastructure\ApiPlatform\Resource;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Delete;
use App\Domain\Manga\Infrastructure\ApiPlatform\State\Processor\DeleteMangaProcessor;
#[ApiResource(
shortName: 'Manga',
operations: [
new Delete(
uriTemplate: '/mangas/{id}',
processor: DeleteMangaProcessor::class,
name: 'delete_manga',
read: false,
openapiContext: [
'summary' => 'Delete a manga',
'description' => 'Permanently deletes a manga and all its associated chapters',
'parameters' => [
[
'name' => 'id',
'in' => 'path',
'required' => true,
'schema' => [
'type' => 'string'
],
'description' => 'The manga ID'
]
],
'responses' => [
'204' => [
'description' => 'Manga successfully deleted'
],
'404' => [
'description' => 'Manga not found'
]
]
]
)
]
)]
class DeleteMangaResource
{
public function __construct(
public string $id
) {}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Domain\Manga\Infrastructure\ApiPlatform\State\Processor;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use App\Domain\Manga\Application\Command\DeleteManga;
use App\Domain\Manga\Application\CommandHandler\DeleteMangaHandler;
use App\Domain\Manga\Domain\Exception\MangaNotFoundException;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
readonly class DeleteMangaProcessor implements ProcessorInterface
{
public function __construct(
private DeleteMangaHandler $handler
) {}
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): int
{
if (!isset($uriVariables['id'])) {
throw new \InvalidArgumentException('Manga ID is required');
}
try {
$command = new DeleteManga($uriVariables['id']);
$this->handler->handle($command);
return Response::HTTP_NO_CONTENT;
} catch (MangaNotFoundException $e) {
throw new NotFoundHttpException('Manga not found');
}
}
}

View File

@@ -15,6 +15,7 @@ class Manga
private readonly string $description,
private readonly string $author,
private readonly string $publicationYear,
private readonly bool $monitored = false,
private readonly array $preferredSources = [],
private readonly array $alternativeSlugs = [],
) {
@@ -73,4 +74,9 @@ class Manga
{
return $this->alternativeSlugs;
}
public function isMonitored(): bool
{
return $this->monitored;
}
}

View File

@@ -5,17 +5,26 @@ namespace App\Domain\Scraping\Infrastructure\EventListener;
use App\Domain\Manga\Domain\Event\ChapterReadyForScraping;
use App\Domain\Scraping\Application\Command\ScrapeChapter;
use App\Domain\Scraping\Application\CommandHandler\ScrapeChapterHandler;
use App\Domain\Scraping\Domain\Contract\Repository\ChapterRepositoryInterface;
use App\Domain\Scraping\Domain\Contract\Repository\MangaRepositoryInterface;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
class AutoScrapingListener
{
public function __construct(
private readonly ScrapeChapterHandler $scrapeChapterHandler
private readonly ScrapeChapterHandler $scrapeChapterHandler,
private readonly ChapterRepositoryInterface $chapterRepository,
private readonly MangaRepositoryInterface $mangaRepository,
) {}
#[AsMessageHandler]
public function onChapterReadyForScraping(ChapterReadyForScraping $event): void
{
$this->scrapeChapterHandler->handle(new ScrapeChapter($event->chapterId->getValue()));
$chapter = $this->chapterRepository->getById($event->chapterId->getValue());
$manga = $this->mangaRepository->getById($chapter->mangaId);
if ($manga->isMonitored()) {
$this->scrapeChapterHandler->handle(new ScrapeChapter($event->chapterId->getValue()));
}
}
}

View File

@@ -35,14 +35,15 @@ readonly class LegacyMangaRepository implements MangaRepositoryInterface
}
return new Manga(
(string) $mangaEntity->getId(),
$mangaEntity->getTitle(),
$mangaEntity->getSlug(),
$mangaEntity->getDescription() ?? '',
$mangaEntity->getAuthor() ?? '',
(string) ($mangaEntity->getPublicationYear() ?? ''),
$preferredSourceIds,
$mangaEntity->getAlternativeSlugs() ?? []
id: (string) $mangaEntity->getId(),
title: $mangaEntity->getTitle(),
slug: $mangaEntity->getSlug(),
description: $mangaEntity->getDescription() ?? '',
author: $mangaEntity->getAuthor() ?? '',
publicationYear: (string) ($mangaEntity->getPublicationYear() ?? ''),
monitored: $mangaEntity->isMonitored() ?? false,
preferredSources: $preferredSourceIds,
alternativeSlugs: $mangaEntity->getAlternativeSlugs() ?? []
);
}