[ Livewire · תיעוד ]

תיעוד Livewire 3

תיעוד מלא ל-Livewire 3 — מההתקנה ועד הפרודקשן: קומפוננטות, state, טפסים, אירועים, העלאת קבצים, בדיקות, אבטחה וביצועים.

  • 3.x
  • Laravel 10 / 11 / 12
  • PHP 8.1+
  • 24 פרקים
  • 40 דק׳ קריאה

1. מה זה Livewire

Livewire הוא framework מסוג full-stack ל-Laravel שמאפשר לבנות ממשקים דינמיים ב-PHP וב-Blade בלי לצאת מה-backend. לא צריך לכתוב SPA נפרד, לא צריך להקים שכבת REST או GraphQL, ולא צריך לתחזק מודל נתונים שני ב-JavaScript.

איך זה עובד

קומפוננטת Livewire היא מחלקת PHP ועוד view של Blade. בטעינה הראשונה של העמוד הקומפוננטה מרונדרת בצד השרת כ-markup רגיל. מכאן והלאה, runtime קטן ב-JS מיירט את פעולות המשתמש (click, input, submit) ושולח בקשת AJAX עם ה-state הנוכחי של הקומפוננטה; השרת מרנדר מחדש את הקומפוננטה ומחזיר markup עדכני. ה-runtime משווה בין עץ ה-DOM הישן לחדש ומעדכן רק את מה שהשתנה — העמוד לא נטען מחדש אף פעם.

מחזור בקשה אחד
Browser                          Server
   │                                │
   │  click on wire:click="save"    │
   ├───────── POST /livewire/update ┤
   │  { snapshot, calls, updates }  │
   │                                ├── restore component from snapshot
   │                                ├── apply property updates
   │                                ├── call save()
   │                                ├── run render()
   │  { snapshot, html, effects }   │
   ├◄───────────────────────────────┤
   ├── morph the DOM (diffs only)   │
   ▼                                ▼

מתי Livewire הוא הבחירה הנכונה

  • פאנלי ניהול, מערכות CRM, דשבורדים, כלים פנימיים — הרבה טפסים וטבלאות, מעט אנימציה מורכבת.
  • טפסים עם שדות תלויים, אשפים מרובי שלבים, ולידציה בזמן אמת.
  • טבלאות עם חיפוש, סינון, מיון ועימוד.
  • צוות חזק ב-PHP שלא רוצה לתחזק stack נפרד ל-frontend.
  • שחרור פיצ'רים מהר בלי לשכפל לוגיקה בין PHP ל-JS.

מתי לבחור במשהו אחר

  • ממשקים שמתעדכנים ללא הפסקה: עורכים אונליין, canvas, בוני drag-and-drop, משחקים.
  • אפליקציות offline-first ו-PWA עם סנכרון מקומי מורכב.
  • אפליקציות מובייל — הן צריכות API, לא markup שעובר ברשת.
  • חיבורים חלשים או עם latency גבוה: כל מחזור הוא הלוך-חזור ברשת.
כלל אצבע: אם אינטראקציה צריכה נתונים מהשרת, המקום שלה ב-Livewire. אם היא ויזואלית בלבד (פתיחת תפריט, החלפת טאב, הצגת tooltip), המקום שלה ב-Alpine.js בצד הלקוח, בלי שום קריאה לשרת.

גרסאות

התיעוד הזה מכסה את Livewire 3.x. דרישות: PHP 8.1+, Laravel 10 ומעלה. גרסה 3 כוללת את Alpine.js מובנה, שינתה את שמות ה-API של האירועים (dispatch במקום emit), הוסיפה PHP attributes (#[Computed], #[Validate], #[Url]), והפכה את wire:model לדחוי כברירת מחדל.

2. התקנה והגדרה

התקנה בפרויקט Laravel קיים היא פקודה אחת.

Bash
composer require livewire/livewire

ב-Livewire 3 אין צורך לחבר את ה-assets בעצמכם: החבילה מזריקה את @livewireStyles ואת @livewireScripts ל-layout. עם layout לא סטנדרטי או CSP קפדני אפשר למקם את ה-directives ידנית.

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

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

    {{ $slot }}

    @livewireScripts
</body>
</html>
טעות נפוצה. אם קומפוננטה מרונדרת אבל לא מגיבה ללחיצות, כמעט תמיד חסר ב-layout @livewireScripts — או שהוא ממוקם לפני ה-build של Alpine.js שלכם. Livewire 3 כבר כולל את Alpine; מופע שני של Alpine שובר את הריאקטיביות.

פרסום קובץ ה-config

Bash
php artisan livewire:publish --config

ההגדרות החשובות ב-config/livewire.php:

הגדרהתפקיד
class_namespaceה-namespace של מחלקות הקומפוננטות. ברירת המחדל: App\Livewire.
view_pathהתיקייה שמכילה את ה-views של הקומפוננטות ב-Blade.
layoutה-layout שמשמש קומפוננטות עמוד מלא.
temporary_file_uploadדיסק, משך חיים וכללים להעלאות זמניות.
inject_assetsהזרקה אוטומטית של CSS/JS. כבו אם אתם ממקמים את ה-directives בעצמכם.
navigate.show_progress_barפס התקדמות עבור wire:navigate.

בדיקת ההתקנה

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

3. הקומפוננטה הראשונה

קומפוננטה היא שני קבצים: מחלקה ו-view.

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">Reset</button>
</div>
אלמנט שורש אחד בדיוק. ל-view של קומפוננטה חייב להיות אלמנט שורש אחד בלבד. גם הערות וטקסט חופשי ברמה העליונה שוברים את אלגוריתם ה-morph. אם צריך כמה בלוקים, עטפו אותם ב-<div>.

שלוש דרכים לרנדר קומפוננטה

Blade
{{-- 1. תחביר תגית (מומלץ) --}}
<livewire:counter />

{{-- 2. Directive --}}
@livewire('counter')

{{-- 3. עם פרמטרים --}}
<livewire:counter :start="10" title="Order counter" />

קומפוננטות עמוד מלא

אפשר לחבר קומפוננטה ישירות ל-route ולרנדר אותה כעמוד עצמאי.

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('Dashboard')]
    public function render()
    {
        return view('livewire.dashboard');
    }
}

