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

Питання на співбесіді: Filament

Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.

19 питань

Ресурс - це опис того, як модель виглядає в адмінці: таблиця, форма й сторінки навколо них.

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 - заголовок сторінки.

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

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.

Filament використовує політики моделей Laravel. Якщо для моделі ресурсу зареєстровано політику, Filament перевіряє її методи:

  • viewAny() - чи бачить користувач ресурс узагалі: без нього ресурс зникає з навігації, а сторінки віддають 403.
  • view(), create(), update(), delete() - доступ до перегляду, створення, редагування й видалення конкретного запису.
  • deleteAny(), forceDeleteAny(), restoreAny() - масові операції. Filament за замовчуванням не перевіряє delete() для кожного запису окремо - це повільно. Якщо потрібно, у масової дії є ->authorizeIndividualRecords().
  • reorder() - зміна порядку рядків у таблиці.
class PostPolicy
{
    public function viewAny(User $user): bool
    {
        return $user->hasRole('editor');
    }

    public function update(User $user, Post $post): bool
    {
        return $user->isAdmin() || $post->author_id === $user->id;
    }
}

Що варто знати:

  • Немає політики - немає обмежень: якщо політику не зареєстровано, Filament дозволяє все. Тому політика для кожної моделі в панелі - обов'язкова звичка.
  • Перевірки повторюються на кожному Livewire-запиті, а не лише при відкритті сторінки: якщо доступ відібрали, наступна дія вже буде заборонена.
  • Доступ до панелі визначає canAccessPanel() на моделі користувача (інтерфейс FilamentUser). Без нього на проді в панель не пустить нікого.
  • Власні дії й сторінки політики ресурсу автоматично не покривають - їм потрібні ->authorize() чи canAccess().
  • Обмеження видимих рядків (редактор бачить лише свої пости) - через getEloquentQuery() ресурсу, а не лише через політику.

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

Filament дає три основні способи працювати зі зв'язками, і вибір залежить від типу зв'язку та обсягу даних.

1. Select / CheckboxList з relationship() - вибрати наявні записи прямо у формі. Підходить для BelongsTo, MorphTo і BelongsToMany:

Select::make('author_id')
    ->relationship('author', 'name')
    ->searchable()
    ->preload();

Select::make('tags')
    ->multiple()
    ->relationship(titleAttribute: 'name');   // зберігається в pivot-таблицю

Зміни зберігаються разом з основною формою.

2. Repeater з relationship() - редагувати кілька дочірніх записів (HasMany) усередині форми батька: позиції замовлення, телефони контакту. Добре, коли записів небагато і вони не мають сенсу окремо від батька.

3. Relation manager - окрема інтерактивна таблиця під формою редагування чи перегляду. Підтримує HasMany, HasManyThrough, BelongsToMany, MorphMany, MorphToMany:

php artisan make:filament-relation-manager CategoryResource posts title
public static function getRelations(): array
{
    return [PostsRelationManager::class];
}

У ньому є пошук, фільтри, пагінація, дії створення, редагування, AttachAction/DetachAction, AssociateAction.

Як обирати:

Ситуація Інструмент
вибрати одного автора чи кілька тегів Select
2-10 залежних рядків, що редагуються разом з батьком Repeater
десятки й сотні пов'язаних записів, потрібні пошук і пагінація relation manager

Що варто знати про relation managers:

  • вони завантажуються ліниво і зберігають зміни одразу, а не разом із формою батька;
  • на сторінці перегляду вони за замовчуванням лише для читання;
  • фільтр списку в модальному вікні AttachAction - це лише відображення. Обмежити, які записи взагалі можна приєднати, треба через recordSelectOptionsQuery(), інакше підроблений запит приєднає будь-який запис.

Докладніше в документації: Керування зв'язками

Глобальний пошук - поле у верхній панелі, що шукає одразу по всіх ресурсах. Ресурс бере в ньому участь, якщо в нього задано атрибут-назву запису:

