feat: analyse import + all tests fixed
This commit is contained in:
parent
fbe9619224
commit
3170a7c60e
40
src/Domain/Shared/Domain/Contract/FileUploadInterface.php
Normal file
40
src/Domain/Shared/Domain/Contract/FileUploadInterface.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Shared\Domain\Contract;
|
||||
|
||||
use App\Domain\Shared\Domain\Model\FileUpload;
|
||||
|
||||
interface FileUploadInterface
|
||||
{
|
||||
/**
|
||||
* Déplace un fichier uploadé vers un répertoire temporaire
|
||||
*/
|
||||
public function moveUploadedFile(string $sourcePath, string $targetDirectory, string $originalName): string;
|
||||
|
||||
/**
|
||||
* Vérifie si un fichier existe
|
||||
*/
|
||||
public function fileExists(string $filePath): bool;
|
||||
|
||||
/**
|
||||
* Supprime un fichier
|
||||
*/
|
||||
public function deleteFile(string $filePath): void;
|
||||
|
||||
/**
|
||||
* Déplace un fichier d'un emplacement à un autre
|
||||
*/
|
||||
public function moveFile(string $sourcePath, string $targetPath): void;
|
||||
|
||||
/**
|
||||
* Crée un répertoire s'il n'existe pas
|
||||
*/
|
||||
public function createDirectory(string $path): void;
|
||||
|
||||
/**
|
||||
* Valide le format d'un fichier
|
||||
*/
|
||||
public function validateFileFormat(string $filePath, array $allowedExtensions): bool;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Shared\Domain\Contract;
|
||||
|
||||
/**
|
||||
* Service centralisé de gestion des chemins et de l'enregistrement des fichiers
|
||||
* liés aux mangas (manga/volume/chapter) et des archives CBZ.
|
||||
*/
|
||||
interface MangaPathManagerInterface
|
||||
{
|
||||
/**
|
||||
* Retourne (et crée si nécessaire) le dossier du manga.
|
||||
*/
|
||||
public function getMangaDirectory(string $mangaTitle, string $publicationYear): string;
|
||||
|
||||
/**
|
||||
* Retourne (et crée si nécessaire) le dossier du volume.
|
||||
*/
|
||||
public function getVolumeDirectory(string $mangaTitle, string $publicationYear, int $volumeNumber): string;
|
||||
|
||||
/**
|
||||
* Construit (et garantit l'existence des dossiers) le chemin complet d'un CBZ de chapitre.
|
||||
*/
|
||||
public function buildChapterCbzPath(string $mangaTitle, string $publicationYear, int $volumeNumber, string $chapterNumber): string;
|
||||
|
||||
/**
|
||||
* Construit (et garantit l'existence des dossiers) le chemin complet d'un CBZ de volume.
|
||||
*/
|
||||
public function buildVolumeCbzPath(string $mangaTitle, string $publicationYear, int $volumeNumber): string;
|
||||
|
||||
/**
|
||||
* Crée une archive CBZ à partir d'une liste de fichiers et l'écrit au chemin fourni.
|
||||
*
|
||||
* @param array<int, string> $files Chemins absolus des fichiers à packager
|
||||
*/
|
||||
public function createCbzArchive(array $files, string $cbzPath): void;
|
||||
|
||||
/**
|
||||
* Déplace un fichier existant vers une destination. Crée les dossiers si nécessaire.
|
||||
*/
|
||||
public function moveFileTo(string $sourcePath, string $destinationPath): void;
|
||||
|
||||
/**
|
||||
* Indique si un fichier existe et est lisible.
|
||||
*/
|
||||
public function fileExists(string $path): bool;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Shared\Domain\Contract;
|
||||
|
||||
use App\Domain\Shared\Domain\Model\FileMetadata;
|
||||
|
||||
interface MetadataExtractorInterface
|
||||
{
|
||||
/**
|
||||
* Extrait les métadonnées d'un fichier
|
||||
*/
|
||||
public function extractMetadata(string $filePath, string $originalFileName): FileMetadata;
|
||||
|
||||
/**
|
||||
* Vérifie si le fichier peut être traité par cet extracteur
|
||||
*/
|
||||
public function canHandle(string $filePath): bool;
|
||||
}
|
||||
23
src/Domain/Shared/Domain/Contract/NotificationInterface.php
Normal file
23
src/Domain/Shared/Domain/Contract/NotificationInterface.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Shared\Domain\Contract;
|
||||
|
||||
interface NotificationInterface
|
||||
{
|
||||
/**
|
||||
* Envoie une notification de succès
|
||||
*/
|
||||
public function sendSuccess(string $message): void;
|
||||
|
||||
/**
|
||||
* Envoie une notification d'erreur
|
||||
*/
|
||||
public function sendError(string $message): void;
|
||||
|
||||
/**
|
||||
* Envoie une notification avec un statut personnalisé
|
||||
*/
|
||||
public function sendUpdate(array $data): void;
|
||||
}
|
||||
17
src/Domain/Shared/Domain/Event/ChapterImported.php
Normal file
17
src/Domain/Shared/Domain/Event/ChapterImported.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Shared\Domain\Event;
|
||||
|
||||
readonly class ChapterImported
|
||||
{
|
||||
public function __construct(
|
||||
public string $mangaSlug,
|
||||
public int $volume,
|
||||
public float|string $chapterNumber,
|
||||
public string $cbzPath,
|
||||
) {}
|
||||
}
|
||||
|
||||
|
||||
16
src/Domain/Shared/Domain/Event/VolumeImported.php
Normal file
16
src/Domain/Shared/Domain/Event/VolumeImported.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Shared\Domain\Event;
|
||||
|
||||
readonly class VolumeImported
|
||||
{
|
||||
public function __construct(
|
||||
public string $mangaSlug,
|
||||
public int $volume,
|
||||
public string $cbzPath,
|
||||
) {}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Shared\Domain\Exception;
|
||||
|
||||
class FileProcessingException extends \RuntimeException
|
||||
{
|
||||
public static function invalidFormat(string $fileName, array $allowedFormats): self
|
||||
{
|
||||
return new self(
|
||||
sprintf(
|
||||
'Le fichier "%s" doit être au format %s.',
|
||||
$fileName,
|
||||
implode(' ou ', $allowedFormats)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public static function uploadFailed(string $fileName, string $reason): self
|
||||
{
|
||||
return new self(
|
||||
sprintf('Une erreur est survenue lors de l\'upload du fichier "%s" : %s', $fileName, $reason)
|
||||
);
|
||||
}
|
||||
|
||||
public static function fileNotFound(string $filePath): self
|
||||
{
|
||||
return new self(sprintf('Le fichier "%s" n\'a pas été trouvé.', $filePath));
|
||||
}
|
||||
|
||||
public static function metadataExtractionFailed(string $fileName, string $reason): self
|
||||
{
|
||||
return new self(
|
||||
sprintf('Impossible d\'extraire les métadonnées du fichier "%s" : %s', $fileName, $reason)
|
||||
);
|
||||
}
|
||||
}
|
||||
42
src/Domain/Shared/Domain/Model/FileMetadata.php
Normal file
42
src/Domain/Shared/Domain/Model/FileMetadata.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Shared\Domain\Model;
|
||||
|
||||
readonly class FileMetadata
|
||||
{
|
||||
public function __construct(
|
||||
public string $title,
|
||||
public ?int $volume = null,
|
||||
public ?int $chapter = null,
|
||||
public ?string $author = null,
|
||||
public ?string $description = null,
|
||||
public array $additionalData = []
|
||||
) {
|
||||
}
|
||||
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
return new self(
|
||||
title: $data['title'] ?? '',
|
||||
volume: $data['volume'] ?? null,
|
||||
chapter: $data['chapter'] ?? null,
|
||||
author: $data['author'] ?? null,
|
||||
description: $data['description'] ?? null,
|
||||
additionalData: $data['additionalData'] ?? []
|
||||
);
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'title' => $this->title,
|
||||
'volume' => $this->volume,
|
||||
'chapter' => $this->chapter,
|
||||
'author' => $this->author,
|
||||
'description' => $this->description,
|
||||
'additionalData' => $this->additionalData,
|
||||
];
|
||||
}
|
||||
}
|
||||
49
src/Domain/Shared/Domain/Model/FileUpload.php
Normal file
49
src/Domain/Shared/Domain/Model/FileUpload.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Shared\Domain\Model;
|
||||
|
||||
readonly class FileUpload
|
||||
{
|
||||
public function __construct(
|
||||
public string $id,
|
||||
public string $originalName,
|
||||
public string $path,
|
||||
public string $extension,
|
||||
public int $size,
|
||||
public \DateTimeImmutable $uploadedAt
|
||||
) {
|
||||
}
|
||||
|
||||
public static function create(
|
||||
string $originalName,
|
||||
string $path,
|
||||
int $size
|
||||
): self {
|
||||
return new self(
|
||||
id: uniqid('file_', true),
|
||||
originalName: $originalName,
|
||||
path: $path,
|
||||
extension: strtolower(pathinfo($originalName, PATHINFO_EXTENSION)),
|
||||
size: $size,
|
||||
uploadedAt: new \DateTimeImmutable()
|
||||
);
|
||||
}
|
||||
|
||||
public function isValidFormat(array $allowedExtensions): bool
|
||||
{
|
||||
return in_array($this->extension, $allowedExtensions, true);
|
||||
}
|
||||
|
||||
public function getFormattedSize(int $precision = 2): string
|
||||
{
|
||||
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
$bytes = max($this->size, 0);
|
||||
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
|
||||
$pow = min($pow, count($units) - 1);
|
||||
$bytes /= (1 << (10 * $pow));
|
||||
|
||||
return round($bytes, $precision) . ' ' . $units[$pow];
|
||||
}
|
||||
}
|
||||
111
src/Domain/Shared/Infrastructure/Service/MangaFileManager.php
Normal file
111
src/Domain/Shared/Infrastructure/Service/MangaFileManager.php
Normal file
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Shared\Infrastructure\Service;
|
||||
|
||||
use App\Domain\Shared\Domain\Contract\FileUploadInterface;
|
||||
use App\Domain\Shared\Domain\Contract\MangaPathManagerInterface;
|
||||
|
||||
/**
|
||||
* Implémentation centralisée basée sur la logique éprouvée de CbzGenerator.
|
||||
*/
|
||||
readonly class MangaFileManager implements MangaPathManagerInterface
|
||||
{
|
||||
public function __construct(
|
||||
private string $projectDir,
|
||||
private FileUploadInterface $fileUpload,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getMangaDirectory(string $mangaTitle, string $publicationYear): string
|
||||
{
|
||||
$mangaDirName = ucfirst($this->slugify($mangaTitle)) . ' (' . $publicationYear . ')';
|
||||
$dir = sprintf('%s/public/cbz/%s', $this->projectDir, $mangaDirName);
|
||||
$this->ensureDirectory($dir);
|
||||
return $dir;
|
||||
}
|
||||
|
||||
public function getVolumeDirectory(string $mangaTitle, string $publicationYear, int $volumeNumber): string
|
||||
{
|
||||
$mangaDir = $this->getMangaDirectory($mangaTitle, $publicationYear);
|
||||
$dir = sprintf('%s/volume_%02d', $mangaDir, $volumeNumber);
|
||||
$this->ensureDirectory($dir);
|
||||
return $dir;
|
||||
}
|
||||
|
||||
public function buildChapterCbzPath(string $mangaTitle, string $publicationYear, int $volumeNumber, string $chapterNumber): string
|
||||
{
|
||||
$volumeDir = $this->getVolumeDirectory($mangaTitle, $publicationYear, $volumeNumber);
|
||||
return sprintf(
|
||||
'%s/%s_vol%d_ch%s.cbz',
|
||||
$volumeDir,
|
||||
$this->slugify($mangaTitle),
|
||||
$volumeNumber,
|
||||
$chapterNumber,
|
||||
);
|
||||
}
|
||||
|
||||
public function buildVolumeCbzPath(string $mangaTitle, string $publicationYear, int $volumeNumber): string
|
||||
{
|
||||
$volumeDir = $this->getVolumeDirectory($mangaTitle, $publicationYear, $volumeNumber);
|
||||
return sprintf(
|
||||
'%s/%s_vol%d.cbz',
|
||||
$volumeDir,
|
||||
$this->slugify($mangaTitle),
|
||||
$volumeNumber,
|
||||
);
|
||||
}
|
||||
|
||||
/** @param array<int, string> $files */
|
||||
public function createCbzArchive(array $files, string $cbzPath): void
|
||||
{
|
||||
$zip = new \ZipArchive();
|
||||
if ($zip->open($cbzPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) {
|
||||
throw new \RuntimeException('Failed to create CBZ archive');
|
||||
}
|
||||
|
||||
foreach ($files as $file) {
|
||||
if (!file_exists($file)) {
|
||||
throw new \RuntimeException("File not found: $file");
|
||||
}
|
||||
$zip->addFile($file, basename($file));
|
||||
}
|
||||
|
||||
if (!$zip->close()) {
|
||||
throw new \RuntimeException('Failed to close CBZ archive');
|
||||
}
|
||||
}
|
||||
|
||||
public function moveFileTo(string $sourcePath, string $destinationPath): void
|
||||
{
|
||||
$destinationDir = dirname($destinationPath);
|
||||
$this->ensureDirectory($destinationDir);
|
||||
$this->fileUpload->moveFile($sourcePath, $destinationPath);
|
||||
}
|
||||
|
||||
public function fileExists(string $path): bool
|
||||
{
|
||||
return $this->fileUpload->fileExists($path);
|
||||
}
|
||||
|
||||
private function ensureDirectory(string $dir): void
|
||||
{
|
||||
if (!is_dir($dir)) {
|
||||
$this->fileUpload->createDirectory($dir);
|
||||
}
|
||||
}
|
||||
|
||||
private function slugify(string $text): string
|
||||
{
|
||||
$text = preg_replace('~[^\pL\d]+~u', '-', $text);
|
||||
$text = iconv('utf-8', 'us-ascii//TRANSLIT', $text);
|
||||
$text = preg_replace('~[^-\w]+~', '', $text);
|
||||
$text = trim($text, '-');
|
||||
$text = preg_replace('~-+~', '-', $text);
|
||||
$text = strtolower($text);
|
||||
return $text ?: 'n-a';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Shared\Infrastructure\Service;
|
||||
|
||||
use App\Domain\Shared\Domain\Contract\FileUploadInterface;
|
||||
use App\Domain\Shared\Domain\Exception\FileProcessingException;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
use Symfony\Component\HttpFoundation\File\Exception\FileException;
|
||||
|
||||
readonly class SymfonyFileUpload implements FileUploadInterface
|
||||
{
|
||||
public function __construct(
|
||||
private Filesystem $filesystem,
|
||||
private string $uploadsDirectory
|
||||
) {
|
||||
}
|
||||
|
||||
public function moveUploadedFile(string $sourcePath, string $targetDirectory, string $originalName): string
|
||||
{
|
||||
try {
|
||||
$targetPath = $targetDirectory . '/' . uniqid() . '_' . $originalName;
|
||||
$this->filesystem->copy($sourcePath, $targetPath);
|
||||
|
||||
return $targetPath;
|
||||
} catch (FileException $e) {
|
||||
throw FileProcessingException::uploadFailed($originalName, $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function fileExists(string $filePath): bool
|
||||
{
|
||||
return $this->filesystem->exists($filePath);
|
||||
}
|
||||
|
||||
public function deleteFile(string $filePath): void
|
||||
{
|
||||
if ($this->filesystem->exists($filePath)) {
|
||||
$this->filesystem->remove($filePath);
|
||||
}
|
||||
}
|
||||
|
||||
public function moveFile(string $sourcePath, string $targetPath): void
|
||||
{
|
||||
try {
|
||||
$this->filesystem->rename($sourcePath, $targetPath, true);
|
||||
} catch (FileException $e) {
|
||||
throw FileProcessingException::uploadFailed(
|
||||
basename($sourcePath),
|
||||
$e->getMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function createDirectory(string $path): void
|
||||
{
|
||||
if (!$this->filesystem->exists($path)) {
|
||||
$this->filesystem->mkdir($path, 0755);
|
||||
}
|
||||
}
|
||||
|
||||
public function validateFileFormat(string $filePath, array $allowedExtensions): bool
|
||||
{
|
||||
$extension = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
|
||||
return in_array($extension, $allowedExtensions, true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Shared\Infrastructure\Service;
|
||||
|
||||
use App\Domain\Shared\Domain\Contract\NotificationInterface;
|
||||
use Symfony\Component\Mercure\HubInterface;
|
||||
use Symfony\Component\Mercure\Update;
|
||||
|
||||
readonly class SymfonyNotification implements NotificationInterface
|
||||
{
|
||||
public function __construct(
|
||||
private HubInterface $hub
|
||||
) {
|
||||
}
|
||||
|
||||
public function sendSuccess(string $message): void
|
||||
{
|
||||
$this->sendUpdate([
|
||||
'status' => 'success',
|
||||
'message' => $message
|
||||
]);
|
||||
}
|
||||
|
||||
public function sendError(string $message): void
|
||||
{
|
||||
$this->sendUpdate([
|
||||
'status' => 'error',
|
||||
'message' => $message
|
||||
]);
|
||||
}
|
||||
|
||||
public function sendUpdate(array $data): void
|
||||
{
|
||||
$update = new Update(
|
||||
'notifications',
|
||||
json_encode($data)
|
||||
);
|
||||
|
||||
$this->hub->publish($update);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user