"Pourquoi diable mon code ne supprime-t-il pas l'ancienne image ?" - j'ai passé trois heures à déboguer ce problème avant de découvrir l'évidence. Si vous avez déjà vécu cette frustration, vous n'êtes pas seul.
Un développeur de notre équipe a récemment perdu une demi-journée entière parce que le système d'upload de photos qu'il avait créé conservait mystérieusement les anciennes images, remplissant progressivement le disque de stockage. La cause ? Un simple accesseur Eloquent qui transformait les chemins de fichiers en URLs complètes.
Plongeons dans ce problème sournois et découvrons comment le résoudre proprement avec une architecture professionnelle.
Avant d'aborder le problème spécifique, clarifions quelques concepts essentiels de Laravel :
Un accesseur est une méthode spéciale dans un modèle Eloquent qui permet de transformer automatiquement la valeur d'un attribut lorsqu'on y accède. Par convention, un accesseur est nommé get{AttributeName}Attribute
.
Par exemple, si vous avez un champ avatar
dans votre base de données qui stocke un chemin de fichier comme 'avatars/user123.jpg'
, vous pourriez créer un accesseur qui transforme automatiquement ce chemin en URL complète :
<?php
public function getAvatarAttribute($value)
{
return $value ? url(Storage::url($value)) : null;
}
Avec cet accesseur, chaque fois que vous accédez à $user->avatar
, vous obtiendrez l'URL complète (https://votresite.com/storage/avatars/user123.jpg
) plutôt que le chemin brut.
Le système de stockage de Laravel est une abstraction qui vous permet de manipuler des fichiers sur différents "disques" (local, Amazon S3, etc.). Lorsque vous utilisez des méthodes comme Storage::delete()
, le système s'attend à recevoir un chemin relatif par rapport au disque spécifié, pas une URL complète.
Par exemple :
Storage::disk('public')->delete('avatars/user123.jpg')
— CORRECT ✅
Storage::disk('public')->delete('https://votresite.com/storage/avatars/user123.jpg')
— INCORRECT ❌
Vous développez une API pour gérer les profils utilisateurs avec des avatars. Votre modèle User
possède un accesseur bien pratique :
<?php
public function getAvatarAttribute($value)
{
return $value ? url(Storage::url($value)) : null;
}
Cet accesseur est parfait pour l'affichage, car il transforme automatiquement 'avatars/user123.jpg'
en 'https://votresite.com/storage/avatars/user123.jpg'
.
Lorsque vous essayez de remplacer une image, votre code ressemble probablement à ceci :
<?php // Dans votre contrôleur
if ($user->avatar) {
Storage::disk('public')->delete($user->avatar); // 💥 ÉCHEC !
}
Cette tentative échoue silencieusement car $user->avatar
retourne une URL complète et non le chemin relatif attendu par Storage::delete()
.
Pour mieux comprendre le problème et sa solution, décomposons le processus de remplacement d'image en étapes logiques :
Problème : Lorsque vous accédez à $user->avatar
, l'accesseur transforme la valeur en URL complète.
Solution : Utilisez $user->getRawOriginal('avatar')
pour obtenir la valeur brute stockée en base de données, sans la transformation de l'accesseur.
<?php
// Incorrect
$currentAvatar = $user->avatar; // Renvoie "https://votresite.com/storage/avatars/user123.jpg"
// Correct
$currentAvatar = $user->getRawOriginal('avatar'); // Renvoie "avatars/user123.jpg"
Avant de supprimer, vérifiez que le fichier existe réellement pour éviter les erreurs :
<?php
if ($currentAvatar && Storage::disk('public')->exists($currentAvatar)) {
// Procéder à la suppression
}
Maintenant que vous avez le chemin correct, vous pouvez supprimer le fichier :
<?php
Storage::disk('public')->delete($currentAvatar);
Stockez le nouveau fichier et enregistrez son chemin :
<?php
$path = $request->file('avatar')->store('avatars', 'public');
$user->avatar = $path;
$user->save();
Prévoyez un nettoyage en cas d'échec pour éviter les fichiers orphelins :
<?php
try {
// Étapes 3 et 4
} catch (\Exception $e) {
// Nettoyer les fichiers si nécessaire
throw $e;
}
Voici une approche professionnelle avec séparation des responsabilités :
<?php
namespace App\Traits;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
trait HasUploadedFiles
{
/**
* Remplace un fichier et met à jour l'attribut du modèle
*
* @param UploadedFile|null $newFile
* @param string $attribute
* @param string $directory
* @param string $disk
* @return string|null
*/
public function replaceFile(?UploadedFile $newFile, string $attribute, string $directory, string $disk = 'public')
{
// Si pas de nouveau fichier, on ne fait rien
if (!$newFile) {
return null;
}
// Suppression de l'ancien fichier
$this->deleteFile($attribute, $disk);
// Stockage du nouveau fichier
$path = $newFile->store($directory, $disk);
return $path;
}
/**
* Supprime un fichier associé à un attribut
*
* @param string $attribute
* @param string $disk
* @return bool
*/
public function deleteFile(string $attribute, string $disk = 'public')
{
$filePath = $this->getRawOriginal($attribute);
if ($filePath && Storage::disk($disk)->exists($filePath)) {
return Storage::disk($disk)->delete($filePath);
}
return false;
}
}
<?php
namespace App\Models;
use App\Traits\HasUploadedFiles;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
use HasUploadedFiles;
// Accesseur qui transforme le chemin en URL
public function getAvatarAttribute($value)
{
return $value ? url(Storage::url($value)) : null;
}
// Ajoutez un événement de suppression pour nettoyer automatiquement
protected static function boot()
{
parent::boot();
static::deleting(function ($user) {
$user->deleteFile('avatar');
});
}
}
<?php
namespace App\Services;
use App\Models\User;
use Illuminate\Http\UploadedFile;
class UserAvatarService
{
/**
* Met à jour l'avatar d'un utilisateur
*
* @param User $user
* @param UploadedFile|null $avatar
* @return User
*/
public function updateAvatar(User $user, ?UploadedFile $avatar)
{
if ($avatar) {
$path = $user->replaceFile($avatar, 'avatar', 'avatars');
$user->avatar = $path;
$user->save();
}
return $user;
}
}
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Services\UserAvatarService;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
class UserAvatarController extends Controller
{
protected $avatarService;
public function __construct(UserAvatarService $avatarService)
{
$this->avatarService = $avatarService;
}
/**
* Met à jour l'avatar de l'utilisateur connecté
*
* @param Request $request
* @return Response
*/
public function update(Request $request)
{
$request->validate([
'avatar' => 'required|image|mimes:jpeg,png,jpg,webp|max:2048', // 2MB max
]);
$user = $this->avatarService->updateAvatar(
auth()->user(),
$request->file('avatar')
);
return response()->json([
'message' => 'Avatar mis à jour avec succès',
'user' => $user
]);
}
}
<?php
// Dans routes/api.php
Route::middleware('auth:sanctum')->group(function () {
Route::post('/user/avatar', [App\Http\Controllers\Api\UserAvatarController::class, 'update']);
});
Séparation des responsabilités : Le contrôleur gère uniquement la requête, le service encapsule la logique métier, et le trait fournit la fonctionnalité de base de gestion des fichiers.
Réutilisabilité : Le trait HasUploadedFiles
peut être utilisé avec n'importe quel modèle qui gère des fichiers (produits avec images, articles avec vignettes, documents, etc.).
Maintenance facilitée : En isolant la logique de gestion des fichiers, les futures modifications seront plus simples à implémenter.
Nettoyage automatique : L'événement de suppression dans le modèle garantit qu'aucun fichier orphelin ne restera sur le serveur.
Toujours utiliser getRawOriginal()
lorsque vous travaillez avec des chemins de fichiers transformés par des accesseurs.
Cette simple habitude vous évitera des heures de débogage et de frustration, tout en gardant votre disque de stockage propre et optimisé.
Avec cette architecture en place, vous pouvez facilement étendre votre système pour gérer plusieurs types de fichiers par modèle, implémenter des validations spécifiques, ou même ajouter des fonctionnalités comme le redimensionnement d'images à la volée.
N'oubliez pas : dans le développement Laravel, les petits détails font souvent la différence entre une application robuste et un cauchemar de maintenance.