aslain.dev
0%
01 Hizmetler 02 Hakkımda 03 Projeler 04 Stack 05 Blog 06 İletişim
← Tüm makaleler Webentwicklung

Laravel Datei-Upload: Storage und Bildvalidierung

Laravel Datei-Upload ist eines jener Themen, die in wenigen Zeilen erledigt sind, wenn man es richtig macht, aber Sicherheitslücken, kaputte Bilder und einen unordentlichen storage-Ordner hervorbringen, wenn man es falsch macht. In diesem Leitfaden bauen wir einen Bild-Upload-Ablauf von Anfang bis Ende: die aus einem Formular eingehende Datei validieren, sie mit der Storage-Facade auf die Festplatte schreiben, sie über den public-Symlink zugänglich machen und schließlich bestätigen, dass es sich wirklich um ein gültiges Bild handelt. Alles hier Beschriebene ist ein realer, produktionsreifer Ansatz — keine erfundenen Helfer-Pakete oder Befehle.

Das Disk-Modell: local, public und storage

Laravel abstrahiert Dateioperationen über das Konzept einer Disk. Die Konfiguration liegt in config/filesystems.php und liefert standardmäßig zwei lokale Disks:

  • local — unter storage/app/private, für private Dateien, die nicht direkt aus dem Web erreichbar sein sollen.
  • public — unter storage/app/public, für Dateien, die öffentlich sein sollen (Avatare, Titelbilder, Anhänge).

Der entscheidende Unterschied ist dieser: Der Ordner storage/app/public liegt außerhalb deines Webroots (public/), der Browser kann ihn also nicht direkt erreichen. Ein Symlink schlägt die Brücke. Du musst ihn nur einmal ausführen:

php artisan storage:link

Dieser Befehl erstellt einen symbolischen Link von public/storage nach storage/app/public. Nun wird die Datei storage/app/public/avatars/foo.png unter der URL /storage/avatars/foo.png erreichbar. In manchen Shared-Hosting-Umgebungen ist die PHP-Funktion symlink() deaktiviert; erstelle den Link in diesem Fall über die Shell mit ln -sfn storage/app/public public/storage.

Den Upload validieren

Keine Datei sollte jemals ohne Validierung auf die Festplatte geschrieben werden. Wenn du die Formularanfrage in einem Controller verarbeitest, kannst du die Regeln direkt an validate() übergeben:

public function store(Request $request)
{
    $request->validate([
        'avatar' => ['required', 'image', 'mimes:jpg,jpeg,png,webp', 'max:2048'],
    ]);

    // ...
}

Jede Regel hat hier ihren Zweck:

  • image — verlangt, dass die Datei ein Bild ist (jpeg, png, bmp, gif, svg oder webp).
  • mimes — beschränkt die akzeptierten Endungen ausdrücklich. SVG wegzulassen ist meist klug, da darin Skripte eingebettet werden können.
  • max:2048 — die Größenbeschränkung ist in Kilobyte, hier also 2 MB.

Bei komplexeren Formularen hält das Auslagern der Regeln in eine mit php artisan make:request StoreAvatarRequest erzeugte Form-Request-Klasse den Controller schlank.

Mit der Storage-Facade auf die Festplatte schreiben

Sobald die Validierung besteht, ist das Speichern der Datei eine einzige Zeile. Das hochgeladene Datei-Objekt (Illuminate\Http\UploadedFile) arbeitet direkt mit den Disk-Methoden:

use Illuminate\Support\Facades\Storage;

$path = $request->file('avatar')->store('avatars', 'public');
// $path = "avatars/9aXk2...png"

store() erzeugt einen eindeutigen, nicht erratbaren Namen für die Datei und gibt den relativen Pfad zurück. Speichere diesen Pfad (zum Beispiel avatars/9aXk2...png) in der Datenbank — nicht die vollständige URL. So brechen deine Daten nicht, wenn sich deine Domain oder Disk-Konfiguration ändert.

Wenn du einen bestimmten Namen festlegen willst, verwende storeAs():

$path = $request->file('avatar')->storeAs(
    'avatars', $user->id.'.'.$request->file('avatar')->extension(), 'public'
);

Um die Datei später anzuzeigen, wandle den relativen Pfad in eine öffentliche URL um:

$url = Storage::disk('public')->url($path);
// /storage/avatars/9aXk2...png

Dieselbe Logik gilt auf der Blade-Seite:

<img src="{{ Storage::url($user->avatar) }}" alt="Avatar">

Ist es wirklich ein Bild? Jenseits der Validierung

Die Regeln image und mimes prüfen MIME-Typ und Endung, garantieren aber kein lesbares, unbeschädigtes Bild. Um die Bildabmessungen einzuschränken, gibt es die 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',
    ],
]);

Wenn du das Bild zusätzlich skalieren, zuschneiden oder optimieren willst, ist die Bibliothek Intervention Image eine gängige und zuverlässige Wahl. Sie wird mit Composer installiert:

composer require intervention/image

Eine typische Verwendung ist, die hochgeladene Datei in den Speicher zu lesen, sie auf eine bestimmte Breite zu skalieren und zurück auf die Festplatte zu schreiben:

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());

Dieser Schritt hat auch einen versteckten Sicherheitsvorteil: Das erneute Kodieren des Bildes entfernt weitgehend jegliche in der Datei eingebettete schädliche Daten.

Alte Dateien aufräumen und löschen

Wenn ein Nutzer seinen Avatar ändert, bleibt der alte als Müll zurück. Die vorherige Datei vor dem Speichern der neuen zu löschen ist eine gute Angewohnheit:

if ($user->avatar) {
    Storage::disk('public')->delete($user->avatar);
}

$user->avatar = $request->file('avatar')->store('avatars', 'public');
$user->save();

Um zu prüfen, ob eine Datei existiert, verwende Storage::disk('public')->exists($path). Für Operationen wie Massenlöschung oder Ordnerauflistung gibt es außerdem die Methoden files(), allFiles() und deleteDirectory().

Private Dateien und sichere Downloads

Nicht jede Datei muss öffentlich sein. Rechnungen, Verträge oder persönliche Dokumente sollten auf der local-Disk (storage/app/private) bleiben und nur einem Nutzer ausgeliefert werden, der eine Autorisierungsprüfung besteht. Dafür leitest du über einen Controller statt über einen Symlink:

public function download(Document $document)
{
    $this->authorize('view', $document);

    return Storage::disk('local')->download($document->path);
}

Hier ist die Datei niemals über eine direkte URL erreichbar; jede Anfrage wird über eine Policy geprüft. Ein Symlink für öffentliche Bilder, ein autorisierter Download für sensible Dokumente — diese Unterscheidung klar zu halten ist die Grundlage einer guten Architektur.

Häufige Fragen

Ich habe storage:link ausgeführt, aber die Bilder werden immer noch nicht angezeigt — warum?

Die häufigsten Ursachen sind, dass der Symlink auf dem Server nicht erstellt wurde (public/storage wurde beim Deploy nicht mitkopiert, oder die PHP-Funktion symlink() ist deaktiviert), oder dass der falsche relative Pfad — oder eine vollständige URL — in der Datenbank gespeichert ist. Erstelle den Symlink über die Shell mit ln -sfn und baue die URL immer mit Storage::url().

Was ist die sicherste Größen- und Typbeschränkung für hochgeladene Dateien?

Es gibt keine einzelne richtige Zahl; lege sie nach deinen Anforderungen fest. Die allgemeine Regel: Setze mit mimes nur die erwarteten Typen auf die Whitelist, lass SVG weg, setze eine sinnvolle max-Grenze und kodiere das Bild, wo möglich, mit Intervention Image neu.

Ändert das Verschieben der Dateien zu Amazon S3 meinen Code?

Fast gar nicht. Dank der Disk-Abstraktion genügt es, Storage::disk('s3') zu schreiben oder die Standard-Disk zu wechseln; deine Aufrufe von store(), url() und delete() bleiben gleich. Du musst lediglich das Paket league/flysystem-aws-s3-v3 hinzufügen.

Möchtest du deinen Datei-Upload-Ablauf absichern? Ob du eine Laravel-Anwendung von Grund auf baust oder ein bestehendes Storage-Setup absicherst, wir können zusammenarbeiten — nimm Kontakt mit mir auf.

Devamı için