protected static ?string $recordTitleAttribute = 'title';

Пошук по кількох полях і зв'язках:

public static function getGloballySearchableAttributes(): array
{
    return ['title', 'slug', 'author.name'];
}

Додаткові деталі під назвою результату:

public static function getGlobalSearchResultDetails(Model $record): array
{
    return [
        'Автор' => $record->author->name,
        'Категорія' => $record->category->name,
    ];
}

Головна пастка - N+1. Деталі звертаються до зв'язків, і без жадібного завантаження кожен результат робить окремі запити. Зв'язки треба підвантажити в запиті пошуку:

public static function getGlobalSearchEloquentQuery(): Builder
{
    return parent::getGlobalSearchEloquentQuery()->with(['author', 'category']);
}

Що ще впливає на швидкість:

  • пошук будується на LIKE '%...%' по кожному атрибуту - звичайний B-tree-індекс тут не допомагає. На великих таблицях варто обмежити кількість атрибутів і ресурсів у пошуку;
  • результатів на ресурс за замовчуванням не більше 50 ($globalSearchResultsLimit);
  • ресурс, якому пошук не потрібен, вимикають $isGloballySearchable = false;
  • ресурси без доступу (політика viewAny) у пошук не потрапляють.

Зручності: гарячі клавіші для фокусу на полі пошуку задають у панелі через globalSearchKeyBindings(['command+k', 'ctrl+k']), а до результатів можна додати дії через getGlobalSearchResultActions().

Докладніше в документації: Глобальний пошук

Filament працює й без додаткових кроків, але кілька команд помітно впливають на швидкість і на те, чи взагалі відкриється панель на продакшені.

1. Кешування компонентів і іконок:

php artisan filament:optimize

Це скорочення для двох команд:

  • filament:cache-components - індексує ресурси, сторінки, віджети й relation managers у bootstrap/cache/filament, щоб не сканувати каталоги на кожному запиті;
  • icons:cache - кешує Blade Icons, які Filament використовує всюди.

Скинути кеш - php artisan filament:optimize-clear.

Локально кеш компонентів вмикати не варто: нові ресурси й сторінки не з'являться, доки кеш не перебудувати.

2. Публікація ресурсів після оновлення пакета. У composer.json Laravel-проєкту з Filament зазвичай є скрипт:

"post-autoload-dump": [
    "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
    "@php artisan package:discover --ansi",
    "@php artisan filament:upgrade"
]

filament:upgrade оновлює опубліковані CSS і JavaScript Filament. Якщо ці файли не оновилися (наприклад, composer install запускався з --no-scripts), після оновлення пакета панель може зламатися через старі ресурси.

3. Звичайні оптимізації Laravel - config:cache, route:cache, view:cache - Filament підтримує.

4. Доступ. На продакшені користувач має реалізувати FilamentUser::canAccessPanel(), інакше всі отримають 403.

5. Черги. Імпорт і експорт, а також сповіщення в базу даних і через трансляцію працюють через черги. Без запущеного воркера імпорт «зависне», а сповіщення не з'являться.

6. Файли. FileUpload за замовчуванням зберігає файли з видимістю private. Якщо зображення мають бути публічними, потрібні ->visibility('public'), правильний APP_URL і storage:link для диска public.

Докладніше в документації: Деплой

Relation manager - таблиця пов'язаних записів, що показується під формою редагування чи переглядом запису ресурсу: коментарі поста, товари категорії, ролі користувача.

php artisan make:filament-relation-manager CategoryResource posts title

Після створення його реєструють у ресурсі:

public static function getRelations(): array
{
    return [PostsRelationManager::class];
}

Attach чи associate - залежить від типу зв'язку:

Дії Зв'язок Що відбувається
AttachAction, DetachAction, DetachBulkAction BelongsToMany, MorphToMany рядок у pivot-таблиці додається чи видаляється, сам запис лишається
AssociateAction, DissociateAction HasMany, MorphMany у пов'язаного запису змінюється зовнішній ключ (чи стає null)
CreateAction, EditAction, DeleteAction будь-який створення, редагування й видалення самих пов'язаних записів

