Refatoring : Working state
This commit is contained in:
@@ -3,260 +3,29 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Media;
|
||||
|
||||
use App\Media\Exception\FileTooLargeException;
|
||||
use App\Media\Exception\InvalidMimeTypeException;
|
||||
use App\Media\Exception\StorageException;
|
||||
use App\Media\Application\MediaApplicationService;
|
||||
use App\Media\Infrastructure\LocalMediaStorage;
|
||||
use App\Post\PostRepositoryInterface;
|
||||
use App\Shared\Pagination\PaginatedResult;
|
||||
use PDOException;
|
||||
use Psr\Http\Message\UploadedFileInterface;
|
||||
|
||||
final class MediaService implements MediaServiceInterface
|
||||
/**
|
||||
* Pont de compatibilité : l'implémentation métier principale vit désormais dans
|
||||
* App\Media\Application\MediaApplicationService.
|
||||
*/
|
||||
final class MediaService extends MediaApplicationService implements MediaServiceInterface
|
||||
{
|
||||
private const ALLOWED_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
||||
private const WEBP_CONVERTIBLE = ['image/jpeg', 'image/png'];
|
||||
private const MIME_EXTENSIONS = [
|
||||
'image/jpeg' => 'webp',
|
||||
'image/png' => 'webp',
|
||||
'image/gif' => 'gif',
|
||||
'image/webp' => 'webp',
|
||||
];
|
||||
private const MIME_EXTENSIONS_FALLBACK = [
|
||||
'image/jpeg' => 'jpg',
|
||||
'image/png' => 'png',
|
||||
'image/gif' => 'gif',
|
||||
'image/webp' => 'webp',
|
||||
];
|
||||
private const MAX_PIXELS = 40000000;
|
||||
|
||||
public function __construct(
|
||||
private readonly MediaRepositoryInterface $mediaRepository,
|
||||
private readonly PostRepositoryInterface $postRepository,
|
||||
private readonly string $uploadDir,
|
||||
private readonly string $uploadUrl,
|
||||
private readonly int $maxSize,
|
||||
MediaRepositoryInterface $mediaRepository,
|
||||
PostRepositoryInterface $postRepository,
|
||||
string $uploadDir,
|
||||
string $uploadUrl,
|
||||
int $maxSize,
|
||||
) {
|
||||
}
|
||||
|
||||
public function findAll(): array
|
||||
{
|
||||
return $this->mediaRepository->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return PaginatedResult<Media>
|
||||
*/
|
||||
public function findPaginated(int $page, int $perPage): PaginatedResult
|
||||
{
|
||||
$page = max(1, $page);
|
||||
$total = $this->mediaRepository->countAll();
|
||||
$offset = ($page - 1) * $perPage;
|
||||
|
||||
return new PaginatedResult(
|
||||
$this->mediaRepository->findPage($perPage, $offset),
|
||||
$total,
|
||||
$page,
|
||||
$perPage,
|
||||
parent::__construct(
|
||||
mediaRepository: $mediaRepository,
|
||||
postRepository: $postRepository,
|
||||
mediaStorage: new LocalMediaStorage($uploadDir),
|
||||
uploadUrl: $uploadUrl,
|
||||
maxSize: $maxSize,
|
||||
);
|
||||
}
|
||||
|
||||
public function findByUserId(int $userId): array
|
||||
{
|
||||
return $this->mediaRepository->findByUserId($userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return PaginatedResult<Media>
|
||||
*/
|
||||
public function findByUserIdPaginated(int $userId, int $page, int $perPage): PaginatedResult
|
||||
{
|
||||
$page = max(1, $page);
|
||||
$total = $this->mediaRepository->countByUserId($userId);
|
||||
$offset = ($page - 1) * $perPage;
|
||||
|
||||
return new PaginatedResult(
|
||||
$this->mediaRepository->findByUserPage($userId, $perPage, $offset),
|
||||
$total,
|
||||
$page,
|
||||
$perPage,
|
||||
);
|
||||
}
|
||||
|
||||
public function findById(int $id): ?Media
|
||||
{
|
||||
return $this->mediaRepository->findById($id);
|
||||
}
|
||||
|
||||
public function store(UploadedFileInterface $uploadedFile, int $userId): string
|
||||
{
|
||||
$size = $uploadedFile->getSize();
|
||||
|
||||
if (!is_int($size)) {
|
||||
throw new StorageException('Impossible de déterminer la taille du fichier uploadé');
|
||||
}
|
||||
|
||||
if ($size > $this->maxSize) {
|
||||
throw new FileTooLargeException($this->maxSize);
|
||||
}
|
||||
|
||||
$tmpPathRaw = $uploadedFile->getStream()->getMetadata('uri');
|
||||
|
||||
if (!is_string($tmpPathRaw) || $tmpPathRaw === '') {
|
||||
throw new StorageException('Impossible de localiser le fichier temporaire uploadé');
|
||||
}
|
||||
|
||||
$tmpPath = $tmpPathRaw;
|
||||
$finfo = new \finfo(FILEINFO_MIME_TYPE);
|
||||
$mime = $finfo->file($tmpPath);
|
||||
|
||||
if ($mime === false || !in_array($mime, self::ALLOWED_MIME_TYPES, true)) {
|
||||
throw new InvalidMimeTypeException($mime === false ? 'unknown' : $mime);
|
||||
}
|
||||
|
||||
$this->assertReasonableDimensions($tmpPath);
|
||||
|
||||
$converted = false;
|
||||
if (in_array($mime, self::WEBP_CONVERTIBLE, true)) {
|
||||
$convertedPath = $this->convertToWebP($tmpPath);
|
||||
if ($convertedPath !== null) {
|
||||
$tmpPath = $convertedPath;
|
||||
$converted = true;
|
||||
}
|
||||
}
|
||||
|
||||
$rawHash = hash_file('sha256', $tmpPath);
|
||||
|
||||
if ($rawHash === false) {
|
||||
if ($converted) {
|
||||
@unlink($tmpPath);
|
||||
}
|
||||
throw new StorageException('Impossible de calculer le hash du fichier');
|
||||
}
|
||||
|
||||
$hash = $rawHash;
|
||||
$existing = $this->mediaRepository->findByHashForUser($hash, $userId);
|
||||
|
||||
if ($existing !== null) {
|
||||
if ($converted) {
|
||||
@unlink($tmpPath);
|
||||
}
|
||||
return $existing->getUrl();
|
||||
}
|
||||
|
||||
if (!is_dir($this->uploadDir) && !@mkdir($this->uploadDir, 0755, true)) {
|
||||
throw new StorageException("Impossible de créer le répertoire d'upload");
|
||||
}
|
||||
|
||||
$extension = $converted ? self::MIME_EXTENSIONS[$mime] : self::MIME_EXTENSIONS_FALLBACK[$mime];
|
||||
$filename = uniqid('', true) . '_' . bin2hex(random_bytes(4)) . '.' . $extension;
|
||||
$destPath = $this->uploadDir . DIRECTORY_SEPARATOR . $filename;
|
||||
|
||||
if ($converted) {
|
||||
if (!copy($tmpPath, $destPath)) {
|
||||
@unlink($tmpPath);
|
||||
throw new StorageException('Impossible de déplacer le fichier converti');
|
||||
}
|
||||
@unlink($tmpPath);
|
||||
} else {
|
||||
$uploadedFile->moveTo($destPath);
|
||||
}
|
||||
|
||||
$url = $this->uploadUrl . '/' . $filename;
|
||||
$media = new Media(0, $filename, $url, $hash, $userId);
|
||||
|
||||
try {
|
||||
$this->mediaRepository->create($media);
|
||||
} catch (PDOException $e) {
|
||||
@unlink($destPath);
|
||||
|
||||
$duplicate = $this->mediaRepository->findByHashForUser($hash, $userId);
|
||||
if ($duplicate !== null) {
|
||||
return $duplicate->getUrl();
|
||||
}
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
public function getUsageSummary(Media $media, int $sampleLimit = 5): array
|
||||
{
|
||||
return [
|
||||
'count' => $this->postRepository->countByEmbeddedMediaUrl($media->getUrl()),
|
||||
'posts' => $this->postRepository->findByEmbeddedMediaUrl($media->getUrl(), $sampleLimit),
|
||||
];
|
||||
}
|
||||
|
||||
public function delete(Media $media): void
|
||||
{
|
||||
$filePath = $this->uploadDir . DIRECTORY_SEPARATOR . $media->getFilename();
|
||||
|
||||
if (file_exists($filePath)) {
|
||||
@unlink($filePath);
|
||||
}
|
||||
|
||||
$this->mediaRepository->delete($media->getId());
|
||||
}
|
||||
|
||||
private function assertReasonableDimensions(string $path): void
|
||||
{
|
||||
$size = @getimagesize($path);
|
||||
|
||||
if ($size === false) {
|
||||
throw new StorageException('Impossible de lire les dimensions de l\'image');
|
||||
}
|
||||
|
||||
[$width, $height] = $size;
|
||||
|
||||
if ($width <= 0 || $height <= 0) {
|
||||
throw new StorageException('Dimensions d\'image invalides');
|
||||
}
|
||||
|
||||
if (($width * $height) > self::MAX_PIXELS) {
|
||||
throw new StorageException('Image trop volumineuse en dimensions pour être traitée');
|
||||
}
|
||||
}
|
||||
|
||||
private function convertToWebP(string $sourcePath): ?string
|
||||
{
|
||||
if (!function_exists('imagewebp')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$data = file_get_contents($sourcePath);
|
||||
|
||||
if ($data === false || $data === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$image = imagecreatefromstring($data);
|
||||
|
||||
if ($image === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
imagealphablending($image, false);
|
||||
imagesavealpha($image, true);
|
||||
|
||||
$tmpFile = tempnam(sys_get_temp_dir(), 'slim_webp_');
|
||||
|
||||
if ($tmpFile === false) {
|
||||
imagedestroy($image);
|
||||
return null;
|
||||
}
|
||||
|
||||
@unlink($tmpFile);
|
||||
$tmpPath = $tmpFile . '.webp';
|
||||
|
||||
if (!imagewebp($image, $tmpPath, 85)) {
|
||||
imagedestroy($image);
|
||||
@unlink($tmpPath);
|
||||
return null;
|
||||
}
|
||||
|
||||
imagedestroy($image);
|
||||
|
||||
return $tmpPath;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user