Senior: питання на співбесіді з теми «Дії, інфолисти й віджети»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
4 питання
ImportAction дає користувачу завантажити CSV, зіставити його колонки з полями моделі й імпортувати рядки у фоні. Рядки, що не пройшли валідацію, збираються в окремий CSV «невдалих рядків» для завантаження.
Підготовка:
php artisan make:queue-batches-table
php artisan make:notifications-table
php artisan vendor:publish --tag=filament-actions-migrations
php artisan migrate
php artisan make:filament-importer Product --generate
Імпорт використовує пакети джоб (job batches) і сповіщення в базі даних - без воркера черги нічого не відбудеться.
Імпортер:
use Filament\Actions\Imports\ImportColumn;
use Filament\Actions\Imports\Importer;
class ProductImporter extends Importer
{
protected static ?string $model = Product::class;
public static function getColumns(): array
{
return [
ImportColumn::make('sku')->requiredMapping()->rules(['required', 'max:32']),
ImportColumn::make('name')->requiredMapping()->rules(['required', 'max:255']),
ImportColumn::make('price')->numeric()->rules(['numeric', 'min:0']),
ImportColumn::make('category')->relationship(resolveUsing: 'slug'),
];
}
public function resolveRecord(): ?Product
{
// оновлювати наявні товари за SKU, нові - створювати
return Product::firstOrNew(['sku' => $this->data['sku']]);
}
}
ImportAction::make()
->importer(ProductImporter::class)
->chunkSize(250)
->maxRows(50_000);
CSV ділиться на частини (за замовчуванням по 100 рядків), кожна обробляється окремою джобою.
Ризики й як їх закрити:
- немає авторизації кожного запису. Імпорт не викликає політики: хто може запустити імпорт, той створить чи оновить будь-який запис, який поверне
resolveRecord(). Для недовірених користувачів перевірки треба додати в хукиbeforeCreate()/beforeUpdate()імпортера; resolveRecord()за слабким ключем (назва, email без нормалізації) - дублікати чи перезапис чужих даних;- CSV-формули. У файлі невдалих рядків значення лишаються як були. Комірка
=HYPERLINK(...)чи=cmd|..., відкрита в Excel, може виконатися як формула. Варто попереджати користувачів або очищати значення, що починаються з=,+,-,@; - файл невдалих рядків за замовчуванням може завантажити лише користувач, що запустив імпорт. Власна політика
ImportPolicyзамінює цю логіку повністю - перевірку автора треба повторити в ній; - розмір.
maxRows()і розумнийchunkSize()захищають від файлу на мільйони рядків, що «покладе» чергу; - кастинг перед валідацією - ціни з комою (
12,50) чи дати вдд.мм.рррртреба привести вcastStateUsing(), інакше всі рядки впадуть на валідації.
ExportAction формує CSV чи XLSX у фоні: користувач вибирає колонки, отримує сповіщення, коли файл готовий, і завантажує його. Під капотом - пакети джоб і сповіщення в базі даних.
php artisan make:filament-exporter Order --generate
use Filament\Actions\Exports\ExportColumn;
use Filament\Actions\Exports\Exporter;
class OrderExporter extends Exporter
{
protected static ?string $model = Order::class;
public static function getColumns(): array
{
return [
ExportColumn::make('number'),
ExportColumn::make('customer.name')->label('Клієнт'),
ExportColumn::make('items_count')->counts('items'),
ExportColumn::make('total'),
ExportColumn::make('created_at'),
];
}
}
use Filament\Actions\ExportAction;
use Filament\Actions\Exports\Enums\ExportFormat;
use Illuminate\Database\Eloquent\Builder;
ExportAction::make()
->exporter(OrderExporter::class)
->formats([ExportFormat::Xlsx, ExportFormat::Csv])
->modifyQueryUsing(fn (Builder $query) => $query->whereBelongsTo(auth()->user()->team))
->fileDisk('s3')
->chunkSize(500)
->maxRows(100_000);
Звідки беруться дані. У таблиці експорт бере поточний запит таблиці - з пошуком, фільтрами й сортуванням. Користувач експортує те, що бачить у списку (усі сторінки).
Безпека:
- політики для кожного запису не перевіряються. Якщо таблиця показує лише «свої» записи завдяки скоупу ресурсу - добре. Але експорт поза таблицею (з кнопки на сторінці) бере всю модель. Обмеження варто дублювати в
modifyQueryUsing()або вmodifyQuery()експортера; - диск. Готові файли зберігаються на диску. Filament навмисно уникає публічного диска (якщо диск за замовчуванням
publicі єlocal, експорт піде вlocal), але на продакшені краще явно вказати приватнийs3/r2; - CSV-формули. Значення з бази потрапляють у файл як є. Текст, що починається з
=,+,-,@і вводився користувачами, Excel може виконати. Захист -formatStateUsing()на колонках з довільним текстом (наприклад, префікс'); - чутливі колонки (телефони, email, суми) варто не додавати в експортер «про всяк випадок» - користувач вибирає з того, що ви дозволили.
Продуктивність:
- експорт ділиться на частини (за замовчуванням по 100 рядків) - для простих рядків
chunkSize()можна збільшити, для важких зменшити; - зв'язки й агрегати в колонках варто жадібно завантажити в
modifyQuery()експортера, інакше кожна частина робить N+1; maxRows()не дасть випадково поставити в чергу мільйонний експорт.
У Filament три способи доставити сповіщення, і вони розв'язують різні задачі:
| Спосіб | Куди | Зберігається | Коли отримує |
|---|---|---|---|
send() |
поточний користувач, через сесію | ні | на наступному рендері |
sendToDatabase($user) |
таблиця notifications |
так | при опитуванні (30 с) чи через вебсокет |
broadcast($user) |
вебсокет-канал користувача | ні | миттєво, якщо сторінка відкрита |
Broadcast-сповіщення - тимчасовий «тост», що з'являється в браузері користувача в реальному часі, але ніде не зберігається. Якщо вкладка закрита - сповіщення втрачено.
Notification::make()
->title('Експорт готовий')
->success()
->broadcast($user);
Налаштування (окрім звичайного broadcasting у Laravel - Reverb чи Pusher):
- опублікувати конфіг Filament (
php artisan vendor:publish --tag=filament-config); - у
config/filament.phpрозкоментувати й заповнити розділbroadcasting.echo; - переконатися, що в
.envє потрібні змінніVITE_*; - мати запущені воркер черги (сповіщення ставиться в чергу) і вебсокет-сервер.
Поєднання найкращого з двох світів - сповіщення в базі даних з миттєвою доставкою:
Notification::make()
->title('Новий відгук на вакансію')
->sendToDatabase($recruiter, isEventDispatched: true);
Запис зберігається в базі (видно в дзвіночку й пізніше), а подія через вебсокет змушує панель одразу підтягнути нові сповіщення, без очікування опитування. Після цього опитування можна вимкнути: databaseNotificationsPolling(null).
Що обирати:
- результат дії, яку користувач щойно виконав, -
send(); - щось, що користувач має побачити навіть пізніше (призначене завдання, відгук, результат імпорту) -
sendToDatabase(); - миттєва, але неважлива подія для відкритої сторінки (хтось редагує той самий запис) -
broadcast().
Пастки:
- приватні канали авторизуються через
routes/channels.php- перевірте, що користувач отримує лише свій канал; - черга й вебсокет-сервер - дві окремі точки відмови: без воркера сповіщення не відправиться, без Reverb - не дійде;
- опитування на сотнях вкладок часто дорожче за один вебсокет-сервер - на великих панелях broadcast окупається.
Дії тестують через livewire() на тій сторінці чи компоненті, де вони розміщені.
Дія на сторінці (наприклад, у шапці сторінки редагування):
use App\Filament\Resources\Orders\Pages\EditOrder;
use App\Models\Order;
use function Pest\Livewire\livewire;
it('cancels an order', function () {
$order = Order::factory()->paid()->create();
livewire(EditOrder::class, ['record' => $order->getRouteKey()])
->callAction('cancel', data: ['reason' => 'Клієнт передумав'])
->assertHasNoFormErrors()
->assertNotified('Замовлення скасовано');
expect($order->refresh()->status)->toBe(OrderStatus::Cancelled);
});
Дія в рядку таблиці - через TestAction:
use Filament\Actions\Testing\TestAction;
livewire(ListOrders::class)
->callAction(TestAction::make('cancel')->table($order), data: ['reason' => '...']);
// масова дія
livewire(ListOrders::class)
->selectTableRecords($orders->pluck('id')->all())
->callAction(TestAction::make('cancel')->table()->bulk());
Валідація даних модального вікна:
livewire(EditOrder::class, ['record' => $order->getRouteKey()])
->callAction('cancel', data: ['reason' => ''])
->assertHasFormErrors(['reason' => 'required']);
Зупинка дії ($action->halt() у хуку):
livewire(EditOrder::class, ['record' => $shipped->getRouteKey()])
->callAction('cancel')
->assertActionHalted('cancel');
Видимість і права:
$this->actingAs(User::factory()->support()->create());
livewire(ListOrders::class)
->assertActionHidden(TestAction::make('refund')->table($order))
->assertActionVisible(TestAction::make('view')->table($order));
Сповіщення:
->assertNotified()/->assertNotified('Заголовок')/->assertNotNotified()- для сповіщень через сесію;- сповіщення в базі даних - це звичайні сповіщення Laravel:
Notification::fake()(фасад Laravel) іassertSentTo($user, \Filament\Notifications\DatabaseNotification::class).
Що варто перевіряти обов'язково:
- заборону, а не лише успіх: користувач без прав не бачить дію, а підроблений виклик нічого не змінює в базі;
- побічні ефекти через фейки:
Mail::fake(),Queue::fake(),Bus::fake(), - а не реальні листи й джоби; - для дій у шапці таблиці -
TestAction::make('export')->table()без запису.