קומפוננטות inline

בקומפוננטות קטנות מאוד אפשר להחזיר את ה-view כמחרוזת ולוותר לגמרי על קובץ Blade.

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

4. מאפייני קומפוננטה

מאפיינים ציבוריים של המחלקה זמינים אוטומטית ב-view של Blade ונשמרים בין בקשות. הם ה-state של הקומפוננטה.

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');
    }
}

מה אפשר לשמור

ה-state עובר סריאליזציה ל-JSON בין בקשות, ולכן מגוון הטיפוסים הנתמכים מוגבל.

מותראסור
string, int, float, bool, null Closures
array של ערכים סקלריים Resources, streams
מודלי Eloquent ו-collections של מודלים אובייקטים שרירותיים ללא תמיכת Wireable/Synth
Carbon, DateTime, Stringable, enums אובייקטים שמחזיקים חיבורים, PDO handles וכדומה
מודלי Eloquent כמאפיינים. מודל נשמר כמזהה ונטען מחדש ממסד הנתונים בכל בקשה. נוח, אבל זה עולה query אחד בכל מחזור. אם צריך רק כמה שדות, שמרו ערכים סקלריים במקום את המודל כולו.

אתחול: mount()

mount() הוא ה-constructor של הקומפוננטה. הוא רץ פעם אחת, ברינדור הראשון, ומקבל את הפרמטרים שהועברו מ-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" />

הגנה על מאפיינים: #[Locked]

כברירת מחדל הלקוח יכול לשנות את הערך של כל מאפיין ציבורי. עבור מזהים וכל דבר שמשפיע על בקרת גישה זו פרצה. ה-attribute #[Locked] אוסר שינויים מצד ה-frontend.

PHP
use Livewire\Attributes\Locked;

class InvoiceEditor extends Component
{
    #[Locked]
    public int $invoiceId;   // הלקוח לא יכול להחליף למזהה של מישהו אחר

    public string $comment = '';
}

מאפיינים שמוסתרים מ-JSON

מאפייני protected ו-private לא עוברים סריאליזציה ומתאפסים בין בקשות. השתמשו בהם רק לערכים שמחושבים מחדש בכל מחזור.

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

    // מתאפס אחרי כל בקשה — אל תשמרו כאן state.
    protected array $cache = [];
}

5. wire:model וקישור נתונים

wire:model מקשר שדה טופס למאפיין של הקומפוננטה. ב-Livewire 3 הקישור דחוי כברירת מחדל: הערך לא נשלח לשרת בכל הקשה, אלא יחד עם הפעולה הבאה (לחיצה, submit).

Blade
{{-- דחוי: הערך נשלח עם הבקשה הבאה --}}
<input type="text" wire:model="name">

{{-- מיידי: בקשה בכל שינוי --}}
<input type="text" wire:model.live="search">

{{-- 500 ms אחרי שההקלדה נעצרת --}}
<input type="text" wire:model.live.debounce.500ms="search">

{{-- לכל היותר פעם ב-2 s בזמן ההקלדה --}}
<input type="text" wire:model.live.throttle.2s="search">

{{-- רק כשהשדה מאבד פוקוס --}}
<input type="text" wire:model.blur="email">

{{-- מתעדכן מיד בצד הלקוח, השרת מתעדכן מאוחר יותר --}}
<input type="text" wire:model.lazy="draft">

Modifiers

Modifierהתנהגותמתי להשתמש
—שליחה דחויהשדות טופס רגילים
.liveבקשה בכל שינויחיפוש חי, select-ים תלויים
.blurבקשה באיבוד פוקוסולידציה של שדה ביציאה ממנו
.debounce.Xmsהמתנה להפסקה בהקלדהחיפוש תוך כדי הקלדה
.throttle.Xsלכל היותר פעם אחת בכל פרק זמןqueries יקרים
.numberהמרה למספרשדות מספריים
.booleanהמרה לבוליאניselect-ים של כן/לא
.fillלקיחת הערך ההתחלתי מה-markupטפסים ממולאים מראש

כל סוגי השדות

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

{{-- checkbox בודד: bool --}}
<input type="checkbox" wire:model="agreed">

{{-- קבוצת checkboxes: מערך --}}
<input type="checkbox" value="php"  wire:model="skills">
<input type="checkbox" value="js"   wire:model="skills">
<input type="checkbox" value="sql"  wire:model="skills">

{{-- כפתורי radio --}}
<input type="radio" value="card" wire:model="payment">
<input type="radio" value="cash" wire:model="payment">

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

{{-- Select מרובה --}}
<select wire:model="tagIds" multiple>
    @foreach ($tags as $tag)
        <option value="{{ $tag->id }}">{{ $tag->name }}</option>
    @endforeach
</select>

נתונים מקוננים

סימון נקודה עובד גם למערכים וגם למאפיינים של מודלים.

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">
קישור למודל. כדי ש-wire:model="post.title" יעבוד, השדה חייב להיות מותר בכלל ולידציה (rules או #[Validate]) — אחרת Livewire זורק חריגה. זו ההגנה מפני mass assignment.

6. פעולות (Actions)

פעולה היא מתודה ציבורית של הקומפוננטה שנקראת מה-view. היא מחליפה את הצירוף הרגיל של fetch() ו-handler בצד השרת.

Blade
{{-- לחיצה --}}
<button wire:click="save">Save</button>

{{-- שליחת טופס (מיורטת) --}}
<form wire:submit="save">
    <input wire:model="title">
    <button type="submit">Send</button>
</form>

{{-- מקשים --}}
<input wire:keydown.enter="search" wire:keydown.escape="clear">

{{-- אירועי DOM אחרים --}}
<div wire:mouseenter="preload">…</div>
<select wire:change="applyFilter">…</select>

פרמטרים

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

    $this->authorize('delete', $post);   // תמיד לבדוק הרשאות

    $post->delete();

    $this->dispatch('notify', message: 'Post deleted');
}
לעולם אל תסמכו על הפרמטרים. כל מתודה של קומפוננטה נגישה כ-endpoint HTTP ציבורי. לקוח יכול לקרוא ל-delete(999) עם כל מזהה שהוא. בדיקת הרשאות בתוך המתודה היא חובה.

