Praca z plikami i uploady w Livewire

Obsługa przesyłania plików (upload) jest jednym z częstych wymagań w aplikacjach webowych. Na szczęście Livewire, w ścisłej integracji z Laravel, pozwala na bardzo prostą i bezpieczną pracę z uploadem. W dzisiejszym artykule omówimy podstawy przesyłania plików w Livewire, walidacji plików i zapisywania ich w storage Laravel. Poruszymy też kwestie bezpieczeństwa, w tym limity rozmiarów i typów.

Artykuł nawiązuje do poprzednich tematów:

Konfiguracja i podstawowy przykład uploadu pliku w Livewire

Livewire wspiera mechanizm uploadu plików, wykorzystując pod spodem standardowe funkcje Laravel do obsługi żądania i walidacji. Aby rozpocząć, należy upewnić się, że:

  1. W pliku konfiguracyjnym config/filesystems.php mamy zdefiniowany dysk, na który będziemy zapisywać pliki (domyślnie public lub local).
  2. Katalog, w którym zapisywane są pliki (np. storage/app/public) jest poprawnie zlinkowany do publicznej ścieżki (symlink do public/storage).

Podstawowy komponent z polem uploadu

Załóżmy, że tworzymy prosty komponent PhotoUpload. W nim mamy właściwość $photo, którą zwiążemy z polem <input type="file" />.

Klasa komponentu

				
					<?php

namespace App\Http\Livewire;

use Livewire\Component;
use Livewire\WithFileUploads;

class PhotoUpload extends Component
{
    use WithFileUploads;

    public $photo;

    public function save()
    {
        // Zapisujemy plik w storage
        $this->photo->store('photos', 'public');
        // Można też np. emitować event informujący o sukcesie
        session()->flash('message', 'Zdjęcie zostało zapisane!');
    }

    public function render()
    {
        return view('livewire.photo-upload');
    }
}

				
			

Zwróć uwagę na:

  • use WithFileUploads; – niezbędne, aby Livewire wiedziało, jak obsługiwać upload plików.
  • $photo – właściwość, której wartość zostanie ustawiona przez wire:model lub wire:model.defer.

Widok photo-upload.blade.php

				
					<div>
    @if (session()->has('message'))
        <div class="alert alert-success">{{ session('message') }}</div>
    @endif

    <form wire:submit.prevent="save">
        <input type="file" wire:model="photo">
        @error('photo') <span class="error">{{ $message }}</span> @enderror

        <button type="submit">Prześlij</button>
    </form>
</div>

				
			

Tutaj kluczowe jest:

  • wire:model="photo" – dzięki temu Livewire będzie przesyłać zawartość pliku do komponentu w momencie wyboru pliku.
  • form wire:submit.prevent="save" – wysyła formularz do metody save() w komponencie bez przeładowywania strony.

Po wyborze pliku i kliknięciu „Prześlij” Livewire automatycznie obsłuży upload oraz zapisze go w wybranym miejscu (/storage/app/public/photos/...) dzięki wywołaniu $this->photo->store('photos', 'public').

Walidacja plików

Walidacja plików w Livewire działa podobnie do standardowej walidacji Laravel. Możemy użyć reguł:

  • required – plik musi być przesłany.
  • image – plik musi być grafiką (jpg, png, bmp, gif, svg, webp).
  • mimes:jpg,png lub mimetypes:image/jpeg,image/png – określenie dopuszczalnych typów MIME.
  • max:1024 – maksymalny rozmiar pliku (w kilobajtach).

Przykładowa walidacja:

				
					public function save()
{
    $this->validate([
        'photo' => 'required|image|max:1024', // maksymalnie 1MB
    ]);

    $path = $this->photo->store('photos', 'public');
    session()->flash('message', "Plik zapisany w: $path");
}

				
			

Jeżeli plik nie przejdzie walidacji (np. jest za duży), w widoku pojawi się odpowiedni komunikat błędu przy polu photo.

Zapis pliku do storage i obsługa potencjalnych błędów

1. Ustawianie folderu docelowego

Metoda store($folder, $disk) zapisuje plik w podanym folderze na wskazanym dysku.

  • $folder – nazwa folderu (lub ścieżka), np. photos/profile-avatars.
  • $disk – nazwa dysku z config/filesystems.php, np. public lub s3.

2. Obsługa błędów

Jeśli w trakcie walidacji dojdzie do błędu (np. przekroczony rozmiar pliku), Livewire automatycznie wyrzuci wyjątek walidacyjny i zwróci komunikat do widoku.
Zachowanie w widoku:

				
					@error('photo')
    <div class="error">
        {{ $message }}
    </div>
