Laravel bestandsupload is zo'n onderwerp dat in een paar regels past als je het goed doet, maar dat beveiligingsgaten, kapotte afbeeldingen en een rommelige storage-map oplevert als je het verkeerd doet. In deze gids bouwen we een flow voor het uploaden van afbeeldingen van begin tot eind: het bestand dat uit een formulier komt valideren, het met de Storage-facade naar schijf schrijven, het via de public-symlink toegankelijk maken en ten slotte bevestigen dat het echt een geldige afbeelding is. Alles wat hier beschreven wordt is een echte, productieklare aanpak — geen verzonnen helper-pakketten of commando's.
Het schijfmodel: local, public en storage
Laravel abstraheert bestandsbewerkingen via het concept van een disk. De configuratie staat in config/filesystems.php en levert standaard twee lokale disks:
- local — onder
storage/app/private, voor privébestanden die niet direct vanaf het web bereikbaar mogen zijn. - public — onder
storage/app/public, voor bestanden die openbaar bedoeld zijn (avatars, omslagafbeeldingen, bijlagen).
Het cruciale onderscheid is dit: de map storage/app/public ligt buiten je webroot (public/), dus de browser kan er niet rechtstreeks bij. Een symlink slaat de brug. Je hoeft dit maar één keer uit te voeren:
php artisan storage:link
Dit commando maakt een symbolische link van public/storage naar storage/app/public. Nu wordt het bestand storage/app/public/avatars/foo.png bereikbaar op de URL /storage/avatars/foo.png. Op sommige gedeelde hostingomgevingen is de PHP-functie symlink() uitgeschakeld; maak de link in dat geval vanuit de shell met ln -sfn storage/app/public public/storage.
De upload valideren
Geen enkel bestand mag ooit naar schijf worden geschreven zonder gevalideerd te zijn. Bij het afhandelen van het formulierverzoek in een controller kun je de regels rechtstreeks aan validate() doorgeven:
public function store(Request $request)
{
$request->validate([
'avatar' => ['required', 'image', 'mimes:jpg,jpeg,png,webp', 'max:2048'],
]);
// ...
}
Elke regel heeft hier zijn nut:
image— vereist dat het bestand een afbeelding is (jpeg, png, bmp, gif, svg of webp).mimes— beperkt expliciet de geaccepteerde extensies. SVG weglaten is meestal verstandig, omdat er scripts in kunnen worden ingebed.max:2048— de groottelimiet is in kilobytes, dus hier 2 MB.
Voor complexere formulieren houdt het verplaatsen van de regels naar een Form Request-klasse, gegenereerd met php artisan make:request StoreAvatarRequest, de controller schoon.
Naar schijf schrijven met de Storage-facade
Zodra de validatie slaagt, is het opslaan van het bestand één regel. Het geüploade bestandsobject (Illuminate\Http\UploadedFile) werkt direct met de disk-methoden:
use Illuminate\Support\Facades\Storage;
$path = $request->file('avatar')->store('avatars', 'public');
// $path = "avatars/9aXk2...png"
store() genereert een unieke, niet te raden naam voor het bestand en geeft het relatieve pad terug. Bewaar dat pad (bijvoorbeeld avatars/9aXk2...png) in de database — niet de volledige URL. Zo breken je gegevens niet als je domein of disk-configuratie verandert.
Als je een specifieke naam wilt instellen, gebruik dan storeAs():
$path = $request->file('avatar')->storeAs(
'avatars', $user->id.'.'.$request->file('avatar')->extension(), 'public'
);
Later, om het bestand weer te geven, zet je het relatieve pad om in een openbare URL:
$url = Storage::disk('public')->url($path);
// /storage/avatars/9aXk2...png
Dezelfde logica geldt aan de Blade-kant:
<img src="{{ Storage::url($user->avatar) }}" alt="Avatar">
Is het echt een afbeelding? Voorbij validatie
De regels image en mimes controleren het MIME-type en de extensie, maar garanderen geen leesbare, onbeschadigde afbeelding. Om de afmetingen van de afbeelding te beperken is er de regel dimensions:
$request->validate([
'avatar' => [
'required', 'image', 'mimes:jpg,jpeg,png,webp', 'max:2048',
'dimensions:min_width=200,min_height=200,max_width=4000,max_height=4000',
],
]);
Als je de afbeelding ook wilt vergroten/verkleinen, bijsnijden of optimaliseren, is de bibliotheek Intervention Image een gangbare en betrouwbare keuze. Hij installeert met Composer:
composer require intervention/image
Een typisch gebruik is het geüploade bestand in het geheugen lezen, het naar een bepaalde breedte schalen en terugschrijven naar schijf:
use Intervention\Image\ImageManager;
use Intervention\Image\Drivers\Gd\Driver;
$manager = new ImageManager(new Driver());
$image = $manager->read($request->file('avatar'));
$image->scaleDown(width: 800);
Storage::disk('public')->put($path, (string) $image->encode());
Deze stap heeft ook een verborgen beveiligingsvoordeel: het opnieuw coderen van de afbeelding verwijdert grotendeels eventuele schadelijke data die in het bestand is ingebed.
Oude bestanden opruimen en verwijderen
Wanneer een gebruiker zijn avatar wijzigt, blijft de oude als rommel achter. Het vorige bestand verwijderen voordat je het nieuwe opslaat is een goede gewoonte:
if ($user->avatar) {
Storage::disk('public')->delete($user->avatar);
}
$user->avatar = $request->file('avatar')->store('avatars', 'public');
$user->save();
Om te controleren of een bestand bestaat, gebruik je Storage::disk('public')->exists($path). Voor bewerkingen zoals bulkverwijdering of het oplijsten van mappen zijn er ook de methoden files(), allFiles() en deleteDirectory().
Privébestanden en veilige downloads
Niet elk bestand hoeft openbaar te zijn. Facturen, contracten of persoonlijke documenten moeten op de local-disk (storage/app/private) blijven en alleen worden geserveerd aan een gebruiker die een autorisatiecontrole doorstaat. Hiervoor route je via een controller in plaats van een symlink:
public function download(Document $document)
{
$this->authorize('view', $document);
return Storage::disk('local')->download($document->path);
}
Hier is het bestand nooit bereikbaar via een directe URL; elk verzoek wordt gecontroleerd via een policy. Een symlink voor openbare afbeeldingen, een geautoriseerde download voor gevoelige documenten — dit onderscheid helder houden is de basis van een goede architectuur.
Veelgestelde vragen
Ik heb storage:link uitgevoerd maar de afbeeldingen verschijnen nog steeds niet — waarom?
De meest voorkomende oorzaken zijn dat de symlink niet op de server is aangemaakt (public/storage is niet meegekopieerd bij de deploy, of de PHP-functie symlink() is uitgeschakeld), of dat het verkeerde relatieve pad — of een volledige URL — in de database is opgeslagen. Maak de symlink vanuit de shell met ln -sfn en bouw de URL altijd met Storage::url().
Wat is de veiligste grootte- en typelimiet voor geüploade bestanden?
Er is geen enkel juist getal; stel het in op je behoeften. De algemene regel: whitelist met mimes alleen de types die je verwacht, laat SVG weg, stel een verstandige max-limiet in en, waar mogelijk, hercodeer de afbeelding met Intervention Image.
Verandert het verplaatsen van bestanden naar Amazon S3 mijn code?
Bijna niet. Dankzij de disk-abstractie volstaat het om Storage::disk('s3') te zeggen of de standaard-disk te wijzigen; je aanroepen van store(), url() en delete() blijven hetzelfde. Je hoeft alleen het pakket league/flysystem-aws-s3-v3 toe te voegen.
Wil je je bestandsuploadflow verstevigen? Of je nu een Laravel-applicatie vanaf nul bouwt of een bestaande storage-opzet beveiligt, we kunnen samenwerken — neem contact met me op.