Middle: питання на співбесіді з теми «Форми й схеми Filament»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
4 питання
За замовчуванням зміна поля не відправляє запит на сервер - стан піде разом із наступним запитом чи відправкою форми. Щоб форма перебудувалася одразу після зміни поля, його роблять live().
Залежний список:
use Filament\Forms\Components\Select;
use Filament\Schemas\Components\Utilities\Get;
Select::make('country_id')
->options(Country::query()->pluck('name', 'id'))
->live(),
Select::make('city_id')
->options(fn (Get $get): array => City::query()
->where('country_id', $get('country_id'))
->pluck('name', 'id')
->all())
->disabled(fn (Get $get): bool => blank($get('country_id'))),
Генерація slug із заголовка:
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Components\Utilities\Set;
use Illuminate\Support\Str;
TextInput::make('title')
->live(onBlur: true)
->afterStateUpdated(function (Get $get, Set $set, ?string $old, ?string $state) {
if (($get('slug') ?? '') !== Str::slug($old)) {
return; // slug уже змінили вручну - не перезаписуємо
}
$set('slug', Str::slug($state));
}),
TextInput::make('slug'),
Утиліти:
Get $get- прочитати значення іншого поля. Є й типізовані методи:$get->string('email'),$get->integer('qty'),$get->enum('status', Status::class);Set $set- змінити значення іншого поля;afterStateUpdated()- що зробити після зміни, з доступом до$stateі$old.
Варіанти live():
live()- запит на кожну зміну (для select, чекбоксів);live(onBlur: true)- коли поле втратило фокус (для тексту);live(debounce: 500)- після паузи у введенні.
Пастки:
live()на текстовому полі безonBlur/debounce- запит на кожне натискання клавіші, і форма «підгальмовує»;- кожен такий запит перерендерює всю форму. Для великих форм є часткове оновлення (
partiallyRenderComponentsAfterStateUpdated()) або логіка на JavaScript без запиту (afterStateUpdatedJs()); $set()не викликаєafterStateUpdated()поля, яке змінює, якщо не передатиshouldCallUpdatedHooks: true;- приховане поле (
hidden()) не зберігається - якщо значення має потрапити в базу, ховати його треба інакше або задавати в обробнику збереження.
Дегідратація - етап, на якому форма збирає дані полів у масив для збереження (getState()). На ньому можна перетворювати значення й виключати поля.
Перетворити значення поля:
use Filament\Forms\Components\TextInput;
use Illuminate\Support\Facades\Hash;
TextInput::make('password')
->password()
->dehydrateStateUsing(fn (string $state): string => Hash::make($state))
->saved(fn (?string $state): bool => filled($state)) // порожнє - не чіпати пароль
->required(fn (string $operation): bool => $operation === 'create');
(Якщо в моделі є каст 'password' => 'hashed', хешувати вручну не треба.)
Виключити поле зі збереження:
TextInput::make('password_confirmation')
->password()
->same('password')
->saved(false);
Поле валідується, але в масив даних не потрапляє.
Що не зберігається за замовчуванням:
- вимкнені поля (
disabled()) - щоб користувач не міг підмінити значення через Livewire. Явнийsaved()повертає збереження, але тоді значення знову контролює клієнт; - приховані поля (
hidden()).
Додати чи змінити дані на рівні сторінки ресурсу:
// CreatePost
protected function mutateFormDataBeforeCreate(array $data): array
{
$data['user_id'] = auth()->id();
return $data;
}
// EditPost
protected function mutateFormDataBeforeSave(array $data): array
{
$data['last_edited_by_id'] = auth()->id();
return $data;
}
protected function mutateFormDataBeforeFill(array $data): array
{
// підготувати дані запису перед заповненням форми
return $data;
}
Для модальних дій (CreateAction, EditAction) те саме робить mutateDataUsing().
Що де робити:
- перетворення одного поля - у самому полі (
dehydrateStateUsing()), щоб правило жило поруч з полем; - дані, яких немає у формі (автор, тенант, хто редагував), - у
mutateFormDataBeforeCreate/Save(). Ніколи не через приховане поле: його значення приходить з браузера; - повністю власне збереження -
handleRecordCreation()/handleRecordUpdate()на сторінці.
Repeater - список однотипних елементів з однаковим набором полів: учасники, позиції, контакти.
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
Repeater::make('members')
->schema([
TextInput::make('name')->required(),
Select::make('role')->options(['member' => 'Учасник', 'owner' => 'Власник'])->required(),
])
->columns(2)
->minItems(1)
->maxItems(10);
Builder - список різнотипних блоків у довільному порядку. Класичний приклад - вміст сторінки з блоків «заголовок», «абзац», «зображення», «цитата»:
use Filament\Forms\Components\Builder;
use Filament\Forms\Components\Builder\Block;
Builder::make('content')
->blocks([
Block::make('heading')->schema([TextInput::make('text')->required()]),
Block::make('image')->schema([FileUpload::make('url')->image()]),
]);
Два способи зберігання:
1. JSON-колонка (за замовчуванням). Увесь масив зберігається в одну колонку, модель потребує касту array. Просто, але шукати й фільтрувати по вмісту незручно.
2. Зв'язок HasMany для Repeater:
Repeater::make('items')
->relationship()
->schema([...])
->orderColumn('sort');
Кожен елемент - окремий запис у таблиці. Перетягування для зміни порядку в режимі зв'язку працює лише з orderColumn().
Корисні можливості:
distinct()на полі всередині - значення не повторюються між елементами (наприклад, «правильна відповідь» лише одна);table([...])- показати елементиRepeaterтаблицею замість карток;itemLabel()- підпис згорнутого елемента за його вмістом.
Пастка з $get(). Усередині елемента $get('field') шукає поле в цьому ж елементі. Щоб дістатися поля зовні - $get('../client_id') (на рівень вище) чи $get('../../client_id').
Коли не варто. Якщо елементів сотні або з ними працюють окремо від батька (свої статуси, пошук), краще relation manager: Repeater рендерить усі елементи в одній формі, і велика кількість робить її повільною.
Wizard ділить довгу форму на кроки з чіткою послідовністю. Поля кожного кроку валідуються при переході далі - користувач не дізнається про помилку на першому кроці лише в самому кінці.
use Filament\Schemas\Components\Wizard;
use Filament\Schemas\Components\Wizard\Step;
use Filament\Support\Exceptions\Halt;
Wizard::make([
Step::make('Компанія')
->schema([
TextInput::make('company_name')->required(),
TextInput::make('edrpou')->required()->length(8),
]),
Step::make('Контакт')
->schema([
TextInput::make('email')->email()->required(),
])
->afterValidation(function (Get $get) {
if (Company::where('edrpou', $get('edrpou'))->exists()) {
Notification::make()->danger()->title('Компанія вже зареєстрована')->send();
throw new Halt(); // лишитися на поточному кроці
}
}),
Step::make('Підтвердження')
->schema([
Checkbox::make('terms')->accepted(),
]),
])
->persistStepInQueryString();
Що тут є:
afterValidation()/beforeValidation()- хуки кроку; винятокHaltне пускає на наступний крок;persistStepInQueryString()- номер кроку в URL, оновлення сторінки не скидає користувача на початок;skippable()- дозволити перескакувати кроки (для редагування, де дані вже заповнені);submitAction()- кнопка відправки на останньому кроці.
У ресурсі Filament для сторінки створення є окремий підхід - трейт HasWizard на сторінці CreateRecord і метод getSteps(). Тоді кнопка «Створити» з'являється лише на останньому кроці. У модальному вікні дії wizard задають через ->steps([...]).
Пастки:
- валідація кроку перевіряє лише його поля. Правила, що залежать від полів з інших кроків, варто ставити на останній крок або перевіряти ще раз під час збереження;
- дані всіх кроків живуть у стані Livewire-компонента до відправки - якщо закрити вкладку, вони пропадуть. Для довгих анкет корисне збереження чернетки;
- крок, прихований через
hidden(), не валідується і не зберігається - зручно для умовних гілок, але легко пропустити обов'язкові дані.