Vstoupíme do světa Laravelu a Livewire, kde se naučíme, jak vytvořit Livewire komponentu pro CRUD operace. Laravel je jedním z nejpopulárnějších PHP frameworků a Livewire je jeho výkonný doplněk, který umožňuje vytvářet moderní, dynamické rozhraní přímo v Laravelu. CRUD operace - vytváření (Create), čtení (Read), úprava (Update) a mazání (Delete) - jsou základními stavebními kameny jakékoliv aplikace. V tomto článku se zaměříme na to, jak vytvořit Livewire komponentu v Laravelu, která umožňuje provádět tyto základní operace. Navíc se naučíme, jak tyto komponenty testovat, aby byla naše aplikace robustní a spolehlivá. Připravte se na cestu plnou kódu, inovací a nejlepších postupů v oblasti vývoje webových aplikací.
Co je Livewire a Laravel
Než se pustíme do samotného návodu, kde si ukážeme jak krok za krokem vytvořít logiku v Livewire komponentě, je dobré se seznámit s oběma frameworky. Možná se mezi Vámi najdou nováčci, kteří slyší o Laravelu nebo Livewire poprvé.
Laravel
Laravel je open-source PHP framework, který je vysoce ceněný pro svou elegantní syntaxi a schopnost usnadnit vývoj webových aplikací tím, že poskytuje nástroje pro běžné úkoly, jako je routování, autentizace, relace a cachování. Laravel je postaven na komponentách Symfony a jeho zdrojový kód je hostován na GitHubu.
Livewire
Livewire na druhé straně je plnohodnotný framework pro Laravel, který umožňuje vytvářet dynamické rozhraní jednoduše a bez nutnosti psaní JavaScriptu. Livewire využívá koncept komponent, které jsou podobné jako v Reactu nebo Vue, ale jsou napsané v čistém PHP. To znamená, že můžete vytvářet komplexní, reaktivní aplikace s jediným jazykem.
Společně Laravel a Livewire tvoří silnou kombinaci pro vývoj webových aplikací. Laravel poskytuje základní infrastrukturu a Livewire přidává dynamickou vrstvu, která umožňuje vývojářům vytvářet interaktivní uživatelské rozhraní s minimem kódu a složitosti.
Úvod k naší ukázce
Abyste lépe pochopili ukázku, nejdříve si projděte dříve vytvořený článek na práci s CRUD operacemi v samotném Laravelu, kde se kromě základních infromací (např. co je crud) dozvíte i části kódu, které jsou předpokladem pro další kroky v tomto návodu (migrace, model, akce, apod.). V tomto návodu se pak předpokládá, že máte již nainstalovaný framework Laravel spolu s Laravel Livewire.
Pro testování Livewire komponenty je pak také použit balíček Pest, který jsme v předchozím návodu také využili.
Vytvoření Livewire komponenty
Vytvoření Livewire komponenty v Laravelu je proces, který je jednoduchý a přímočarý. Začneme tím, že otevřeme terminál a navigujeme do kořenového adresáře našeho Laravel projektu. Zde můžeme vytvořit novou Livewire komponentu pomocí následujícího příkazu:
php artisan make:livewire ComponentName
Nahraďte "ComponentName" názvem vaší komponenty. Tento příkaz vytvoří dvě nové soubory: třídu komponenty a přidružený Blade view/šablonu. Třída komponenty je umístěna v "app/Http/Livewire" a Blade view je v "resources/views/livewire".
Třída komponenty je místo, kde definujete veškerou logiku komponenty. Může obsahovat veřejné vlastnosti, které jsou automaticky synchronizovány mezi backendem a frontendem, a metody, které mohou reagovat na události uživatele.
Blade view je místo, kde definujete HTML rozhraní komponenty. Můžete zde použít jakýkoliv platný Blade syntax/direktivu a můžete také přistupovat k veřejným vlastnostem a metodám třídy komponenty.
Vytvoření Livewire komponenty je tedy otázkou vytvoření těchto dvou souborů a definování potřebné logiky a rozhraní. Jakmile máte tyto soubory připravené, můžete začít používat svou Livewire komponentu v jakémkoliv Laravel view pomocí Livewire direktivy:
@livewire('component-name')
Opět nahraďte "component-name" názvem vaší komponenty. Tato direktiva vloží vaši Livewire komponentu do Vaší šablony.
Ukázka Laravel Livewire komponenty pro CRUD operace
Naším cílem je však vytvořit komponentu, která bude zpracovávat data z tabulky "example_items", kterou jsme si vytvořili dle předchozího návodu. Pojďme se tedy podívat, jak by mohl vypadat kód ve třídě komponenty a v Blade šabloně.
Třída Livewire komponenty ("app/Http/Livewire/Examples")
<?php
namespace App\Http\Livewire\Examples;
use App\Actions\Examples\ExampleItems\CreateExampleItemAction;
use App\Actions\Examples\ExampleItems\RemoveExampleItemAction;
use App\Actions\Examples\ExampleItems\UpdateExampleItemAction;
use App\Enums\Examples\ExampleItemType;
use App\Models\Examples\ExampleItem;
use Illuminate\Validation\Rules\Enum;
use Livewire\Component;
use Livewire\WithPagination;
class ExampleItemManager extends Component
{
use WithPagination;
public $q;
public $sortBy = 'created_at';
public $sortAsc = false;
public $item;
public $exampleItemTypes;
public $confirmingItemDeletion = false;
public $confirmingItemAdd = false;
protected $queryString = [
'q' => ['except' => ''],
'sortBy' => ['except' => 'created_at'],
'sortAsc' => ['except' => false],
];
public function mount()
{
$this->exampleItemTypes = ExampleItemType::all();
}
public function render()
{
$items = ExampleItem::when($this->q, function($query) {
return $query->where(function( $query) {
$query->where('title', 'like', '%' . $this->q . '%');
});
})
->orderBy($this->sortBy, $this->sortAsc ? 'ASC' : 'DESC')
->paginate(20);
return view('livewire.examples.example-item-manager', [
'items' => $items,
]);
}
public function updatingQ()
{
$this->resetPage();
}
public function sortBy($field)
{
if( $field == $this->sortBy) {
$this->sortAsc = !$this->sortAsc;
}
$this->sortBy = $field;
}
public function confirmItemAdd()
{
$this->reset(['item']);
$this->confirmingItemAdd = true;
}
public function confirmItemEdit(ExampleItem $exampleItem)
{
$this->resetErrorBag();
$this->item = $exampleItem->toArray();
$this->confirmingItemAdd = true;
}
public function saveItem()
{
$validatedData = $this->validate([
'item.title' => 'required|string|max:255',
'item.body' => 'nullable|string',
'item.is_active' => 'nullable|boolean',
'item.type' => [
'required',
'string',
'max:8',
new Enum(ExampleItemType::class),
],
]);
if(isset($this->item['id'])) {
(new UpdateExampleItemAction())->execute(
ExampleItem::find($this->item['id']),
$validatedData['item']
);
$this->dispatchBrowserEvent('alert',[
'type'=>'success',
'message'=> __('Example Item was successfully updated.')
]);
} else {
(new CreateExampleItemAction())->execute($validatedData['item']);
$this->dispatchBrowserEvent('alert',[
'type'=>'success',
'message'=> __('Example Item was successfully created.')
]);
}
$this->reset(['item']);
$this->confirmingItemAdd = false;
}
public function confirmItemDeletion($id)
{
$this->confirmingItemDeletion = $id;
}
public function deleteItem(ExampleItem $exampleItem)
{
(new RemoveExampleItemAction())->execute($exampleItem);
$this->confirmingItemDeletion = false;
$this->dispatchBrowserEvent('alert',[
'type'=>'success',
'message'=> __('Example Item was successfully removed.')
]);
}
}
Blade view pro Livewire komponentu ("resources/views/livewire/examples")
<div class="p-4 border-b border-gray-200">
<div class="mt-2 text-2xl flex justify-between">
<div class="text-gray-900 dark:text-gray-100">{{ __('Example Item Manager') }}</div>
<div class="mr-2">
<x-button wire:click="confirmItemAdd" class="bg-blue-500 hover:bg-blue-700">
{{ __('Add New Item') }}
</x-button>
</div>
</div>
<div class="mt-6">
<div class="flex justify-between">
<div class="">
<input wire:model.debounce.500ms="q" type="search" placeholder="{{ __('Search') }}" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" />
</div>
</div>
<div class="flex flex-col">
<div class="overflow-x-auto sm:-mx-6 lg:-mx-8">
<div class="inline-block min-w-full py-2 sm:px-6 lg:px-8">
<div class="overflow-hidden">
<table class="min-w-full text-left text-sm font-light">
<thead class="border-b font-medium dark:border-neutral-500 dark:text-gray-100">
<tr>
<th scope="col" class="px-6 py-4">
<div class="flex items-center">
<button wire:click="sortBy('id')">{{ __('ID') }}</button>
@if($sortBy=='id')
@if($sortAsc)
<span class="ml-2"><i class="fas fa-sort-up"></i></span>
@else
<span class="ml-2"><i class="fas fa-sort-down"></i></span>
@endif
@endif
</div>
</th>
<th scope="col" class="px-6 py-4">
<div class="flex items-center">
<button wire:click="sortBy('title')">{{ __('Title') }}</button>
@if($sortBy=='title')
@if($sortAsc)
<span class="ml-2"><i class="fas fa-sort-up"></i></span>
@else
<span class="ml-2"><i class="fas fa-sort-down"></i></span>
@endif
@endif
</div>
</th>
<th scope="col" class="px-6 py-4">
<div class="flex items-center">
<button wire:click="sortBy('is_active')">{{ __('Is active') }}</button>
@if($sortBy=='is_active')
@if($sortAsc)
<span class="ml-2"><i class="fas fa-sort-up"></i></span>
@else
<span class="ml-2"><i class="fas fa-sort-down"></i></span>
@endif
@endif
</div>
</th>
<th scope="col" class="px-6 py-4">
<div class="flex items-center">
<button wire:click="sortBy('type')">{{ __('Type') }}</button>
@if($sortBy=='type')
@if($sortAsc)
<span class="ml-2"><i class="fas fa-sort-up"></i></span>
@else
<span class="ml-2"><i class="fas fa-sort-down"></i></span>
@endif
@endif
</div>
</th>
<th scope="col" class="px-6 py-4">
<div class="flex items-center">
<button wire:click="sortBy('created_at')">{{ __('Created at') }}</button>
@if($sortBy=='created_at')
@if($sortAsc)
<span class="ml-2"><i class="fas fa-sort-up"></i></span>
@else
<span class="ml-2"><i class="fas fa-sort-down"></i></span>
@endif
@endif
</div>
</th>
<th scope="col" class="px-6 py-4">
<div class="flex items-center">
<button wire:click="sortBy('updated_at')">{{ __('Updated at') }}</button>
@if($sortBy=='updated_at')
@if($sortAsc)
<span class="ml-2"><i class="fas fa-sort-up"></i></span>
@else
<span class="ml-2"><i class="fas fa-sort-down"></i></span>
@endif
@endif
</div>
</th>
<th scope="col" class="px-6 py-4">
{{ __('Actions') }}
</th>
</tr>
</thead>
<tbody>
@forelse($items as $item)
<tr class="border-b transition duration-300 ease-in-out dark:text-gray-100 hover:bg-neutral-100 dark:border-neutral-500 dark:hover:bg-neutral-600 dark:hover:text-gray-200">
<td class="whitespace-nowrap px-6 py-4 font-medium">{{ $item->id }}</td>
<td class="whitespace-nowrap px-6 py-4">{{ $item->title }}</td>
<td class="whitespace-nowrap px-6 py-4">{{ $item->is_active ? __('Yes') : __('No') }}</td>
<td class="whitespace-nowrap px-6 py-4">{{ $item->type->value }}</td>
<td class="whitespace-nowrap px-6 py-4">{{ $item->created_at->diffForHumans() }}</td>
<td class="whitespace-nowrap px-6 py-4">{{ $item->updted_at?->diffForHumans() }}</td>
<td class="whitespace-nowrap px-6 py-4">
<a href="{{ route('example-items.show', ['language' => app()->getLocale(), 'example_item' => $item->id]) }}" class="inline-flex items-center px-4 py-2 bg-gray-800 dark:bg-gray-200 border border-transparent rounded-md font-semibold text-xs text-white dark:text-gray-800 uppercase tracking-widest hover:bg-gray-700 dark:hover:bg-white focus:bg-gray-700 dark:focus:bg-white active:bg-gray-900 dark:active:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 transition ease-in-out duration-150">
{{ __('Show') }}
</a>
<x-button wire:click="confirmItemEdit( {{ $item->id }})" class="inline-flex items-center px-4 py-2 bg-gray-800 dark:bg-gray-200 border border-transparent rounded-md font-semibold text-xs text-white dark:text-gray-800 uppercase tracking-widest hover:bg-gray-700 dark:hover:bg-white focus:bg-gray-700 dark:focus:bg-white active:bg-gray-900 dark:active:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 transition ease-in-out duration-150">
{{ __('Edit') }}
</x-button>
<x-danger-button wire:click="confirmItemDeletion({{ $item->id }})" wire:loading.attr="disabled">
{{ __('Delete') }}
</x-danger-button>
</td>
</tr>
@empty
<tr class="border-b transition duration-300 ease-in-out dark:text-gray-100 hover:bg-neutral-100 dark:border-neutral-500 dark:hover:bg-neutral-600 dark:hover:text-gray-200">
<td class="whitespace-nowrap px-6 py-4 font-medium">
{{ __('No item found') }}
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<div class="mt-4">
{{ $items->links() }}
</div>
<x-dialog-modal wire:model="confirmingItemAdd">
<x-slot name="title">
{{ isset( $this->item['id']) ? 'Edit Item' : 'Add Item'}}
</x-slot>
<x-slot name="content">
<div class="">
<x-label for="title" value="{{ __('Title') }}" />
<x-input id="title" type="text" wire:model.defer="item.title" class="mt-2 bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" />
<x-input-error for="item.title" class="mt-2" />
</div>
<div class="mt-4">
<x-label for="body" value="{{ __('Body') }}" />
<textarea wire:model.defer="item.body"
id="body"
class="block mt-2 p-2.5 w-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
rows="4"></textarea>
<x-input-error for="item.body" class="mt-2" />
</div>
<div class="mt-4">
<div class="flex items-center justify-start">
<x-input wire:model.defer="item.is_active" id="is-active" type="checkbox" value="" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600" />
<x-label for="is-active" value="{{ __('Is active') }}" class="ml-2" />
</div>
<x-input-error for="item.is_active" class="mt-2" />
</div>
<div class="mt-4">
<x-label for="type" value="{{ __('Type') }}" />
<select wire:model.defer="item.type" id="type" class="mt-2 bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
<option selected>{{ __('Choose a type') }}</option>
@foreach ($exampleItemTypes as $exampleItemType)
<option value="{{ $exampleItemType['value'] }}">
{{ $exampleItemType['name'] }}
</option>
@endforeach
</select>
<x-input-error for="item.type" class="mt-2" />
</div>
</x-slot>
<x-slot name="footer">
<x-secondary-button wire:click="$set('confirmingItemAdd', false)" wire:loading.attr="disabled">
{{ __('Nevermind') }}
</x-secondary-button>
<x-button class="ml-2 bg-blue-500 hover:bg-blue-700" wire:click="saveItem()" wire:loading.attr="disabled">
{{ __('Save') }}
</x-button>
</x-slot>
</x-dialog-modal>
<x-confirmation-modal wire:model="confirmingItemDeletion">
<x-slot name="title">
{{ __('Delete Item') }}
</x-slot>
<x-slot name="content">
{{ __('Are you sure you want to delete Item? ') }}
</x-slot>
<x-slot name="footer">
<x-secondary-button wire:click="$set('confirmingItemDeletion', false)" wire:loading.attr="disabled">
{{ __('Nevermind') }}
</x-secondary-button>
<x-danger-button class="ml-2" wire:click="deleteItem({{ $confirmingItemDeletion }})" wire:loading.attr="disabled">
{{ __('Delete') }}
</x-danger-button>
</x-slot>
</x-confirmation-modal>
</div>
Testování Livewire komponenty
Testování je klíčovou součástí vývoje softwaru a Livewire není výjimkou. Laravel poskytuje vynikající nástroje pro testování, které můžeme využít i při testování našich Livewire komponent.
Pro testování Livewire komponent můžeme využít Laravelový testovací framework PHPUnit. Laravel poskytuje metodu test(), kterou můžeme použít k vytvoření testovacích případů pro naše komponenty. Nicméně jelikož jsem zástance jednoduchosti a mám radši čitelnější kód, využijeme balíček Pest (s využitím pluginu pro Livewire). Po jeho instalaci může vypadat test komponenty následovně:
<?php
use App\Enums\Examples\ExampleItemType;
use App\Models\Examples\ExampleItem;
use Livewire\Livewire;
it('can render the livewire item manager component', function () {
Livewire::test('examples.example-item-manager')
->assertStatus(200);
});
it('can add new item', function () {
Livewire::test('examples.example-item-manager')
->set('item.title', 'Test item')
->set('item.body', 'Test body')
->set('item.is_active', true)
->set('item.type', ExampleItemType::TYPE2->value)
->call('saveItem')
->assertSee('Test item');
});
it('can edit existing item', function () {
$item = ExampleItem::factory()->create();
Livewire::test('examples.example-item-manager')
->call('confirmItemEdit', $item->id)
->set('item.title', 'Test edit')
->call('saveItem')
->assertSee('Test edit');
});
it('can delete an existing item', function () {
$item = ExampleItem::factory()->create();
Livewire::test('examples.example-item-manager')
->call('confirmItemDeletion', $item->id)
->call('deleteItem', $item->id)
->assertDontSee($item->title);
});
it('can search for items', function () {
$item1 = ExampleItem::factory()->create(['title' => 'First Item']);
$item2 = ExampleItem::factory()->create(['title' => 'Second Item']);
Livewire::test('examples.example-item-manager')
->set('q', 'First')
->assertSee($item1->title)
->assertDontSee($item2->title);
});
it('can sort items', function () {
$item1 = ExampleItem::factory()->create(['title' => 'First Item']);
$item2 = ExampleItem::factory()->create(['title' => 'Second Item']);
$component = Livewire::test('examples.example-item-manager');
// sorting is ascending
$component->set('sortBy', 'title')->set('sortAsc', true)->call('render');
$this->assertEquals([$item1->id, $item2->id], $component->viewData('items')->pluck('id')->toArray());
// now sort descending
$component->set('sortBy', 'title')->set('sortAsc', false)->call('render');
$this->assertEquals([$item2->id, $item1->id], $component->viewData('items')->pluck('id')->toArray());
});
Testování je nezbytné pro zajištění kvality našeho kódu a pro ověření, že naše aplikace funguje tak, jak má. Laravel a Livewire poskytují všechny nástroje, které potřebujeme k vytvoření robustních a spolehlivých testů pro naše aplikace.
Závěrem
V tomto článku jsme se podrobně podívali na to, jak vytvořit Livewire komponentu v Laravelu pro CRUD operace včetně testování. Prošli jsme si základy Laravelu a Livewire, vytvořili jsme Livewire komponentu, implementovali jsme CRUD operace a naučili jsme se, jak testovat naši komponentu.
Důležité body, které vychází z tohoto návodu, jsou:
- Laravel a Livewire jsou silná kombinace pro vývoj webových aplikací.
- CRUD operace jsou základními operacemi, které můžeme provádět na našich datech.
- Testování je nezbytné pro zajištění kvality našeho kódu a pro ověření, že naše aplikace funguje tak, jak má.
Doufám, že tento návod vám pomůže vytvořit své vlastní Livewire komponenty a že vás inspiruje k dalšímu prozkoumávání možností, které Laravel a Livewire nabízejí.