Model binding בפרמטרים

Livewire טוען מודלים לפי מזהה באותו אופן שה-router של Laravel עושה זאת.

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

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

Modifiers של פעולות

Blade
{{-- אישור בדפדפן לפני השליחה --}}
<button wire:click="delete" wire:confirm="Delete for good? This cannot be undone.">Delete</button>

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

{{-- רץ פעם אחת --}}
<button wire:click.once="init">Initialise</button>

{{-- רק כשלחצו בדיוק על האלמנט הזה --}}
<div wire:click.self="close">…</div>

פעולות קסם

פעולהמה היא עושה
$refreshרינדור מחדש בלי לשנות את ה-state
$set('prop', value)השמת ערך למאפיין
$toggle('prop')היפוך מאפיין בוליאני
$dispatch('event')שליחת אירוע Livewire
$parent.method()קריאה למתודה של קומפוננטת האב
Blade
<button wire:click="$refresh">Refresh</button>
<button wire:click="$set('tab', 'settings')">Settings</button>
<button wire:click="$toggle('showFilters')">Filters</button>
<button wire:click="$parent.closeModal()">Close</button>

הפניות (Redirects)

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

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

    session()->flash('status', 'Post created');

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

7. מחזור החיים של בקשה

הכרת סדר הביצוע מעלימה 90% מהבאגים ה"מסתוריים". יש שני מסלולים: הרינדור הראשוני (בקשת HTTP רגילה לעמוד) ועדכונים עוקבים (בקשות AJAX של Livewire).

רינדור ראשוני

סדר
1. The component instance is created
2. boot()
3. mount($params)
4. booted()
5. hydrate hooks do NOT run
6. render()
7. Markup is inserted into the page

עדכון עוקב

סדר
 1. POST /livewire/update arrives with the state snapshot
 2. The snapshot checksum is verified (tamper protection)
 3. The component instance is created
 4. boot()
 5. State is restored from the snapshot
 6. hydrate() and hydrateFoo() for each property
 7. booted()
 8. updating($prop, $value) / updatingFoo($value)   — before assignment
 9. Properties receive their new values
10. updated($prop, $value) / updatedFoo($value)     — after assignment
11. Queued calls (actions) are executed
12. rendering()
13. render()
14. rendered($view, $html)
15. dehydrate() — state is packed back into the snapshot
16. Response: new markup plus effects (events, redirects, browser dispatches)

Snapshot ו-checksum

כל ה-state של הקומפוננטה עובר ללקוח וחזרה בתוך ה-attribute wire:snapshot. כדי למנוע מהלקוח לשנות אותו, Livewire חותם על ה-snapshot ב-HMAC שנגזר מ-APP_KEY. חתימה שאינה תואמת נדחית עם "Livewire encountered corrupt data".

ההשלכה המעשית. ככל שיותר state יושב במאפיינים ציבוריים, כך יותר נתונים עוברים ברשת בכל מחזור. collection של 500 מודלים במאפיין ציבורי פירושו מגה-בייטים בכל לחיצה. שמרו על state מינימלי וטענו רשימות ב-render() או דרך #[Computed].

מה קורה ל-DOM

Livewire לא מחליף את הצומת בשלמותו, אלא מבצע morph: הוא עובר על העץ הישן ועל החדש ומשנה רק את ההבדלים. בזכות זה נשמרים הפוקוס בשדות, מיקום הגלילה וה-state של Alpine. כשמבנה של רשימה משתנה, האלגוריתם צריך רמז — ראו wire:key בפרק 10.

8. Lifecycle hooks

Hooks מאפשרים להתערב בכל שלב של המחזור.

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

    // רץ בתחילת כל בקשה, לפני שחזור ה-state.
    public function boot(): void
    {
        // מקום טוב לתלויות שלא ניתן לעשות להן סריאליזציה.
    }

    // רק ברינדור הראשון.
    public function mount(Product $product): void
    {
        $this->product = $product;
        $this->name    = $product->name;
        $this->price   = $product->price;
    }

    // אחרי שחזור ה-state, בכל בקשה עוקבת.
    public function hydrate(): void
    {
    }

    // אחרי boot() ושחזור ה-state.
    public function booted(): void
    {
    }

    // לפני שינוי של מאפיין כלשהו.
    public function updating(string $property, mixed $value): void
    {
    }

    // אחרי שינוי של מאפיין כלשהו.
    public function updated(string $property, mixed $value): void
    {
        $this->validateOnly($property);
    }

    // רק עבור המאפיין $price.
    public function updatedPrice(mixed $value): void
    {
        $this->price = round((float) $value, 2);
    }

    // עבור המפתח המקונן 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
    {
    }

    // לפני שה-state נארז ל-snapshot.
    public function dehydrate(): void
    {
    }

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

כללי שמות

מאפייןHook
$priceupdatedPrice()
$isActiveupdatedIsActive()
$form['email']updatedFormEmail()
$post->titleupdatedPostTitle()
המהלך הסטנדרטי. איפוס העימוד כשפילטר משתנה שייך ל-hook: public function updatedSearch() { $this->resetPage(); }

9. ולידציה ו-Form objects

Livewire משתמש ב-validator של Laravel — אותם כללים, הודעות ולוקליזציה.

אפשרות 1: attributes (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: 'Please agree to the processing of your data.')]
    public bool $consent = false;

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

        Contact::create($data);

        $this->reset();

        session()->flash('status', 'Message sent');
    }
}

אפשרות 2: המתודה rules()

נדרשת כשהכללים תלויים ב-state.

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' => 'That email address is already registered.',
    ];
}

protected function validationAttributes(): array
{
    return [
        'email' => 'email address',
    ];
}

ולידציה בזמן אמת

