Stavba Průzkumníku anatomie: Dynamické načítání souborů v Livewire
Detailní pohled na implementaci komponenty AnatomyExplorer. Zjistěte, jak v reálném čase filtrovat soubory pomocí wire:model.live s debouncingem a jak bezpečně předávat data ze služeb do šablony.
Stavba reálné komponenty: AnatomyExplorer v naší aplikaci
Jedním z nejvýraznějších prvků platformy agy-laravel je živý interaktivní prohlížeč kódu na stránce /anatomie. Umožňuje studentům prohledávat desítky souborů projektu, filtrovat je podle kategorií (foundation, core, views, database, deployment) a v reálném čase si prohlížet jejich syntaxi a řádkování.
Tato funkce je celá postavena na jediné komponentě: app/Livewire/AnatomyExplorer.php.
1. Reaktivní obousměrný binding: wire:model.live
V tradičním Livewire 2 se hodnota vstupního pole odesílala na server až při ztrátě focusu (událost change nebo blur). V Livewire 3 máme direktivu wire:model.live, která synchronizuje hodnotu na server okamžitě při psaní (událost input).
Pokud by se však HTTP požadavek odesílal po každém stisknutém znaku, při rychlém psaní by došlo k zahlcení sítě a serveru. Proto v šabloně resources/views/livewire/anatomy-explorer.blade.php využíváme modifikátor debounce:
<input type="text"
wire:model.live.debounce.250ms="search"
placeholder="Hledat v souborech...">
Modifikátor .debounce.250ms odloží odeslání požadavku na server o 250 milisekund od okamžiku, kdy uživatel naposledy stiskl klávesu. Pokud uživatel píše plynule, odešle se pouze jediný požadavek na konci psaní.
2. Prevence „Dehydration Overhead“: Co ukládat do stavu a co ne
Častou chybou začátečníků v Livewire je ukládání velkých kolekcí nebo celých modelů do veřejných vlastností:
// ❌ ŠPATNĚ: Celý katalog a obsah aktivního souboru by se serializoval do JSON snapshotu!
public array $allCatalog;
public AnatomyFileData $activeFile;
Pokud byste to udělali, klientský JSON snapshot by měl desítky až stovky kilobajtů a při každém kliknutí by se přenášel tam a zpět.
Správné řešení použité v naší třídě AnatomyExplorer:
- Do stavu komponenty ukládáme pouze minimální primitivní identifikátory:
public string $selectedPath = 'bootstrap/app.php'; public string $categoryFilter = 'all'; public string $search = ''; - Objemná data (filtrovaný seznam souborů a obsah aktivního souboru) načítáme až v metodě
render(AnatomyInspectorService $inspectorService)a předáváme je jako parametry přímo do Blade view. - Tím pádem data existují pouze v paměti během generování HTML a klientský snapshot zůstává mikroskopický (pouhých pár bajtů pro tři řetězce).
3. Magické akce $set a okamžitý reset filtrů
V šabloně si všimněte křížku pro vymazání hledaného textu:
<button type="button" wire:click="$set('search', '')">✕</button>
Namísto vytváření zbytečné metody public function clearSearch(): void { $this->search = ''; } v PHP třídě využíváme vestavěnou magickou akci Livewire $set('nazevVlastnosti', hodnota). Ta šetří kód a udržuje PHP třídu čistou a soustředěnou na byznys logiku.
// app/Livewire/AnatomyExplorer.php
class AnatomyExplorer extends Component
{
// Do stavu ukládáme výhradně primitivní skaláry (minimální payload snapshotu)
public string $selectedPath = 'bootstrap/app.php';
public string $categoryFilter = 'all';
public string $search = '';
public function selectFile(string $path): void
{
$this->selectedPath = $path;
}
public function setCategory(string $category): void
{
$this->categoryFilter = $category;
}
// Těžká data (obsah souborů, filtrování) generujeme až v render()
public function render(AnatomyInspectorService $inspectorService): View
{
$allCatalog = $inspectorService->getCuratedProjectCatalog();
$filteredCatalog = array_filter($allCatalog, function (AnatomyFileNodeData $item): bool {
if ($this->categoryFilter !== 'all' && $item->category !== $this->categoryFilter) {
return false;
}
if (trim($this->search) !== '') {
$term = mb_strtolower(trim($this->search));
return str_contains(mb_strtolower($item->path), $term)
|| str_contains(mb_strtolower($item->description), $term);
}
return true;
});
$activeFile = $inspectorService->inspectFile($this->selectedPath);
return view('livewire.anatomy-explorer', [
'catalog' => array_values($filteredCatalog),
'allCatalog' => $allCatalog,
'activeFile' => $activeFile,
]);
}
}
🎯 Ověření znalostí z lekce
Kvíz otestuje pochopení architektury a reálného kódu probíraného v této lekci.
Proč v AnatomyExplorer nepředáváme $activeFile jako veřejnou (public) vlastnost komponenty, ale vracíme ji z metody render()?
Jaký je přesný účinek modifikátoru .debounce.250ms v direktivě wire:model.live.debounce.250ms="search"?
Jak funguje magická akce wire:click="$set('search', '')" v Blade šabloně?
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | namespace App\Livewire; |
| 6 | |
| 7 | use App\Data\AnatomyFileNodeData; |
| 8 | use App\Services\AnatomyInspectorService; |
| 9 | use Illuminate\Contracts\View\View; |
| 10 | use Livewire\Component; |
| 11 | |
| 12 | class AnatomyExplorer extends Component |
| 13 | { |
| 14 | public string $selectedPath = 'bootstrap/app.php'; |
| 15 | |
| 16 | public string $categoryFilter = 'all'; |
| 17 | |
| 18 | public string $search = ''; |
| 19 | |
| 20 | public function mount(?string $initialPath = null): void |
| 21 | { |
| 22 | if ($initialPath !== null && trim($initialPath) !== '') { |
| 23 | $this->selectedPath = trim($initialPath); |
| 24 | } |
| 25 | } |
| 26 | |
| 27 | public function selectFile(string $path): void |
| 28 | { |
| 29 | $this->selectedPath = $path; |
| 30 | } |
| 31 | |
| 32 | public function setCategory(string $category): void |
| 33 | { |
| 34 | $this->categoryFilter = $category; |
| 35 | } |
| 36 | |
| 37 | public function render(AnatomyInspectorService $inspectorService): View |
| 38 | { |
| 39 | /** @var list<AnatomyFileNodeData> $allCatalog */ |
| 40 | $allCatalog = $inspectorService->getCuratedProjectCatalog(); |
| 41 | |
| 42 | $filteredCatalog = array_filter($allCatalog, function (AnatomyFileNodeData $item): bool { |
| 43 | if ($this->categoryFilter !== 'all' && $item->category !== $this->categoryFilter) { |
| 44 | return false; |
| 45 | } |
| 46 | |
| 47 | if (trim($this->search) !== '') { |
| 48 | $term = mb_strtolower(trim($this->search)); |
| 49 | $pathMatch = str_contains(mb_strtolower($item->path), $term); |
| 50 | $descMatch = str_contains(mb_strtolower($item->description), $term); |
| 51 | $catMatch = str_contains(mb_strtolower($item->category), $term); |
| 52 | |
| 53 | return $pathMatch || $descMatch || $catMatch; |
| 54 | } |
| 55 | |
| 56 | return true; |
| 57 | }); |
| 58 | |
| 59 | $activeFile = $inspectorService->inspectFile($this->selectedPath); |
| 60 | |
| 61 | return view('livewire.anatomy-explorer', [ |
| 62 | 'catalog' => array_values($filteredCatalog), |
| 63 | 'allCatalog' => $allCatalog, |
| 64 | 'activeFile' => $activeFile, |
| 65 | ]); |
| 66 | } |
| 67 | } |
| 68 | |