Livewire 3 — kompletný sprievodca

Livewire umožňuje stavať dynamické rozhrania v Laraveli pomocou PHP a Blade — bez samostatnej frontend aplikácie a bez API vrstvy. Táto príručka vedie od inštalácie po produkciu: komponenty, stav, formuláre, udalosti, nahrávanie súborov, testovanie, bezpečnosť a výkon.

Livewire 3.x Laravel 10 / 11 / 12 PHP 8.1+ 24 kapitol

1. Čo je Livewire

Livewire je plnohodnotný framework pre Laravel, ktorý umožňuje stavať dynamické rozhrania v PHP a Blade bez toho, aby ste opustili backend. Nepíšete samostatnú SPA, nestaviate REST/GraphQL vrstvu a neudržiavate druhý dátový model v JavaScripte.

Ako to funguje

Livewire komponent je PHP trieda a prislúchajúca Blade šablóna. Pri prvom načítaní stránky sa komponent vykreslí na serveri ako bežné HTML. Ďalej malý JS runtime zachytáva interakcie používateľa (klik, písanie, submit), posiela AJAX požiadavku na server s aktuálnym stavom komponentu, server komponent znova vykreslí a vráti nové HTML. Runtime porovná staré a nové DOM stromy a bodovo opraví rozdiely — stránka sa nenačítava odznova.

Schéma jedného cyklu
Prehliadač                       Server
   │                                │
   │  klik na wire:click="save"     │
   ├───────── POST /livewire/update ┤
   │  { snapshot, calls, updates }  │
   │                                ├── obnoviť komponent zo snapshotu
   │                                ├── aplikovať zmeny vlastností
   │                                ├── zavolať metódu save()
   │                                ├── spustiť render()
   │  { snapshot, html, effects }   │
   ├◄───────────────────────────────┤
   ├── morph DOM (len rozdiely)     │
   ▼                                ▼

Kedy je Livewire správna voľba

  • Administrácie, CRM, dashboardy, interné panely — veľa formulárov a tabuliek, málo zložitej animácie.
  • Formuláre so závislými poľami, viackrokoví sprievodcovia, živá validácia.
  • Tabuľky s vyhľadávaním, filtrami, radením a stránkovaním.
  • Tím je silný v PHP a nechce udržiavať samostatný frontend stack.
  • Potrebujete rýchlo dodávať funkcionalitu bez duplikovania logiky medzi PHP a JS.

Kedy siahnuť po niečom inom

  • Rozhrania s vysokou frekvenciou zmien: online editory, plátno, drag-and-drop nástroje, hry.
  • Offline-first aplikácie a PWA so zložitou lokálnou synchronizáciou.
  • Mobilné aplikácie — tam potrebujete API, nie HTML-over-the-wire.
  • Slabé alebo pomalé pripojenie: každý cyklus je sieťový round-trip.
Praktické pravidlo: ak interakcia potrebuje dáta zo servera, patrí do Livewire. Ak je interakcia čisto vizuálna (otvoriť menu, prepnúť záložku, zobraziť tooltip), patrí do Alpine.js na klientovi bez volania servera.

Verzie