Прапорці --attach і --associate команди генерації одразу додають відповідні дії.

Detach проти Delete: DetachAction лише розриває зв'язок, а DeleteAction видаляє сам запис - для всіх, хто з ним пов'язаний. Плутанина тут коштує даних.

Режим лише для читання. На сторінці перегляду (ViewRecord) relation manager-и за замовчуванням ховають дії зміни - щоб сторінка перегляду лишалася переглядом. Вимкнути можна для одного менеджера (перевизначити isReadOnly() і повернути false) або для всієї панелі через ->readOnlyRelationManagersOnResourceViewPagesByDefault(false).

Нестандартна назва зворотного зв'язку: Filament обмежує запити через зворотний зв'язок (від поста до категорії). Якщо він називається не за правилами Laravel - ->inverseRelationship('section') у table().

Бейджі на вкладці:

public static function getBadge(Model $ownerRecord, string $pageClass): ?string
{
    return (string) $ownerRecord->posts()->count();
}

Пам'ятайте, що це запит на кожне відкриття сторінки - для важких підрахунків є відкладене завантаження бейджів.

Доступ: canViewForRecord() за замовчуванням перевіряє політику viewAny пов'язаної моделі. CreateAction, EditAction, DeleteAction перевіряють відповідні методи політики (create, update, delete). А от AttachAction, DetachAction, AssociateAction і DissociateAction політику за замовчуванням не перевіряють - лише режим «тільки для читання». Якщо прив'язувати записи можна не всім, обмежте ці дії явно (->authorize(...) чи ->visible(...)), інакше будь-хто з доступом до сторінки редагування може змінювати зв'язки.

М'яке видалення: прапорець --soft-deletes додає фільтр видалених, відновлення й остаточне видалення.

Докладніше в документації: Filament: керування зв'язками

Filament має два рівні налаштування вигляду: CSS (як усе виглядає) і render hooks (що ще вивести в певному місці інтерфейсу).

1. Власна тема. Стандартна панель використовує вже скомпільований CSS Filament - власні класи Tailwind у ваших Blade-шаблонах сторінок і віджетів у ньому відсутні й просто не працюють. Для цього потрібна тема:

php artisan make:filament-theme admin

Команда встановлює залежності Tailwind CSS, створює resources/css/filament/admin/theme.css, додає файл у input плагіна Laravel у vite.config.js і реєструє тему в провайдері панелі:

return $panel->viteTheme('resources/css/filament/admin/theme.css');

Після цього тему збирає Vite (npm run build для продакшену). У CSS-файлі теми можна додати власні стилі й джерела класів (@source для ваших шаблонів).

Простіші налаштування без теми: кольори панелі - ->colors([...]), шрифт - ->font(...), логотип - ->brandLogo(...). CSS hooks - класи на кшталт fi-sidebar чи fi-btn, на які Filament розраховує для перевизначення стилів у вашій темі.

2. Render hooks - точки в шаблонах Filament, куди можна вставити свій HTML: банер, лічильник, скрипт аналітики, кнопку в шапці.

use Filament\Support\Facades\FilamentView;
use Filament\View\PanelsRenderHook;
use Illuminate\Contracts\View\View;

FilamentView::registerRenderHook(
    PanelsRenderHook::BODY_START,
    fn (): View => view('filament.maintenance-banner'),
);

// або в конфігурації конкретної панелі
$panel->renderHook(PanelsRenderHook::USER_MENU_BEFORE, fn () => view('filament.env-badge'));

Область дії (scopes) - хук лише на певних сторінках:

FilamentView::registerRenderHook(
    PanelsRenderHook::PAGE_START,
    fn (): View => view('warning-banner'),
    scopes: [EditUser::class, CreateUser::class],
);

