Junior: питання на співбесіді з теми «Filament»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
6 питань
Ресурс - це опис того, як модель виглядає в адмінці: таблиця, форма й сторінки навколо них.
php artisan make:filament-resource Vacancy --generate
Команда створює клас ресурсу, сторінки List/Create/Edit, а форму й таблицю виносить в окремі файли (Schemas/VacancyForm.php, Tables/VacancyTable.php). Прапорці --embed-schemas і --embed-table лишають їх усередині класу ресурсу.
Дві головні частини - форма й таблиця:
public static function form(Schema $schema): Schema
{
return $schema->components([
TextInput::make('title')
->required()
->maxLength(255),
Select::make('level')
->options(VacancyLevel::class)
->required(),
Toggle::make('is_published'),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('title')->searchable()->sortable(),
TextColumn::make('company.name')->label('Компанія'),
IconColumn::make('is_published')->boolean(),
])
->filters([
SelectFilter::make('level')->options(VacancyLevel::class),
]);
}
Що це дає без додаткового коду: список із пошуком, сортуванням і пагінацією, форми створення й редагування з валідацією, масові дії, фільтри.
На чому побудовано: Livewire відповідає за реактивність без написання JS, Alpine - за дрібні взаємодії в браузері, Tailwind - за оформлення. Тому Filament - це PHP-код, а не окремий фронтенд-застосунок.
Що варто знати одразу: форма й таблиця - це звичайні PHP-масиви обʼєктів, тож видимість поля, опції списку чи доступність дії задаються замиканнями й можуть залежати від користувача та стану запису.
Дія (Action) у Filament - кнопка з необов'язковим модальним вікном і логікою, що виконується після підтвердження. Дії живуть у заголовку сторінки, в рядках таблиці, у масових операціях.
use Filament\Actions\Action;
use Filament\Forms\Components\Select;
Action::make('updateAuthor')
->label('Змінити автора')
->schema([
Select::make('authorId')
->label('Автор')
->options(User::query()->pluck('name', 'id'))
->required(),
])
->action(function (array $data, Post $record): void {
$record->author()->associate($data['authorId']);
$record->save();
})
Як це працює:
schema()описує поля модального вікна. Дані з форми приходять у замиканняaction()масивом$data, вже провалідованими.- У замикання автоматично підставляються залежності за іменем:
$record(поточний запис у таблиці чи на сторінці),$data,$livewire. fillForm()заповнює форму наявними даними.
Корисні налаштування:
->requiresConfirmation()- просте підтвердження без форми (для видалення, архівування).->visible(fn (Post $record) => ...)/->authorize('update')- показувати дію лише тим, кому можна.->successNotificationTitle('Збережено')- сповіщення після виконання.
Важливо: дія - не лише кнопка. Її логіка виконується на сервері через Livewire, тож перевірку прав роблять у самій дії (authorize), а не лише приховуванням кнопки.
Панель у Filament - окрема адмінка зі своїм адресом, входом, навігацією й оформленням. Кожну описує клас-провайдер, який створює php artisan filament:install --panels (наприклад, app/Providers/Filament/AdminPanelProvider.php):
public function panel(Panel $panel): Panel
{
return $panel
->default()
->id('admin')
->path('admin')
->login()
->passwordReset()
->emailVerification()
->profile()
->colors(['primary' => Color::Indigo])
->brandName('Laravel Ukraine')
->discoverResources(in: app_path('Filament/Resources'), for: 'App\Filament\Resources')
->discoverPages(in: app_path('Filament/Pages'), for: 'App\Filament\Pages')
->middleware([/* стек web: сесія, CSRF, ... */])
->authMiddleware([Authenticate::class]);
}
Що тут задається:
| Метод | Що робить |
|---|---|
id() |
ідентифікатор панелі - потрібен, коли панелей кілька |
path() / domain() |
адреса: /admin чи окремий піддомен |
login(), registration(), passwordReset(), emailVerification(), profile() |
вбудовані сторінки автентифікації - вмикаються лише явно |
colors(), brandName(), brandLogo(), favicon() |
оформлення |
discoverResources() / resources([...]) |
які ресурси, сторінки й віджети підключити |
middleware() / authMiddleware() |
стек для всіх сторінок і для захищених |
spa(), topNavigation(), databaseNotifications() |
режим навігації без перезавантаження, верхнє меню, сповіщення |
registration() - обережно: публічна реєстрація в адмінку майже ніколи не потрібна. Без неї користувачів створюють сидером, командою make:filament-user чи з самої панелі.
Хто може зайти - вирішує не провайдер, а модель користувача. На продакшені Filament пускає лише тих, для кого canAccessPanel() повертає true:
class User extends Authenticatable implements FilamentUser
{
public function canAccessPanel(Panel $panel): bool
{
return $panel->getId() === 'admin' && $this->is_admin;
}
}
Без інтерфейсу FilamentUser локально заходить будь-хто з акаунтом, а на продакшені - ніхто (403).
Кілька панелей - кілька провайдерів з різними id() і path(): адмінка для команди й кабінет для клієнтів, кожна зі своїми ресурсами, authGuard() і правилами входу.
Типові помилки: провайдер не зареєстровано в bootstrap/providers.php (панель просто не відкривається), змішування ресурсів двох панелей через спільний каталог для discoverResources(), і забута перевірка в canAccessPanel() для другої панелі.
За замовчуванням у локальному оточенні (APP_ENV=local) будь-який користувач моделі User може увійти в панель. Це зручно для розробки, але на продакшені правило інше: доступ отримують лише ті, кому це явно дозволено. Якщо нічого не налаштувати, після деплою адміністратор бачить 403.
Рішення - контракт FilamentUser:
use Filament\Models\Contracts\FilamentUser;
use Filament\Panel;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable implements FilamentUser
{
public function canAccessPanel(Panel $panel): bool
{
if ($panel->getId() === 'admin') {
return $this->is_admin && $this->hasVerifiedEmail();
}
return true; // інші панелі, наприклад кабінет клієнта
}
}
Що важливо:
- метод отримує
$panel, тож для кількох панелей умови мають бути різними. Типова помилка -return true«на швидку руку», після чого будь-який зареєстрований користувач сайту потрапляє в адмінку; canAccessPanel()- лише вхідні двері. Що саме користувач може бачити й робити всередині, вирішують політики моделей (viewAny,update,delete) - Filament перевіряє їх для ресурсів автоматично;- перевірку краще будувати на ролі чи прапорці в базі, а не на списку email у коді;
- якщо на сайті є власний вхід поза панеллю, користувач, авторизований там, усе одно проходить через
canAccessPanel()при відкритті панелі.
Як перевірити до деплою: тестом з APP_ENV не local - зайти звичайним користувачем на /admin і переконатися, що відповідь 403, а адміністратором - 200.
Кожен ресурс і сторінка автоматично з'являються в боковому меню панелі. Вигляд пункту налаштовують статичними властивостями й методами ресурсу.
use BackedEnum;
use Filament\Support\Icons\Heroicon;
use UnitEnum;
class OrderResource extends Resource
{
protected static string | BackedEnum | null $navigationIcon = Heroicon::OutlinedShoppingBag;
protected static string | UnitEnum | null $navigationGroup = 'Магазин';
protected static ?int $navigationSort = 2;
protected static ?string $navigationLabel = 'Замовлення';
public static function getNavigationBadge(): ?string
{
return (string) static::getModel()::where('status', 'new')->count();
}
public static function getNavigationBadgeColor(): ?string
{
return 'warning';
}
}
Що тут відбувається:
- іконка - у Filament 5 зручно брати з enum
Heroicon(автодоповнення замість рядків на кшталтheroicon-o-...); - група - пункти з однаковою групою збираються під спільним заголовком. Групу можна задати рядком або enum, щоб не дублювати назву в різних ресурсах;
- порядок -
$navigationSortу межах групи; - бейдж - число чи короткий текст біля пункту, наприклад кількість нових замовлень.
Пастки:
- бейдж рахується на кожному завантаженні будь-якої сторінки панелі. Важкий
count()по великій таблиці сповільнює всю адмінку. Для таких випадків - індекс під умову або кешування значення на хвилину; $shouldRegisterNavigation = falseховає пункт меню, але не забороняє доступ до сторінки за URL. Обмежувати доступ треба політиками чиcanAccess();- для великої панелі групи можна згорнути або винести частину ресурсів у кластер (
make:filament-cluster) - окремий розділ зі своєю піднавігацією.
Не все в адмінці - це 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 - заголовок сторінки.