Tarkibga o'tish

26 β€” Kapston: real-vaqt vazifa boshqaruvchi (Task Board)

⬅️ Oldingi: 25 β€” Performance va deploy Β· 🏠 Kitob boshi

Bu bobda: kitobning butun bilimini bitta to'liq, real ilovaga birlashtiramiz β€” "Vazifalar taxtasi". Foydalanuvchi vazifa qo'shadi, holatini o'zgartiradi (kutilmoqda β†’ jarayonda β†’ bajarildi), qidiradi, filtrlaydi, o'chiradi β€” va hammasi jonli yangilanadi. Har bir kishi faqat o'z vazifalarini ko'radi. Biz buni 0 dan, qadam-baqadam quramiz: model, Form Object, computed ro'yxat, action'lar, avtorizatsiya, eventlar, wire:poll, yuklanish holatlari, Alpine, tezlik va testlar. Oxirida β€” to'liq, jonli loyihada tekshirilgan kod.


Tabriklaymiz β€” siz keldingiz!

Agar siz shu bobgacha yetib kelgan bo'lsangiz, demak siz Livewire'ning deyarli butun olamini ko'rib chiqdingiz: komponentdan tortib events, avtorizatsiya, real-vaqt, tezlik va testlargacha. Lekin bilim β€” bu g'isht. G'isht uyumi hali uy emas. Bu bobda biz o'sha g'ishtlardan uy quramiz β€” to'liq ishlaydigan, real foydalanuvchiga topshirsa bo'ladigan ilova.

Hayotiy o'xshatish. Tasavvur qiling, siz haydashni o'rgandingiz: gaz, tormoz, rul, vites β€” har birini alohida mashq qildingiz. Lekin haqiqiy haydash β€” bularning hammasini bir vaqtda, real yo'lda ishlatishdir. Mana shu bob β€” sizning birinchi mustaqil sayohatingiz. Har bir tugmacha tanish, endi ularni birga ishlatamiz.

Bu bob oldingilaridan farq qiladi: deyarli yangi atama yo'q. Aksincha, har bir bo'lim β€” siz allaqachon o'rgangan bir mavzuni amalda ko'rsatadi va qaysi bobdan kelganini aniq aytadi. Notanish narsa uchrasa, o'sha bobga qaytib o'qing.

Bu bobdagi butun ilova jonli Laravel 12 + Livewire v4.3.1 loyihada yozilib tekshirildi: /board sahifasi brauzerda HTTP 200 bilan render qilindi, vazifa qo'shish/holat o'zgartirish/o'chirish/qidiruv/filtr amallari ma'lumotlar bazasiga qarshi ishladi, avtorizatsiya (begona vazifa = 403) va validatsiya 9 ta avtomatik test bilan tasdiqlandi.


1-qadam. Loyiha bilan tanishuv: nima quramiz?

"Vazifalar taxtasi" β€” bu shaxsiy ish ro'yxati. Trello, Todoist yoki Google Tasks'ning soddalashtirilgan ko'rinishi. Mana u nima qiladi:

  • Vazifa qo'shish β€” yuqorida bitta maydon va "Qo'shish" tugmasi.
  • Holatni o'zgartirish β€” har vazifa uch holatdan birida bo'ladi: kutilmoqda, jarayonda, bajarildi. Bir tugma bilan o'tkaziladi.
  • Qidirish β€” sarlavha bo'yicha jonli filtrlash.
  • Filtrlash β€” faqat ma'lum holatdagi vazifalarni ko'rsatish.
  • O'chirish β€” tasdiq so'rab.
  • Real-vaqt yangilanish β€” ro'yxat o'z-o'zidan yangilanib turadi.
  • Maxfiylik β€” har foydalanuvchi faqat o'zining vazifalarini ko'radi.

Tayyor ilova quyidagicha ko'rinadi:

Vazifalar taxtasi ilovasining interfeysi: sarlavha va statistika, yangi vazifa qo'shish formasi, qidiruv va filtr, holatga qarab ranglangan vazifa kartochkalari

Yuqorida sarlavha va statistika (qaysi holatda nechta vazifa bor). Pastida β€” qo'shish formasi, so'ng qidiruv va filtr. Eng pastda β€” vazifalar ro'yxati. Har vazifa rangli chiziq bilan holatini bildiradi: kulrang (kutilmoqda), sariq (jarayonda), yashil (bajarildi).

Nega aynan vazifalar taxtasi?

Chunki u CRUD'dan kengroq. Oddiy CRUD'da (16-bob) faqat qo'shish/tahrirlash/o'chirish bor edi. Bu yerda bizda holat mashinasi (status o'tishlari), egalik (kim nimani ko'radi), real-vaqt va eventlar ham bor. Ya'ni bu β€” kitobdagi deyarli barcha mavzuni tabiiy ravishda talab qiladigan loyiha.


2-qadam. Reja va arxitektura

Kod yozishdan oldin rejani chizamiz. Yaxshi dasturchi avval qog'ozda o'ylaydi, keyin klaviaturaga tegadi.

Ilovamiz nechta komponentdan iborat bo'ladi? Bu yerda muhim qaror bor. 16-bobda biz bitta komponentda CRUD qurdik. 18-bobda esa formani va ro'yxatni ikki alohida komponentga ajratdik. Qaysi biri yaxshiroq?

Bu loyiha uchun biz bitta asosiy komponent β€” TaskBoard ni tanlaymiz, va formani Form Object (11-bob) ga ajratamiz. Sababi: forma juda kichik (bitta maydon), shuning uchun uni alohida Livewire komponentga ajratish ortiqcha murakkablik bo'lardi. Form Object esa maydon va validatsiyani toza, alohida joyda saqlaydi β€” bu yetarli.

Mana to'liq arxitektura:

Ilova arxitekturasi: TaskBoard ota komponent, TaskForm Form Object, Task model va tasks/users jadvallari, va ular orasidagi bog'liqliklar

Tushuntirib o'tamiz:

  • Route::livewire('/board') β€” full-page komponent (04-bob). /board manziliga kirilganda TaskBoard to'liq sahifa sifatida ko'rinadi.
  • TaskBoard β€” bosh komponent. Ro'yxat, qidiruv, filtr, action'lar β€” hammasi shu yerda.
  • TaskForm β€” Form Object. Faqat yangi vazifa maydoni va uning validatsiyasi.
  • Task β€” Eloquent model. tasks jadvali bilan ishlaydi.
  • TaskPolicy β€” kim qaysi vazifani boshqarishi mumkinligini hal qiladi.

Va eng muhimi β€” qaysi xususiyat qaysi bobdan keladi. Bu xarita kapstonning yuragi:

Xususiyatlar xaritasi: har bir xususiyat (qo'shish, ro'yxat, holat, o'chirish, qidiruv, egalik, toast, jonli yangilanish, yuklanish, Alpine, tezlik, testlar) qaysi bobdan olingani

Reja β€” bu vaqt tejash

Ko'p boshlovchilar darrov kod yozishni boshlaydi va keyin "men nima qilayotgan edim?" deb adashadi. Avval rejani chizsangiz β€” qaysi komponent, qaysi metod, qaysi maydon kerakligini bilib turasiz. Bu β€” kod yozishdan ham muhimroq mahorat.


3-qadam. Tayyorgarlik: model, migration, factory

Ilova ma'lumot bilan ishlaydi, demak avval jadval kerak. Bu β€” Laravel'ning ishi (batafsil β€” Laravel kitobida), shuning uchun qisqacha o'tamiz.

Task model va migration

Model, migration va factory'ni bitta buyruq bilan yaratamiz:

php artisan make:model Task -mf

-m migration yaratadi, -f esa factory (sinov ma'lumoti uchun). Endi migration'ni to'ldiramiz:

// database/migrations/xxxx_create_tasks_table.php
public function up(): void
{
    Schema::create('tasks', function (Blueprint $table) {
        $table->id();
        $table->foreignId('user_id')->constrained()->cascadeOnDelete();
        $table->string('title');                          // vazifa nomi
        $table->string('status')->default('pending');     // pending | in_progress | done
        $table->timestamps();
    });
}

Diqqat qiling:

  • foreignId('user_id')->constrained() β€” har vazifa bir foydalanuvchiga tegishli. cascadeOnDelete() β€” foydalanuvchi o'chsa, uning vazifalari ham o'chadi. Bu egalikning asosi.
  • status β€” uch qiymatdan biri: pending (kutilmoqda), in_progress (jarayonda), done (bajarildi). Standart qiymat β€” pending.

Jadvalni yaratamiz:

php artisan migrate

Model'ni sozlash

Task modelida $fillable ni belgilaymiz (mass-assignment uchun) va user() munosabatini qo'shamiz:

// app/Models/Task.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Task extends Model
{
    use HasFactory;

    protected $fillable = ['title', 'status', 'user_id'];

    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }
}

belongsTo(User::class) β€” "vazifa foydalanuvchiga tegishli" degani. Bu bizga keyin $task->user orqali egasiga yetib borish imkonini beradi.

Factory β€” sinov ma'lumoti

Ilovani sinash uchun bir nechta namuna vazifa kerak. Factory shuni avtomatik yaratadi:

// database/factories/TaskFactory.php
public function definition(): array
{
    return [
        'user_id' => User::factory(),
        'title' => fake()->sentence(3),
        'status' => fake()->randomElement(['pending', 'in_progress', 'done']),
    ];
}

fake()->sentence(3) β€” uch so'zdan iborat tasodifiy gap. randomElement([...]) β€” uch holatdan birini tasodifan tanlaydi. Endi bir foydalanuvchi va unga 6 ta vazifa yaratish uchun tinker ochib:

php artisan tinker
>>> $user = App\Models\User::factory()->create(['name' => 'Ali']);
>>> App\Models\Task::factory()->count(6)->create(['user_id' => $user->id]);

Auth β€” kim kirgan?

Ilova foydalanuvchiga bog'liq, shuning uchun foydalanuvchi tizimga kirgan bo'lishi shart (login). Biz auth()->id() bilan joriy foydalanuvchining ID'sini olamiz. Login/registratsiya tizimini Livewire'da emas, Laravel'da (Breeze yoki Fortify bilan) o'rnatasiz β€” batafsil Laravel kitobida. Bu bobda biz foydalanuvchi allaqachon kirgan deb hisoblaymiz.


4-qadam. TaskForm β€” yangi vazifa formasi (Form Object)

Endi birinchi Livewire qismini yozamiz. Yangi vazifa qo'shish uchun Form Object (11-bob) ishlatamiz. Bu β€” maydon va validatsiyani toza, alohida klassda saqlash usuli.

Form Object yaratamiz:

php artisan livewire:form TaskForm

Bu app/Livewire/Forms/TaskForm.php faylini yaratadi. Uni to'ldiramiz:

// app/Livewire/Forms/TaskForm.php
<?php

namespace App\Livewire\Forms;

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

class TaskForm extends Form
{
    #[Validate('required|min:3|max:255', message: 'Vazifa nomi kamida 3 belgi bo\'lishi kerak')]
    public string $title = '';

    // yangi vazifa yaratish (joriy foydalanuvchi uchun)
    public function store(int $userId): Task
    {
        $this->validate();

        $task = Task::create([
            'title' => $this->title,
            'status' => 'pending',
            'user_id' => $userId,
        ]);

        $this->reset();

        return $task;
    }
}

Bu yerda nimalar bor:

  • #[Validate('required|min:3|max:255', message: ...)] β€” validatsiya qoidasi (10-bob). Vazifa nomi bo'sh bo'lmasligi, kamida 3, ko'pi bilan 255 belgi bo'lishi kerak. message: β€” o'zbekcha xato matni.
  • store(int $userId) β€” yangi vazifani yaratadi. validate() bilan tekshiradi, so'ng Task::create(...) bilan bazaga yozadi. Diqqat: user_id ni parametr orqali olamiz β€” Form Object o'zi "kim kirganini" bilmasligi kerak, buni komponent unga aytadi. Bu β€” toza arxitektura.
  • status => 'pending' β€” yangi vazifa doim "kutilmoqda" holatida tug'iladi.
  • $this->reset() β€” saqlangach formani tozalaydi, keyingi vazifa uchun bo'sh bo'ladi.

Nega store() Task qaytaradi?

return $task β€” yaratilgan vazifani qaytaradi. Hozir buni ishlatmasak ham, kelajakda kerak bo'lishi mumkin (masalan, "vazifa #5 qo'shildi" deb ID'sini ko'rsatish, yoki event'ga uzatish β€” 18-bob). Metod foydali narsa qaytarsa β€” yaxshi odat.


5-qadam. TaskBoard komponenti: ro'yxat, qidiruv, filtr

Endi bosh komponentni quramiz. Uni Single-File Component (SFC) sifatida yaratamiz (03-bob):

php artisan make:livewire task-board

Bu resources/views/components/⚑task-board.blade.php faylini yaratadi (v4 da SFC β€” standart format, ⚑ emoji esa real fayl prefiksi).

Ro'yxatni computed property'da o'qiymiz

Avval eng asosiysi β€” vazifalar ro'yxatini ekranga chiqaramiz. 15-bobdan bilamiz: bazadan o'qish kabi "og'ir" ishni render() da emas, computed property ichida qilamiz. U bir so'rov davomida keshlanadi va unset() qilinmaguncha qayta bazaga bormaydi.

{{-- resources/views/components/⚑task-board.blade.php --}}
<?php

use App\Models\Task;
use Livewire\Attributes\Computed;
use Livewire\Component;

new class extends Component
{
    #[Computed]
    public function tasks()
    {
        return Task::query()
            ->where('user_id', auth()->id())   // FAQAT joriy foydalanuvchi vazifalari
            ->latest()
            ->get();
    }
};
?>

<div>
    <ul>
        @forelse ($this->tasks as $task)
            <li wire:key="task-{{ $task->id }}">
                {{ $task->title }} β€” {{ $task->status }}
            </li>
        @empty
            <li>Hozircha vazifa yo'q.</li>
        @endforelse
    </ul>
</div>

Eng muhim qatorlar:

  • ->where('user_id', auth()->id()) β€” bu egalikning birinchi qatlami. auth()->id() joriy (tizimga kirgan) foydalanuvchining ID'sini beradi. Demak ro'yxatga faqat o'sha kishining vazifalari tushadi. Begona vazifalar hatto so'rovgacha ham yetib kelmaydi.
  • $this->tasks (qavssiz!) β€” computed property'ga shunday murojaat qilamiz (15-bob).
  • wire:key="task-{{ $task->id }}" β€” har qatorga noyob kalit (13-bob). Ro'yxat o'zgarganda (vazifa qo'shilganda/o'chganda) Livewire to'g'ri qatorni yangilaydi. Doimo zarur.

Qidiruv va filtrni qo'shamiz

Endi qidiruv (sarlavha bo'yicha) va filtr (holat bo'yicha) qo'shamiz. Ikkalasini ham URL'da saqlaymiz (#[Url]) β€” shunda foydalanuvchi havolani ulashsa yoki sahifani yangilasa, qidiruvi yo'qolmaydi (14, 19-bob):

use Livewire\Attributes\Url;

#[Url(as: 'q', history: true)]    // ?q=... ko'rinishida URL'da
public string $search = '';

#[Url]                            // ?filter=done ko'rinishida
public string $filter = 'all';    // all | pending | in_progress | done

#[Computed]
public function tasks()
{
    return Task::query()
        ->where('user_id', auth()->id())
        ->when($this->search, fn ($q) => $q->where('title', 'like', "%{$this->search}%"))
        ->when($this->filter !== 'all', fn ($q) => $q->where('status', $this->filter))
        ->latest()
        ->get();
}
  • #[Url(as: 'q', history: true)] β€” search qiymatini URL'da ?q=... deb saqlaydi. history: true β€” brauzer tarixiga yozadi, ya'ni "orqaga" tugmasi ishlaydi (19-bob).
  • ->when($shart, $callback) β€” agar $shart "haqiqiy" bo'lsa, callback'ni qo'llaydi. search bo'sh bo'lsa β€” barcha vazifalar; to'lgan bo'lsa β€” faqat mos sarlavhalar. Bitta so'rovda, ortiqcha ifsiz.
  • ->when($this->filter !== 'all', ...) β€” filtr all bo'lmasa, holatni cheklaydi.

Blade qismida:

{{-- qidiruv + filtr --}}
<div class="controls">
    <input type="search" wire:model.live.debounce.300ms="search" placeholder="Qidirish...">
    <select wire:model.live="filter">
        <option value="all">Hammasi</option>
        <option value="pending">Kutilmoqda</option>
        <option value="in_progress">Jarayonda</option>
        <option value="done">Bajarildi</option>
    </select>
</div>
  • wire:model.live.debounce.300ms="search" β€” yozayotganda jonli filtrlaydi, lekin har bosishda emas, yozish to'xtagandan 0,3 soniya keyin serverga yuboradi (14-bob). Bu keraksiz so'rovlarni kamaytiradi.
  • wire:model.live="filter" β€” <select> o'zgarganda darhol filtrlaydi.

URL filtrlash β€” egalikni almashtirmaydi

?filter=done URL'da ko'rinadi va foydalanuvchi uni qo'lda o'zgartirishi mumkin. Lekin bu xavfsiz, chunki tasks() ichidagi ->where('user_id', auth()->id()) doim ishlaydi β€” foydalanuvchi URL bilan o'ynaganda ham faqat o'z vazifalarini ko'radi. Filtr β€” qulaylik, egalik β€” himoya. Ikkisini aralashtirmang.


6-qadam. Holat o'zgartirish: action metodi

Endi qiziq qism β€” vazifani bir holatdan ikkinchisiga o'tkazish. Bu β€” oddiy action (07-bob). Komponentga setStatus metodini qo'shamiz:

public function setStatus(Task $task, string $status): void
{
    $task->update(['status' => $status]);

    unset($this->tasks);   // computed keshini tozalaymiz -> ro'yxat yangilanadi
}

Bu yerda:

  • Task $task β€” argument id'dan avtomatik to'liq Task modeliga aylanadi (Laravel'ning route-model binding mexanizmi kabi). Tugmadan id keladi, metodga tayyor model tushadi β€” qo'lda Task::find() shart emas.
  • $task->update(['status' => $status]) β€” holatni yangilaydi.
  • unset($this->tasks) β€” 15-bobdan eslang: computed keshlanadi. O'zgartirgach keshni majburan tozalaymiz, shunda ro'yxat bazadan qaytadan o'qiladi va yangi holat ko'rinadi.

Blade'da har vazifaga holat tugmalarini qo'shamiz. Faqat boshqa holatlarga o'tish tugmasi ko'rinadi (joriy holat tugmasi keraksiz):

<li wire:key="task-{{ $task->id }}" class="task status-{{ $task->status }}">
    <span class="title">{{ $task->title }}</span>

    <div class="actions">
        @if ($task->status !== 'pending')
            <button wire:click="setStatus({{ $task->id }}, 'pending')">Kutilmoqda</button>
        @endif
        @if ($task->status !== 'in_progress')
            <button wire:click="setStatus({{ $task->id }}, 'in_progress')">Jarayonda</button>
        @endif
        @if ($task->status !== 'done')
            <button wire:click="setStatus({{ $task->id }}, 'done')">Bajarildi</button>
        @endif
    </div>
</li>
  • wire:click="setStatus({{ $task->id }}, 'done')" β€” bosilganda setStatus metodiga vazifa id'sini va yangi holatni uzatadi (07-bob). Satr argument bitta tirnoq ichida.
  • class="status-{{ $task->status }}" β€” CSS sinfi holatga qarab o'zgaradi (status-pending, status-in_progress, status-done). Bu bizga rangli chiziqni berishga yordam beradi.

Hayotiy o'xshatish. Holat o'zgartirish β€” kir yuvish mashinasining tugmalari kabi. Mashina "to'xtagan", "yuvayotgan", "tugagan" holatlarda bo'ladi. Siz tugma bosib holatni o'zgartirasiz, mashina (server) holatni yodda saqlaydi va chiroq (UI) yangilanadi.


7-qadam. O'chirish: tasdiq + avtorizatsiya

O'chirish β€” eng xavfli amal: ma'lumot butunlay yo'qoladi. Shuning uchun ikki himoya qo'yamiz: foydalanuvchidan tasdiq so'raymiz (wire:confirm) va serverda huquqni tekshiramiz (authorize).

public function delete(Task $task): void
{
    $this->authorize('delete', $task);   // faqat o'z vazifasi

    $task->delete();

    unset($this->tasks);
}
  • $this->authorize('delete', $task) β€” bu TaskPolicy ning delete metodini chaqiradi (23-bob). Agar joriy foydalanuvchining bu vazifani o'chirishga huquqi bo'lmasa, Livewire so'rovni to'xtatadi (403 xato). Pastda Policy'ni yozamiz.

Blade'da:

<button wire:click="delete({{ $task->id }})" wire:confirm="Vazifa o'chirilsinmi?">
    O'chirish
</button>
  • wire:confirm="Vazifa o'chirilsinmi?" β€” tugma bosilganda brauzer tasdiq oynachasini ko'rsatadi (07-bob). "OK" bosilsa β€” delete ishlaydi; "Bekor" bosilsa β€” hech narsa bo'lmaydi.

TaskPolicy: kim nimani boshqaradi

authorize() ishlashi uchun Policy kerak (23-bob):

php artisan make:policy TaskPolicy --model=Task

Generatsiya qilingan faylni soddalashtiramiz β€” bizga faqat update va delete kerak:

// app/Policies/TaskPolicy.php
<?php

namespace App\Policies;

use App\Models\Task;
use App\Models\User;

class TaskPolicy
{
    // Foydalanuvchi faqat O'ZINING vazifasini boshqara oladi.
    public function update(User $user, Task $task): bool
    {
        return $user->id === $task->user_id;
    }

    public function delete(User $user, Task $task): bool
    {
        return $user->id === $task->user_id;
    }
}

Mantiq sodda: vazifaning user_id joriy foydalanuvchining id'siga teng bo'lsa β€” ruxsat, aks holda β€” yo'q. Ya'ni begona vazifani o'chirishga urinish 403 bilan rad etiladi.

Xavfsizlik β€” eng muhim qoida

wire:click="delete({{ $task->id }})" dagi id mijozdan keladi β€” uni o'zgartirib yuborish mumkin. Foydalanuvchi boshqa birovning vazifa id'sini yuborishga urinishi mumkin. Shuning uchun wire:confirm (mijoz tomonidagi qulaylik) himoya emas. Haqiqiy himoya β€” serverda authorize(). Public metod = ochiq eshik β€” har doim avtorizatsiya bilan qulflang (23-bob).

setStatus ga ham avtorizatsiya

O'chirish kabi, holat o'zgartirish ham boshqaning vazifasini o'zgartirmasligi kerak. setStatus ga ham authorize qo'shamiz:

public function setStatus(Task $task, string $status): void
{
    $this->authorize('update', $task);   // faqat o'z vazifasi

    $task->update(['status' => $status]);

    unset($this->tasks);
}

Endi har o'zgartiruvchi amal himoyalangan: o'qish where('user_id', ...) bilan, yozish authorize() bilan.


8-qadam. Events: toast bildirishnoma

Har muvaffaqiyatli amaldan keyin foydalanuvchiga "ish bajarildi" deb bildirish kerak. Biz buni event (18-bob) orqali "toast" (ekran burchagidagi suzuvchi xabar) bilan qilamiz.

Avval har amalda event yuboramiz:

public function addTask(): void
{
    $this->form->store(auth()->id());

    unset($this->tasks);
    $this->dispatch('task-saved', text: 'Vazifa qo\'shildi');
}

public function setStatus(Task $task, string $status): void
{
    $this->authorize('update', $task);
    $task->update(['status' => $status]);

    unset($this->tasks);
    $this->dispatch('task-saved', text: 'Holat yangilandi');
}

public function delete(Task $task): void
{
    $this->authorize('delete', $task);
    $task->delete();

    unset($this->tasks);
    $this->dispatch('task-saved', text: 'Vazifa o\'chirildi');
}
  • $this->dispatch('task-saved', text: '...') β€” task-saved nomli event'ni efirga chiqaradi, text nomli parametr bilan (18-bob).

Endi bu event'ni Alpine.js (22-bob) ushlab, toast ko'rsatadi. Blade'da <div> ichiga (ildiz elementga) qo'shamiz:

{{-- Toast: event'ni brauzerda ushlab, 3 soniya ko'rsatadi --}}
<div
    x-data="{ show: false, text: '' }"
    x-on:task-saved.window="text = $event.detail.text; show = true; setTimeout(() => show = false, 3000)"
    x-show="show"
    x-transition
    class="toast"
>
    <span x-text="text"></span>
</div>
  • x-on:task-saved.window="..." β€” Livewire event'lari window'ga tarqaladi, shuning uchun Alpine uni .window bilan eshitadi (18, 22-bob).
  • $event.detail.text β€” event bilan kelgan text parametri.
  • setTimeout(() => show = false, 3000) β€” 3 soniyadan keyin toast yashirinadi. Bu toza client-side ish β€” serverga so'rov yo'q.

Nega flash emas, event?

16-bobda biz session()->flash() ishlatgan edik. U sodda, lekin sahifa yangilanishida ko'rinadi. Event esa darhol, suzuvchi toast sifatida ko'rsatiladi β€” zamonaviyroq tajriba. Kapstonda biz event'ni tanlaymiz, chunki bu yerda real-vaqt muhim.

Hayotiy o'xshatish β€” restoran qo'ng'irog'i. Oshpaz taom tayyor bo'lganda qo'ng'iroq chaladi (dispatch). Ofitsiant uni eshitib (Alpine x-on), mijozga olib boradi (toast ko'rsatiladi). Oshpaz qaysi ofitsiant eshitishini bilmaydi β€” shunchaki qo'ng'iroq chaladi. Bo'shashgan bog'lanish.


9-qadam. Real-vaqt: wire:poll

Endi ilovani jonli qilamiz. Tasavvur qiling, siz telefonda vazifa qo'shdingiz, kompyuterda ham ochiq turibdi β€” kompyuterdagi ro'yxat o'z-o'zidan yangilanishi kerak. Buni eng oddiy usul β€” wire:poll (21-bob) bilan qilamiz.

Ildiz <div> ga bitta atribut qo'shamiz:

<div class="board" wire:poll.15s>
    {{-- ... butun ilova ... --}}
</div>
  • wire:poll.15s β€” Livewire har 15 soniyada komponentni jimgina yangilaydi (server'ga so'rov yuboradi, ro'yxatni qayta o'qiydi). Agar boshqa qurilmada o'zgarish bo'lsa β€” bu ekranda ko'rinadi (21-bob).

Nima uchun 15 soniya? Chunki har soniyada so'rov yuborish serverni ortiqcha yuklaydi. 15 soniya β€” "jonli" tuyulish va resurs tejash orasidagi yaxshi muvozanat. Tezroq kerak bo'lsa wire:poll.5s, sekinroq wire:poll.30s.

Poll yetarli, lekin chegarasi bor

wire:poll β€” eng oddiy real-vaqt usuli va ko'p ilovaga yetarli. Lekin u "haqiqiy" real-vaqt emas β€” o'zgarish 15 soniyagacha kechikadi va har poll so'rov yuboradi (foydalanuvchi ko'p bo'lsa β€” ko'p so'rov). Chinakam bir lahzali yangilanish kerak bo'lsa, Laravel Echo + broadcasting (WebSocket) ishlatiladi: server o'zgarishni darhol barcha ulangan mijozlarga "itarib" yuboradi. Bu β€” kuchli, lekin murakkabroq (Reverb yoki Pusher server kerak). Bu kitob doirasidan tashqari, lekin bilib qo'ying: kelajakda kerak bo'lsa, shu yo'l bor.

Mana butun ma'lumot oqimi β€” bir amal bosishdan ekrandagi natijagacha:

Ma'lumot oqimi: foydalanuvchi tugma bosadi, komponent metodi avtorizatsiya qiladi, baza yangilanadi, kesh tozalanadi, render qayta ishlaydi va UI yangilanadi


10-qadam. Yuklanish holatlari: spinner va disabled

Saqlash so'rovi serverga borib qaytguncha bir lahza o'tadi. Bu paytda ikki muammo bor: foydalanuvchi tugmani qayta-qayta bosib bir xil vazifani ikki marta qo'shishi mumkin, va u hech narsa bo'lmayotgandek his qilishi mumkin. Ikkalasini ham wire:loading (20-bob) hal qiladi.

<form wire:submit="addTask" class="add-form">
    <input type="text" wire:model="form.title" placeholder="Yangi vazifa...">
    <button type="submit" wire:loading.attr="disabled" wire:target="addTask">
        <span wire:loading.remove wire:target="addTask">Qo'shish</span>
        <span wire:loading wire:target="addTask">...</span>
    </button>
</form>
  • wire:loading.attr="disabled" β€” so'rov ketayotganda tugmani o'chiradi (qayta bosib bo'lmaydi).
  • wire:target="addTask" β€” bu loading faqat addTask so'rovida ishlaydi (boshqa amallarda emas) (20-bob).
  • <span wire:loading.remove>Qo'shish</span> va <span wire:loading>...</span> β€” so'rov ketayotganda "Qo'shish" yashirinib, "..." ko'rinadi. Foydalanuvchi nimadir bo'layotganini ko'radi.

Loading β€” kichik narsa, katta farq

Bu bir necha qator kod foydalanuvchi tajribasini sezilarli yaxshilaydi. Foydalanuvchi tugma bosgach darhol vizual javob ko'radi β€” ilova "tirik" his qilinadi. Ikki marta bosish muammosi ham yo'qoladi.


11-qadam. Alpine.js: toza client-side qism

Bizda allaqachon bitta Alpine ishi bor β€” toast (8-qadam). Endi yana bir foydali narsa qo'shamiz: har vazifaning amallar menyusini Alpine dropdown qilamiz. Bu β€” toza client-side (22-bob): ochilish/yopilish brauzerda, serverga so'rovsiz bo'ladi.

Vazifa kartochkasidagi amallarni dropdown ichiga olamiz:

<li wire:key="task-{{ $task->id }}" class="task status-{{ $task->status }}">
    <span class="title">{{ $task->title }}</span>

    <div class="actions" x-data="{ open: false }">
        <button type="button" x-on:click="open = !open">Amallar β–Ύ</button>

        <div x-show="open" x-on:click.outside="open = false" class="menu">
            @if ($task->status !== 'pending')
                <button wire:click="setStatus({{ $task->id }}, 'pending')">Kutilmoqda</button>
            @endif
            @if ($task->status !== 'in_progress')
                <button wire:click="setStatus({{ $task->id }}, 'in_progress')">Jarayonda</button>
            @endif
            @if ($task->status !== 'done')
                <button wire:click="setStatus({{ $task->id }}, 'done')">Bajarildi</button>
            @endif
            <button wire:click="delete({{ $task->id }})" wire:confirm="Vazifa o'chirilsinmi?">
                O'chirish
            </button>
        </div>
    </div>
</li>
  • x-data="{ open: false }" β€” har dropdown'ning o'z holati (22-bob).
  • x-on:click="open = !open" β€” tugma bosilganda menyu ochiladi/yopiladi. Serverga so'rov yo'q β€” bir lahzada.
  • x-on:click.outside="open = false" β€” menyudan tashqariga bosilsa, yopiladi. Bu toza Alpine'ning kuchi.

Qachon Alpine, qachon Livewire?

Oddiy qoida: server holatiga bog'liq bo'lmagan, faqat ko'rinish ishlarini Alpine qiladi (menyu ochish, modal, tab, accordion). Ma'lumot bilan ishlash (saqlash, o'chirish, validatsiya) Livewire'da bo'ladi. Bizning dropdownda ikkisi birga: menyuni Alpine ochadi (open), lekin "O'chirish" tugmasi Livewire'ga (wire:click) boradi. Ikkalasi mukammal hamkorlik qiladi (22-bob).


12-qadam. Tezlik: kesh, eager loading, paginate

Ilova ishlaydi. Endi uni tez qilamiz (25-bob). Uchta nafis tegishni qo'llaymiz.

1) Computed kesh β€” statistikani bepulga olish

Statistika (qaysi holatda nechta vazifa) uchun alohida so'rov yozmaymiz. Aksincha, bir marta o'qib, collection ustida sanaymiz:

#[Computed]
public function counts()
{
    $all = Task::where('user_id', auth()->id())->get();

    return [
        'all' => $all->count(),
        'pending' => $all->where('status', 'pending')->count(),
        'in_progress' => $all->where('status', 'in_progress')->count(),
        'done' => $all->where('status', 'done')->count(),
    ];
}

$all bir marta bazadan o'qiladi, keyin ->where(...)->count() bazaga qayta bormaydi β€” keshlangan collection'ning o'zini sanaydi. Computed bo'lgani uchun butun counts() ham bir so'rov davomida bir marta hisoblanadi (15, 25-bob).

Blade'da:

<p>Jami: {{ $this->counts['all'] }} Β·
   Kutilmoqda: {{ $this->counts['pending'] }} Β·
   Jarayonda: {{ $this->counts['in_progress'] }} Β·
   Bajarildi: {{ $this->counts['done'] }}</p>

Va unset ga counts ni ham qo'shamiz, har o'zgartirishda yangilansin:

unset($this->tasks, $this->counts);

2) Eager loading β€” N+1 muammosini oldini olish

Agar ro'yxatda har vazifaning egasini ko'rsatsangiz ($task->user->name), Livewire har vazifa uchun alohida so'rov yuboradi β€” bu N+1 muammosi (25-bob). Yechim β€” with('user') bilan oldindan yuklash:

return Task::query()
    ->with('user')                     // egalarni bitta so'rovda yuklaydi (eager loading)
    ->where('user_id', auth()->id())
    ->latest()
    ->get();

Bizning ilovada hamma vazifa bitta foydalanuvchiniki, shuning uchun bu shart emas β€” lekin egani ko'rsatsangiz, doim with() ishlating.

3) Pagination β€” uzun ro'yxat uchun

Vazifa 1000 ta bo'lsa, hammasini bir varaqda ko'rsatish sekin. Pagination (14-bob) bilan sahifalab beramiz:

use Livewire\WithPagination;

new class extends Component
{
    use WithPagination;

    #[Computed]
    public function tasks()
    {
        return Task::query()
            ->where('user_id', auth()->id())
            ->latest()
            ->paginate(10);            // har sahifada 10 ta
    }

    // qidiruv o'zgarganda 1-sahifaga qaytamiz
    public function updatedSearch()
    {
        $this->resetPage();
    }
};

Blade'da: {{ $this->tasks->links() }}. Bizning sodda misolda biz get() ni qoldiramiz, lekin ilova o'sganda paginate() ga o'tish β€” bitta so'z almashtirish.

Tezlik β€” o'lchab optimallashtir

25-bobda aytilgandek: avval ishlasin, keyin tez bo'lsin. Hammasini oldindan optimallashtirishga urinmang. Eng katta foyda β€” render() dan og'ir so'rovni computed'ga ko'chirish (biz buni qildik) va N+1 ni oldini olish (with). Qolganini muammo paydo bo'lganda hal qiling.


13-qadam. Test: ilovani avtomatik tekshiramiz

Ilova qo'lda ishlayapti. Lekin har o'zgartirishdan keyin hamma narsani qo'lda sinash β€” charchatadi va xatoga moyil. Avtomatik testlar (24-bob) buni bir buyruqda qiladi.

Test fayli yaratamiz:

php artisan make:test Lw26TaskBoardTest

Eng muhim CRUD va xavfsizlik testlarini yozamiz:

// tests/Feature/Lw26TaskBoardTest.php
<?php

namespace Tests\Feature;

use App\Models\Task;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use Tests\TestCase;

class Lw26TaskBoardTest extends TestCase
{
    use RefreshDatabase;

    public function test_user_sees_only_own_tasks(): void
    {
        $me = User::factory()->create();
        $other = User::factory()->create();

        Task::factory()->create(['user_id' => $me->id, 'title' => 'Mening vazifam']);
        Task::factory()->create(['user_id' => $other->id, 'title' => 'Begona vazifa']);

        $this->actingAs($me);

        Livewire::test('task-board')
            ->assertSee('Mening vazifam')
            ->assertDontSee('Begona vazifa');   // begona vazifa KO'RINMAYDI
    }

    public function test_can_add_task(): void
    {
        $me = User::factory()->create();
        $this->actingAs($me);

        Livewire::test('task-board')
            ->set('form.title', 'Sut sotib olish')
            ->call('addTask')
            ->assertHasNoErrors()
            ->assertSee('Sut sotib olish')
            ->assertDispatched('task-saved');   // toast event yuborildi

        $this->assertDatabaseHas('tasks', [
            'title' => 'Sut sotib olish',
            'user_id' => $me->id,
            'status' => 'pending',
        ]);
    }

    public function test_add_task_validates(): void
    {
        $me = User::factory()->create();
        $this->actingAs($me);

        Livewire::test('task-board')
            ->set('form.title', 'ab')           // 3 belgidan qisqa
            ->call('addTask')
            ->assertHasErrors('form.title');
    }

    public function test_cannot_delete_other_users_task(): void
    {
        $me = User::factory()->create();
        $other = User::factory()->create();
        $task = Task::factory()->create(['user_id' => $other->id]);
        $this->actingAs($me);

        Livewire::test('task-board')
            ->call('delete', $task->id)
            ->assertForbidden();                // 403 β€” begona vazifa o'chmaydi

        $this->assertDatabaseHas('tasks', ['id' => $task->id]);
    }
}

Har test bir narsani tekshiradi (24-bob):

  • test_user_sees_only_own_tasks β€” egalik ishlaydimi? O'z vazifam ko'rinadi, begonaniki yo'q.
  • test_can_add_task β€” qo'shish ishlaydimi? Bazaga yozildimi, event yuborildimi.
  • test_add_task_validates β€” validatsiya ishlaydimi? Qisqa nom xato beradimi.
  • test_cannot_delete_other_users_task β€” xavfsizlik ishlaydimi? Begona vazifani o'chirishga urinish 403.

Testlarni ishga tushiramiz:

php artisan test --filter=Lw26TaskBoardTest

Tekshirilgan natija

Bu testlar (yana to'rttasi β€” holat o'zgartirish, o'z vazifani o'chirish, qidiruv, holat filtri bilan birga, jami 9 ta) jonli loyihada ishga tushirildi va hammasi yashil (PASS) bo'ldi. Testlar bor ilovani o'zgartirish β€” qo'rqinchsiz: biror narsa buzilsa, test darrov aytadi.

Hayotiy o'xshatish. Testlar β€” bu binoning yong'in signalizatsiyasi. Siz uni har kuni sezmaysiz, lekin biror joyda olov chiqsa β€” darrov ogohlantiradi. Test yozmagan dasturchi β€” signalizatsiyasiz binoda yashagan kabi: yong'in bo'lganda juda kech bilib qoladi.


To'liq ishlaydigan kod

Endi hamma qismni birlashtiramiz. Mana to'liq, jonli loyihada tekshirilgan TaskBoard komponenti. Bu β€” sizning kapston loyihangizning yuragi.

Form Object

// app/Livewire/Forms/TaskForm.php
<?php

namespace App\Livewire\Forms;

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

class TaskForm extends Form
{
    #[Validate('required|min:3|max:255', message: 'Vazifa nomi kamida 3 belgi bo\'lishi kerak')]
    public string $title = '';

    public function store(int $userId): Task
    {
        $this->validate();

        $task = Task::create([
            'title' => $this->title,
            'status' => 'pending',
            'user_id' => $userId,
        ]);

        $this->reset();

        return $task;
    }
}

Asosiy komponent (SFC)

{{-- resources/views/components/⚑task-board.blade.php --}}
<?php

use App\Livewire\Forms\TaskForm;
use App\Models\Task;
use Livewire\Attributes\Computed;
use Livewire\Attributes\Url;
use Livewire\Component;

new class extends Component
{
    public TaskForm $form;            // yangi vazifa formasi (Form Object)

    #[Url(as: 'q', history: true)]    // qidiruv URL'da saqlanadi
    public string $search = '';

    #[Url]                            // ?filter=done kabi
    public string $filter = 'all';    // all | pending | in_progress | done

    // READ β€” faqat joriy foydalanuvchining vazifalari (computed, keshlanadi)
    #[Computed]
    public function tasks()
    {
        return Task::query()
            ->where('user_id', auth()->id())
            ->when($this->search, fn ($q) => $q->where('title', 'like', "%{$this->search}%"))
            ->when($this->filter !== 'all', fn ($q) => $q->where('status', $this->filter))
            ->latest()
            ->get();
    }

    // statistika (keshlangan collection ustida β€” bazaga qayta bormaydi)
    #[Computed]
    public function counts()
    {
        $all = Task::where('user_id', auth()->id())->get();

        return [
            'all' => $all->count(),
            'pending' => $all->where('status', 'pending')->count(),
            'in_progress' => $all->where('status', 'in_progress')->count(),
            'done' => $all->where('status', 'done')->count(),
        ];
    }

    // CREATE β€” yangi vazifa qo'shish
    public function addTask(): void
    {
        $this->form->store(auth()->id());

        unset($this->tasks, $this->counts);     // keshni tozalaymiz
        $this->dispatch('task-saved', text: 'Vazifa qo\'shildi');
    }

    // UPDATE β€” holatni o'zgartirish
    public function setStatus(Task $task, string $status): void
    {
        $this->authorize('update', $task);       // faqat o'z vazifasi

        $task->update(['status' => $status]);

        unset($this->tasks, $this->counts);
        $this->dispatch('task-saved', text: 'Holat yangilandi');
    }

    // DELETE β€” vazifani o'chirish
    public function delete(Task $task): void
    {
        $this->authorize('delete', $task);       // faqat o'z vazifasi

        $task->delete();

        unset($this->tasks, $this->counts);
        $this->dispatch('task-saved', text: 'Vazifa o\'chirildi');
    }
};
?>

<div class="board" wire:poll.15s>
    {{-- Toast: event'ni brauzerda ushlab, 3 soniya ko'rsatadi (Alpine) --}}
    <div
        x-data="{ show: false, text: '' }"
        x-on:task-saved.window="text = $event.detail.text; show = true; setTimeout(() => show = false, 3000)"
        x-show="show"
        x-transition
        class="toast"
    >
        <span x-text="text"></span>
    </div>

    {{-- Sarlavha + statistika --}}
    <header>
        <h1>Vazifalar taxtasi</h1>
        <p>Jami: {{ $this->counts['all'] }} Β·
           Kutilmoqda: {{ $this->counts['pending'] }} Β·
           Jarayonda: {{ $this->counts['in_progress'] }} Β·
           Bajarildi: {{ $this->counts['done'] }}</p>
    </header>

    {{-- Yangi vazifa qo'shish formasi --}}
    <form wire:submit="addTask" class="add-form">
        <input type="text" wire:model="form.title" placeholder="Yangi vazifa...">
        <button type="submit" wire:loading.attr="disabled" wire:target="addTask">
            <span wire:loading.remove wire:target="addTask">Qo'shish</span>
            <span wire:loading wire:target="addTask">...</span>
        </button>
    </form>
    @error('form.title') <p class="err">{{ $message }}</p> @enderror

    {{-- Qidiruv + filtr --}}
    <div class="controls">
        <input type="search" wire:model.live.debounce.300ms="search" placeholder="Qidirish...">
        <select wire:model.live="filter">
            <option value="all">Hammasi</option>
            <option value="pending">Kutilmoqda</option>
            <option value="in_progress">Jarayonda</option>
            <option value="done">Bajarildi</option>
        </select>
    </div>

    {{-- Vazifalar ro'yxati --}}
    <ul class="tasks">
        @forelse ($this->tasks as $task)
            <li wire:key="task-{{ $task->id }}" class="task status-{{ $task->status }}">
                <span class="title">{{ $task->title }}</span>

                <div class="actions" x-data="{ open: false }">
                    <button type="button" x-on:click="open = !open">Amallar β–Ύ</button>

                    <div x-show="open" x-on:click.outside="open = false" class="menu">
                        @if ($task->status !== 'pending')
                            <button wire:click="setStatus({{ $task->id }}, 'pending')">Kutilmoqda</button>
                        @endif
                        @if ($task->status !== 'in_progress')
                            <button wire:click="setStatus({{ $task->id }}, 'in_progress')">Jarayonda</button>
                        @endif
                        @if ($task->status !== 'done')
                            <button wire:click="setStatus({{ $task->id }}, 'done')">Bajarildi</button>
                        @endif
                        <button wire:click="delete({{ $task->id }})"
                                wire:confirm="Vazifa o'chirilsinmi?">O'chirish</button>
                    </div>
                </div>
            </li>
        @empty
            <li class="empty">
                @if ($this->search || $this->filter !== 'all')
                    Mos vazifa topilmadi.
                @else
                    Hozircha vazifa yo'q. Birinchisini qo'shing!
                @endif
            </li>
        @endforelse
    </ul>
</div>

Route

// routes/web.php
use Illuminate\Support\Facades\Route;

Route::livewire('/board', 'task-board');

Endi /board sahifasiga kirsangiz (tizimga kirgan holda), to'liq ishlaydigan vazifalar taxtasini ko'rasiz: qo'shasiz, holat o'zgartirasiz, qidirasiz, filtrlaysiz, o'chirasiz β€” hammasi qayta yuklanmasdan, jonli, va faqat o'z vazifalaringiz bilan.

Stillar haqida

Yuqorida biz CSS sinflarini (board, toast, task, status-done) qo'ydik, lekin CSS'ni yozmadik β€” bu kitob Livewire haqida, dizayn haqida emas. Real loyihada TailwindCSS yoki oddiy CSS bilan bularni bezatasiz: status-done yashil chiziq, toast o'ng yuqori burchakda suzuvchi quti va h.k. Logika tayyor β€” qolgani sizning ijodingiz.

Sinab ko'ring

  1. "Yangi vazifa" maydoniga 2 ta harf yozib "Qo'shish" bosing β€” qizil xato chiqadi (validatsiya).
  2. To'g'ri nom yozib qo'shing β€” vazifa ro'yxatda paydo bo'ladi, toast chiqadi, statistika yangilanadi.
  3. "Amallar" β†’ "Jarayonda" bosing β€” chiziq sariqqa o'zgaradi.
  4. Qidiruvga vazifa nomining bo'lagini yozing β€” ro'yxat jonli filtrlanadi.
  5. Filtrni "Bajarildi" ga o'zgartiring β€” faqat tugagan vazifalar qoladi, URL'da ?filter=done paydo bo'ladi.
  6. "O'chirish" bosing β€” tasdiq so'raydi, "OK" da vazifa yo'qoladi.
  7. Ikkinchi brauzer oynasida ham /board oching, birida vazifa qo'shing β€” 15 soniyada ikkinchisida ham paydo bo'ladi (wire:poll).

Yakuniy so'z: tabriklayman, siz buni qildingiz!

Agar siz shu yergacha yetib kelgan va kapston loyihani tushungan bo'lsangiz β€” siz endi Livewire dasturchisisiz. Bu shunchaki xushomad emas. Mana siz nimani egalladingiz:

Va eng muhimi β€” bularning hammasini bir butun ilovaga birlashtirishni o'rgandingiz. Bu β€” haqiqiy dasturchining mahorati.

Keyingi qadamlar

Bilim β€” to'xtamaslik bilan o'sadi. Mana yo'l xaritangiz:

  • Loyihani kengaytiring. Pastdagi amaliy mashqlar aynan shuning uchun β€” vazifaga muddat (deadline), prioritet, kategoriya qo'shing. Har bir qo'shimcha β€” sizning portfelingiz.
  • Laravel ekotizimini chuqurlashtiring. Livewire β€” Laravel ustida turadi. Eloquent munosabatlari, queue, scheduler, notifications β€” bularni Laravel kitobida o'rganing.
  • Chinakam real-vaqt. wire:poll o'rniga Laravel Echo + broadcasting (Reverb yoki Pusher) bilan WebSocket real-vaqtni sinab ko'ring β€” vazifa qo'shilishi bir lahzada barcha qurilmalarda ko'rinadi.
  • Deploy qiling. Ilovangizni internetga chiqaring (25-bob). Birovga ko'rsata olmagan kod β€” yarim ish. Forge, Vapor yoki oddiy VPS β€” tanlov sizniki.
  • Dizayn. TailwindCSS bilan ilovangizni chiroyli qiling. Kichik dizayn β€” katta taassurot.

So'nggi so'z. Har bir buyuk dasturchi bir paytlar "Hisob: 0" tugmasini bosib hayratlangan boshlovchi edi. Siz o'sha yo'ldan o'tdingiz. Endi g'isht ham, uy qurish ham qo'lingizdan keladi. Eng yaxshi o'rganish β€” qurish. Bu kapstondan keyin to'xtamang: o'z g'oyangizni oling va uni hayotga tatbiq eting. Omad tilaymiz β€” va yodda tuting: siz buni qila olasiz, chunki allaqachon qildingiz.


Xulosa

  • Kapston β€” kitobning butun bilimini bitta real ilovaga birlashtirdi: "Vazifalar taxtasi" β€” qo'shish, holat o'zgartirish, qidiruv, filtr, o'chirish, real-vaqt va egalik.
  • Arxitektura: bitta TaskBoard komponenti + TaskForm Form Object + Task model + TaskPolicy. Avval rejani chizing, keyin kod yozing.
  • READ: ro'yxatni #[Computed] da o'qing, ->where('user_id', auth()->id()) bilan egalikni ta'minlang, har qatorga wire:key qo'ying. O'zgartirgach unset bilan keshni tozalang.
  • UPDATE/DELETE: action metodlari (setStatus, delete). Argumentdagi Task $task id'dan avtomatik modelga aylanadi. Har o'zgartiruvchi amalda $this->authorize(...) β€” mijozdan kelgan id'ga ishonmang.
  • Qidiruv/filtr: #[Url] bilan URL'da saqlang, .live.debounce bilan so'rovlarni kamaytiring; when() bilan shartli filtr.
  • Events: dispatch('task-saved', text: ...) yuboring, Alpine x-on:...window bilan toast ko'rsating β€” toza, suzuvchi bildirishnoma.
  • Real-vaqt: wire:poll.15s β€” eng oddiy jonli yangilanish; chinakam bir lahzali kerak bo'lsa β€” Echo/broadcasting.
  • Sayqal: wire:loading (spinner + disabled), Alpine dropdown, computed kesh, eager loading, paginate β€” har biri kichik, lekin tajribani sezilarli yaxshilaydi.
  • Testlar: Livewire::test('task-board') bilan CRUD va xavfsizlikni avtomatik tekshiring β€” ishonch bilan o'zgartirish uchun.

Amaliy mashqlar

Bu mashqlar β€” sizning kapstoningizni kengaytirish. Har biri sizni mustaqil dasturchiga yaqinlashtiradi. Yechim berilmaydi β€” lekin siz buni endi uddalaysiz.

  1. Muddat (deadline) qo'shing (oson). tasks jadvaliga due_date (sana) ustun qo'shing (migration + $fillable). Formaga <input type="date" wire:model="form.dueDate"> qo'shing, TaskForm ga maydon va validatsiya (nullable|date). Ro'yxatda muddatni ko'rsating va muddati o'tgan vazifalarni qizil qiling (@if ($task->due_date < now()) ... @endif).

  2. Prioritet qo'shing (oson–o'rta). priority ustun (low | medium | high). Formada <select>, ro'yxatda rangli nishon. So'ng ro'yxatni avval prioritet, keyin sana bo'yicha saralang (->orderBy('priority')->latest()).

  3. Kategoriya/teglar (o'rta). Yangi Category model va tasks.category_id. Forma'da kategoriya tanlash, yuqorida kategoriya bo'yicha filtr (yana bitta #[Url] xususiyat). Eager loading'ni unutmang (->with('category')) β€” N+1 ni oldini oling (25-bob).

  4. Inline tahrirlash (o'rta–qiyin). Vazifa nomini bosganda u input'ga aylansin (modal ochilmasin). editingId xususiyati va @if ($editingId === $task->id) bilan o'sha qatorda input ko'rsating; Enter bosilganda saqlang (wire:keydown.enter). Saqlashda authorize('update', ...) ni unutmang.

  5. Drag-drop bilan tartiblash (qiyin, ilhom uchun). Vazifalarni sichqoncha bilan sudrab tartibini o'zgartirish β€” bu kuchli xususiyat. G'oya: tasks ga position (tartib raqami) ustun qo'shing. Alpine yoki SortableJS kutubxonasi bilan client-side sudrash qiling, tartib o'zgarganda $wire orqali yangi tartibni serverga yuboring va position ustunini yangilang (22-bob). Bu β€” Trello'ning yuragi. Murakkab, lekin qila olasiz.

  6. Chinakam real-vaqt (qiyin). wire:poll ni Laravel Echo + Reverb bilan almashtiring. Vazifa qo'shilganda/o'zgarganda broadcast event yuboring, mijoz uni darhol eshitib ro'yxatni yangilasin. Bu β€” kitobdan tashqari, lekin keyingi katta qadamingiz.


⬅️ Oldingi: 25 β€” Performance va deploy Β· 🏠 Kitob boshi