Що варто знати:

  • хук виконується на кожному рендері - важкі запити в замиканні сповільнюють усю панель;
  • через $panel->renderHook() хук прив'язаний до однієї панелі, через FilamentView - до всіх;
  • перевизначення шаблонів пакета (vendor:publish з видами Filament) ламається при оновленнях - render hooks і CSS hooks існують саме для того, щоб цього не робити;
  • власні Blade-шаблони (сторінки, віджети) з класами Tailwind без теми виглядатимуть «голими» - це найчастіша причина питання «чому мої класи не працюють».

Докладніше в документації: Filament: render hooks

Filament будує таблицю з Eloquent-запиту, тож усі звичайні проблеми продуктивності лишаються - але ховаються за декларативним синтаксисом.

N+1 у колонках. Колонка зі звʼязком робить запит на рядок:

TextColumn::make('company.name')   // + 1 запит на кожен рядок

Лікується модифікацією запиту таблиці:

public static function getEloquentQuery(): Builder
{
    return parent::getEloquentQuery()->with(['company', 'technologies']);
}

Лічильники. ->counts('vacancies') замість завантаження всієї колекції заради count().

Дорогі обчислення в state(). Замикання виконується для кожного рядка, тож звернення до бази всередині - те саме N+1, лише написане інакше.

Сортування й пошук за звʼязком роблять join, і без індексу на зовнішньому ключі це помітно:

TextColumn::make('company.name')
    ->searchable()      // where по приєднаній таблиці
    ->sortable()

Що ще сповільнює адмінку:

  • ->paginated([10, 25, 50, 'all']) з опцією all на великій таблиці вивантажує все в памʼять одним кліком користувача.
  • Глобальний пошук по багатьох ресурсах виконує запит на кожен зареєстрований ресурс.
  • Мініатюри без кешу: ImageColumn, що генерує прев'ю на льоту, робить це для кожного рядка.
  • Опції Select без ->searchable() вивантажують усі записи довідника у форму.

Як шукати причину: Debugbar або Telescope на сторінці ресурсу покажуть кількість запитів. Таблиця на 25 рядків має вкладатися в одиниці запитів; десятки - ознака незавантаженого звʼязку.

Multi-tenancy у Filament - це панель, де кожен користувач працює в межах «тенанта» (команди, компанії, організації), і бачить лише його дані.

public function panel(Panel $panel): Panel
{
    return $panel->tenant(Team::class);
}

Модель користувача реалізує HasTenants: getTenants() повертає тенантів, до яких у нього є доступ, canAccessTenant() - чи можна відкрити конкретного. Поточний тенант - у URL панелі й доступний через Filament::getTenant().

Що Filament робить автоматично:

  • Глобальний скоуп на запити ресурсів панелі: таблиця показує лише записи поточного тенанта, а чужий запис за URL дає 404.
  • Прив'язка нових записів до тенанта при створенні через ресурс.

Де дані можуть протекти:

  • Моделі без ресурсу в панелі скоуп не отримують. Запит до них у віджеті чи власній сторінці поверне дані всіх тенантів. Рішення - tenant-middleware, що додає глобальні скоупи, або явна фільтрація.
  • Код поза панеллю (черги, команди, API, сервіс-провайдери) не знає поточного тенанта - там скоупу немає.
  • withoutGlobalScopes() без аргументів знімає й скоуп тенанта.
  • Валідація unique і exists у Laravel не використовує Eloquent-скоупи, тож «email уже зайнятий» перевіриться по всіх тенантах. Для повної ізоляції - scopedUnique() / scopedExists().
  • Власні властивості Livewire-сторінок з моделями Filament повторно не запитує й не авторизує - їх треба перевіряти самостійно.

Висновок: вбудована tenancy - зручний інструмент, але не гарантія ізоляції. Документація Filament прямо попереджає: безпека реалізації - відповідальність розробника. Тести на «користувач тенанта A не бачить даних тенанта B» для кожного ресурсу й віджета - обов'язкові.

Докладніше в документації: Multi-tenancy