Táto príručka popisuje Livewire 3.x. Požiadavky: PHP 8.1+, Laravel 10 a vyššie. V tretej verzii je Alpine.js súčasťou balíka, zmenili sa názvy udalostí (dispatch namiesto emit), pribudli PHP atribúty (#[Computed], #[Validate], #[Url]) a wire:model je predvolene odložený.

2. Inštalácia a nastavenie

Inštalácia do existujúceho Laravel projektu je jeden príkaz.

Bash
composer require livewire/livewire

V Livewire 3 nemusíte assety pripájať zvlášť: balík sám vkladá @livewireStyles a @livewireScripts do layoutu. Ak však používate neštandardný layout alebo CSP, direktívy môžete uviesť ručne.

resources/views/layouts/app.blade.php
<!DOCTYPE html>
<html lang="sk">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>{{ $title ?? 'Aplikácia' }}</title>

    <link rel="stylesheet" href="{{ asset('css/app.css') }}">
    @livewireStyles
</head>
<body class="antialiased">

    {{ $slot }}

    @livewireScripts
</body>
</html>
Častá chyba. Ak sa komponent vykreslí, ale nereaguje na kliknutia, takmer vždy chýba @livewireScripts v layoute alebo je uvedený pred vaším vlastným Alpine.js. Livewire 3 už Alpine obsahuje — druhá inštancia Alpine rozbije reaktivitu.

Publikovanie konfigurácie

Bash
php artisan livewire:publish --config

Hlavné parametre v config/livewire.php:

ParameterÚčel
class_namespaceNamespace tried komponentov. Predvolene App\Livewire.
view_pathPriečinok Blade šablón komponentov.
layoutLayout pre full-page komponenty.
temporary_file_uploadDisk, životnosť a pravidlá pre dočasné nahrávanie.
inject_assetsAutomatické vkladanie CSS/JS. Vypnite, ak direktívy uvádzate ručne.
navigate.show_progress_barIndikátor priebehu pre wire:navigate.

Overenie inštalácie

Bash
php artisan livewire:make Counter
# CLASS: app/Livewire/Counter.php
# VIEW:  resources/views/livewire/counter.blade.php

3. Prvý komponent

Komponent tvoria dva súbory: trieda a šablóna.

app/Livewire/Counter.php
<?php

namespace App\Livewire;

use Livewire\Component;

class Counter extends Component
{
    public int $count = 0;

    public function increment(): void
    {
        $this->count++;
    }

    public function decrement(): void
    {
        $this->count--;
    }

    public function reset(): void
    {
        $this->count = 0;
    }

    public function render()
    {
        return view('livewire.counter');
    }
}
resources/views/livewire/counter.blade.php
<div class="flex items-center gap-3">
    <button type="button" wire:click="decrement" class="btn">−</button>

    <span class="text-2xl font-bold">{{ $count }}</span>

    <button type="button" wire:click="increment" class="btn">+</button>

    <button type="button" wire:click="reset" class="btn btn-ghost">Vynulovať</button>
</div>
Jeden koreňový prvok. Šablóna komponentu musí mať presne jeden koreňový prvok. Komentáre a text na najvyššej úrovni tiež rozbijú morph algoritmus. Ak potrebujete viac blokov, zabaľte ich do <div>.

Tri spôsoby vykreslenia komponentu

Blade
{{-- 1. Tag syntax (odporúčané) --}}
<livewire:counter />

{{-- 2. Direktíva --}}
@livewire('counter')

{{-- 3. S parametrami --}}
<livewire:counter :start="10" title="Počítadlo objednávok" />

Full-page komponenty

Komponent môžete pripojiť priamo na route — vtedy sa vykreslí ako samostatná stránka.

routes/web.php
use App\Livewire\Dashboard;

Route::get('/dashboard', Dashboard::class)->middleware('auth')->name('dashboard');
app/Livewire/Dashboard.php
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;

class Dashboard extends Component
{
    #[Layout('layouts.app')]
    #[Title('Ovládací panel')]
    public function render()
    {
        return view('livewire.dashboard');
    }
}

Inline komponenty

Pri veľmi malých komponentoch môžete šablónu vrátiť ako reťazec — samostatný Blade súbor netreba.

PHP
public function render()
{
    return <<<'BLADE'
        <div>
            <button wire:click="$refresh">Obnoviť</button>
        </div>
    BLADE;
}

4. Vlastnosti komponentu

Verejné vlastnosti triedy sú automaticky dostupné v Blade šablóne a zachovávajú sa medzi požiadavkami. Práve ony tvoria stav komponentu.

PHP
class UserProfile extends Component
{
    public string $name = '';
    public ?int $age = null;
    public array $tags = [];
    public bool $isPublic = false;

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

Aké typy možno ukladať

Medzi požiadavkami sa stav serializuje do JSON, preto je množina povolených typov obmedzená.

MožnoNemožno
string, int, float, bool, null Uzávery (Closure)
array so skalárnymi hodnotami Zdroje (resource), streamy
Eloquent modely a Collection modelov Ľubovoľné objekty bez podpory Wireable/Synth
Carbon, DateTime, Stringable, enum Objekty s odkazmi na spojenia, PDO a podobne
Eloquent modely vo vlastnostiach. Model sa ukladá ako identifikátor a pri každej požiadavke sa znova načíta z databázy. Je to pohodlné, ale každý cyklus pridá SQL dotaz. Ak potrebujete len pár polí, ukladajte skaláre, nie celý model.

Inicializácia: mount()

Metóda mount() je konštruktor komponentu. Spustí sa raz, pri prvom vykreslení, a dostane parametre odovzdané z Blade.

PHP
use App\Models\Order;

class OrderCard extends Component
{
    public Order $order;
    public string $mode;

    public function mount(Order $order, string $mode = 'compact'): void
    {
        $this->order = $order;
        $this->mode  = $mode;
    }
}
Blade
<livewire:order-card :order="$order" mode="full" :key="$order->id" />

Ochrana vlastností: #[Locked]

Predvolene môže klient zmeniť hodnotu ktorejkoľvek verejnej vlastnosti. Pri identifikátoroch a všetkom, čo ovplyvňuje prístupové práva, je to bezpečnostná diera. Atribút #[Locked] zmenu z frontendu zakáže.

PHP
use Livewire\Attributes\Locked;

class InvoiceEditor extends Component
{
    #[Locked]
    public int $invoiceId;   // klient nepodstrčí cudzie id

    public string $comment = '';
}

Vlastnosti skryté pred JSON

Vlastnosti s modifikátormi protected a private sa neserializujú a medzi požiadavkami sa resetujú. Používajte ich len pre hodnoty, ktoré sa v každom cykle prepočítajú nanovo.

PHP
class Report extends Component
{
    public string $period = 'month';

    // Po každej požiadavke sa vynuluje — neukladajte sem stav.
    protected array $cache = [];
}

5. wire:model a viazanie dát

wire:model viaže pole formulára na vlastnosť komponentu. V Livewire 3 je viazanie predvolene odložené: hodnota neodchádza na server pri každom stlačení klávesy, ale spolu s nasledujúcou akciou (klik, submit).

Blade
{{-- Odložene: hodnota pôjde s ďalšou požiadavkou --}}
<input type="text" wire:model="name">

{{-- Okamžite: požiadavka pri každej zmene --}}
<input type="text" wire:model.live="search">

{{-- S oneskorením 500 ms po dopísaní --}}
<input type="text" wire:model.live.debounce.500ms="search">

{{-- Najviac raz za 2 s, kým používateľ píše --}}
<input type="text" wire:model.live.throttle.2s="search">

{{-- Až pri strate fokusu --}}
<input type="text" wire:model.blur="email">

{{-- Vlastnosť sa mení okamžite na klientovi, server sa dozvie neskôr --}}
<input type="text" wire:model.lazy="draft">

Modifikátory

ModifikátorSprávanieKedy použiť
Odložené odoslanieBežné polia formulára
.livePožiadavka pri každej zmeneŽivé vyhľadávanie, závislé selecty
.blurPožiadavka pri strate fokusuValidácia poľa po opustení
.debounce.XmsPočkať na pauzu v písaníVyhľadávanie počas písania
.throttle.XsNajviac raz za intervalNáročné dotazy
.numberPreviesť na čísloČíselné polia
.booleanPreviesť na booleanSelecty áno/nie
.fillVziať počiatočnú hodnotu z HTMLPredvyplnené formuláre

Všetky typy polí

Blade
{{-- Text, textarea --}}
<input type="text" wire:model="title">
<textarea wire:model="body"></textarea>

{{-- Jeden checkbox: bool --}}
<input type="checkbox" wire:model="agreed">

{{-- Skupina checkboxov: array --}}
<input type="checkbox" value="php"  wire:model="skills">
<input type="checkbox" value="js"   wire:model="skills">
<input type="checkbox" value="sql"  wire:model="skills">

{{-- Rádiové tlačidlá --}}
<input type="radio" value="card" wire:model="payment">
<input type="radio" value="cash" wire:model="payment">

{{-- Select --}}
<select wire:model.live="categoryId">
    <option value="">Všetky kategórie</option>
    @foreach ($categories as $category)
        <option value="{{ $category->id }}">{{ $category->name }}</option>
    @endforeach
</select>

{{-- Viacnásobný select --}}
<select wire:model="tagIds" multiple>
    @foreach ($tags as $tag)
        <option value="{{ $tag->id }}">{{ $tag->name }}</option>
    @endforeach
</select>

Vnorené dáta

Bodková notácia funguje pre polia aj pre vlastnosti modelov.

PHP
public array $form = [
    'name'    => '',
    'address' => ['city' => '', 'street' => ''],
];

public Post $post;
Blade
<input wire:model="form.name">
<input wire:model="form.address.city">
<input wire:model="post.title">
Viazanie na model. Aby wire:model="post.title" fungovalo, pole musí byť povolené validačným pravidlom (rules alebo #[Validate]) — inak Livewire vyhodí výnimku. Je to ochrana pred hromadným priradením.

6. Akcie (actions)

Akcia je verejná metóda komponentu volaná zo šablóny. Nahrádza obvyklú kombináciu fetch() plus obslužný kód na serveri.

Blade
{{-- Klik --}}
<button wire:click="save">Uložiť</button>

{{-- Odoslanie formulára (submit je zachytený) --}}
<form wire:submit="save">
    <input wire:model="title">
    <button type="submit">Odoslať</button>
</form>

{{-- Klávesy --}}
<input wire:keydown.enter="search" wire:keydown.escape="clear">

{{-- Ďalšie DOM udalosti --}}
<div wire:mouseenter="preload">…</div>
<select wire:change="applyFilter">…</select>

Parametre

Blade
<button wire:click="delete({{ $post->id }})">Zmazať</button>
<button wire:click="setStatus('published')">Publikovať</button>
<button wire:click="move({{ $item->id }}, 'up')">Hore</button>
PHP
public function delete(int $postId): void
{
    $post = Post::findOrFail($postId);

    $this->authorize('delete', $post);   // autorizáciu overujeme vždy

    $post->delete();

    $this->dispatch('notify', message: 'Príspevok bol zmazaný');
}
Parametrom nikdy neverte. Každá metóda komponentu je dostupná ako verejný HTTP endpoint. Klient môže zavolať delete(999) s ľubovoľným id. Kontrola oprávnení vnútri metódy je povinná.

Model binding v parametroch

Livewire vie rozlúštiť modely podľa id rovnako ako router v Laraveli.

PHP
public function archive(Post $post): void
{
    $this->authorize('update', $post);

    $post->update(['archived_at' => now()]);
}

Modifikátory akcií

Blade
{{-- Potvrdenie v prehliadači pred odoslaním --}}
<button wire:click="delete" wire:confirm="Naozaj zmazať? Akciu nemožno vrátiť.">Zmazať</button>

{{-- preventDefault / stopPropagation --}}
<a href="#" wire:click.prevent="open">Otvoriť</a>
<div wire:click.stop="select">…</div>

{{-- Spustí sa raz --}}
<button wire:click.once="init">Inicializovať</button>

{{-- Len ak sa kliklo priamo na tento prvok --}}
<div wire:click.self="close">…</div>

Magické akcie

AkciaČo robí
$refreshZnova vykreslí komponent bez zmeny stavu
$set('prop', value)Priradí vlastnosti hodnotu
$toggle('prop')Prepne boolean vlastnosť
$dispatch('event')Odošle Livewire udalosť
$parent.method()Zavolá metódu rodičovského komponentu
Blade
<button wire:click="$refresh">Obnoviť</button>
<button wire:click="$set('tab', 'settings')">Nastavenia</button>
<button wire:click="$toggle('showFilters')">Filtre</button>
<button wire:click="$parent.closeModal()">Zavrieť</button>

Presmerovania

PHP
public function store(): void
{
    $this->validate();

    $post = Post::create($this->only('title', 'body'));

    session()->flash('status', 'Príspevok bol vytvorený');

    $this->redirect(route('posts.show', $post), navigate: true);
}

7. Životný cyklus požiadavky

Pochopenie poradia volaní odstráni 90 % „záhadných“ chýb. Rozlišujeme prvé vykreslenie (bežná HTTP požiadavka na stránku) a následné aktualizácie (AJAX požiadavky Livewire).

Prvé vykreslenie

Poradie
1. Vytvorí sa inštancia komponentu
2. boot()
3. mount($params)
4. booted()
5. hydrate hooky sa NEvolajú
6. render()
7. HTML sa vloží do stránky

Následná aktualizácia

Poradie
 1. Príde POST /livewire/update so snapshotom stavu
 2. Overí sa kontrolný súčet snapshotu (ochrana pred podvrhnutím)
 3. Vytvorí sa inštancia komponentu
 4. boot()
 5. Stav sa obnoví zo snapshotu
 6. hydrate() a hydrateFoo() pre každú vlastnosť
 7. booted()
 8. updating($prop, $value) / updatingFoo($value)   — pred priradením
 9. Vlastnosti dostanú nové hodnoty
10. updated($prop, $value) / updatedFoo($value)     — po priradení
11. Zavolajú sa metódy z frontu calls (akcie)
12. rendering()
13. render()
14. rendered($view, $html)
15. dehydrate() — stav sa zabalí späť do snapshotu
16. Odpoveď: nové HTML plus effects (udalosti, presmerovania, dispatch do prehliadača)

Snapshot a kontrolný súčet

Celý stav komponentu cestuje na klienta a späť v atribúte wire:snapshot. Aby klient dáta nepodvrhol, Livewire snapshot podpisuje HMAC podpisom odvodeným od APP_KEY. Pri nezhode podpisu je požiadavka odmietnutá chybou „Livewire encountered corrupt data“.

Praktický dôsledok. Čím viac stavu je vo verejných vlastnostiach, tým viac dát putuje sieťou v každom cykle. Kolekcia 500 modelov vo verejnej vlastnosti znamená megabajty prenosu pri každom kliknutí. Držte vo vlastnostiach minimum a zoznamy získavajte v render() alebo cez #[Computed].

Čo sa deje s DOM

Livewire nenahrádza celý uzol, ale vykoná morph: prejde starý aj nový strom a zmení iba rozdiely. Preto zostáva zachovaný fokus v poli, pozícia scrollu aj stav Alpine. Ak sa mení štruktúra zoznamu, morph algoritmus potrebuje pomôcku — pozri wire:key v kapitole 10.

8. Hooky životného cyklu

Hooky umožňujú zasiahnuť do ktorejkoľvek fázy cyklu.

PHP
class ProductEditor extends Component
{
    public Product $product;
    public string $name = '';
    public float $price = 0;

    // Spustí sa na začiatku KAŽDEJ požiadavky, pred obnovením stavu.
    public function boot(): void
    {
        // Vhodné miesto pre závislosti, ktoré sa nedajú serializovať.
    }

    // Len pri prvom vykreslení.
    public function mount(Product $product): void
    {
        $this->product = $product;
        $this->name    = $product->name;
        $this->price   = $product->price;
    }

    // Po obnovení stavu, v každej ďalšej požiadavke.
    public function hydrate(): void
    {
    }

    // Po boot() a obnovení stavu.
    public function booted(): void
    {
    }

    // Pred zmenou ľubovoľnej vlastnosti.
    public function updating(string $property, mixed $value): void
    {
    }

    // Po zmene ľubovoľnej vlastnosti.
    public function updated(string $property, mixed $value): void
    {
        $this->validateOnly($property);
    }

    // Len pre vlastnosť $price.
    public function updatedPrice(mixed $value): void
    {
        $this->price = round((float) $value, 2);
    }

    // Pre vnorený kľúč form.email
    public function updatedFormEmail(mixed $value): void
    {
        $this->form['email'] = strtolower(trim($value));
    }

    public function rendering(): void
    {
    }

    public function rendered(mixed $view, string $html): void
    {
    }

    // Pred zabalením stavu do snapshotu.
    public function dehydrate(): void
    {
    }

    public function render()
    {
        return view('livewire.product-editor');
    }
}

Pravidlá pomenovania

VlastnosťHook
$priceupdatedPrice()
$isActiveupdatedIsActive()
$form['email']updatedFormEmail()
$post->titleupdatedPostTitle()
Typický postup. Reset stránkovania pri zmene filtra sa robí práve v hooku: public function updatedSearch() { $this->resetPage(); }

9. Validácia a Form objekty

Livewire používa validátor Laravelu — pravidlá, správy aj lokalizácia sú rovnaké.

Spôsob 1: atribúty (Livewire 3)

PHP
use Livewire\Attributes\Validate;

class ContactForm extends Component
{
    #[Validate('required|string|min:2|max:80')]
    public string $name = '';

    #[Validate('required|email:rfc,dns')]
    public string $email = '';

    #[Validate('required|string|min:20|max:2000')]
    public string $message = '';

    #[Validate('accepted', message: 'Je potrebný súhlas so spracovaním údajov.')]
    public bool $consent = false;

    public function submit(): void
    {
        $data = $this->validate();

        Contact::create($data);

        $this->reset();

        session()->flash('status', 'Správa bola odoslaná');
    }
}

Spôsob 2: metóda rules()

Potrebná vtedy, keď pravidlá závisia od stavu.

PHP
protected function rules(): array
{
    return [
        'email' => [
            'required',
            'email',
            Rule::unique('users', 'email')->ignore($this->userId),
        ],
        'password' => $this->userId ? 'nullable|min:8|confirmed' : 'required|min:8|confirmed',
    ];
}

protected function messages(): array
{
    return [
        'email.unique' => 'Tento e-mail je už zaregistrovaný.',
    ];
}

protected function validationAttributes(): array
{
    return [
        'email' => 'e-mailová adresa',
    ];
}

Živá validácia

PHP
public function updated(string $property): void
{
    $this->validateOnly($property);   // overujeme len zmenené pole
}
Blade
<form wire:submit="submit" novalidate>
    <label for="email">E-mail</label>
    <input id="email" type="email" wire:model.blur="email"
           class="@error('email') border-red-500 @enderror">

    @error('email')
        <p class="text-sm text-red-600">{{ $message }}</p>
    @enderror

    <button type="submit" wire:loading.attr="disabled">
        <span wire:loading.remove wire:target="submit">Odoslať</span>
        <span wire:loading wire:target="submit">Odosielame…</span>
    </button>
</form>

Ručná správa chýb

PHP
$this->addError('email', 'Doména je na čiernej listine.');
$this->resetValidation('email');
$this->resetValidation();               // vynulovať všetky
$this->validateOnly('email');

// Vyhodenie chyby ako v bežnom Laravel kontroléri
throw ValidationException::withMessages([
    'code' => 'Nesprávny overovací kód.',
]);

Form objekty

Keď formulár narastie, presuňte ho do samostatnej triedy. Komponent zostane tenký a pravidlá aj dáta formulára sa dajú znovu použiť pri vytváraní aj úprave.

Bash
php artisan livewire:form PostForm
app/Livewire/Forms/PostForm.php
<?php

namespace App\Livewire\Forms;

use App\Models\Post;
use Livewire\Attributes\Validate;
use Livewire\Form;

class PostForm extends Form
{
    public ?Post $post = null;

    #[Validate('required|string|min:5|max:180')]
    public string $title = '';

    #[Validate('required|string|min:50')]
    public string $body = '';

    #[Validate('nullable|date|after_or_equal:today')]
    public ?string $publishAt = null;

    public function setPost(Post $post): void
    {
        $this->post      = $post;
        $this->title     = $post->title;
        $this->body      = $post->body;
        $this->publishAt = $post->publish_at?->toDateString();
    }

    public function store(): Post
    {
        $this->validate();

        return Post::create($this->except('post'));
    }

    public function update(): void
    {
        $this->validate();

        $this->post->update($this->except('post'));
    }
}
app/Livewire/PostEditor.php
class PostEditor extends Component
{
    public PostForm $form;

    public function mount(?Post $post = null): void
    {
        if ($post?->exists) {
            $this->form->setPost($post);
        }
    }

    public function save(): void
    {
        $this->form->post
            ? $this->form->update()
            : $this->form->store();

        $this->redirect(route('posts.index'), navigate: true);
    }
}
Blade
<form wire:submit="save">
    <input wire:model="form.title">
    @error('form.title') <span>{{ $message }}</span> @enderror

    <textarea wire:model="form.body"></textarea>
    @error('form.body') <span>{{ $message }}</span> @enderror

    <button type="submit">Uložiť</button>
</form>

10. Vnorené komponenty a wire:key

Komponenty sa dajú vnárať. Každý vnorený komponent je nezávislá jednotka s vlastným stavom a vlastným aktualizačným cyklom: klik vnútri potomka rodiča nevykreslí znova.

Blade
<div>
    <h1>Objednávky</h1>

    @foreach ($orders as $order)
        <livewire:order-row :order="$order" :key="'order-'.$order->id" />
    @endforeach
</div>
:key je v cykloch povinný. Bez jedinečného kľúča si morph algoritmus prvky pomýli pri radení, filtrovaní alebo mazaní: hodnoty polí sa „presťahujú“ do susedných riadkov. Kľúč musí byť stabilný — $loop->index nestačí, použite id záznamu.

Odovzdávanie dát nadol

Parametre sa odovzdávajú raz, v mount(). Pri ďalších vykresleniach rodiča sa potomok automaticky neaktualizuje — žije vlastným životom.

PHP
use Livewire\Attributes\Reactive;

class OrderTotal extends Component
{
    // S #[Reactive] hodnota príde od rodiča pri každom jeho vykreslení.
    #[Reactive]
    public int $quantity;
}

Volanie rodiča

Blade
<button wire:click="$parent.refreshList()">Obnoviť zoznam</button>

Obojsmerná väzba: #[Modelable]

Umožňuje použiť wire:model priamo na komponente — hodí sa pri widgetoch typu pole.

app/Livewire/RatingInput.php
use Livewire\Attributes\Modelable;

class RatingInput extends Component
{
    #[Modelable]
    public int $value = 0;

    public function set(int $value): void
    {
        $this->value = $value;
    }
}
Blade
{{-- V rodičovskej šablóne --}}
<livewire:rating-input wire:model.live="review.rating" />

Podmienené vykreslenie vnorených komponentov

Blade
@if ($showDetails)
    <livewire:order-details :order-id="$orderId" :key="'details-'.$orderId" />
@endif
Kedy rozdeliť na komponenty. Vytvorte vnorený komponent vtedy, keď má blok vlastný stav alebo sa aktualizuje nezávisle (riadok tabuľky, modálne okno, widget). Ak je blok iba značkovanie, použite bežný Blade partial — je lacnejší.

11. Udalosti

Udalosti spájajú komponenty, ktoré nie sú vo vzťahu rodič — potomok. V Livewire 3 sa odosielajú cez dispatch() (vo verzii 2 to bolo emit()).

Odoslanie

PHP
// Jednoduchá udalosť
$this->dispatch('post-created');

// S pomenovanými parametrami
$this->dispatch('post-created', postId: $post->id, title: $post->title);

// Len konkrétnemu komponentu
$this->dispatch('refresh')->to(OrderList::class);

// Len sebe samému
$this->dispatch('recalculate')->self();
Blade
{{-- Priamo zo šablóny --}}
<button wire:click="$dispatch('open-modal', { name: 'create-order' })">Nová objednávka</button>

Prijatie

PHP
use Livewire\Attributes\On;

class OrderList extends Component
{
    public array $orders = [];

    #[On('post-created')]
    public function onPostCreated(int $postId, string $title): void
    {
        $this->orders[] = ['id' => $postId, 'title' => $title];
    }

    // Dynamický názov udalosti
    #[On('order-updated.{orderId}')]
    public function onOrderUpdated(): void
    {
        $this->refreshList();
    }
}

Alternatíva je pole $listeners (kompatibilné s verziou 2):

PHP
protected $listeners = [
    'post-created' => 'onPostCreated',
    'refresh'      => '$refresh',
];

Udalosti do prehliadača

Livewire vie odoslať bežnú DOM udalosť — zachytí ju Alpine.js alebo váš vlastný JS. Je to správny spôsob, ako zobrazovať toasty a otvárať modálne okná.

PHP
$this->dispatch('notify', type: 'success', message: 'Objednávka bola uložená');
Blade
<div x-data="{ show: false, message: '' }"
     x-on:notify.window="message = $event.detail.message; show = true; setTimeout(() => show = false, 3000)"
     x-show="show"
     x-transition
     class="toast">
    <span x-text="message"></span>
</div>
JavaScript
document.addEventListener('notify', (event) => {
    console.log(event.detail.message);
});
Nezneužívajte udalosti. Každá udalosť znamená ďalší cyklus požiadavky pre každý načúvajúci komponent. Ak sa dva bloky menia vždy spolu, býva lacnejšie spojiť ich do jedného komponentu.

12. Vypočítané vlastnosti

#[Computed] poskytuje odvodené dáta bez toho, aby sa ukladali do stavu. Výsledok sa cachuje počas jednej požiadavky, takže trojnásobné použitie v šablóne nespôsobí tri SQL dotazy.

PHP
use Livewire\Attributes\Computed;
use Illuminate\Support\Collection;

class Cart extends Component
{
    public array $items = [];

    #[Computed]
    public function products(): Collection
    {
        return Product::whereIn('id', array_keys($this->items))->get();
    }

    #[Computed]
    public function subtotal(): float
    {
        return $this->products->sum(
            fn (Product $product) => $product->price * $this->items[$product->id]
        );
    }

    #[Computed]
    public function total(): float
    {
        return round($this->subtotal * 1.2, 2);   // s DPH
    }
}
Blade
<div>
    @foreach ($this->products as $product)
        <div>{{ $product->name }} — {{ $this->items[$product->id] }} ks</div>
    @endforeach

    <p>Medzisúčet: {{ number_format($this->subtotal, 2, ',', ' ') }} €</p>
    <p>Spolu s DPH: {{ number_format($this->total, 2, ',', ' ') }} €</p>
</div>
Prístup cez $this. V šablóne je vypočítaná vlastnosť dostupná ako $this->products, nie $products. Tým sa líši od bežných verejných vlastností.

Cachovanie medzi požiadavkami

PHP
// Cache na 5 minút v spoločnej cache aplikácie
#[Computed(persist: true, seconds: 300)]
public function statistics(): array
{
    return [
        'orders'  => Order::whereMonth('created_at', now()->month)->count(),
        'revenue' => Order::whereMonth('created_at', now()->month)->sum('total'),
    ];
}

// Spoločná cache pre všetkých používateľov
#[Computed(cache: true, key: 'global-stats')]
public function globalStats(): array
{
    return app(StatsService::class)->build();
}

Zrušenie cache

PHP
public function addItem(int $productId): void
{
    $this->items[$productId] = ($this->items[$productId] ?? 0) + 1;

    unset($this->products, $this->subtotal, $this->total);   // zahodiť cache
}

Dáta v render() vs #[Computed]

Ani jedna možnosť neukladá dáta do stavu. Rozdiel je v dosahu: render() odovzdá premenné iba šablóne, kým #[Computed] je dostupné v PHP metódach aj v šablóne a navyše sa cachuje.

PHP
public function render()
{
    return view('livewire.orders', [
        'orders' => Order::query()
            ->when($this->search, fn ($q) => $q->where('number', 'like', "%{$this->search}%"))
            ->latest()
            ->paginate(20),
    ]);
}

13. Stav v URL a v relácii

Filtre, vyhľadávanie a aktívna záložka by mali prežiť obnovenie stránky a mali by sa dať zdieľať odkazom. Atribút #[Url] synchronizuje vlastnosť s query stringom.

PHP
use Livewire\Attributes\Url;

class ProductCatalog extends Component
{
    #[Url]
    public string $search = '';

    // Iný názov parametra v URL
    #[Url(as: 'cat')]
    public ?int $categoryId = null;

    // Nezobrazovať v URL, kým sa hodnota rovná počiatočnej
    #[Url(except: '')]
    public string $sort = 'popular';

    // Zachovať medzi prechodmi wire:navigate
    #[Url(keep: true)]
    public int $perPage = 24;

    // Použiť history.pushState namiesto replaceState
    #[Url(history: true)]
    public string $tab = 'all';
}

Výsledok: /catalog?search=notebook&cat=5&sort=price-asc&tab=sale

Ukladanie do relácie

PHP
use Livewire\Attributes\Session;

class Sidebar extends Component
{
    #[Session]
    public bool $collapsed = false;

    #[Session(key: 'admin.table.density')]
    public string $density = 'comfortable';
}

Ručná práca s reláciou a flash správami

PHP
public function save(): void
{
    $this->validate();

    Setting::updateOrCreate(['key' => 'theme'], ['value' => $this->theme]);

    session()->flash('status', 'Nastavenia boli uložené');
}
Blade
@if (session('status'))
    <div class="docs-note docs-note--tip">{{ session('status') }}</div>
@endif
Nedávajte do URL všetko. Každá #[Url] vlastnosť pri zmene volá History API. Pri poliach, ktoré sa menia s každým stlačením klávesy, kombinujte s wire:model.live.debounce.

14. Nahrávanie súborov

Trait WithFileUploads pridáva plnohodnotné nahrávanie: súbor odchádza na server hneď po výbere, uloží sa do dočasného úložiska a je dostupný ako objekt TemporaryUploadedFile.

PHP
use Livewire\WithFileUploads;
use Livewire\Attributes\Validate;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;

class AvatarUploader extends Component
{
    use WithFileUploads;

    #[Validate('required|image|mimes:jpg,jpeg,png,webp|max:4096')]   // do 4 MB
    public $avatar;

    #[Validate(['documents.*' => 'file|mimes:pdf,docx|max:10240'])]
    public array $documents = [];

    public function save(): void
    {
        $this->validate();

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

        auth()->user()->update(['avatar_path' => $path]);

        $this->reset('avatar');

        $this->dispatch('notify', message: 'Avatar bol aktualizovaný');
    }

    public function removeDocument(int $index): void
    {
        unset($this->documents[$index]);

        $this->documents = array_values($this->documents);
    }
}
Blade
<form wire:submit="save">
    <input type="file" wire:model="avatar" accept="image/*">

    {{-- Priebeh nahrávania --}}
    <div wire:loading wire:target="avatar" class="text-sm">Nahrávame súbor…</div>

    {{-- Náhľad pred uložením --}}
    @if ($avatar)
        <img src="{{ $avatar->temporaryUrl() }}" alt="Náhľad" class="w-32 h-32 object-cover rounded">
    @endif

    @error('avatar') <p class="text-red-600">{{ $message }}</p> @enderror

    <button type="submit" wire:loading.attr="disabled">Uložiť</button>
</form>

Presný indikátor priebehu

Blade
<div x-data="{ progress: 0, uploading: false }"
     x-on:livewire-upload-start="uploading = true"
     x-on:livewire-upload-finish="uploading = false; progress = 0"
     x-on:livewire-upload-error="uploading = false"
     x-on:livewire-upload-progress="progress = $event.detail.progress">

    <input type="file" wire:model="avatar">

    <div x-show="uploading" class="h-2 bg-gray-200 rounded">
        <div class="h-2 bg-green-600 rounded" :style="`width: ${progress}%`"></div>
    </div>
</div>

Nastavenie dočasného úložiska

config/livewire.php
'temporary_file_upload' => [
    'disk'       => 's3',            // alebo null → predvolený disk
    'rules'      => ['file', 'max:12288'],
    'directory'  => 'livewire-tmp',
    'middleware' => 'throttle:60,1',
    'preview_mimes' => ['png', 'jpeg', 'jpg', 'webp', 'gif', 'mp4', 'pdf'],
    'max_upload_time' => 5,          // minút do automatického čistenia
],
Skontrolujte limity PHP. Hodnoty upload_max_filesize, post_max_size a max_execution_time v php.ini, ako aj client_max_body_size v nginx musia byť aspoň také veľké ako vaše max: v pravidlách. Inak sa nahrávanie preruší bez zrozumiteľnej chyby.

15. Stránkovanie

Trait WithPagination pripojí stránkovanie Laravelu bez obnovovania stránky.

PHP
use Livewire\WithPagination;
use Livewire\Attributes\Url;

class OrderTable extends Component
{
    use WithPagination;

    #[Url]
    public string $search = '';

    #[Url]
    public string $status = '';

    public string $sortField = 'created_at';
    public string $sortDirection = 'desc';
    public int $perPage = 25;

    // Pri zmene filtrov sa vraciame na prvú stranu.
    public function updatedSearch(): void
    {
        $this->resetPage();
    }

    public function updatedStatus(): void
    {
        $this->resetPage();
    }

    public function sortBy(string $field): void
    {
        if ($this->sortField === $field) {
            $this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
        } else {
            $this->sortField = $field;
            $this->sortDirection = 'asc';
        }

        $this->resetPage();
    }

    public function render()
    {
        return view('livewire.order-table', [
            'orders' => Order::query()
                ->with('customer')
                ->when($this->search, fn ($q) => $q->where('number', 'like', "%{$this->search}%"))
                ->when($this->status, fn ($q) => $q->where('status', $this->status))
                ->orderBy($this->sortField, $this->sortDirection)
                ->paginate($this->perPage),
        ]);
    }
}
Blade
<div>
    <input type="search" wire:model.live.debounce.400ms="search" placeholder="Hľadať podľa čísla">

    <table>
        <thead>
            <tr>
                <th wire:click="sortBy('number')" style="cursor:pointer">Číslo</th>
                <th wire:click="sortBy('created_at')" style="cursor:pointer">Dátum</th>
                <th wire:click="sortBy('total')" style="cursor:pointer">Suma</th>
            </tr>
        </thead>
        <tbody>
            @forelse ($orders as $order)
                <tr wire:key="order-{{ $order->id }}">
                    <td>{{ $order->number }}</td>
                    <td>{{ $order->created_at->format('d.m.Y') }}</td>
                    <td>{{ number_format($order->total, 2, ',', ' ') }}</td>
                </tr>
            @empty
                <tr><td colspan="3">Nenašli sa žiadne objednávky</td></tr>
            @endforelse
        </tbody>
    </table>

    {{ $orders->links() }}
</div>

Užitočné detaily

PHP
// Vlastný názov parametra strany — treba, ak sú na stránke dva paginátory
protected string $paginationTheme = 'tailwind';   // alebo 'bootstrap'

public function render()
{
    return view('livewire.dashboard', [
        'orders'   => Order::paginate(10, pageName: 'orders-page'),
        'invoices' => Invoice::paginate(10, pageName: 'invoices-page'),
    ]);
}
Výkon. Na veľkých tabuľkách vykoná paginate() navyše dotaz COUNT(*). Ak celkový počet strán nepotrebujete, použite simplePaginate() — je citeľne lacnejší.

16. Stavy načítavania

Každá akcia je sieťová požiadavka. Bez indikácie pôsobí rozhranie zaseknuto. Livewire ponúka deklaratívne direktívy, ktoré nepotrebujú ani riadok JS.

Blade
{{-- Zobraziť počas ľubovoľnej požiadavky komponentu --}}
<div wire:loading>Načítava sa…</div>

{{-- Skryť počas požiadavky --}}
<div wire:loading.remove>Obsah</div>

{{-- Len pre konkrétnu akciu --}}
<button wire:click="save">Uložiť</button>
<span wire:loading wire:target="save">Ukladáme…</span>

{{-- Pre viacero cieľov --}}
<span wire:loading wire:target="save,delete,publish">Spracúvame…</span>

{{-- Pre konkrétnu vlastnosť --}}
<span wire:loading wire:target="search">Hľadáme…</span>

{{-- Vylúčiť cieľ --}}
<div wire:loading wire:target.except="search">Aktualizujeme…</div>

Modifikátory

Blade
{{-- Zablokovať tlačidlo --}}
<button wire:click="save" wire:loading.attr="disabled">Uložiť</button>

{{-- Pridať CSS triedu --}}
<button wire:click="save" wire:loading.class="opacity-50 cursor-wait">Uložiť</button>

{{-- Odobrať triedu --}}
<div wire:loading.class.remove="bg-white">…</div>

{{-- Oneskorenie: indikátor len ak požiadavka trvá dlhšie než 300 ms --}}
<div wire:loading.delay>Načítava sa…</div>

{{-- Presné prahy: shortest 50ms, shorter 100ms, short 150ms,
     default 200ms, long 300ms, longer 500ms, longest 1s --}}
<div wire:loading.delay.long>Načítava sa…</div>

{{-- Riadenie zobrazenia --}}
<div wire:loading.flex>…</div>
<div wire:loading.grid>…</div>
<div wire:loading.inline-flex>…</div>

Neuložené zmeny: wire:dirty

Blade
<input wire:model="title">

<span wire:dirty wire:target="title" class="text-amber-600">Máte neuložené zmeny</span>

<button wire:click="save" wire:dirty.class="ring-2 ring-amber-400">Uložiť</button>

Strata spojenia: wire:offline

Blade
<div wire:offline class="banner banner--warn">
    Nie ste pripojení k serveru. Zmeny sa neukladajú.
</div>

Skeleton pri prvom načítaní

Blade
<div wire:init="loadHeavyData">
    @if ($loaded)
        {{-- skutočné dáta --}}
    @else
        <div class="skeleton h-40 w-full animate-pulse bg-gray-200"></div>
    @endif
</div>

18. Lenivé načítanie a polling

Lenivé načítanie komponentu

Náročný komponent netreba vykresľovať v prvej odpovedi: stránka sa odovzdá okamžite so zástupným obsahom a telo sa dotiahne druhou požiadavkou.

Blade
<livewire:revenue-chart lazy />
PHP
use Livewire\Attributes\Lazy;

#[Lazy]
class RevenueChart extends Component
{
    public function placeholder(): string
    {
        return <<<'BLADE'
            <div class="skeleton h-64 w-full animate-pulse rounded bg-gray-200"></div>
        BLADE;
    }

    public function render()
    {
        return view('livewire.revenue-chart', [
            'points' => app(RevenueService::class)->monthly(),   // náročný dotaz
        ]);
    }
}

Predvolene sa lenivý komponent načíta hneď po vykreslení stránky. Variant #[Lazy(isolate: false)] spojí požiadavky viacerých lenivých komponentov do jednej, a lazy="on-load" / lazy="on-scroll" riadia moment načítania.

Blade
{{-- Načítať, keď sa blok dostane do viewportu --}}
<livewire:revenue-chart lazy="on-scroll" />

Odložená inicializácia: wire:init

Blade
<div wire:init="loadStats">
    @if ($stats)
        …
    @else
        <div class="skeleton"></div>
    @endif
</div>

Dopytovanie servera: wire:poll

Blade
{{-- Obnovovať každé 2 s (predvolená hodnota je 2500 ms) --}}
<div wire:poll>…</div>

{{-- Vlastný interval --}}
<div wire:poll.5s>…</div>
<div wire:poll.750ms>…</div>

{{-- Volať konkrétnu metódu --}}
<div wire:poll.10s="refreshQueue">…</div>

{{-- Zastaviť dopytovanie, keď je záložka neaktívna --}}
<div wire:poll.visible.5s="refreshQueue">…</div>

{{-- Zastaviť po 5 minútach nečinnosti používateľa --}}
<div wire:poll.keep-alive.5s>…</div>
Cena dopytovania. wire:poll.2s na stránke otvorenej u 100 zamestnancov znamená 3000 požiadaviek za minútu na váš PHP proces. Vždy pridajte .visible, zväčšite interval na rozumnú hodnotu a pri skutočne živých dátach zvážte websockety (Laravel Echo + Reverb).

19. Alpine.js, $wire a JS hooky

Alpine.js je súčasťou Livewire 3. Vnútri komponentu je dostupný objekt $wire — proxy k stavu a metódam PHP komponentu priamo z JavaScriptu.

Blade
<div x-data="{ open: false }">
    {{-- Čisto klientský stav: server sa nezapája --}}
    <button x-on:click="open = !open">Detaily</button>

    <div x-show="open" x-transition>
        {{-- Čítanie vlastnosti komponentu --}}
        <p x-text="$wire.title"></p>

        {{-- Zápis vlastnosti --}}
        <button x-on:click="$wire.title = 'Nový nadpis'">Premenovať</button>

        {{-- Volanie metódy (vracia Promise) --}}
        <button x-on:click="$wire.save()">Uložiť</button>

        {{-- Metóda s čakaním na výsledok --}}
        <button x-on:click="await $wire.calculate(); open = false">Prepočítať</button>

        {{-- Volanie bez opätovného vykreslenia --}}
        <button x-on:click="$wire.$set('tab', 'stats', false)">Štatistiky</button>
    </div>
</div>

Obojsmerná väzba: $wire.entangle

Blade
<div x-data="{ query: $wire.entangle('search') }">
    <input x-model="query">
    <p x-show="query.length > 0">Hľadáme: <span x-text="query"></span></p>
</div>

{{-- Odložená synchronizácia: neposielať požiadavku pri každom znaku --}}
<div x-data="{ query: $wire.entangle('search').live }">…</div>

Vlastné skripty vnútri komponentu

Blade
@script
<script>
    // Spustí sa raz pri inicializácii komponentu.
    const chart = new Chart(document.getElementById('sales'), {
        type: 'line',
        data: @json($chartData),
    });

    // $wire je dostupný aj tu.
    $wire.on('data-updated', ({ points }) => {
        chart.data.datasets[0].data = points;
        chart.update();
    });
</script>
@endscript
Blade
{{-- Načítať externú knižnicu raz pre celú stránku --}}
@assets
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
@endassets

Ignorovanie podstromu

Ak si widget tretej strany spravuje DOM sám, morph algoritmus mu iba prekáža.

Blade
<div wire:ignore>
    <select id="select2-field">…</select>
</div>

{{-- Ignorovať len potomkov, ale aktualizovať atribúty samotného prvku --}}
<div wire:ignore.self>…</div>

Globálne JS hooky

JavaScript
document.addEventListener('livewire:init', () => {

    // Pred odoslaním každej požiadavky
    Livewire.hook('request', ({ uri, options, payload, respond, succeed, fail }) => {
        options.headers['X-Tenant'] = window.tenantId;

        succeed(({ status, json }) => {
            console.debug('Livewire odpovedal', status);
        });

        fail(({ status, preventDefault }) => {
            if (status === 419) {
                preventDefault();
                window.location.reload();   // vypršal CSRF token
            }
        });
    });

    // Pred a po morphe jednotlivého prvku
    Livewire.hook('morph.updated', ({ el, component }) => {});

    // Komponent bol inicializovaný
    Livewire.hook('component.init', ({ component }) => {});
});

// Programový prístup ku komponentom
Livewire.dispatch('refresh-orders');
Livewire.find('component-id').call('save');
Livewire.all().forEach((component) => component.$refresh());

20. Testovanie

Livewire ponúka vlastné testovacie API nad Laravelom. Testy sú rýchle — prehliadač netreba, funguje bežný PHPUnit alebo Pest.

tests/Feature/CounterTest.php
<?php

use App\Livewire\Counter;
use Livewire\Livewire;

it('zvyšuje počítadlo', function () {
    Livewire::test(Counter::class)
        ->assertSet('count', 0)
        ->call('increment')
        ->assertSet('count', 1)
        ->call('increment')
        ->assertSet('count', 2)
        ->call('decrement')
        ->assertSet('count', 1);
});
tests/Feature/ContactFormTest.php
use App\Livewire\ContactForm;
use App\Models\Contact;
use Livewire\Livewire;

it('validuje povinné polia', function () {
    Livewire::test(ContactForm::class)
        ->set('name', '')
        ->set('email', 'nie-je-email')
        ->call('submit')
        ->assertHasErrors([
            'name'  => 'required',
            'email' => 'email',
        ])
        ->assertNoRedirect();
});

it('uloží správne vyplnenú požiadavku', function () {
    Livewire::test(ContactForm::class)
        ->set('name', 'Irena Kováčová')
        ->set('email', 'irena@example.com')
        ->set('message', str_repeat('Potrebujeme web pre kliniku. ', 3))
        ->set('consent', true)
        ->call('submit')
        ->assertHasNoErrors()
        ->assertDispatched('notify');

    expect(Contact::where('email', 'irena@example.com')->exists())->toBeTrue();
});

Hlavné asercie

MetódaOveruje
assertSet('prop', $value)Hodnotu vlastnosti
assertNotSet('prop', $value)Že hodnota je iná
assertSee('text')Že text je v HTML
assertDontSee('text')Že text chýba
assertSeeHtml('<b>')Že značkovanie je prítomné
assertHasErrors(['email'])Chyby validácie
assertHasNoErrors()Že chyby nie sú
assertDispatched('event')Že udalosť bola odoslaná
assertRedirect(route(…))Že prebehlo presmerovanie
assertStatus(403)HTTP status
assertForbidden()Zamietnutý prístup
assertCount('items', 3)Veľkosť poľa alebo kolekcie

Autentifikácia, parametre, udalosti

PHP
// V mene používateľa
Livewire::actingAs($admin)
    ->test(OrderTable::class)
    ->assertSee('Všetky objednávky');

// S parametrami mount()
Livewire::test(OrderCard::class, ['order' => $order, 'mode' => 'full'])
    ->assertSee($order->number);

// Prijatie udalosti
Livewire::test(OrderList::class)
    ->dispatch('order-created', orderId: 42)
    ->assertSee('Objednávka č. 42');

// Nahranie súboru
use Illuminate\Http\UploadedFile;

Livewire::test(AvatarUploader::class)
    ->set('avatar', UploadedFile::fake()->image('avatar.jpg', 400, 400))
    ->call('save')
    ->assertHasNoErrors();

// Komponent vnútri stránky
$this->get('/dashboard')
    ->assertSeeLivewire(Dashboard::class)
    ->assertDontSeeLivewire(AdminPanel::class);
Čo testovať prednostne. Autorizáciu v akciách, validačné pravidlá a prechody stavov. Práve tam sa funkcionalita láme a práve tam sa ukrývajú bezpečnostné diery.

21. Bezpečnosť

Hlavná zásada. Každá verejná metóda komponentu je otvorený HTTP endpoint. Každú verejnú vlastnosť môže klient zmeniť. Pristupujte ku komponentu ako ku kontroléru, nie ako k internej triede.

1. Autorizácia v každej akcii

PHP
public function delete(int $postId): void
{
    $post = Post::findOrFail($postId);

    $this->authorize('delete', $post);   // Policy — povinne

    $post->delete();
}
PHP
// Autorizácia celého komponentu
public function mount(Project $project): void
{
    $this->authorize('view', $project);

    $this->project = $project;
}

2. #[Locked] pre identifikátory

PHP
use Livewire\Attributes\Locked;

class InvoiceEditor extends Component
{
    #[Locked]
    public int $invoiceId;      // z frontendu sa nedá podvrhnúť

    public string $note = '';
}

Bez #[Locked] stačí jeden riadok v konzole prehliadača:

JavaScript (útok)
Livewire.find('...').set('invoiceId', 999)   // cudzia faktúra

3. Pravidlá pre viazanie na modely

wire:model="post.title" funguje len vtedy, ak je pole povolené validačným pravidlom — takto Livewire chráni pred hromadným priradením.

PHP
protected function rules(): array
{
    return [
        'post.title' => 'required|string|max:180',
        'post.body'  => 'required|string',
        // 'post.user_id' zámerne NEuvádzame — inak by sa dal príspevok prepísať na iného autora
    ];
}

4. Escapovanie výstupu

Blade
{{-- Bezpečné: escapuje sa --}}
{{ $comment->body }}

{{-- Nebezpečné: surové HTML od používateľa --}}
{!! $comment->body !!}

{{-- Ak HTML potrebujete, sanitizujte na serveri --}}
{!! clean($comment->body) !!}

5. Obmedzenie frekvencie

PHP
use Illuminate\Support\Facades\RateLimiter;

public function login(): void
{
    $key = 'login:' . request()->ip();

    if (RateLimiter::tooManyAttempts($key, 5)) {
        throw ValidationException::withMessages([
            'email' => 'Príliš veľa pokusov. Skúste to o minútu.',
        ]);
    }

    RateLimiter::hit($key, 60);

    // …
}

6. Neukladajte tajomstvá do vlastností

PHP
class PaymentForm extends Component
{
    public string $cardLast4 = '';        // v poriadku

    // NIE: pôjde do prehliadača v čitateľnej podobe vnútri wire:snapshot
    // public string $apiSecret = '';
    // public string $fullCardNumber = '';

    protected function gateway(): PaymentGateway
    {
        return app(PaymentGateway::class);   // tajomstvá zostanú na serveri
    }
}

Kontrolný zoznam pred nasadením

  • $this->authorize() je v každej akcii, ktorá mení dáta.
  • Všetky identifikátory sú označené #[Locked].
  • Validačné pravidlá neobsahujú služobné polia (user_id, role, price).
  • Verejné vlastnosti neobsahujú tokeny, kľúče ani zbytočné osobné údaje.
  • Nahrávanie súborov je obmedzené podľa MIME typov a veľkosti.
  • Prihlasovacie a kontaktné formuláre sú chránené rate limitingom.
  • {!! !!} sa používa len pre dôveryhodné alebo sanitizované HTML.

22. Výkon

1. Minimum stavu vo verejných vlastnostiach

Zle
public Collection $products;   // 500 modelov cestuje na klienta a späť

public function mount(): void
{
    $this->products = Product::with('category', 'images')->get();
}
Dobre
public function render()
{
    return view('livewire.catalog', [
        'products' => Product::with('category')->paginate(24),
    ]);
}

2. Neposielajte požiadavku pri každom stlačení klávesy

Blade
{{-- Zle: požiadavka pri každom znaku --}}
<input wire:model.live="search">

{{-- Dobre --}}
<input wire:model.live.debounce.400ms="search">

3. Rozdeľte náročné stránky na komponenty

Aktualizácia vnoreného komponentu rodiča znova nevykreslí. Dashboard zo šiestich nezávislých widgetov beží citeľne svižnejšie než jeden monolit.

4. Cachujte drahé výpočty

PHP
#[Computed(persist: true, seconds: 600)]
public function monthlyRevenue(): array
{
    return app(RevenueService::class)->byMonth();
}

5. Sledujte N+1

PHP
// Zle — dotaz na každý riadok v šablóne
$orders = Order::paginate(50);

// Dobre
$orders = Order::with(['customer', 'items.product'])->paginate(50);

Zapnite prísny režim vo vývoji, aby N+1 spadlo s chybou:

app/Providers/AppServiceProvider.php
public function boot(): void
{
    Model::preventLazyLoading(! app()->isProduction());
}

6. wire:key v zoznamoch

Bez kľúčov prestavuje morph algoritmus viac uzlov, než treba — a robí to nesprávne.

7. Lenivé načítanie náročných blokov

Blade
<livewire:revenue-chart lazy="on-scroll" />

8. Opatrne s pollingom

wire:poll.visible.10s namiesto wire:poll ušetrí serveru rád veľkosti požiadaviek.

PríznakPravdepodobná príčina
Oneskorenie 300–800 ms pri každej akciiNáročný render() alebo N+1
Obrovské HTML stránkyKolekcie modelov vo verejných vlastnostiach
Rastúca záťaž PHP-FPMAgresívny wire:poll alebo .live bez debounce
Polia „skáču“ pri aktualizácii zoznamuChýba wire:key
Prekresľuje sa celá stránkaVšetko v jednom komponente, chýba rozdelenie

23. Časté chyby a riešenia

„Component must have a single root element“

V šablóne je viac koreňových uzlov alebo text či komentár na najvyššej úrovni. Zabaľte všetko do jedného <div>.

Zle
<h1>Nadpis</h1>
<p>Text</p>
Dobre
<div>
    <h1>Nadpis</h1>
    <p>Text</p>
</div>

„Livewire encountered corrupt data“

Nesedí podpis snapshotu. Príčiny: zmenil sa APP_KEY, stránka je otvorená z cache po nasadení, dve záložky s rôznymi reláciami. Zvyčajne pomôže obnovenie stránky; po nasadení php artisan optimize:clear.

Kliknutia nefungujú, konzola je prázdna

  • V layoute chýba @livewireScripts.
  • Je pripojená druhá inštancia Alpine.js (v Livewire 3 je Alpine už vnútri).
  • Chyba JS vyššie na stránke prerušila vykonávanie.
  • Prvok je vnútri wire:ignore.

Hodnoty polí sa „presúvajú“ medzi riadkami

V cykle chýba wire:key alebo :key. Kľúč musí byť stabilný a jedinečný.

Blade
@foreach ($rows as $row)
    <div wire:key="row-{{ $row->id }}">…</div>
@endforeach

„Unable to set component data. Public property not found“

Naviazali ste wire:model na neexistujúcu alebo neverejnú vlastnosť. Skontrolujte zápis a modifikátor prístupu.

„Cannot bind to model data without validation rules“

Väzba typu wire:model="post.title" vyžaduje pravidlo pre post.title v rules() alebo #[Validate].

Widget tretej strany sa po aktualizácii rozpadne

Select2, Flatpickr, TinyMCE a podobné si menia DOM samy. Zabaľte ich do wire:ignore a synchronizujte ručne.

Blade
<div wire:ignore x-data x-init="
    const picker = flatpickr($refs.input, {
        onChange: (dates, str) => $wire.set('date', str),
    });
">
    <input x-ref="input" type="text">
</div>

Nahrávanie súboru ticho zlyhá

Skontrolujte upload_max_filesize a post_max_size v PHP, ako aj client_max_body_size v nginx. Hodnoty musia prevyšovať limit v pravidle max:.

Modálne okno sa po uložení nezavrie

Odošlite udalosť do prehliadača a spracujte ju v Alpine namiesto spoliehania sa na prekreslenie.

PHP
$this->dispatch('close-modal', name: 'order-form');

Skripty po wire:navigate nefungujú

Presuňte inicializáciu z DOMContentLoaded do livewire:navigated.

Chyba 419 (Page Expired)

Vypršala relácia. Zvýšte SESSION_LIFETIME alebo zachyťte status v hooku request a stránku obnovte — príklad je v kapitole 19.

24. Ťahák

Blade direktívy

DirektívaÚčel
wire:modelViazanie poľa na vlastnosť (odložené)
wire:model.liveViazanie s požiadavkou pri každej zmene
wire:model.blurOdoslanie pri strate fokusu
wire:clickVolanie metódy pri kliknutí
wire:submitSpracovanie odoslania formulára
wire:keydown.enterReakcia na klávesu
wire:changeReakcia na change
wire:confirmPotvrdenie pred akciou
wire:loadingIndikátor načítavania
wire:targetZúženie indikátora na akciu
wire:dirtyExistujú neuložené zmeny
wire:offlineBez spojenia
wire:pollPeriodická aktualizácia
wire:initZavolať metódu hneď po vykreslení
wire:navigateSPA prechod cez odkaz
wire:keyIdentifikátor prvku v cykle
wire:ignoreVyňať podstrom z morphu
wire:transitionAnimácia zobrazenia a skrytia
wire:streamStreamovanie obsahu

PHP atribúty

AtribútÚčel
#[Validate]Validačné pravidlo vlastnosti
#[Locked]Zákaz zmeny z frontendu
#[Computed]Vypočítaná vlastnosť s cache
#[Url]Synchronizácia s query stringom
#[Session]Uloženie hodnoty do relácie
#[On]Poslucháč udalosti
#[Reactive]Aktualizácia parametra od rodiča
#[Modelable]Podpora wire:model na komponente
#[Lazy]Lenivé načítanie komponentu
#[Layout]Layout pre full-page komponent
#[Title]Titulok stránky
#[Renderless]Metóda bez prekreslenia

Užitočné metódy komponentu

PHP
$this->reset();                       // vrátiť všetky vlastnosti na počiatočné hodnoty
$this->reset('search', 'page');       // vrátiť vybrané
$this->only('title', 'body');         // pole z časti vlastností
$this->except('password');            // všetko okrem uvedeného
$this->fill(['title' => 'Nový']);     // hromadné priradenie
$this->pull('draft');                 // získať a vynulovať

$this->validate();
$this->validateOnly('email');
$this->resetValidation();
$this->addError('email', 'Správa');

$this->dispatch('saved', id: $post->id);
$this->redirect('/orders', navigate: true);
$this->redirectRoute('orders.index', navigate: true);

$this->skipRender();                  // v tomto cykle neprekresľovať
$this->js('alert("Hotovo")');         // spustiť JS na klientovi
$this->stream(to: 'answer', content: $chunk);

Artisan príkazy

Bash
php artisan livewire:make Orders/OrderTable
php artisan livewire:make Counter --inline
php artisan livewire:form PostForm
php artisan livewire:attribute ValidPhone
php artisan livewire:publish --config
php artisan livewire:publish --assets

Oficiálne zdroje