@enderror

				
			

3. Wyświetlanie przesłanego pliku

Po zapisaniu pliku w folderze public, można łatwo wyświetlić go w widoku, używając funkcji asset(), bądź z użyciem Storage::url($path):

				
					@if($savedPath)
    <img decoding="async" src="data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%200%200'%3E%3C/svg%3E" alt="Przesłany plik" data-lazy-src="{{ Storage::url($savedPath) }}" /><noscript><img decoding="async" src="{{ Storage::url($savedPath) }}" alt="Przesłany plik" /></noscript>
@endif

				
			

Bezpieczeństwo i ograniczenia

  1. Limity rozmiaru – max:X w regułach walidacji to najprostszy sposób. Można też ustawić limit uploadu w php.ini (upload_max_filesize, post_max_size), co jest istotne, gdy pliki mogą być większe niż kilka MB.
  2. Typy plików – Wskazówki image, mimes:... czy mimetypes:... minimalizują ryzyko, że użytkownik załaduje niebezpieczny plik (np. skrypt).
  3. Folder docelowy – Najlepiej zapisywać dane w storage/app/public i używać symbolicznego linku public/storage, by pliki były dostępne, ale trzymane poza główną ścieżką aplikacji.
  4. Skanowanie antywirusowe – w krytycznych przypadkach warto rozważyć automatyczne skanowanie przesłanych plików (np. przez serwisy typu ClamAV).

Mini-lab: Formularz ładujący plik (np. avatar)

Stwórzmy kompletne ćwiczenie, w którym użytkownik może załadować plik ze zdjęciem profilowym (avatar) i je wyświetlić.

1. Komponent ProfileUpload

a) Generowanie pliku

				
					php artisan make:livewire ProfileUpload

				
			

b) Klasa ProfileUpload.php

Poniżej przykładowa implementacja:

				
					<?php

namespace App\Http\Livewire;

use Livewire\Component;
use Livewire\WithFileUploads;
use Illuminate\Support\Facades\Storage;

class ProfileUpload extends Component
{
    use WithFileUploads;

    public $avatar;    // Plik tymczasowy z wire:model
    public $currentAvatarPath; // Ścieżka do obecnego avatara

    public function mount($initialPath = null)
    {
        // Możemy wczytać obecny avatar użytkownika, jeśli istnieje
        $this->currentAvatarPath = $initialPath;
    }

    public function saveAvatar()
    {
        // Walidacja
        $this->validate([
            'avatar' => 'required|image|max:1024', // maks. 1MB
        ]);

        // Zapis pliku
        $path = $this->avatar->store('avatars', 'public');

        // Ustawiamy nową ścieżkę
        $this->currentAvatarPath = $path;

        // (Opcjonalnie) tutaj można zaktualizować rekord użytkownika w bazie
        // User::find(auth()->id())->update(['avatar' => $path]);

        // Komunikat sukcesu
        session()->flash('message', 'Avatar został zaktualizowany!');
    }

    public function deleteAvatar()
    {
        if ($this->currentAvatarPath) {
            Storage::disk('public')->delete($this->currentAvatarPath);
            $this->currentAvatarPath = null;
            session()->flash('message', 'Avatar został usunięty.');
        }
    }

    public function render()
    {
        return view('livewire.profile-upload');
    }
}

				
			

Tutaj:

  • Przechowujemy ścieżkę do obecnego avatara (np. $currentAvatarPath) i wyświetlamy go, jeśli istnieje.
  • W saveAvatar() walidujemy obraz, zapisujemy go na dysku public/avatars, a następnie ustawiamy $currentAvatarPath.
  • Metoda deleteAvatar() kasuje plik z dysku i ustawia ścieżkę na null.

c) Widok profile-upload.blade.php

				
					<div>
    <h2>Aktualizacja zdjęcia profilowego</h2>

    @if (session()->has('message'))
        <div class="alert alert-success">{{ session('message') }}</div>
    @endif

    
    @if ($currentAvatarPath)
        <div class="mb-4">
            <img decoding="async" src="data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20150%20150'%3E%3C/svg%3E" alt="Avatar" width="150" height="150" data-lazy-src="{{ Storage::url($currentAvatarPath) }}"><noscript><img decoding="async" src="{{ Storage::url($currentAvatarPath) }}" alt="Avatar" width="150" height="150"></noscript>
        </div>
    @endif

    
    <form wire:submit.prevent="saveAvatar">
        <div class="mb-4">
            <input type="file" wire:model="avatar" accept="image/*">
            @error('avatar') <span class="error text-red-600">{{ $message }}</span> @enderror
        </div>

        <button type="submit" class="bg-blue-500 text-white px-3 py-2 rounded">
            Zapisz
        </button>
    </form>

    
    @if ($currentAvatarPath)
        <button wire:click="deleteAvatar" class="bg-red-500 text-white px-3 py-2 rounded mt-3">
            Usuń avatar
        </button>
    @endif
