Питання на співбесіді з Livewire і Filament
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
113 питань
Дія (action) у Filament - це кнопка разом із логікою: що станеться після натискання, чи відкриється модальне вікно, хто її бачить. Усі дії в Filament 5 живуть у просторі імен Filament\Actions - і в таблицях, і на сторінках, і у формах.
Готові дії для типових операцій з Eloquent:
CreateAction,EditAction,ViewAction,DeleteAction;ReplicateAction- копія запису (excludeAttributes(['slug'])- не копіювати унікальні поля);ForceDeleteAction,RestoreAction- для моделей зSoftDeletes;ImportAction,ExportAction- CSV/XLSX через черги;- масові варіанти:
DeleteBulkAction,ForceDeleteBulkAction,RestoreBulkAction.
use Filament\Actions\DeleteAction;
use Filament\Actions\EditAction;
use Filament\Actions\ReplicateAction;
->recordActions([
EditAction::make(),
ReplicateAction::make()->excludeAttributes(['slug']),
DeleteAction::make(),
])
Готові дії вже вміють модальне вікно, сповіщення про успіх і перевірку політики моделі в ресурсі.
Власна дія з підтвердженням:
use Filament\Actions\Action;
use Filament\Support\Icons\Heroicon;
Action::make('cancel')
->label('Скасувати замовлення')
->icon(Heroicon::OutlinedXCircle)
->color('danger')
->requiresConfirmation()
->modalHeading('Скасувати замовлення?')
->modalDescription('Клієнт отримає лист, а оплату буде повернено.')
->modalSubmitActionLabel('Так, скасувати')
->action(fn (Order $record) => $record->cancel())
->successNotificationTitle('Замовлення скасовано');
requiresConfirmation() відкриває стандартне вікно підтвердження, і код в action() виконається лише після згоди.
Пастки:
url()і підтвердження несумісні: якщо дія лише відкриває посилання (->url(...)), вікно підтвердження не з'явиться. Для підтвердження перед переходом потрібенaction()з редиректом усередині;- кілька дій з однаковою назвою в одному місці конфліктують - ім'я в
make()має бути унікальним; - підтвердження - не авторизація. Хто може натиснути кнопку, вирішують
visible()/authorize(), а не модальне вікно.
Спливаючі сповіщення («тости») створює клас Filament\Notifications\Notification з флюентним API:
use Filament\Actions\Action;
use Filament\Notifications\Notification;
Notification::make()
->title('Замовлення збережено')
->body('Клієнту надіслано лист з підтвердженням.')
->success()
->send();
Стан і вигляд:
success(),warning(),danger(),info()- колір та іконка;icon(),iconColor()- власна іконка;duration(5000)/seconds(5)- скільки показувати (за замовчуванням 6 секунд);persistent()- не зникати, доки користувач не закриє.
Дії в сповіщенні:
Notification::make()
->title('Імпорт завершено з помилками')
->warning()
->persistent()
->actions([
Action::make('view')
->label('Переглянути')
->button()
->url(route('imports.show', $import)),
])
->send();
Як це працює. Сповіщення записується в сесію і показується на наступному рендері. Тому його можна надіслати звідки завгодно: з дії, з контролера перед редиректом, зі звичайного Livewire-компонента. Є і JavaScript-версія: new FilamentNotification().title('...').send().
У готових діях (CreateAction, EditAction, сторінки ресурсів) сповіщення про успіх уже є - текст міняють через successNotificationTitle(), а прибирають - successNotification(null).
Пастки:
send()показує сповіщення поточному користувачу в браузері. Щоб повідомити іншого користувача чи зберегти повідомлення надовго - сповіщення в базу даних (sendToDatabase()) чи трансляція (broadcast());- заголовок може містити простий безпечний HTML. Дані користувача варто вставляти обережно - санітайзер Filament пропускає атрибути
style; - надіслане з джоби в черзі сповіщення через
send()нікого не досягне: у воркера немає сесії браузера.
Infolist - схема для перегляду даних запису у вигляді «підпис - значення». Вона використовується на сторінці перегляду ресурсу (ViewRecord), у модальних вікнах ViewAction, у relation managers.
use Filament\Infolists\Components\IconEntry;
use Filament\Infolists\Components\RepeatableEntry;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
public static function infolist(Schema $schema): Schema
{
return $schema
->components([
Section::make('Замовлення')
->schema([
TextEntry::make('number')->copyable(),
TextEntry::make('customer.name')->label('Клієнт'),
TextEntry::make('status')->badge(),
TextEntry::make('total')->money('UAH'),
IconEntry::make('is_paid')->boolean(),
TextEntry::make('created_at')->dateTime('d.m.Y H:i'),
])
->columns(3)
->columnSpanFull(),
RepeatableEntry::make('items')
->schema([
TextEntry::make('product.name'),
TextEntry::make('qty'),
])
->columns(2)
->columnSpanFull(),
]);
}
Записи (entries) живуть у Filament\Infolists\Components: TextEntry, IconEntry, ImageEntry, ColorEntry, CodeEntry, KeyValueEntry, RepeatableEntry. Макет - ті самі Section, Grid, Tabs зі схем.
Чим краще за форму з disabled():
- форматування як у таблицях: бейджі, гроші, дати, іконки, markdown, копіювання в буфер, посилання;
- жодних полів введення - менше HTML, немає стану форми в Livewire-компоненті;
- зв'язки й JSON читаються крапковою нотацією:
customer.name,meta.title; - вимкнена форма виглядає як «щось зламалося» і погано читається.
Що варто знати:
- сторінку перегляду додають у ресурс опцією
--viewпри генерації (або вручну вgetPages()); - якщо метод
infolist()не визначено, Filament показує на сторінці перегляду вимкнену форму; TextEntryзhtml()чиmarkdown()санітизує вміст - на відміну від власного Blade з{!! !!};- relation managers на сторінці перегляду за замовчуванням працюють лише в режимі читання.
Віджет статистики - кілька карток з числом, описом і міні-графіком.
php artisan make:filament-widget OrdersOverview --stats-overview
use Filament\Support\Icons\Heroicon;
use Filament\Widgets\StatsOverviewWidget;
use Filament\Widgets\StatsOverviewWidget\Stat;
class OrdersOverview extends StatsOverviewWidget
{
protected ?string $pollingInterval = '60s';
protected function getStats(): array
{
$today = Order::whereDate('created_at', today());
return [
Stat::make('Замовлень сьогодні', $today->count())
->description('+12% до вчора')
->descriptionIcon(Heroicon::ArrowTrendingUp)
->color('success')
->chart([7, 3, 4, 5, 6, 3, 9]),
Stat::make('Виручка сьогодні', Number::currency($today->sum('total'), 'UAH', 'uk')),
Stat::make('Нових клієнтів', Customer::whereDate('created_at', today())->count()),
];
}
}
Віджети з каталогу app/Filament/Widgets автоматично з'являються на дашборді панелі.
Корисні налаштування:
protected static ?int $sort = 1;- порядок на дашборді;protected int | string | array $columnSpan = 'full';- ширина;public static function canView(): bool- кому показувати;protected static bool $isLazy = false;- вимкнути ліниве завантаження.
Дві важливі поведінки за замовчуванням:
- опитування кожні 5 секунд. Віджет статистики (і графік) сам оновлюється раз на 5 секунд. Якщо в
getStats()важкі агрегати по великих таблицях, кожен відкритий дашборд створює постійне навантаження на базу. Інтервал варто збільшити ($pollingInterval = '60s') або вимкнути (null); - ліниве завантаження. Віджети завантажуються окремим запитом після сторінки, тож повільний віджет не блокує решту дашборда.
Пастки:
canView()ховає віджет з дашборда, але якщо той самий віджет вбудовано на іншу сторінку, умову треба перевірити й там;- важкі метрики варто кешувати (
Cache::rememberна кілька хвилин) - точність до секунди на дашборді рідко потрібна; - число без форматування (
1234567) погано читається: кращеNumber::format()чиNumber::abbreviate().
Публічні властивості Livewire живуть лише в межах сторінки: перезавантажили сторінку - фільтри, вибрана вкладка, сортування повернулися до значень за замовчуванням. Є два вбудовані способи це змінити.
#[Session] - значення властивості зберігається в сесії Laravel і відновлюється при наступному монтуванні компонента:
use Livewire\Attributes\Session;
use Livewire\Component;
class Orders extends Component
{
#[Session]
public string $status = 'all';
#[Session(key: 'orders-per-page')]
public int $perPage = 25;
}
Як це працює всередині: після кожного запиту (dehydrate) Livewire записує значення в сесію, а в mount() читає його, якщо ключ існує. Без явного key ключ генерується з назви компонента й властивості. У ключі можна використати інші властивості: #[Session(key: 'search-{author.id}')] - окремий стан для кожного автора.
#[Url] - значення зберігається в рядку запиту (?status=paid).
Порівняння:
#[Session] |
#[Url] |
|
|---|---|---|
| де живе значення | на сервері, у сесії | в адресі сторінки |
| видно в адресі | ні | так |
| можна поділитися посиланням | ні | так |
| переживає перезавантаження | так | так |
| працює в іншій вкладці | так, сесія спільна | лише якщо відкрити ту саму адресу |
| індексується пошуковиками | ні | так (окремі URL) |
Коли що обирати:
#[Url]- фільтри й пошук, якими користувач хоче поділитися чи додати в закладки: «замовлення зі статусом paid»;#[Session]- особисті налаштування інтерфейсу: розгорнута бічна панель, кількість рядків на сторінку, останній вибраний тип звіту.
Що пам'ятати:
- сесія спільна для всіх вкладок: користувач відкрив дві вкладки з різними фільтрами - вони перезаписуватимуть одна одну;
- сесія не безкоштовна: не зберігайте там великі масиви чи моделі - їх серіалізація відбувається на кожному запиті;
- значення з сесії - не перевірене: якщо властивість впливає на запит до бази (назва колонки сортування), її все одно треба перевіряти списком дозволених значень.
Не все в адмінці - це CRUD ресурсу. Налаштування сайту, звіт, імпорт, панель модерації - для таких екранів є власні сторінки.
php artisan make:filament-page Settings
Команда створює два файли: клас сторінки в app/Filament/Pages і Blade-шаблон у resources/views/filament/pages. Сторінка автоматично з'являється в навігації панелі.
Сторінка - це повноцінний Livewire-компонент з макетом панелі. Тому на ній є все, що вміє Livewire, а також форми, таблиці й дії Filament.
Форма на сторінці:
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Pages\Page;
use Filament\Schemas\Schema;
class Settings extends Page
{
protected string $view = 'filament.pages.settings';
public ?array $data = [];
public function mount(): void
{
$this->form->fill(SiteSettings::current()->toArray());
}
public function form(Schema $schema): Schema
{
return $schema
->components([
TextInput::make('site_name')->required(),
Toggle::make('registration_open'),
])
->statePath('data');
}
public function save(): void
{
SiteSettings::current()->update($this->form->getState());
Notification::make()->title('Збережено')->success()->send();
}
}
<x-filament-panels::page>
<form wire:submit="save">
{{ $this->form }}
<x-filament::button type="submit">Зберегти</x-filament::button>
</form>
</x-filament-panels::page>
Таблиця на сторінці: клас реалізує HasTable і підключає трейт InteractsWithTable, а в методі table(Table $table) описує запит і колонки - так само, як у ресурсі. У шаблоні - {{ $this->table }}.
Дії в заголовку - метод getHeaderActions(), як на сторінках ресурсів.
Доступ: сторінка доступна всім, хто має доступ до панелі. Обмеження - перевизначити canAccess():
public static function canAccess(): bool
{
return auth()->user()->can('manage-settings');
}
canAccess() ховає пункт навігації і забороняє прямий перехід за адресою. Але публічні методи Livewire (як save()) - окремі точки входу: критичні дії варто додатково перевіряти всередині методу.
Навігація: $navigationIcon, $navigationGroup, $navigationSort, $navigationLabel, а $title - заголовок сторінки.
Livewire сильний там, де інтерфейс - це форми, таблиці, фільтри й адмінки поверх серверних даних: логіка й валідація в PHP, без окремого API і дублювання правил на фронтенді. Але кожна його взаємодія - це запит на сервер і рендер компонента, і з цього випливають межі.
Коли Livewire - поганий вибір:
- миттєва реакція на кожен рух: перетягування з анімацією, малювання, редактори з частими змінами, ігри. Затримка мережі (навіть 50-100 мс) помітна;
- робота офлайн чи на нестабільному з'єднанні - без сервера інтерфейс не працює;
- складний клієнтський стан, що майже не стосується сервера: багатокрокові конструктори, полотна, планувальники з десятками елементів на екрані;
- кілька клієнтів одного API: мобільний застосунок і вебверсія - API все одно потрібен, і Livewire додає другий шлях до тих самих даних;
- висока кількість одночасних користувачів з частими діями: кожна дія навантажує PHP-воркери, тоді як SPA ходить на сервер лише за даними.
Що обрати замість:
| Ситуація | Інструмент |
|---|---|
| дрібна інтерактивність без сервера: меню, вкладки, модальні вікна | Alpine.js - разом з Livewire або без нього |
| багатий інтерфейс на React чи Vue, але маршрутизація й дані з Laravel без окремого API | Inertia |
| кілька клієнтів, офлайн, окрема фронтенд-команда | SPA чи мобільний застосунок + API (Sanctum) |
| сайт зі статичним вмістом | звичайний Blade, кеш на краю мережі |
Поєднання частіше, ніж вибір:
- Livewire + Alpine: клієнтські дрібниці на Alpine, дані й збереження на Livewire.
$wireу Alpine дає доступ до властивостей і методів компонента; - острівці й ізоляція: важку частину сторінки винести в окремий компонент чи острівець, щоб її оновлення не перерендерювали все;
- окремий віджет на JavaScript (графік, редактор) усередині Livewire-сторінки з
wire:ignore.
Як зважувати: склад команди (PHP-розробники чи фронтенд), вимоги до швидкості реакції, кількість клієнтів API. Переписування з Livewire на SPA посеред проєкту дороге - тому питання «чи буде мобільний застосунок» варто поставити на початку.
wire:model приймає не лише ім'я властивості, а й шлях усередині неї через крапку.
Масиви й вкладені дані:
public array $address = ['city' => '', 'street' => ''];
public array $items = [];
public array $roles = [];
<input wire:model="address.city">
<input wire:model="address.street">
@foreach ($items as $index => $item)
<div wire:key="item-{{ $item['id'] }}">
<input wire:model="items.{{ $index }}.qty">
</div>
@endforeach
{{-- кілька чекбоксів у масив --}}
<input type="checkbox" value="editor" wire:model="roles">
<input type="checkbox" value="author" wire:model="roles">
Хук updated отримує повний шлях: updatedItems($value, $key) з $key = '2.qty' - видно, який рядок змінився.
Об'єкти форм - те саме, але властивості типізовані й валідація поруч:
<input wire:model="form.title">
Чому не модель напряму. У Livewire 3 і 4 цей код кидає виняток:
public Post $post;
<input wire:model="post.title"> {{-- Can't set model properties directly --}}
Причини:
- безпека: властивості компонента - це дані, які надсилає браузер. Зв'язування з моделлю дозволило б змінювати будь-який атрибут моделі запитом, включно з тими, що не виводяться у формі (
is_admin,user_id), - аналог масового присвоєння без$fillable; - стан: модель серіалізується лише як клас і ключ, а при кожному запиті завантажується з бази заново - незбережені зміни атрибутів між запитами губилися б.
Як правильно: окремі властивості чи об'єкт форми, а при збереженні - явне заповнення провалідованими даними:
public function save(): void
{
$this->post->update($this->form->validate());
}
Стару поведінку можна ввімкнути (legacy_model_binding у конфігурації разом з правилами валідації для кожного поля), але для нового коду це не рекомендований шлях.
Пастки з масивами:
- без
wire:keyу циклі після видалення рядка введені значення «переїжджають» в сусідні поля; - індекси масиву після видалення:
unset($this->items[$i])лишає дірку в ключах;array_values()після видалення - іwire:keyза стабільним ідентифікатором, а не індексом; - великий масив у властивості - весь масив серіалізується в кожен запит. Для сотень рядків краще окремі дочірні компоненти чи редагування по одному.
Filament - фреймворк Server-Driven UI для Laravel: інтерфейси (адмінки, панелі) описуються на PHP через структуровані об'єкти, а не верстку.
public static function form(Schema $schema): Schema
{
return $schema->components([
TextInput::make('title')->required(),
Select::make('status')->options(Status::class),
]);
}
- Побудований на Livewire, Alpine.js і Tailwind CSS.
- Будівельні блоки: Resources, Forms, Tables, Actions, Infolists, Widgets.
- Компоненти ініціалізуються статичними
make()-методами; динамічні значення задаються замиканнями з утилітамиGet/Set.
Усе публічне в компоненті доступне з браузера, а не лише те, що є в шаблоні.
Публічні методи можна викликати з консолі DevTools з будь-якими аргументами, навіть якщо в шаблоні на них немає wire:click:
public function delete(int $id): void
{
$post = Post::findOrFail($id);
$this->authorize('delete', $post); // без цього - видалить будь-що
$post->delete();
}
Кнопка «Видалити», яку бачать лише адміністратори, нічого не захищає: метод однаково можна викликати.
Публічні властивості зберігаються на клієнті між запитами й можуть бути змінені:
public int $postId; // користувач може підставити чужий ID
Що робити:
- Авторизувати кожну дію на сервері - так само, як у контролері: політики,
authorize(), перевірка власника. #[Locked]на властивостях, які не мають змінюватися з клієнта, - Livewire кине виняток при спробі підміни.- Модель замість ID: якщо у властивості лежить Eloquent-модель (
public Post $post), Livewire сам стежить, щоб її ID не підмінили. - Внутрішні методи -
protectedчиprivate: допоміжнийpublic function deleteQuietly()- теж ендпоінт. - Не класти в публічні властивості секрети: вони потрапляють у HTML-знімок компонента.
Правило: Livewire-метод - це маршрут, а його параметри й публічні властивості - запит. Усе, що ви перевіряли б у контролері, перевіряйте й тут.
Компонент Livewire не живе на сервері між запитами - на кожен запит його створюють заново зі знімка. Хуки дають змогу виконати код у потрібний момент цього циклу.
| Хук | Коли |
|---|---|
mount() |
один раз, при створенні компонента (перший рендер) |
boot() |
на кожен запит: і перший, і наступні |
hydrate() |
на кожен наступний запит, після відновлення зі знімка (не на першому) |
updating($property, $value) |
перед зміною властивості з браузера |
updated($property) |
після зміни властивості |
rendering() / rendered() |
до й після рендеру шаблону |
dehydrate() |
наприкінці кожного запиту, перед серіалізацією |
exception($e, $stopPropagation) |
при винятку в компоненті |
mount() замість конструктора - приймає параметри (атрибути тегу, параметри маршруту) і задає початковий стан:
public function mount(Post $post): void
{
$this->title = $post->title;
}
boot() - для того, що не зберігається між запитами: захищені (protected) властивості, сервіси. Вони не потрапляють у знімок, тож без boot() на наступних запитах будуть порожніми.
updating / updated - реакція на зміну конкретної властивості, з назвою в методі:
public function updatedSearch(): void
{
$this->resetPage(); // новий пошук - на першу сторінку пагінації
}
public function updatedPreferences($value, $key): void
{
// для масивів: $key - змінений ключ ('theme'), $value - нове значення
}
updating може кинути виняток і не дати змінити властивість - але для захисту від підробки краще #[Locked].
Пастки:
updatedспрацьовує лише на зміни з браузера (wire:model,$set), а не на присвоєння в PHP-коді:$this->search = 'x'у методі хук не викличе;- важкі запити в
boot()чиhydrate()виконуються на кожен запит, навіть коли дані не потрібні. Для даних, що потрібні лише в шаблоні, кращі обчислювані властивості (#[Computed]); - хуки працюють і в трейтах (
bootWithSorting,updatedWithSorting) та в об'єктах форм.
Між запитами стан компонента серіалізується в JSON і назад, тож публічна властивість може мати лише тип, який Livewire уміє перетворити.
Підтримується з коробки:
- примітиви:
string,int,float,bool,array,null; BackedEnum,Collection, EloquentModelіEloquent\Collection,DateTime,Carbon,Stringable;- власні типи - через
Wireableчи синтезатори.
Замикання, ресурси, незбережувані сервіси в публічну властивість покласти не можна. Для них - protected властивості, ініціалізовані в boot().
Як серіалізуються моделі: у знімок потрапляє не вміст моделі, а клас і ключ (для колекції - клас і список ключів):
"post": [null, { "class": "App\\Models\\Post", "key": 1, "s": "mdl" }]
На кожному наступному запиті Livewire завантажує модель з бази заново. На PHP 8.4+ Livewire 4 відновлює її як лінивий проксі: запит виконується лише при першому зверненні до моделі в цьому запиті.
Наслідки:
- запит до бази на кожну дію, що торкається моделі, а для колекції -
whereInза всіма ключами. Колекція на тисячу моделей у властивості - тисяча рядків з бази на кожен клік; - завантажені зв'язки не відновлюються.
Post::with('author')при першому завантаженні не діє на наступних запитах - шаблон, що виводить$post->authorдля кожного елемента, отримує N+1; - обмеження запиту губляться:
->select(['id', 'title'])чи умови, накладені при першому завантаженні, на наступних запитах не застосуються - модель завантажиться повністю; - назва класу видна в браузері. Приховати її допомагає
Relation::morphMap()- тоді в знімку буде псевдонім; - зміни моделі, не збережені в базу, губляться між запитами - відновлюється те, що в базі.
Добра новина: ідентифікатор моделі в публічній властивості автоматично захищений від підміни - як з #[Locked].
Що робити замість великих колекцій у властивостях:
#[Computed]
public function posts()
{
return Post::query()->where('author_id', $this->authorId)->latest()->paginate(20);
}
@foreach ($this->posts as $post) ... @endforeach
Обчислювана властивість виконується лише тоді, коли її читає шаблон, і не потрапляє в знімок. Публічні властивості лишають для стану інтерфейсу: фільтрів, введених значень, ідентифікаторів.
У Livewire кожен компонент незалежний: має власний знімок стану і оновлюється окремим запитом. Параметри, передані дочірньому компоненту, потрапляють у його mount() один раз - при створенні.
{{-- батьківський --}}
<livewire:todo-count :$todos />
Якщо батьківський компонент додав завдання, todo-count про це не дізнається: при оновленні батька дочірні компоненти не рендеряться заново - замість них Livewire вставляє «заглушки» й пропускає їх під час морфінгу DOM.
Це зроблено свідомо: мінімум даних у кожному запиті. Оновлення батька не тягне за собою стан і рендер усіх нащадків.
Способи синхронізувати:
1. #[Reactive] - параметр оновлюється, коли змінюється в батьківському компоненті:
new class extends Component {
#[Reactive]
public $todos;
};
Ціна - при кожному оновленні батька, де змінився параметр, дочірній компонент теж оновлюється. Змінювати реактивний параметр у самому дочірньому компоненті не можна.
2. Новий wire:key - компонент створюється заново:
<livewire:todo-count :$todos :wire:key="$todos->pluck('id')->join('-')" />
3. Події - дочірній слухає #[On('todo-added')] і перечитує дані сам.
4. Острівці (@island) замість дочірнього компонента. Якщо компонент виділяли лише заради ізольованого оновлення частини екрана, острівець робить це в межах одного компонента - без параметрів і подій.
Зворотний напрямок (дитина → батько):
$parent.remove(id)у шаблоні дитини - прямий виклик методу батька;- подія, яку слухає батько;
#[Modelable]- щоб на дочірньому компоненті працювавwire:modelбатька.
Обов'язкове правило для циклів: дочірні компоненти в @foreach отримують унікальний wire:key (:wire:key="$post->id"), інакше при зміні списку стан компонентів «переплутається».
Коли потрібен окремий компонент, а не острівець: повторне використання в різних місцях, власні хуки життєвого циклу й авторизація, складний ізольований стан.
Публічну властивість можна змінити з браузера: через wire:model, $wire.postId = 5 в консолі чи підробленим запитом. Для властивостей, на які спирається безпека, це небезпечно.
new class extends Component {
public $postId; // користувач може підмінити на чужий id
public function delete(): void
{
Post::find($this->postId)->delete(); // видалить чужий пост
}
};
#[Locked] забороняє зміну властивості з клієнта. Спроба - виняток, дія не виконується:
use Livewire\Attributes\Locked;
new class extends Component {
#[Locked]
public int $postId;
public function mount(int $id): void
{
$this->postId = $id;
}
};
Що #[Locked] не робить:
- не забороняє змінювати властивість у PHP-коді компонента - якщо ви самі присвоїте їй значення з введення користувача, захисту немає;
- не замінює авторизацію. Заблокований
postIdгарантує, що id не підмінили післяmount(). Але чи має користувач право на цей пост узагалі, треба перевірити - уmount()і в кожній дії, що змінює дані ($this->authorize('delete', $post)); - не приховує значення - воно все одно видно в знімку в HTML.
Коли захист є без атрибута: публічна властивість з моделлю Eloquent (public Post $post) автоматично захищена від підміни ідентифікатора. Тому передавати в компонент модель замість голого id часто і зручніше, і безпечніше.
Кандидати на #[Locked]:
- ідентифікатори записів, з якими працює компонент;
- ролі, прапорці доступу, ціни й знижки, обчислені на сервері;
- будь-яке значення, що визначає, що дозволено зробити, а не що користувач вводить.
Альтернатива - не тримати таке значення в публічній властивості взагалі: protected властивість, відновлена в boot(), чи обчислювана властивість.
Об'єкт форми - окремий клас, куди виносять поля, правила валідації й логіку збереження. Компонент стає коротшим, а форму можна використати в кількох компонентах.
php artisan livewire:form PostForm # app/Livewire/Forms/PostForm.php
namespace App\Livewire\Forms;
use App\Models\Post;
use Illuminate\Validation\Rule;
use Livewire\Form;
class PostForm extends Form
{
public ?Post $post = null;
public string $title = '';
public string $content = '';
protected function rules(): array
{
return [
'title' => ['required', 'max:255', Rule::unique('posts')->ignore($this->post)],
'content' => ['required', 'min:10'],
];
}
public function setPost(Post $post): void
{
$this->post = $post;
$this->fill($post->only(['title', 'content']));
}
public function save(): Post
{
$this->validate();
$post = $this->post ?? new Post(['author_id' => auth()->id()]);
$post->fill($this->only(['title', 'content']))->save();
return $post;
}
}
Компоненти створення й редагування:
// створення
public PostForm $form;
public function save(): void
{
$post = $this->form->save();
$this->redirectRoute('posts.show', $post);
}
// редагування
public function mount(Post $post): void
{
$this->authorize('update', $post);
$this->form->setPost($post);
}
У шаблоні поля адресуються через назву властивості з формою:
<input wire:model="form.title">
@error('form.title') <p>{{ $message }}</p> @enderror
Корисні методи форми: $this->form->all(), only([...]), reset() (повертає значення за замовчуванням), pull() (отримати значення й одразу скинути), validate().
Що варто знати:
- ключі помилок мають префікс назви властивості:
form.title, а неtitle- і в@error, і в тестах (assertHasErrors('form.title')); - хуки
updated...працюють і всередині об'єкта форми; - авторизація лишається в компоненті чи в методі форми - об'єкт форми не робить дії безпечними сам по собі;
reset()повертає типізовані властивості без значення за замовчуванням у неініціалізований стан - полям варто задавати значення за замовчуванням.
Аналог у звичайному Laravel - Form Request, але об'єкт форми ще й тримає стан між запитами компонента.
Питання з реальних технічних співбесід - 113 питань у 8 темах, розібраних із відповідями. Нижче - розбивка за рівнями та темами, якщо хочете звузити підготовку.
- Теми
- Livewire 21 Filament 19 Дії, інфолисти й віджети 13 Продуктивність Livewire 12 Події, Alpine і навігація 12 Таблиці Filament 12 Форми й схеми Filament 12 Форми й валідація 12
Готуєтесь до співбесіди не просто так: зараз на сайті 145 відкритих вакансій Laravel і PHP. Переглянути вакансії