Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

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 рендерить усі елементи в одній формі, і велика кількість робить її повільною.

Докладніше в документації: 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(), не валідується і не зберігається - зручно для умовних гілок, але легко пропустити обов'язкові дані.

Докладніше в документації: Майстри