</div>

				
			

Ważne elementy w widoku:

  • wire:model="avatar" z accept="image/*", by ułatwić użytkownikowi wybór tylko pliku graficznego.
  • Wyświetlanie informacji o błędzie (@error('avatar')...).
  • Przycisk do usunięcia pliku, jeśli aktualnie istnieje avatar.

Integracja
Możesz użyć <livewire:profile-upload :initial-path="$user->avatar" /> w dowolnym szablonie Blade, aby wstawić formularz zmiany zdjęcia profilowego. initialPath może pochodzić z bazy (np. $user->avatar).

Wgranie i wyświetlenie zdjęcia profilowego

W tym zadaniu stworzymy komponent ProfileUpload, który pozwala na wgranie (upload) zdjęcia użytkownika (np. avatara), zapis do storage, a następnie wyświetlenie go w interfejsie.

Krok 1: Wygeneruj komponent

				
					php artisan make:livewire ProfileUpload

				
			

W folderze app/Http/Livewire pojawi się plik ProfileUpload.php, a w folderze resources/views/livewire – profile-upload.blade.php.

Krok 2: Klasa ProfileUpload.php

Oto przykładowa implementacja:

				
					<?php

namespace App\Http\Livewire;

use Livewire\Component;
use Livewire\WithFileUploads;
use Illuminate\Support\Facades\Storage;

class ProfileUpload extends Component
{
    use WithFileUploads;

    /**
     * Ta właściwość będzie powiązana z <input type="file">
     */
    public $avatar;

    /**
     * Przechowuje aktualną ścieżkę do avatara (jeśli istnieje).
     * Możemy ją np. pobrać z bazy danych w `mount`.
     */
    public $currentAvatarPath;

    /**
     * Metoda wywoływana podczas inicjalizacji komponentu
     * (np. gdy przekazujemy parametry przez <livewire:profile-upload :initialPath="$user->avatar" />).
     */
    public function mount($initialPath = null)
    {
        $this->currentAvatarPath = $initialPath;
    }

    /**
     * Metoda zapisująca wgrany plik.
     */
    public function saveAvatar()
    {
        // Walidacja pliku (tylko obrazy, max 1 MB)
        $this->validate([
            'avatar' => 'required|image|max:1024', // w kB
        ]);

        // Zapis pliku w folderze 'avatars' na dysku 'public'
        $path = $this->avatar->store('avatars', 'public');

        // Ustawiamy nową ścieżkę
        $this->currentAvatarPath = $path;

        // Opcjonalnie: możesz zaktualizować bazę, np.:
        // User::find(auth()->id())->update(['avatar' => $path]);

        // Komunikat o sukcesie
        session()->flash('message', 'Avatar został pomyślnie zapisany!');
    }

    public function render()
    {
        return view('livewire.profile-upload');
    }
}

				
			

Omówienie

  1. use WithFileUploads; – trait, który umożliwia obsługę przesyłania plików w Livewire.
  2. $avatar – publiczna właściwość, do której przypiszemy plik z formularza (wire:model="avatar").
  3. saveAvatar() – metoda, która waliduje plik i zapisuje go w storage/app/public/avatars.

Krok 3: Widok profile-upload.blade.php

				
					<div>
    <h2>Wgraj lub zaktualizuj swój avatar</h2>

    {{-- Komunikat o powodzeniu --}}
    @if (session()->has('message'))
        <div style="margin-bottom: 1em; color: green;">
            {{ session('message') }}
        </div>
    @endif

    {{-- Podgląd aktualnego avatara (o ile istnieje) --}}
    @if ($currentAvatarPath)
        <div style="margin-bottom: 1em;">
            <img decoding="async" src="data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20150%20150'%3E%3C/svg%3E" alt="Avatar" width="150" height="150" data-lazy-src="{{ Storage::url($currentAvatarPath) }}"><noscript><img decoding="async" src="{{ Storage::url($currentAvatarPath) }}" alt="Avatar" width="150" height="150"></noscript>
        </div>
    @endif

    {{-- Formularz przesyłania pliku --}}
    <form wire:submit.prevent="saveAvatar">
        <div style="margin-bottom: 1em;">
            <input type="file" wire:model="avatar" accept="image/*">
            {{-- Obsługa błędów walidacji --}}
            @error('avatar') 
                <div style="color: red;">{{ $message }}</div>
            @enderror
        </div>
        <button type="submit">Zapisz</button>
    </form>
