Junior: питання на співбесіді з теми «Форми й схеми Filament»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
4 питання
У Filament 5 форми, infolists і макети - це схеми (Filament\Schemas\Schema). Поля форми живуть у Filament\Forms\Components, а макетні компоненти (Section, Grid, Tabs, Fieldset, Wizard) - у Filament\Schemas\Components.
use Filament\Forms\Components\RichEditor;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
public static function configure(Schema $schema): Schema
{
return $schema
->components([
Section::make('Основне')
->schema([
TextInput::make('title')->required()->maxLength(255),
TextInput::make('slug')->required(),
RichEditor::make('body')->columnSpanFull(),
])
->columns(2)
->columnSpanFull(),
Section::make('Публікація')
->schema([
Select::make('status')->options(PostStatus::class)->required(),
]),
]);
}
Сітка - головне, що треба розуміти:
- сторінки створення й редагування ресурсу за замовчуванням розкладають компоненти у дві колонки;
Section,Grid,Fieldset- теж звичайні елементи цієї сітки й займають одну колонку. Тому секція раптом виявляється на половину ширини;columnSpanFull()- розтягнути компонент на всю ширину;columns(2)на секції - внутрішня сітка для її полів;columnSpan(['md' => 2, 'xl' => 1])- різна ширина на різних екранах (брейкпоінти Tailwind).
Інші макетні компоненти:
Tabs- вкладки для довгих форм (стан можна зберігати в URL);Fieldset- рамка з підписом для невеликої групи полів;Grid- сітка без візуального оформлення;Wizard- покрокова форма.
Пастки при оновленні з v3: старі простори імен Filament\Forms\Components\Section чи Filament\Forms\Form у v5 не працюють - макети переїхали в Filament\Schemas\Components, а метод форми приймає Schema $schema і повертає $schema->components([...]).
Валідацію описують методами поля - це ті самі правила Laravel, але з підказками в інтерфейсі (зірочка обов'язкового поля, maxlength в інпуті).
use Filament\Forms\Components\TextInput;
TextInput::make('email')
->email()
->required()
->maxLength(255)
->unique(),
TextInput::make('price')
->numeric()
->minValue(0)
->rules(['decimal:0,2']), // будь-яке правило Laravel
TextInput::make('slug')
->required()
->regex('/^[a-z0-9-]+$/')
->validationMessages([
'regex' => 'Лише малі латинські літери, цифри й дефіс.',
]),
Валідація запускається під час відправки форми. Помилки показуються під полями, а збереження не відбувається.
unique() у ресурсі. Форма знає свою модель і поточний запис, тож на сторінці редагування unique() автоматично ігнорує запис, що редагується: email користувача не «конфліктує» сам із собою. Якщо це не потрібно - unique(ignoreRecord: false).
Пастка з unique(): правило Laravel звертається до таблиці напряму, оминаючи Eloquent. Тому:
- м'яко видалені записи теж вважаються зайнятими;
- мультиорендність не враховується - email буде «зайнятим», якщо він є в іншого тенанта.
Для цього є scopedUnique() - перевірка через модель з усіма глобальними скоупами, включно з SoftDeletes і тенантами.
Умовна валідація:
TextInput::make('password')
->password()
->required(fn (string $operation): bool => $operation === 'create');
$operation - create, edit або view.
Що варто знати:
- правила лише на клієнті не існує: усе перевіряється на сервері, тож підробити
maxlengthу DevTools не допоможе; - поле, яке не зберігається (
saved(false)), все одно валідується; - для повідомлень українською досить локалізації Laravel (
lang/uk/validation.php) - Filament використовує ті самі переклади.
Select з методом relationship() сам завантажує варіанти зі зв'язку і зберігає вибір.
BelongsTo (один автор):
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
Select::make('author_id')
->relationship('author', 'name')
->searchable()
->preload()
->createOptionForm([
TextInput::make('name')->required(),
TextInput::make('email')->email()->required(),
])
->required();
searchable()- пошук по варіантах (за замовчуванням по колонці-назві;searchable(['name', 'email'])- по кількох);preload()- завантажити варіанти разом зі сторінкою, а не під час введення. Добре для десятків записів, погано для десятків тисяч;createOptionForm()- кнопка «+», модальне вікно створення нового автора, який одразу стає вибраним.
BelongsToMany (кілька тегів) - додати multiple():
Select::make('tags')
->multiple()
->relationship(titleAttribute: 'name')
->preload();
Filament сам синхронізує pivot-таблицю під час збереження форми.
Звуження варіантів:
Select::make('category_id')
->relationship('category', 'name', fn (Builder $query) => $query->where('is_active', true));
Що Filament перевіряє сам. Для Select автоматично діє правило «значення має бути серед дозволених варіантів». Підставити в запиті id запису, якого немає у варіантах (наприклад, неактивну категорію), не вийде.
Пастки:
- без
searchable()на великій таблиціSelectнамагається вивантажити всі записи у список; - для рекурсивних зв'язків (
parent_id) потрібенrelationship(..., ignoreRecord: true), щоб запис не можна було зробити батьком самого себе; - якщо поле
multiple()зі зв'язком треба зробитиdisabled(), викликатиdisabled()слід доrelationship()- інакше вимкнене поле все одно збереже зв'язок.
FileUpload завантажує файл через Livewire у тимчасове сховище, а під час збереження форми переносить його на диск і записує в атрибут моделі шлях до файлу.
use Filament\Forms\Components\FileUpload;
FileUpload::make('cover')
->image()
->disk('public')
->directory('covers')
->visibility('public')
->maxSize(2048); // кілобайти
Чому зображення не відкривається - найчастіші причини:
- Видимість
privateза замовчуванням. Файли завантажуються з приватною видимістю, якщо диск неpublic. На S3 чи R2 без->visibility('public')пряме посилання на файл повертає 403. - Диск за замовчуванням - той, що в
FILESYSTEM_DISK, частоlocal. Файли з нього взагалі не віддаються вебсервером. - Немає
storage:linkдля дискаpublic. - Неправильний
APP_URL- URL файлів будується з нього, і превью в FilePond не завантажується.
Кілька файлів:
FileUpload::make('attachments')
->multiple()
->acceptedFileTypes(['application/pdf'])
->maxFiles(5);
Шляхи зберігаються JSON-масивом, тож атрибут моделі потребує касту array.
Безпека імен файлів. За замовчуванням Filament генерує випадкові імена - і це правильно. preserveFilenames() на дисках local/public небезпечний: користувач може завантажити файл з розширенням .php. Якщо оригінальна назва потрібна для показу, її зберігають окремо: ->storeFileNamesIn('attachment_names').
Пастки:
- старі файли не видаляються. Коли користувач замінює чи прибирає файл, попередній лишається на диску. Прибирати треба самостійно, наприклад у спостерігачі моделі;
- ліміт розміру обмежує ще й Livewire (
temporary_file_uploadуconfig/livewire.php, за замовчуванням 12 МБ) таupload_max_filesizeу PHP -maxSize()більший за ці значення не допоможе; acceptedFileTypes()перевіряє MIME-тип, а не розширення - покладатися лише на нього для безпеки не варто.