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

Middle: питання на співбесіді з теми «Inertia й Laravel»

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

4 питання

Деякі дані потрібні майже на кожній сторінці: поточний користувач у шапці, назва застосунку, налаштування. Передавати їх у кожному контролері незручно - для цього є спільні дані в middleware HandleInertiaRequests:

class HandleInertiaRequests extends Middleware
{
    public function share(Request $request): array
    {
        return [
            ...parent::share($request),
            'appName' => config('app.name'),
            'auth' => [
                'user' => fn () => $request->user()?->only('id', 'name', 'avatar_url'),
            ],
        ];
    }
}
<script setup>
import { computed } from 'vue';
import { usePage } from '@inertiajs/vue3';

const page = usePage();
const user = computed(() => page.props.auth.user);
</script>

Спільні дані зливаються з props сторінки - тож простір імен (auth.user) захищає від зіткнень із props контролерів.

Головні правила:

  • спільні дані йдуть з кожною відповіддю - кожен перехід і кожне часткове перезавантаження. Важкий запит тут - податок на всі сторінки. Замикання (fn () => ...) обчислюються лише тоді, коли prop справді потрібен;
  • рідко змінні дані (список країн, налаштування) - Inertia::once() чи метод shareOnce(): клієнт запам'ятовує значення й не отримує його з кожною відповіддю;
  • усе видно в браузері. $request->user() без only() віддасть усі атрибути моделі в JSON кожної сторінки - email, телефон, службові прапорці, усе, що не в $hidden. Передавайте явний перелік полів чи ресурс;
  • ролі й права - готовими булевими значеннями для інтерфейсу (can.manageUsers), а не списком усіх дозволів системи.

Валідаційні помилки адаптер Laravel уже додає в спільні дані (errors), а флеш-дані в Inertia 3 мають окремий механізм Inertia::flash().

Версія ассетів визначається в тому самому middleware (метод version()) - для Vite вона обчислюється автоматично.

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

У класичній SPA сервер на помилку валідації повертає 422 з JSON, а клієнт розкладає помилки по полях. Inertia працює інакше - так само, як звичайні форми Laravel:

  1. форма відправляється Inertia-запитом;
  2. $request->validate() не проходить - Laravel перенаправляє назад і кладе помилки в сесію;
  3. адаптер Inertia додає помилки з сесії в props сторінки (errors);
  4. Inertia бачить непорожній page.props.errors і вважає відправку невдалою - викликає onError, а не onSuccess.

Відповідей 422 Inertia-застосунок не генерує взагалі.

public function store(Request $request)
{
    $request->validate([
        'email' => ['required', 'email', 'unique:users'],
        'name' => ['required', 'max:100'],
    ]);
    // ...
}

З useForm помилки потрапляють у form.errors автоматично. Без нього - через prop:

<script setup>
defineProps({ errors: Object });
</script>
<template>
  <p v-if="errors.email">{{ errors.email }}</p>
</template>

Введене не губиться: після перенаправлення назад Inertia за замовчуванням зберігає стан компонента сторінки, тож поля не очищаються.

Error bags - для сторінки з кількома формами з однаковими назвами полів (дві форми з полем email):

router.post('/companies', data, { errorBag: 'createCompany' });
router.post('/users', data, { errorBag: 'createUser' });
// помилки прийдуть у page.props.errors.createCompany / .createUser

З useForm error bags не потрібні - помилки й так прив'язані до екземпляра форми.

Усі помилки поля, а не лише перша. За замовчуванням адаптер Laravel повертає по одній помилці на поле. Для всіх - protected $withAllErrors = true; у HandleInertiaRequests; тоді кожне поле містить масив рядків.

Часткові перезавантаження й помилки. errors передається завжди, тож порожній набір помилок з сервера затре клієнтські. Щоб зберегти помилки під час router.reload({ only: [...] }), у Inertia 3 є опція preserveErrors: true.

Попередня валідація (до відправки) - Laravel Precognition, підтримку якої вбудовано у форми Inertia.

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

Сторінка списку користувачів має props users і companies (для фільтра). Користувач змінює фільтр - потрібні нові users, а companies не змінилися. Отримувати й перераховувати все - марна робота.

Часткове перезавантаження - запит лише вибраних props тієї самої сторінки:

import { router } from '@inertiajs/vue3';

router.reload({ only: ['users'], data: { company: 3 } });
router.reload({ except: ['companies'] });
<Link href="/users?active=1" :only="['users']">Активні</Link>

Працює лише для візитів на той самий компонент сторінки.

Щоб сервер справді не виконував зайве - props мають бути замиканнями:

return Inertia::render('Users/Index', [
    'users' => fn () => User::query()->filter(request()->all())->paginate(),
    'companies' => fn () => Company::orderBy('name')->get(['id', 'name']),
]);

Без замикання Company::all() виконається на кожен запит, навіть якщо результат не потрапить у відповідь.

Типи props:

Запис Звичайний візит Часткове перезавантаження Обчислюється
User::all() так за запитом завжди
fn () => User::all() так за запитом лише коли потрібен
Inertia::optional(fn () => ...) ні за запитом лише коли потрібен
Inertia::always(...) так завжди завжди
  • Inertia::optional() - prop, який сторінка отримує лише на явний запит (only): дані для вкладки, яку відкривають рідко. У Inertia 3 він замінив видалений Inertia::lazy();
  • Inertia::always() - потрапляє навіть у часткові перезавантаження (так передаються помилки валідації).

Inertia 3: only/except підтримують крапкову нотацію для вкладених props (only: ['auth.notifications']), а optional, defer, merge працюють на будь-якій глибині масиву.

Поєднання з once(): Inertia::optional(fn () => ...)->once() - завантажити один раз і пам'ятати на клієнті між переходами.

Докладніше в документації: Inertia: часткові перезавантаження

Повідомлення «Користувача створено» має з'явитися один раз після дії й не повторюватися. Раніше в Inertia-застосунках його передавали через session()->flash() і спільні дані middleware. У Inertia 3 для цього є окремий механізм - флеш-дані.

На сервері:

use Inertia\Inertia;

public function store(StoreUserRequest $request)
{
    $user = User::create($request->validated());

    Inertia::flash('toast', ['type' => 'success', 'message' => 'Користувача створено']);

    return back();
    // або коротше: return Inertia::flash('newUserId', $user->id)->back();
}

flash() можна поєднувати і з render(): Inertia::render(...)->flash('highlight', $id).

На клієнті - у page.flash, окремо від props:

<script setup>
import { usePage } from '@inertiajs/vue3';
const page = usePage();
</script>

<template>
  <div v-if="page.flash.toast" class="toast">{{ page.flash.toast.message }}</div>
</template>

Або реагувати подією - в одному місці макета:

router.on('flash', (event) => {
  if (event.detail.flash.toast) showToast(event.detail.flash.toast);
});

router.post('/users', data, {
  onFlash: ({ newUserId }) => highlight(newUserId),
});

Чим кращий за флеш через сесію в props:

  • не потрапляє в історію браузера. Props зберігаються в стані історії: користувач натиснув «Назад» - і старе «Збережено» знову на екрані. Флеш-дані в історію не пишуться;
  • не змішується з props сторінки;
  • працює однаково з редиректом і без нього: middleware сам зберігає флеш у сесію при перенаправленні.

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

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

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