</div>

				
			

Omówienie

  1. <input type="file" wire:model="avatar"> – to klucz do wiązania pliku z komponentem.
  2. @error('avatar') ... @enderror – wyświetlamy komunikat, gdy walidacja pliku zawiedzie (np. za duży rozmiar).
  3. <img src="{{ Storage::url($currentAvatarPath) }}" ...> – jeśli mamy aktualny avatar, pokazujemy go w interfejsie.

Użycie komponentu w widoku

				
					<livewire:profile-upload :initialPath="$user->avatar" />

				
			

Dzięki temu po zalogowaniu się user, komponent wczyta ścieżkę do aktualnego avatara (jeśli istnieje) oraz umożliwi jego zmianę.

Walidacja rozmiaru i typu pliku

W zadaniu powyżej już zastosowaliśmy podstawowe reguły – required|image|max:1024. Spróbujmy je rozszerzyć lub doprecyzować:

Krok 1: Modyfikacja metody walidacji

W komponencie (np. ProfileUpload.php) zmieniamy:

				
					public function saveAvatar()
{
    $this->validate([
        'avatar' => 'required|mimes:jpg,png,jpeg|max:1024', 
        // lub 'avatar' => 'required|mimetypes:image/jpeg,image/png|max:1024'
    ]);

    // Reszta logiki pozostaje taka sama...
}

				
			

Teraz pliki muszą być w formacie .jpg, .png lub .jpeg, a rozmiar nie przekracza 1 MB.
Jeśli potrzebujesz innego limitu, np. 2 MB, wystarczy max:2048.

Krok 2: Testowanie

  • Spróbuj wgrać plik .png o rozmiarze ~1,2 MB – pojawi się błąd walidacji o przekroczeniu rozmiaru.
  • Wgraj plik .pdf – pojawi się błąd o nieobsługiwanym formacie.

Zaimplementowanie funkcji kasowania plików

W niektórych aplikacjach po zmianie pliku trzeba usunąć poprzedni z dysku, by nie zalegał. Można też dać użytkownikowi możliwość „usunięcia” aktualnego pliku (np. przycisk „Usuń avatar”).

Krok 1: Dodaj metodę deleteAvatar() w komponencie

Przykład w ProfileUpload.php:

				
					public function deleteAvatar()
{
    if ($this->currentAvatarPath) {
        // Usuwamy plik z dysku 'public'
        \Storage::disk('public')->delete($this->currentAvatarPath);

        // Resetujemy ścieżkę
        $this->currentAvatarPath = null;

        session()->flash('message', 'Avatar został usunięty.');
    }
}

				
			

Krok 2: Dodaj przycisk usuwania w widoku

W profile-upload.blade.php:

				
					@if ($currentAvatarPath)
    <button wire:click="deleteAvatar" style="margin-left: 1em; color: red;">
        Usuń avatar
    </button>
@endif

				
			

Teraz, jeśli użytkownik ma ustawiony jakiś avatar, pojawi się przycisk „Usuń avatar”. Kliknięcie go wywoła deleteAvatar(), co usunie plik z storage/app/public/avatars/....

Krok 3: Testowanie

  1. Załaduj plik (sprawdź, czy pojawia się miniaturka i komunikat o sukcesie).
  2. Usuń plik – sprawdź, czy plik fizycznie zniknął z dysku (storage/app/public/avatars) i czy w interfejsie avatar już się nie wyświetla.

Podsumowanie

Obsługa plików w Livewire jest prosta i naturalnie wpasowuje się w ekosystem Laravel:

  1. wire:model=”photo” – wiąże pole typu file z właściwością w komponencie.
  2. WithFileUploads – trait, bez którego upload by nie zadziałał.
  3. Walidacja – identyczna jak w Laravel (np. required|image|max:1024).
  4. Przechowywanie w storage – wykorzystanie metody store() do zapisu na określonym dysku.
  5. Bezpieczeństwo – weryfikacja typu, rozmiaru i właściwe przechowywanie plików (poza rootem aplikacji), a także konfiguracyjne limity w php.ini.

Dzięki tym technikom możesz tworzyć formularze do uploadu zdjęć, dokumentów czy innych zasobów. Pamiętaj o sprawnym zarządzaniu plikami (usuwanie nieaktualnych) i odpowiednich regułach walidacji, by zachować czystość i bezpieczeństwo w swojej aplikacji.