PHP
public function updated(string $property): void
{
    $this->validateOnly($property);   // בדיקה רק של השדה שהשתנה
}
Blade
<form wire:submit="submit" novalidate>
    <label for="email">Email</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">Send</span>
        <span wire:loading wire:target="submit">Sending…</span>
    </button>
</form>

ניהול שגיאות ידני

PHP
$this->addError('email', 'That domain is blocklisted.');
$this->resetValidation('email');
$this->resetValidation();               // ניקוי הכול
$this->validateOnly('email');

// זריקת חריגה בדיוק כמו ב-controller
throw ValidationException::withMessages([
    'code' => 'Wrong confirmation code.',
]);

Form objects

כשטופס גדל, העבירו אותו למחלקה משלו. הקומפוננטה נשארת רזה, ואת הכללים והנתונים אפשר לשתף בין יצירה לעריכה.

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">Save</button>
</form>

10. קומפוננטות מקוננות ו-wire:key

קומפוננטות ניתנות לקינון. כל קומפוננטת ילד היא יחידה עצמאית עם state ומחזור עדכון משלה: לחיצה בתוך הילד אינה מרנדרת מחדש את ההורה.

Blade
<div>
    <h1>Orders</h1>

    @foreach ($orders as $order)
        <livewire:order-row :order="$order" :key="'order-'.$order->id" />
    @endforeach
</div>
:key הוא חובה בלולאות. ללא מפתח ייחודי אלגוריתם ה-morph מתבלבל בין אלמנטים בעת מיון, סינון או מחיקה: ערכי שדות "עוברים" לשורות סמוכות. המפתח חייב להיות יציב — $loop->index לא יתאים, השתמשו במזהה הרשומה.

העברת נתונים למטה

הפרמטרים מועברים פעם אחת, ב-mount(). ברינדורים הבאים של ההורה הילד אינו מתעדכן אוטומטית — הוא חי את חייו שלו.

PHP
use Livewire\Attributes\Reactive;

class OrderTotal extends Component
{
    // עם #[Reactive] הערך מגיע מההורה בכל רינדור שלו.
    #[Reactive]
    public int $quantity;
}

קריאה להורה

Blade
<button wire:click="$parent.refreshList()">Refresh the list</button>

קישור דו-כיווני: #[Modelable]

מאפשר לשים wire:model על הקומפוננטה עצמה — שימושי לווידג'טים של קלט.

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
{{-- ב-view של ההורה --}}
<livewire:rating-input wire:model.live="review.rating" />

רינדור מותנה של ילדים

Blade
@if ($showDetails)
    <livewire:order-details :order-id="$orderId" :key="'details-'.$orderId" />
@endif
מתי לפצל. הוציאו קומפוננטת ילד כשלבלוק יש state משלו או שהוא מתעדכן באופן עצמאי (שורת טבלה, חלון מודאלי, ווידג'ט). אם הבלוק הוא רק markup, השתמשו ב-partial רגיל של Blade — זה זול יותר.

11. אירועים

אירועים מחברים בין קומפוננטות שאין ביניהן יחסי הורה-ילד. ב-Livewire 3 שולחים באמצעות dispatch() (בגרסה 2 השתמשו ב-emit()).

שליחה

PHP
// אירוע פשוט
$this->dispatch('post-created');

// עם פרמטרים בעלי שם
$this->dispatch('post-created', postId: $post->id, title: $post->title);

// לקומפוננטה ספציפית אחת
$this->dispatch('refresh')->to(OrderList::class);

// לעצמה בלבד
$this->dispatch('recalculate')->self();
Blade
{{-- ישירות מה-view --}}
<button wire:click="$dispatch('open-modal', { name: 'create-order' })">New order</button>

האזנה

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];
    }

    // שם אירוע דינמי
    #[On('order-updated.{orderId}')]
    public function onOrderUpdated(): void
    {
        $this->refreshList();
    }
}

החלופה היא המערך $listeners (תואם לגרסה 2):

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

אירועים לדפדפן

Livewire יכול לשלוח אירוע DOM רגיל, שנקלט על ידי Alpine.js או קוד JS משלכם. זו הדרך הנכונה להציג הודעות toast ולפתוח חלונות מודאליים.

PHP
$this->dispatch('notify', type: 'success', message: 'Order saved');
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);
});
אל תגזימו עם אירועים. כל אירוע עולה מחזור בקשה נוסף לכל קומפוננטה שמאזינה לו. אם שני בלוקים תמיד משתנים יחד, איחודם לקומפוננטה אחת זול יותר בדרך כלל.

12. מאפיינים מחושבים

#[Computed] נותן לכם נתונים נגזרים בלי לשמור אותם ב-state. התוצאה נשמרת במטמון למשך בקשה אחת, כך ששימוש בה שלוש פעמים ב-view אינו עולה שלוש שאילתות.

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);   // כולל מע"מ
    }
}
Blade
<div>
    @foreach ($this->products as $product)
        <div>{{ $product->name }} — {{ $this->items[$product->id] }} pcs</div>
    @endforeach

    <p>Subtotal: {{ number_format($this->subtotal, 2) }} €</p>
    <p>Total incl. VAT: {{ number_format($this->total, 2) }} €</p>
</div>
גישה דרך $this. ב-view מאפיין מחושב הוא $this->products, ולא $products. זה מה שמבדיל אותו ממאפיינים ציבוריים רגילים.

מטמון בין בקשות

PHP
// נשמר במטמון ל-5 דקות במטמון המשותף של האפליקציה
#[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'),
    ];
}

// רשומת מטמון אחת המשותפת לכל המשתמשים
#[Computed(cache: true, key: 'global-stats')]
public function globalStats(): array
{
    return app(StatsService::class)->build();
}

ניקוי המטמון

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

    unset($this->products, $this->subtotal, $this->total);   // ניקוי המטמון
}

נתוני render() לעומת #[Computed]

אף אחד מהם אינו שומר נתונים ב-state. ההבדל הוא בטווח: render() מעביר משתנים ל-view בלבד, בעוד ש-#[Computed] זמין במתודות PHP וב-view, ונשמר במטמון.

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. State ב-URL וב-session

