Working state
This commit is contained in:
@@ -6,37 +6,17 @@ namespace App\Media;
|
||||
use App\Media\Exception\FileTooLargeException;
|
||||
use App\Media\Exception\InvalidMimeTypeException;
|
||||
use App\Media\Exception\StorageException;
|
||||
use App\Media\MediaServiceInterface;
|
||||
use App\Shared\Http\FlashServiceInterface;
|
||||
use App\Shared\Http\SessionManagerInterface;
|
||||
use App\Shared\Pagination\PaginationPresenter;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Slim\Views\Twig;
|
||||
|
||||
/**
|
||||
* Contrôleur du domaine Media.
|
||||
*
|
||||
* Gère deux responsabilités HTTP :
|
||||
* 1. Upload d'images depuis l'éditeur Trumbowyg (réponse JSON)
|
||||
* 2. Administration des médias uploadés (liste, suppression)
|
||||
*
|
||||
* Toute la logique métier (validation, conversion WebP, déduplication,
|
||||
* stockage disque) est déléguée à MediaService via MediaServiceInterface.
|
||||
*
|
||||
* Droits d'accès :
|
||||
* - Upload : tout utilisateur connecté
|
||||
* - Liste : chaque utilisateur voit uniquement ses propres médias ;
|
||||
* l'administrateur et l'éditeur voient tous les médias
|
||||
* - Suppression : propriétaire du média, éditeur ou administrateur
|
||||
*/
|
||||
final class MediaController
|
||||
{
|
||||
/**
|
||||
* @param Twig $view Moteur de templates Twig
|
||||
* @param MediaServiceInterface $mediaService Service de gestion des médias
|
||||
* @param FlashServiceInterface $flash Service de messages flash
|
||||
* @param SessionManagerInterface $sessionManager Gestionnaire de session
|
||||
*/
|
||||
private const PER_PAGE = 12;
|
||||
|
||||
public function __construct(
|
||||
private readonly Twig $view,
|
||||
private readonly MediaServiceInterface $mediaService,
|
||||
@@ -45,45 +25,30 @@ final class MediaController
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Affiche la page de gestion des médias.
|
||||
*
|
||||
* Un éditeur ou un administrateur voit tous les médias.
|
||||
* Un utilisateur avec le rôle 'user' voit uniquement ses propres médias.
|
||||
*
|
||||
* @param Request $req La requête HTTP
|
||||
* @param Response $res La réponse HTTP
|
||||
*
|
||||
* @return Response La page HTML de gestion des médias
|
||||
*/
|
||||
public function index(Request $req, Response $res): Response
|
||||
{
|
||||
$isAdmin = $this->sessionManager->isAdmin() || $this->sessionManager->isEditor();
|
||||
$userId = $this->sessionManager->getUserId();
|
||||
$page = PaginationPresenter::resolvePage($req->getQueryParams());
|
||||
|
||||
$media = $isAdmin
|
||||
? $this->mediaService->findAll()
|
||||
: $this->mediaService->findByUserId((int) $userId);
|
||||
$paginated = $isAdmin
|
||||
? $this->mediaService->findPaginated($page, self::PER_PAGE)
|
||||
: $this->mediaService->findByUserIdPaginated((int) $userId, $page, self::PER_PAGE);
|
||||
|
||||
$usageByMediaId = [];
|
||||
foreach ($paginated->getItems() as $item) {
|
||||
$usageByMediaId[$item->getId()] = $this->mediaService->getUsageSummary($item, 5);
|
||||
}
|
||||
|
||||
return $this->view->render($res, 'admin/media/index.twig', [
|
||||
'media' => $media,
|
||||
'error' => $this->flash->get('media_error'),
|
||||
'success' => $this->flash->get('media_success'),
|
||||
'media' => $paginated->getItems(),
|
||||
'mediaUsage' => $usageByMediaId,
|
||||
'pagination' => PaginationPresenter::fromRequest($req, $paginated),
|
||||
'error' => $this->flash->get('media_error'),
|
||||
'success' => $this->flash->get('media_success'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Traite l'upload d'une image envoyée par le plugin Trumbowyg Upload.
|
||||
*
|
||||
* Vérifie la présence et l'absence d'erreur PSR-7 avant de déléguer
|
||||
* à MediaService. Les erreurs métier (taille, MIME, stockage) sont
|
||||
* converties en réponses JSON avec le code HTTP approprié.
|
||||
*
|
||||
* @param Request $req La requête HTTP multipart contenant le champ "image"
|
||||
* @param Response $res La réponse HTTP
|
||||
*
|
||||
* @return Response JSON {"success": true, "file": "/media/..."} ou {"error": "..."}
|
||||
*/
|
||||
public function upload(Request $req, Response $res): Response
|
||||
{
|
||||
$files = $req->getUploadedFiles();
|
||||
@@ -107,16 +72,7 @@ final class MediaController
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprime un média (fichier sur disque + entrée en base).
|
||||
*
|
||||
* Vérifie que l'utilisateur connecté est le propriétaire du média
|
||||
* ou un administrateur / éditeur. Redirige avec un message flash dans les deux cas.
|
||||
*
|
||||
* @param Request $req La requête HTTP
|
||||
* @param Response $res La réponse HTTP
|
||||
* @param array<string, string> $args Paramètres de route (id)
|
||||
*
|
||||
* @return Response Redirection vers /admin/media
|
||||
* @param array<string, mixed> $args
|
||||
*/
|
||||
public function delete(Request $req, Response $res, array $args): Response
|
||||
{
|
||||
@@ -125,7 +81,6 @@ final class MediaController
|
||||
|
||||
if ($media === null) {
|
||||
$this->flash->set('media_error', 'Fichier introuvable');
|
||||
|
||||
return $res->withHeader('Location', '/admin/media')->withStatus(302);
|
||||
}
|
||||
|
||||
@@ -134,6 +89,25 @@ final class MediaController
|
||||
|
||||
if (!$isAdmin && $media->getUserId() !== $userId) {
|
||||
$this->flash->set('media_error', "Vous n'êtes pas autorisé à supprimer ce fichier");
|
||||
return $res->withHeader('Location', '/admin/media')->withStatus(302);
|
||||
}
|
||||
|
||||
/** @var array<string, mixed> $usage */
|
||||
$usage = $this->mediaService->getUsageSummary($media, 3);
|
||||
$usageCount = isset($usage['count']) && is_int($usage['count']) ? $usage['count'] : 0;
|
||||
/** @var array<int, \App\Post\Post> $usagePosts */
|
||||
$usagePosts = isset($usage['posts']) && is_array($usage['posts']) ? $usage['posts'] : [];
|
||||
|
||||
if ($usageCount > 0) {
|
||||
$titles = array_map(
|
||||
static fn ($post) => '« ' . $post->getTitle() . ' »',
|
||||
$usagePosts
|
||||
);
|
||||
$details = $titles === [] ? '' : ' Utilisé dans : ' . implode(', ', $titles) . '.';
|
||||
$this->flash->set(
|
||||
'media_error',
|
||||
'Ce média est encore référencé dans ' . $usageCount . ' article(s) et ne peut pas être supprimé.' . $details
|
||||
);
|
||||
|
||||
return $res->withHeader('Location', '/admin/media')->withStatus(302);
|
||||
}
|
||||
@@ -144,14 +118,6 @@ final class MediaController
|
||||
return $res->withHeader('Location', '/admin/media')->withStatus(302);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne une réponse JSON de succès avec l'URL du fichier uploadé.
|
||||
*
|
||||
* @param Response $res La réponse HTTP
|
||||
* @param string $fileUrl L'URL publique du fichier
|
||||
*
|
||||
* @return Response La réponse JSON {"success": true, "file": "..."}
|
||||
*/
|
||||
private function jsonSuccess(Response $res, string $fileUrl): Response
|
||||
{
|
||||
$res->getBody()->write(json_encode([
|
||||
@@ -162,15 +128,6 @@ final class MediaController
|
||||
return $res->withHeader('Content-Type', 'application/json')->withStatus(200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne une réponse JSON d'erreur.
|
||||
*
|
||||
* @param Response $res La réponse HTTP
|
||||
* @param string $message Le message d'erreur
|
||||
* @param int $status Le code HTTP de l'erreur
|
||||
*
|
||||
* @return Response La réponse JSON {"error": "..."}
|
||||
*/
|
||||
private function jsonError(Response $res, string $message, int $status): Response
|
||||
{
|
||||
$res->getBody()->write(json_encode(['error' => $message], JSON_THROW_ON_ERROR));
|
||||
|
||||
@@ -5,109 +5,104 @@ namespace App\Media;
|
||||
|
||||
use PDO;
|
||||
|
||||
/**
|
||||
* Dépôt pour la persistance des médias uploadés.
|
||||
*
|
||||
* Responsabilité unique : exécuter les requêtes SQL liées à la table `media`
|
||||
* et retourner des instances de Media hydratées.
|
||||
*/
|
||||
final class MediaRepository implements MediaRepositoryInterface
|
||||
{
|
||||
/**
|
||||
* Fragment SELECT commun à toutes les requêtes de lecture.
|
||||
*/
|
||||
private const SELECT = 'SELECT id, filename, url, hash, user_id, created_at FROM media';
|
||||
|
||||
/**
|
||||
* @param PDO $db Instance de connexion à la base de données
|
||||
*/
|
||||
public function __construct(private readonly PDO $db)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne tous les médias triés du plus récent au plus ancien.
|
||||
*
|
||||
* @return Media[] La liste complète des médias
|
||||
*/
|
||||
public function findAll(): array
|
||||
{
|
||||
$stmt = $this->db->query(self::SELECT . ' ORDER BY id DESC');
|
||||
if ($stmt === false) {
|
||||
throw new \RuntimeException('La requête SELECT sur media a échoué.');
|
||||
}
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
return array_map(fn ($row) => Media::fromArray($row), $rows);
|
||||
return array_map(fn ($row) => Media::fromArray($row), $stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||
}
|
||||
|
||||
public function findPage(int $limit, int $offset): array
|
||||
{
|
||||
$stmt = $this->db->prepare(self::SELECT . ' ORDER BY id DESC LIMIT :limit OFFSET :offset');
|
||||
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
|
||||
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
|
||||
return array_map(fn ($row) => Media::fromArray($row), $stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||
}
|
||||
|
||||
public function countAll(): int
|
||||
{
|
||||
$stmt = $this->db->query('SELECT COUNT(*) FROM media');
|
||||
if ($stmt === false) {
|
||||
throw new \RuntimeException('La requête COUNT sur media a échoué.');
|
||||
}
|
||||
|
||||
return (int) ($stmt->fetchColumn() ?: 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne tous les médias appartenant à un utilisateur donné,
|
||||
* triés du plus récent au plus ancien.
|
||||
*
|
||||
* @param int $userId Identifiant de l'utilisateur
|
||||
*
|
||||
* @return Media[] La liste des médias de cet utilisateur
|
||||
*/
|
||||
public function findByUserId(int $userId): array
|
||||
{
|
||||
$stmt = $this->db->prepare(self::SELECT . ' WHERE user_id = :user_id ORDER BY id DESC');
|
||||
$stmt->execute([':user_id' => $userId]);
|
||||
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
return array_map(fn ($row) => Media::fromArray($row), $rows);
|
||||
return array_map(fn ($row) => Media::fromArray($row), $stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||
}
|
||||
|
||||
public function findByUserPage(int $userId, int $limit, int $offset): array
|
||||
{
|
||||
$stmt = $this->db->prepare(self::SELECT . ' WHERE user_id = :user_id ORDER BY id DESC LIMIT :limit OFFSET :offset');
|
||||
$stmt->bindValue(':user_id', $userId, PDO::PARAM_INT);
|
||||
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
|
||||
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
|
||||
return array_map(fn ($row) => Media::fromArray($row), $stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||
}
|
||||
|
||||
public function countByUserId(int $userId): int
|
||||
{
|
||||
$stmt = $this->db->prepare('SELECT COUNT(*) FROM media WHERE user_id = :user_id');
|
||||
$stmt->execute([':user_id' => $userId]);
|
||||
|
||||
return (int) $stmt->fetchColumn();
|
||||
}
|
||||
|
||||
/**
|
||||
* Trouve un média par son identifiant.
|
||||
*
|
||||
* @param int $id Identifiant du média
|
||||
*
|
||||
* @return Media|null Le média trouvé, ou null s'il n'existe pas
|
||||
*/
|
||||
public function findById(int $id): ?Media
|
||||
{
|
||||
$stmt = $this->db->prepare(self::SELECT . ' WHERE id = :id');
|
||||
$stmt->execute([':id' => $id]);
|
||||
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
return $row ? Media::fromArray($row) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trouve un média par le hash SHA-256 de son contenu.
|
||||
*
|
||||
* Utilisé pour la détection des doublons à l'upload.
|
||||
*
|
||||
* @param string $hash Hash SHA-256 du contenu binaire du fichier
|
||||
*
|
||||
* @return Media|null Le média existant, ou null si aucun doublon
|
||||
*/
|
||||
public function findByHash(string $hash): ?Media
|
||||
{
|
||||
$stmt = $this->db->prepare(self::SELECT . ' WHERE hash = :hash');
|
||||
$stmt = $this->db->prepare(self::SELECT . ' WHERE hash = :hash ORDER BY id DESC LIMIT 1');
|
||||
$stmt->execute([':hash' => $hash]);
|
||||
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
return $row ? Media::fromArray($row) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Persiste un nouveau média en base de données.
|
||||
*
|
||||
* @param Media $media Le média à créer
|
||||
*
|
||||
* @return int L'identifiant généré par la base de données
|
||||
*/
|
||||
public function findByHashForUser(string $hash, int $userId): ?Media
|
||||
{
|
||||
$stmt = $this->db->prepare(self::SELECT . ' WHERE hash = :hash AND user_id = :user_id ORDER BY id DESC LIMIT 1');
|
||||
$stmt->execute([':hash' => $hash, ':user_id' => $userId]);
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
return $row ? Media::fromArray($row) : null;
|
||||
}
|
||||
|
||||
public function create(Media $media): int
|
||||
{
|
||||
$stmt = $this->db->prepare('
|
||||
INSERT INTO media (filename, url, hash, user_id, created_at)
|
||||
VALUES (:filename, :url, :hash, :user_id, :created_at)
|
||||
');
|
||||
$stmt = $this->db->prepare(
|
||||
'INSERT INTO media (filename, url, hash, user_id, created_at)
|
||||
VALUES (:filename, :url, :hash, :user_id, :created_at)'
|
||||
);
|
||||
|
||||
$stmt->execute([
|
||||
':filename' => $media->getFilename(),
|
||||
@@ -120,15 +115,6 @@ final class MediaRepository implements MediaRepositoryInterface
|
||||
return (int) $this->db->lastInsertId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprime un média de la base de données.
|
||||
*
|
||||
* La suppression du fichier physique sur disque est à la charge de l'appelant.
|
||||
*
|
||||
* @param int $id Identifiant du média à supprimer
|
||||
*
|
||||
* @return int Nombre de lignes supprimées (0 si le média n'existe plus)
|
||||
*/
|
||||
public function delete(int $id): int
|
||||
{
|
||||
$stmt = $this->db->prepare('DELETE FROM media WHERE id = :id');
|
||||
|
||||
@@ -3,63 +3,31 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Media;
|
||||
|
||||
/**
|
||||
* Contrat de persistance des médias uploadés.
|
||||
*
|
||||
* Découple les contrôleurs de l'implémentation concrète PDO/SQLite,
|
||||
* facilitant les mocks dans les tests unitaires.
|
||||
*/
|
||||
interface MediaRepositoryInterface
|
||||
{
|
||||
/**
|
||||
* Retourne tous les médias triés du plus récent au plus ancien.
|
||||
*
|
||||
* @return Media[]
|
||||
*/
|
||||
/** @return Media[] */
|
||||
public function findAll(): array;
|
||||
|
||||
/**
|
||||
* Retourne tous les médias d'un utilisateur donné.
|
||||
*
|
||||
* @param int $userId Identifiant de l'utilisateur
|
||||
*
|
||||
* @return Media[]
|
||||
*/
|
||||
/** @return Media[] */
|
||||
public function findPage(int $limit, int $offset): array;
|
||||
|
||||
public function countAll(): int;
|
||||
|
||||
/** @return Media[] */
|
||||
public function findByUserId(int $userId): array;
|
||||
|
||||
/**
|
||||
* Trouve un média par son identifiant.
|
||||
*
|
||||
* @param int $id Identifiant du média
|
||||
*
|
||||
* @return Media|null Le média trouvé, ou null s'il n'existe pas
|
||||
*/
|
||||
/** @return Media[] */
|
||||
public function findByUserPage(int $userId, int $limit, int $offset): array;
|
||||
|
||||
public function countByUserId(int $userId): int;
|
||||
|
||||
public function findById(int $id): ?Media;
|
||||
|
||||
/**
|
||||
* Trouve un média par le hash SHA-256 de son contenu (déduplication).
|
||||
*
|
||||
* @param string $hash Hash SHA-256 du contenu binaire du fichier
|
||||
*
|
||||
* @return Media|null Le média existant, ou null si aucun doublon
|
||||
*/
|
||||
public function findByHash(string $hash): ?Media;
|
||||
|
||||
/**
|
||||
* Persiste un nouveau média en base de données.
|
||||
*
|
||||
* @param Media $media Le média à créer
|
||||
*
|
||||
* @return int L'identifiant généré par la base de données
|
||||
*/
|
||||
public function findByHashForUser(string $hash, int $userId): ?Media;
|
||||
|
||||
public function create(Media $media): int;
|
||||
|
||||
/**
|
||||
* Supprime un média de la base de données.
|
||||
*
|
||||
* @param int $id Identifiant du média à supprimer
|
||||
*
|
||||
* @return int Nombre de lignes supprimées
|
||||
*/
|
||||
public function delete(int $id): int;
|
||||
}
|
||||
|
||||
@@ -6,38 +6,32 @@ namespace App\Media;
|
||||
use App\Media\Exception\FileTooLargeException;
|
||||
use App\Media\Exception\InvalidMimeTypeException;
|
||||
use App\Media\Exception\StorageException;
|
||||
use App\Post\PostRepositoryInterface;
|
||||
use App\Shared\Pagination\PaginatedResult;
|
||||
use PDOException;
|
||||
use Psr\Http\Message\UploadedFileInterface;
|
||||
|
||||
final class MediaService implements MediaServiceInterface
|
||||
{
|
||||
private const ALLOWED_MIME_TYPES = [
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/gif',
|
||||
'image/webp',
|
||||
];
|
||||
|
||||
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,
|
||||
@@ -49,11 +43,45 @@ final class MediaService implements MediaServiceInterface
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -106,7 +134,7 @@ final class MediaService implements MediaServiceInterface
|
||||
}
|
||||
|
||||
$hash = $rawHash;
|
||||
$existing = $this->mediaRepository->findByHash($hash);
|
||||
$existing = $this->mediaRepository->findByHashForUser($hash, $userId);
|
||||
|
||||
if ($existing !== null) {
|
||||
if ($converted) {
|
||||
@@ -119,9 +147,7 @@ final class MediaService implements MediaServiceInterface
|
||||
throw new StorageException("Impossible de créer le répertoire d'upload");
|
||||
}
|
||||
|
||||
$extension = $converted
|
||||
? self::MIME_EXTENSIONS[$mime]
|
||||
: self::MIME_EXTENSIONS_FALLBACK[$mime];
|
||||
$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;
|
||||
|
||||
@@ -141,19 +167,27 @@ final class MediaService implements MediaServiceInterface
|
||||
try {
|
||||
$this->mediaRepository->create($media);
|
||||
} catch (PDOException $e) {
|
||||
$duplicate = $this->mediaRepository->findByHash($hash);
|
||||
@unlink($destPath);
|
||||
|
||||
$duplicate = $this->mediaRepository->findByHashForUser($hash, $userId);
|
||||
if ($duplicate !== null) {
|
||||
@unlink($destPath);
|
||||
return $duplicate->getUrl();
|
||||
}
|
||||
|
||||
@unlink($destPath);
|
||||
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();
|
||||
|
||||
@@ -3,60 +3,33 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Media;
|
||||
|
||||
use App\Shared\Pagination\PaginatedResult;
|
||||
use Psr\Http\Message\UploadedFileInterface;
|
||||
|
||||
/**
|
||||
* Contrat du service de gestion des médias.
|
||||
*
|
||||
* Permet de mocker le service dans les tests unitaires sans dépendre
|
||||
* de la classe concrète finale MediaService.
|
||||
*/
|
||||
interface MediaServiceInterface
|
||||
{
|
||||
/**
|
||||
* Retourne tous les médias triés du plus récent au plus ancien.
|
||||
*
|
||||
* @return Media[]
|
||||
*/
|
||||
/** @return Media[] */
|
||||
public function findAll(): array;
|
||||
|
||||
/**
|
||||
* Retourne tous les médias appartenant à un utilisateur donné.
|
||||
*
|
||||
* @param int $userId Identifiant de l'utilisateur
|
||||
*
|
||||
* @return Media[]
|
||||
* @return PaginatedResult<Media>
|
||||
*/
|
||||
public function findPaginated(int $page, int $perPage): PaginatedResult;
|
||||
|
||||
/** @return Media[] */
|
||||
public function findByUserId(int $userId): array;
|
||||
|
||||
/**
|
||||
* Trouve un média par son identifiant.
|
||||
*
|
||||
* @param int $id Identifiant du média
|
||||
*
|
||||
* @return Media|null Le média trouvé, ou null s'il n'existe pas
|
||||
* @return PaginatedResult<Media>
|
||||
*/
|
||||
public function findByUserIdPaginated(int $userId, int $page, int $perPage): PaginatedResult;
|
||||
|
||||
public function findById(int $id): ?Media;
|
||||
|
||||
/**
|
||||
* Valide, convertit, déduplique et stocke un fichier uploadé.
|
||||
*
|
||||
* @param UploadedFileInterface $uploadedFile Le fichier PSR-7 reçu
|
||||
* @param int $userId Identifiant de l'auteur
|
||||
*
|
||||
* @return string L'URL publique du fichier stocké
|
||||
*
|
||||
* @throws \App\Media\Exception\FileTooLargeException Si la taille dépasse le maximum autorisé
|
||||
* @throws \App\Media\Exception\InvalidMimeTypeException Si le type MIME n'est pas autorisé
|
||||
* @throws \App\Media\Exception\StorageException Si une opération disque échoue
|
||||
*/
|
||||
public function store(UploadedFileInterface $uploadedFile, int $userId): string;
|
||||
|
||||
/**
|
||||
* Supprime un média : fichier physique sur disque et entrée en base.
|
||||
*
|
||||
* @param Media $media Le média à supprimer
|
||||
* @return void
|
||||
*/
|
||||
/** @return array{count:int, posts:array<int, \App\Post\Post>} */
|
||||
public function getUsageSummary(Media $media, int $sampleLimit = 5): array;
|
||||
|
||||
public function delete(Media $media): void;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user