- 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)
122 lines
4.0 KiB
PHP
122 lines
4.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Domain\Manga\Application\QueryHandler;
|
|
|
|
use App\Domain\Manga\Application\Query\FindMangaMatchByFilename;
|
|
use App\Domain\Manga\Application\Response\MangaMatchItem;
|
|
use App\Domain\Manga\Application\Response\MangaMatchResponse;
|
|
use App\Domain\Manga\Domain\Contract\Repository\MangaRepositoryInterface;
|
|
use App\Domain\Manga\Domain\Contract\Service\FilenameAnalyzerInterface;
|
|
use App\Domain\Manga\Domain\Model\Manga;
|
|
|
|
readonly class FindMangaMatchByFilenameHandler
|
|
{
|
|
public function __construct(
|
|
private FilenameAnalyzerInterface $filenameAnalyzer,
|
|
private MangaRepositoryInterface $mangaRepository,
|
|
) {
|
|
}
|
|
|
|
public function handle(FindMangaMatchByFilename $query): MangaMatchResponse
|
|
{
|
|
// Analyser le nom de fichier pour extraire les informations
|
|
$analyzedFilename = $this->filenameAnalyzer->analyze($query->filename);
|
|
|
|
$searchedTitle = $analyzedFilename->getTitle()->getValue();
|
|
$chapterNumber = $analyzedFilename->hasChapterNumber()
|
|
? $analyzedFilename->getChapterNumber()->getValue()
|
|
: null;
|
|
$volumeNumber = $analyzedFilename->hasVolumeNumber()
|
|
? $analyzedFilename->getVolumeNumber()->getValue()
|
|
: null;
|
|
|
|
// Rechercher les mangas correspondants
|
|
$foundMangas = $this->mangaRepository->search($searchedTitle, 1, 10);
|
|
$matches = [];
|
|
|
|
foreach ($foundMangas as $manga) {
|
|
$mangaId = $manga->getId()->getValue();
|
|
|
|
// Calculer un score de correspondance
|
|
$matchScore = $this->calculateMatchScore(
|
|
$manga,
|
|
$searchedTitle
|
|
);
|
|
|
|
$matches[] = new MangaMatchItem(
|
|
id: $mangaId,
|
|
title: $manga->getTitle()->getValue(),
|
|
slug: $manga->getSlug()->getValue(),
|
|
alternativeSlugs: $manga->getAlternativeSlugs(),
|
|
thumbnailUrl: $manga->getImageUrls()->getThumbnail(),
|
|
matchScore: $matchScore,
|
|
chapterNumber: $chapterNumber,
|
|
volumeNumber: $volumeNumber
|
|
);
|
|
}
|
|
|
|
// Trier les résultats par score de correspondance (du plus élevé au plus faible)
|
|
usort($matches, fn ($a, $b) => $b->matchScore <=> $a->matchScore);
|
|
|
|
return new MangaMatchResponse(
|
|
matches: $matches,
|
|
chapterNumber: $chapterNumber,
|
|
volumeNumber: $volumeNumber,
|
|
possibleTitles: [$searchedTitle]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Calcule un score de correspondance entre le manga et le titre recherché
|
|
* Score plus élevé = meilleure correspondance.
|
|
*/
|
|
private function calculateMatchScore(Manga $manga, string $searchedTitle): int
|
|
{
|
|
$score = 0;
|
|
$mangaTitle = $manga->getTitle()->getValue();
|
|
$mangaSlug = $manga->getSlug()->getValue();
|
|
|
|
// Correspondance exacte avec le titre
|
|
if (strtolower($mangaTitle) === strtolower($searchedTitle)) {
|
|
$score += 100;
|
|
}
|
|
|
|
// Correspondance exacte avec le slug
|
|
if (strtolower($mangaSlug) === strtolower($searchedTitle)) {
|
|
$score += 90;
|
|
}
|
|
|
|
// Correspondance avec les slugs alternatifs
|
|
foreach ($manga->getAlternativeSlugs() as $altSlug) {
|
|
if (strtolower($altSlug) === strtolower($searchedTitle)) {
|
|
$score += 80;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Le titre du manga contient le terme recherché
|
|
if (false !== stripos($mangaTitle, $searchedTitle)) {
|
|
$score += 50;
|
|
}
|
|
|
|
// Le terme recherché contient le titre du manga
|
|
if (false !== stripos($searchedTitle, $mangaTitle)) {
|
|
$score += 40;
|
|
}
|
|
|
|
// Similarité de Levenshtein (pour les fautes de frappe)
|
|
$levenshteinDistance = levenshtein(
|
|
strtolower($mangaTitle),
|
|
strtolower($searchedTitle)
|
|
);
|
|
|
|
if ($levenshteinDistance <= 3) {
|
|
$score += (3 - $levenshteinDistance) * 10;
|
|
}
|
|
|
|
return $score;
|
|
}
|
|
}
|