פילטרים, חיפוש והלשונית הפעילה צריכים לשרוד רענון ולהיות ניתנים לשיתוף כקישור. ה-attribute #[Url] שומר מאפיין מסונכרן עם ה-query string.

PHP
use Livewire\Attributes\Url;

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

    // שם פרמטר שונה ב-URL
    #[Url(as: 'cat')]
    public ?int $categoryId = null;

    // לא מופיע ב-URL כל עוד הוא שווה לערך ההתחלתי
    #[Url(except: '')]
    public string $sort = 'popular';

    // נשמר בין ביקורי wire:navigate
    #[Url(keep: true)]
    public int $perPage = 24;

    // שימוש ב-history.pushState במקום replaceState
    #[Url(history: true)]
    public string $tab = 'all';
}

התוצאה: /catalog?search=laptop&cat=5&sort=price-asc&tab=sale

שמירה ב-session

PHP
use Livewire\Attributes\Session;

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

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

Session ו-flash באופן ידני

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

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

    session()->flash('status', 'Settings saved');
}
Blade
@if (session('status'))
    <div class="docs-note docs-note--tip">{{ session('status') }}</div>
@endif
אל תדחפו הכול ל-URL. כל מאפיין #[Url] פונה ל-History API כשהוא משתנה. בשדות שמשתנים בכל הקשה, שלבו אותו עם wire:model.live.debounce.

14. העלאת קבצים

ה-trait WithFileUploads מוסיף העלאות אמיתיות: הקובץ נשלח לשרת ברגע שנבחר, נשמר באחסון זמני והופך לזמין כאובייקט 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')]   // עד 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 updated');
    }

    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/*">

    {{-- התקדמות ההעלאה --}}
    <div wire:loading wire:target="avatar" class="text-sm">Uploading…</div>

    {{-- תצוגה מקדימה לפני השמירה --}}
    @if ($avatar)
        <img src="{{ $avatar->temporaryUrl() }}" alt="Preview" 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">Save</button>
</form>

סרגל התקדמות מדויק

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>

הגדרת האחסון הזמני

config/livewire.php
'temporary_file_upload' => [
    'disk'       => 's3',            // או null עבור דיסק ברירת המחדל
    'rules'      => ['file', 'max:12288'],
    'directory'  => 'livewire-tmp',
    'middleware' => 'throttle:60,1',
    'preview_mimes' => ['png', 'jpeg', 'jpg', 'webp', 'gif', 'mp4', 'pdf'],
    'max_upload_time' => 5,          // דקות עד לניקוי אוטומטי
],
בדקו את מגבלות PHP. upload_max_filesize, post_max_size ו-max_execution_time ב-php.ini, וכן client_max_body_size ב-nginx, חייבים כולם להיות גדולים לפחות כמו כלל ה-max: שלכם. אחרת ההעלאה נכשלת ללא הודעת שגיאה קריאה.

15. עימוד

ה-trait WithPagination מחבר את העימוד של Laravel ללא טעינה מחדש של הדף.

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;

    // חזרה לעמוד הראשון בכל שינוי של פילטר.
    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="Search by number">

    <table>
        <thead>
            <tr>
                <th wire:click="sortBy('number')" style="cursor:pointer">Number</th>
                <th wire:click="sortBy('created_at')" style="cursor:pointer">Date</th>
                <th wire:click="sortBy('total')" style="cursor:pointer">Total</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">No orders found</td></tr>
            @endforelse
        </tbody>
    </table>

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

פרטים שימושיים

PHP
// שם מותאם לפרמטר העמוד — נדרש כשבדף יש שני paginators
protected string $paginationTheme = 'tailwind';   // או 'bootstrap'

public function render()
{
    return view('livewire.dashboard', [
        'orders'   => Order::paginate(10, pageName: 'orders-page'),
        'invoices' => Invoice::paginate(10, pageName: 'invoices-page'),
    ]);
}
ביצועים. בטבלאות גדולות paginate() מריץ COUNT(*) נוסף. אם אינכם צריכים את מספר העמודים הכולל, השתמשו ב-simplePaginate() — זה זול משמעותית.

16. מצבי טעינה

כל פעולה היא בקשת רשת. ללא משוב הממשק מרגיש תקוע. Livewire מגיע עם directives הצהרתיים שאינם דורשים JavaScript כלל.

Blade
{{-- מוצג במהלך כל בקשה מהקומפוננטה הזו --}}
<div wire:loading>Loading…</div>

{{-- מוסתר במהלך בקשה --}}
<div wire:loading.remove>Content</div>

{{-- רק עבור פעולה אחת --}}
<button wire:click="save">Save</button>
<span wire:loading wire:target="save">Saving…</span>

{{-- עבור כמה יעדים --}}
<span wire:loading wire:target="save,delete,publish">Working…</span>

{{-- עבור מאפיין מסוים --}}
<span wire:loading wire:target="search">Searching…</span>

{{-- החרגת יעד --}}
<div wire:loading wire:target.except="search">Updating…</div>

Modifiers

Blade
{{-- השבתת הכפתור --}}
<button wire:click="save" wire:loading.attr="disabled">Save</button>

{{-- הוספת class של CSS --}}
<button wire:click="save" wire:loading.class="opacity-50 cursor-wait">Save</button>

{{-- הסרת class --}}
<div wire:loading.class.remove="bg-white">…</div>

{{-- השהיה: המחוון מוצג רק אם הבקשה נמשכת יותר מ-300 ms --}}
<div wire:loading.delay>Loading…</div>

{{-- ספים מדויקים: shortest 50ms, shorter 100ms, short 150ms,
     default 200ms, long 300ms, longer 500ms, longest 1s --}}
<div wire:loading.delay.long>Loading…</div>

{{-- שליטה במצב התצוגה --}}
<div wire:loading.flex>…</div>
<div wire:loading.grid>…</div>
<div wire:loading.inline-flex>…</div>

שינויים שלא נשמרו: wire:dirty

Blade
<input wire:model="title">

<span wire:dirty wire:target="title" class="text-amber-600">You have unsaved changes</span>

<button wire:click="save" wire:dirty.class="ring-2 ring-amber-400">Save</button>

אובדן חיבור: wire:offline

Blade
<div wire:offline class="banner banner--warn">
    No connection to the server. Changes are not being saved.
</div>

Skeleton בטעינה הראשונה

Blade
<div wire:init="loadHeavyData">
    @if ($loaded)
        {{-- הנתונים האמיתיים --}}
    @else
        <div class="skeleton h-40 w-full animate-pulse bg-gray-200"></div>
    @endif
</div>

18. טעינה עצלה ו-polling

טעינה עצלה של קומפוננטה

קומפוננטה כבדה לא חייבת להירנדר בתגובה הראשונה: העמוד נשלח מיד עם placeholder, והתוכן מגיע בבקשה שנייה.

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(),   // שאילתה יקרה
        ]);
    }
}

כברירת מחדל, קומפוננטה עצלה נטענת מיד אחרי שהעמוד מצויר. #[Lazy(isolate: false)] מאחד את הבקשות של כמה קומפוננטות עצלות לבקשה אחת, ו- lazy="on-load" / lazy="on-scroll" קובעים מתי הטעינה מתבצעת.

Blade
{{-- טעינה כשהבלוק נכנס לתחום התצוגה --}}
<livewire:revenue-chart lazy="on-scroll" />

אתחול נדחה: wire:init

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

תשאול השרת: wire:poll

Blade
{{-- רענון כל 2 שניות (ברירת המחדל היא 2500 ms) --}}
<div wire:poll>…</div>

{{-- מרווח מותאם --}}
<div wire:poll.5s>…</div>
<div wire:poll.750ms>…</div>

{{-- קריאה למתודה מסוימת --}}
<div wire:poll.10s="refreshQueue">…</div>

{{-- עצירת ה-polling כשהלשונית מוסתרת --}}
<div wire:poll.visible.5s="refreshQueue">…</div>

{{-- עצירה אחרי 5 דקות של חוסר פעילות המשתמש --}}
<div wire:poll.keep-alive.5s>…</div>
כמה עולה polling. wire:poll.2s בעמוד שפתוח ב-100 עמדות עבודה הוא 3,000 בקשות בדקה שפוגעות בתהליכי ה-PHP שלכם. הוסיפו תמיד .visible, בחרו מרווח סביר, ושקלו websockets (Laravel Echo + Reverb) עבור נתונים חיים באמת.

19. Alpine.js, $wire ו-JS hooks

Alpine.js מגיע יחד עם Livewire 3. בתוך קומפוננטה זמין האובייקט $wire — proxy אל ה-state והמתודות של קומפוננטת ה-PHP, ישירות מ-JavaScript.

Blade
<div x-data="{ open: false }">
    {{-- state בצד הלקוח בלבד: השרת לא מעורב --}}
    <button x-on:click="open = !open">Details</button>

    <div x-show="open" x-transition>
        {{-- קריאת property --}}
        <p x-text="$wire.title"></p>

        {{-- כתיבת property --}}
        <button x-on:click="$wire.title = 'New title'">Rename</button>

        {{-- קריאה למתודה (מחזירה Promise) --}}
        <button x-on:click="$wire.save()">Save</button>

        {{-- המתנה לתוצאה --}}
        <button x-on:click="await $wire.calculate(); open = false">Calculate</button>

        {{-- קריאה ללא רינדור מחדש --}}
        <button x-on:click="$wire.$set('tab', 'stats', false)">Stats</button>
    </div>
</div>

קישור דו-כיווני: $wire.entangle

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

{{-- סנכרון נדחה: לא לשלוח בקשה על כל תו --}}
<div x-data="{ query: $wire.entangle('search').live }">…</div>

סקריפטים משלכם בתוך קומפוננטה

Blade
@script
<script>
    // רץ פעם אחת כשהקומפוננטה מאותחלת.
    const chart = new Chart(document.getElementById('sales'), {
        type: 'line',
        data: @json($chartData),
    });

    // גם כאן $wire זמין.
    $wire.on('data-updated', ({ points }) => {
        chart.data.datasets[0].data = points;
        chart.update();
    });
</script>
@endscript
Blade
{{-- טעינת ספרייה חיצונית פעם אחת לכל העמוד --}}
@assets
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
@endassets

התעלמות מתת-עץ

כשווידג'ט צד שלישי מנהל את ה-DOM בעצמו, אלגוריתם ה-morph רק מפריע.

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

{{-- התעלמות מהילדים, אך המשך עדכון המאפיינים של האלמנט עצמו --}}
<div wire:ignore.self>…</div>

JS hooks גלובליים

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

    // לפני כל בקשה יוצאת
    Livewire.hook('request', ({ uri, options, payload, respond, succeed, fail }) => {
        options.headers['X-Tenant'] = window.tenantId;

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

        fail(({ status, preventDefault }) => {
            if (status === 419) {
                preventDefault();
                window.location.reload();   // פג תוקף טוקן ה-CSRF
            }
        });
    });

    // לפני ואחרי morph של אלמנט בודד
    Livewire.hook('morph.updated', ({ el, component }) => {});

    // קומפוננטה אותחלה
    Livewire.hook('component.init', ({ component }) => {});
});

// גישה פרוגרמטית לקומפוננטות
Livewire.dispatch('refresh-orders');
Livewire.find('component-id').call('save');
Livewire.all().forEach((component) => component.$refresh());

20. בדיקות

Livewire מגיע עם API בדיקות משלו, שנבנה מעל זה של Laravel. הבדיקות מהירות — אין צורך בדפדפן, PHPUnit או Pest רגילים מספיקים.

tests/Feature/CounterTest.php
<?php

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

it('increments the counter', 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('validates the required fields', function () {
    Livewire::test(ContactForm::class)
        ->set('name', '')
        ->set('email', 'not-an-email')
        ->call('submit')
        ->assertHasErrors([
            'name'  => 'required',
            'email' => 'email',
        ])
        ->assertNoRedirect();
});

it('stores a valid enquiry', function () {
    Livewire::test(ContactForm::class)
        ->set('name', 'Irene Kovac')
        ->set('email', 'irene@example.com')
        ->set('message', str_repeat('We need a website for a clinic. ', 2))
        ->set('consent', true)
        ->call('submit')
        ->assertHasNoErrors()
        ->assertDispatched('notify');

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

ה-assertions העיקריים

מתודהמה נבדק
assertSet('prop', $value)ערך של property
assertNotSet('prop', $value)הערך שונה
assertSee('text')הטקסט מופיע ב-markup
assertDontSee('text')הטקסט לא מופיע
assertSeeHtml('<b>')ה-markup קיים
assertHasErrors(['email'])שגיאות ולידציה
assertHasNoErrors()אין שגיאות
assertDispatched('event')נשלח אירוע
assertRedirect(route(…))בוצעה הפניה
assertStatus(403)סטטוס HTTP
assertForbidden()הגישה נדחתה
assertCount('items', 3)גודל מערך או collection

אימות, פרמטרים, אירועים

PHP
// כמשתמש מחובר
Livewire::actingAs($admin)
    ->test(OrderTable::class)
    ->assertSee('All orders');

// עם פרמטרים ל-mount()
Livewire::test(OrderCard::class, ['order' => $order, 'mode' => 'full'])
    ->assertSee($order->number);

// קבלת אירוע
Livewire::test(OrderList::class)
    ->dispatch('order-created', orderId: 42)
    ->assertSee('Order #42');

// העלאת קובץ
use Illuminate\Http\UploadedFile;

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

// קומפוננטה בתוך עמוד
$this->get('/dashboard')
    ->assertSeeLivewire(Dashboard::class)
    ->assertDontSeeLivewire(AdminPanel::class);
מה לבדוק קודם. הרשאות בתוך actions, כללי ולידציה ומעברי state. שם הפונקציונליות נשברת ושם מסתתרות פרצות האבטחה.

21. אבטחה

העיקרון המרכזי. כל מתודה ציבורית של קומפוננטה היא endpoint HTTP פתוח. כל property ציבורי ניתן לשינוי על ידי הלקוח. התייחסו לקומפוננטה כמו ל-controller, לא כמו למחלקה פנימית.

1. בדקו הרשאות בתוך כל action

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

    $this->authorize('delete', $post);   // policy — לא נתון למשא ומתן

    $post->delete();
}
PHP
// בדיקת הרשאות לקומפוננטה כולה
public function mount(Project $project): void
{
    $this->authorize('view', $project);

    $this->project = $project;
}

2. #[Locked] על מזהים

PHP
use Livewire\Attributes\Locked;

class InvoiceEditor extends Component
{
    #[Locked]
    public int $invoiceId;      // לא ניתן לשינוי מה-frontend

    public string $note = '';
}

בלי #[Locked] מספיקה שורה אחת בקונסולת הדפדפן:

JavaScript (מתקפה)
Livewire.find('...').set('invoiceId', 999)   // החשבונית של מישהו אחר

3. כללים עבור model binding

wire:model="post.title" עובד רק כשהשדה מותר על ידי כלל ולידציה — כך Livewire מתגונן מפני mass assignment.

PHP
protected function rules(): array
{
    return [
        'post.title' => 'required|string|max:180',
        'post.body'  => 'required|string',
        // 'post.user_id' הושמט בכוונה — אחרת אפשר היה להעביר את הפוסט למשתמש אחר
    ];
}

4. בצעו escape לפלט

Blade
{{-- בטוח: עובר escape --}}
{{ $comment->body }}

{{-- מסוכן: markup גולמי ממשתמש --}}
{!! $comment->body !!}

{{-- אם צריך markup, בצעו לו sanitize בצד השרת --}}
{!! clean($comment->body) !!}

5. Rate limiting

PHP
use Illuminate\Support\Facades\RateLimiter;

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

    if (RateLimiter::tooManyAttempts($key, 5)) {
        throw ValidationException::withMessages([
            'email' => 'Too many attempts. Try again in a minute.',
        ]);
    }

    RateLimiter::hit($key, 60);

    // …
}

6. לעולם אל תשמרו סודות ב-properties

PHP
class PaymentForm extends Component
{
    public string $cardLast4 = '';        // תקין

    // אסור: זה מגיע לדפדפן כטקסט גלוי בתוך wire:snapshot
    // public string $apiSecret = '';
    // public string $fullCardNumber = '';

    protected function gateway(): PaymentGateway
    {
        return app(PaymentGateway::class);   // הסודות נשארים בשרת
    }
}

רשימת בדיקה לפני שחרור

  • $this->authorize() מופיע בכל action שמשנה נתונים.
  • כל המזהים מסומנים ב-#[Locked].
  • כללי הוולידציה לא כוללים שדות פנימיים (user_id, role, price).
  • properties ציבוריים לא מחזיקים טוקנים, מפתחות או מידע אישי מיותר.
  • העלאות קבצים מוגבלות לפי סוג MIME וגודל.
  • טפסי התחברות ויצירת קשר מוגבלים ב-rate limiting.
  • {!! !!} משמש רק עבור markup אמין או כזה שעבר sanitize.

22. ביצועים

1. שמרו על state ציבורי קטן

רע
public Collection $products;   // 500 מודלים נוסעים ללקוח ובחזרה

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

2. אל תשלחו בקשה על כל הקשה

Blade
{{-- רע: בקשה על כל תו --}}
<input wire:model.live="search">

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

3. פצלו עמודים כבדים לקומפוננטות

עדכון של קומפוננטת ילד לא מרנדר מחדש את ההורה. דשבורד שבנוי משישה ווידג'טים עצמאיים מרגיש זריז בהרבה ממונולית אחד.

4. שמרו עבודה יקרה ב-cache

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

5. היזהרו מ-N+1

PHP
// רע — שאילתה אחת לכל שורה ב-view
$orders = Order::paginate(50);

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

הפעילו strict mode בסביבת הפיתוח כדי ש-N+1 ייכשל בקול רם:

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

6. wire:key ברשימות

בלי מפתחות, אלגוריתם ה-morph בונה מחדש יותר צמתים מהנדרש — וגם טועה.

7. טעינה עצלה לבלוקים כבדים

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

8. אל תגזימו עם polling

wire:poll.visible.10s במקום wire:poll חוסך לשרת סדר גודל של בקשות.

תסמיןסיבה סבירה
השהיה של 300–800 ms בכל actionrender() יקר או N+1
markup עמוד עצוםcollections של מודלים ב-properties ציבוריים
עומס גובר על PHP-FPMwire:poll אגרסיבי או .live בלי debounce
שדות "קופצים" כשהרשימה מתעדכנתחסר wire:key
העמוד כולו מתרנדר מחדשהכול בקומפוננטה אחת, בלי פיצול

23. שגיאות נפוצות ופתרונות

"Component must have a single root element"

ב-view יש כמה צמתי שורש, או טקסט או הערה ברמה העליונה. עטפו הכול ב-<div> אחד.

רע
<h1>Heading</h1>
<p>Text</p>
טוב
<div>
    <h1>Heading</h1>
    <p>Text</p>
</div>

"Livewire encountered corrupt data"

חתימת ה-snapshot לא תואמת. סיבות: APP_KEY שהשתנה, עמוד שהוגש מ-cache אחרי deploy, שתי לשוניות עם sessions שונים. טעינה מחדש בדרך כלל פותרת; אחרי deploy הריצו php artisan optimize:clear.

לחיצות לא עושות כלום והקונסולה ריקה

  • @livewireScripts חסר ב-layout.
  • נטען מופע שני של Alpine.js (Livewire 3 כבר כולל אותו).
  • שגיאת JS גבוה יותר בעמוד עצרה את הריצה.
  • האלמנט נמצא בתוך wire:ignore.

ערכי שדות "זזים" בין שורות

בלולאה חסר wire:key או :key. המפתח חייב להיות יציב וייחודי.

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

"Unable to set component data. Public property not found"

קישרתם wire:model ל-property שלא קיים או שאינו ציבורי. בדקו את האיות ואת מגדיר הנראות.

"Cannot bind to model data without validation rules"

קישור כמו wire:model="post.title" דורש כלל עבור post.title ב- rules() או ב-#[Validate].

ווידג'ט צד שלישי נשבר אחרי עדכון

Select2, Flatpickr, TinyMCE ודומיהם משנים את ה-DOM בעצמם. עטפו אותם ב- wire:ignore וסנכרנו ידנית.

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>

העלאות קבצים נכשלות בשקט

בדקו את upload_max_filesize ואת post_max_size ב-PHP, וגם את client_max_body_size ב-nginx. הם חייבים לעלות על המגבלה בכלל max: שלכם.

המודאל לא נסגר אחרי שמירה

שלחו אירוע דפדפן וטפלו בו ב-Alpine במקום להסתמך על רינדור מחדש.

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

סקריפטים מפסיקים לעבוד אחרי wire:navigate

העבירו את האתחול מ-DOMContentLoaded ל-livewire:navigated.

שגיאה 419 (Page Expired)

פג תוקף ה-session. הגדילו את SESSION_LIFETIME, או תפסו את הסטטוס ב-hook request וטענו מחדש את העמוד — ראו את הדוגמה בפרק 19.

24. דף עזר

דירקטיבות Blade

דירקטיבהמטרה
wire:modelקישור שדה ל-property (נדחה)
wire:model.liveקישור עם בקשה בכל שינוי
wire:model.blurשליחה באובדן פוקוס
wire:clickקריאה למתודה בלחיצה
wire:submitטיפול בשליחת טופס
wire:keydown.enterתגובה למקש
wire:changeתגובה לשינוי
wire:confirmאישור לפני ביצוע
wire:loadingמחוון טעינה
wire:targetהגבלת המחוון ל-action מסוים
wire:dirtyיש שינויים שלא נשמרו
wire:offlineאין חיבור
wire:pollרענון תקופתי
wire:initקריאה למתודה מיד אחרי הרינדור
wire:navigateניווט קישורים בסגנון SPA
wire:keyזהות אלמנט בתוך לולאה
wire:ignoreהחרגת תת-עץ מ-morph
wire:transitionאנימציית כניסה/יציאה
wire:streamהזרמת תוכן

Attributes של PHP

Attributeמטרה
#[Validate]כלל ולידציה ל-property
#[Locked]איסור שינויים מה-frontend
#[Computed]property מחושב עם caching
#[Url]סנכרון עם ה-query string
#[Session]שמירת הערך ב-session
#[On]מאזין לאירוע
#[Reactive]רענון prop מההורה
#[Modelable]תמיכה ב-wire:model על הקומפוננטה
#[Lazy]טעינה עצלה של הקומפוננטה
#[Layout]Layout לקומפוננטת עמוד מלא
#[Title]כותרת העמוד
#[Renderless]מתודה שמדלגת על רינדור מחדש

מתודות שימושיות של קומפוננטה

PHP
$this->reset();                       // כל ה-properties חוזרים לערכים ההתחלתיים
$this->reset('search', 'page');       // איפוס של חלק מהם
$this->only('title', 'body');         // מערך של חלק מה-properties
$this->except('password');            // הכול חוץ מאלה שצוינו
$this->fill(['title' => 'New']);      // השמה מרובה
$this->pull('draft');                 // קריאה ואיפוס

$this->validate();
$this->validateOnly('email');
$this->resetValidation();
$this->addError('email', 'Message');

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

$this->skipRender();                  // דילוג על רינדור מחדש במחזור הזה
$this->js('alert("Done")');           // הרצת JS בצד הלקוח
$this->stream(to: 'answer', content: $chunk);

פקודות Artisan

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

משאבים רשמיים

מותאם מהתיעוד הרשמי

אם יש סתירה בין התיעוד הזה למקור הרשמי, המקור הרשמי צודק. מצאתם שגיאה? ספרו לנו.