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

Питання на співбесіді з Laravel

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

378 питань

abort() кидає HttpException з указаним кодом - обробник винятків перетворює його на відповідь з цим статусом:

abort(404);
abort(403, 'Цей проєкт архівовано.');

abort_if(! $user->isAdmin(), 403);
abort_unless($post->isPublished(), 404);

Після abort() код далі не виконується - це виняток, а не return.

Коли що використовувати:

  • 404 - ресурсу немає чи користувачу не можна знати, що він існує;
  • 403 - ресурс є, але доступ заборонено. Для перевірки прав краще політики ($this->authorize(), Gate) - вони кидають той самий 403, але логіка прав зібрана в одному місці;
  • findOrFail() замість ручного abort(404) після find() - коротше й без забутої перевірки.

Власні сторінки помилок - просто Blade-шаблони за кодом статусу:

resources/views/errors/404.blade.php
resources/views/errors/403.blade.php
resources/views/errors/500.blade.php
resources/views/errors/503.blade.php   # режим обслуговування

Усередині доступний виняток:

<h1>{{ $exception->getMessage() ?: 'Сторінку не знайдено' }}</h1>

Запасні сторінки для групи кодів: 4xx.blade.php і 5xx.blade.php використовуються, якщо немає шаблону для конкретного коду.

Стандартні шаблони Laravel можна скопіювати як основу:

php artisan vendor:publish --tag=laravel-errors

Типові пастки:

  • сторінка 500 не повинна залежати від того, що могло зламатися: запитів до бази, сесії, складних компонентів. Якщо помилку спричинила база, сторінка помилки, що звертається до бази, впаде теж;
  • повідомлення в abort(403, '...') бачить користувач - жодних технічних деталей;
  • для API шаблони не використовуються: при Accept: application/json Laravel повертає {"message": "..."} з відповідним статусом.

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

Laravel використовує Monolog, а писати в лог можна через фасад Log чи хелпер logger():

use Illuminate\Support\Facades\Log;

Log::info('Замовлення оформлено', ['order_id' => $order->id]);
Log::warning('Платіжний шлюз відповідає повільно', ['ms' => $elapsed]);
Log::error('Не вдалося надіслати рахунок', ['order_id' => $order->id]);

logger('Коротке налагоджувальне повідомлення');   // рівень debug

Рівні логування (за RFC 5424, від найважливішого):

Рівень Коли використовувати
emergency система непрацездатна
alert потрібна негайна дія (база недоступна)
critical критична помилка компонента
error помилка, що не зупиняє застосунок, але потребує уваги
warning щось незвичне, але не помилка (повторна спроба, повільна відповідь)
notice нормальна, але важлива подія
info звичайні події: вхід користувача, оформлення замовлення
debug подробиці для налагодження

Мінімальний рівень задається для каналу (змінна LOG_LEVEL). На продакшені зазвичай info чи warning: повідомлення debug просто відкидаються, не засмічуючи лог.

Другий аргумент - контекст: масив з даними, а не склеєний рядок.

// погано: неможливо шукати й фільтрувати
Log::info("Order {$order->id} paid by {$user->email}");

// добре: структуровані дані
Log::info('Order paid', ['order_id' => $order->id, 'user_id' => $user->id]);

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

Куди пишеться: за замовчуванням у storage/logs/laravel.log (канал stack з single). Канали налаштовуються в config/logging.php.

Чого не писати в лог: паролі, токени, повні номери карток, персональні дані понад необхідне. Логи часто мають ширший доступ, ніж база даних, і зберігаються довше.

Не плутати з dd() і dump(): вони виводять у відповідь і для продакшену не підходять, а лог працює і в черзі, і в консольних командах, і без браузера.

Докладніше в документації: Логування: запис повідомлень

Контролер чи замикання маршруту може повернути майже будь-що - Laravel сам перетворить результат на об'єкт Response:

Що повертаємо Що отримує клієнт
рядок text/html з цим рядком
масив JSON
модель Eloquent чи колекція JSON (з урахуванням $hidden, $appends)
view('posts.show', [...]) HTML з шаблону
response(...) / response()->json(...) повний контроль: статус, заголовки, cookie
redirect(...) відповідь 302 з заголовком Location
API Resource JSON у форматі ресурсу
об'єкт з інтерфейсом Responsable те, що поверне його toResponse()
Route::get('/ping', fn () => 'pong');
Route::get('/settings', fn () => ['theme' => 'dark']);
Route::get('/users/{user}', fn (User $user) => $user);

return response('Created', 201)
    ->header('X-Request-Id', $requestId)
    ->cookie('seen_intro', '1', minutes: 60 * 24 * 30);

return response()->json(['message' => 'Accepted'], 202);
return response()->noContent();     // 204

Чому краще явні відповіді для API:

  • модель напряму віддає всі її атрибути, крім $hidden. Додали нову колонку в таблицю - вона автоматично з'явилася в API. API Resource описує формат явно;
  • статус-код: масив чи модель завжди повертають 200, а створення ресурсу - це 201, видалення - 204;
  • заголовки (кешування, розташування нового ресурсу) задаються лише через об'єкт відповіді.

Відповідь має бути повернута: її треба саме повернути з контролера. Виклик response(...) без return нічого не робить. Щоб перервати виконання з будь-якого місця, використовують виняток: abort(404), throw ValidationException::withMessages(...) чи власний виняток з методом render().

Middleware бачить уже готову відповідь: після $next($request) можна додати заголовки чи cookie до будь-якої відповіді, незалежно від того, що повернув контролер.

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

Основні способи:

return redirect('/dashboard');                         // за шляхом
return redirect()->route('posts.show', $post);         // за іменем маршруту (модель підставить ключ)
return to_route('posts.show', ['post' => $post]);      // коротша форма
return redirect()->action([PostController::class, 'index']);
return back();                                         // на попередню сторінку
return redirect()->away('https://partner.example.com'); // на зовнішній домен без перевірки URL

Разом з повідомленням - флеш-дані сесії, доступні лише в наступному запиті:

return redirect()->route('posts.index')
    ->with('status', 'Публікацію збережено.');
@if (session('status'))
    <div class="alert">{{ session('status') }}</div>
@endif

Повернути з введеними даними й помилками (якщо перевіряєте вручну, а не через валідацію):

return back()
    ->withInput()
    ->withErrors(['email' => 'Цю адресу вже зареєстровано.']);

У формі введене повертається через old('email'), помилки - через @error('email'). Валідація Laravel робить це сама: при помилці вона кидає виняток, що перетворюється саме на такий редирект.

Патерн Post/Redirect/Get: після успішної обробки форми (POST) - завжди редирект, а не відображення сторінки. Інакше оновлення сторінки в браузері повторно відправить форму: другий платіж, дубль коментаря.

Типові помилки:

  • redirect() без return - нічого не відбувається, виконання триває;
  • back() без запасного варіанта: якщо користувач прийшов за прямим посиланням, «назад» може вести на зовнішній сайт чи нікуди. back(fallback: route('home'));
  • редирект за URL з параметра запиту (redirect($request->input('next'))) - відкритий редирект, яким користуються фішери. Для повернення після входу - redirect()->intended(...), а сторонні адреси - лише зі списку дозволених;
  • away() для внутрішніх адрес - він не перевіряє URL взагалі; для своїх сторінок - route().

Коди: redirect() за замовчуванням дає 302. Постійне перенесення сторінки (SEO) - 301: redirect('/new', 301) чи Route::permanentRedirect('/old', '/new') у маршрутах.

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

Три методи для трьох ситуацій:

1. download - завантажити файл, що вже є на диску:

return response()->download(storage_path('app/reports/2026-q3.pdf'));
return response()->download($path, 'Звіт за III квартал.pdf');   // інша назва для користувача
return response()->download($path)->deleteFileAfterSend();      // тимчасовий файл

Заголовок Content-Disposition: attachment - браузер пропонує зберегти файл.

2. file - показати файл у браузері (зображення, PDF):

return response()->file(storage_path('app/invoices/42.pdf'));

Content-Disposition: inline - PDF відкриється у вбудованому переглядачі.

3. streamDownload - файл, якого немає на диску, генерується на льоту:

return response()->streamDownload(function () {
    $out = fopen('php://output', 'w');
    fputcsv($out, ['id', 'email']);

    User::query()->lazyById(1000)->each(function (User $user) use ($out) {
        fputcsv($out, [$user->id, $user->email]);
    });

    fclose($out);
}, 'users.csv', ['Content-Type' => 'text/csv']);

Дані віддаються частинами - експорт мільйона рядків не займає мільйона рядків пам'яті й не створює тимчасових файлів.

З дисків Storage (зокрема S3):

return Storage::download('reports/2026-q3.pdf', 'report.pdf');
return Storage::disk('s3')->response('avatars/42.jpg');   // показати

Безпека - головне при віддачі файлів:

  • шлях ніколи не будується з введення користувача напряму: download(storage_path('app/' . $request->file)) дозволить завантажити ../../.env (обхід шляху). Файл шукають за ідентифікатором у базі, де записано шлях;
  • перевірка прав перед віддачею - політика чи Gate, як для будь-якого ресурсу;
  • приватні файли не кладуть у public/ чи публічний диск - вони мають віддаватися лише через контролер з перевіркою;
  • назва файлу від користувача - через download($path, $safeName), Laravel сам коректно закодує не-ASCII символи в заголовку.

Для великих файлів на S3 краще не проганяти їх через PHP взагалі: тимчасове підписане посилання Storage::temporaryUrl() - і браузер завантажує прямо зі сховища.

Докладніше в документації: Відповіді: завантаження файлів

Коли модель повертається з контролера чи перетворюється через toArray() / toJson(), у результат потрапляють усі атрибути з бази, а також завантажені зв'язки.

$hidden - атрибути, яких не має бути в масиві чи JSON:

class User extends Model
{
    protected $hidden = ['password', 'remember_token', 'two_factor_secret'];
}

У Laravel 13 те саме можна записати атрибутом класу:

#[Hidden(['password', 'remember_token'])]
class User extends Model {}

$visible - навпаки, білий список: лише ці атрибути. Безпечніше, бо нова колонка в таблиці не з'явиться у відповіді автоматично.

$appends - додати обчислене значення з аксесора:

protected $appends = ['is_admin'];

protected function isAdmin(): Attribute
{
    return Attribute::get(fn (): bool => $this->role === 'admin');
}

Без $appends аксесор працює в коді ($user->is_admin), але в JSON не потрапляє.

Тимчасово змінити видимість:

$user->makeVisible('email')->toArray();
$user->makeHidden('phone')->toArray();
$users->append('is_admin');      // для колекції
$user->setVisible(['id', 'name']);

Зв'язки: завантажені зв'язки серіалізуються разом з моделлю (ключ у snake_case). Щоб приховати зв'язок, його назву додають у $hidden.

Дати серіалізуються в ISO 8601 з UTC (2026-10-04T09:00:00.000000Z). Формат змінюють перевизначенням serializeDate() чи в касті: 'published_at' => 'datetime:Y-m-d'.

Підводні камені:

  • $appends з аксесором, що звертається до зв'язку, дає N+1 при серіалізації колекції - кожна модель завантажує зв'язок окремо;
  • $hidden - не захист: у коді атрибут доступний, і його можна вивести в шаблоні чи лог;
  • для API надійніше API Resources: формат описано явно, і він не залежить від того, які колонки є в таблиці.

Докладніше в документації: Серіалізація: приховування атрибутів

Laravel має два способи працювати з рядками, і обидва побудовані на тому самому наборі методів.

1. Статичний клас Str - кожен метод приймає рядок і повертає новий рядок:

use Illuminate\Support\Str;

Str::limit('Довгий текст статті', 10);      // 'Довгий тек...'
Str::headline('laravel_framework');          // 'Laravel Framework'
Str::mask('olena@example.com', '*', 3);      // 'ole**************'
Str::contains($title, ['Laravel', 'PHP']);
Str::uuid();

2. Fluent-рядки - Str::of() чи коротший хелпер str() повертають об'єкт Stringable, і методи можна складати в ланцюжок:

$slug = str('  Нова версія Laravel  ')
    ->squish()                 // прибрати зайві пробіли
    ->limit(50, '')
    ->slug('-', 'uk');

Str::of($path)->afterLast('/')->beforeLast('.');

Між Str::of('...') і str('...') різниці немає: це той самий Stringable. А виклик str() без аргументів повертає об'єкт, що перенаправляє виклики до статичних методів Str: str()->uuid().

Коли що обирати:

Ситуація Що зручніше
одна операція Str::limit($text, 100)
кілька перетворень поспіль str($text)->squish()->lower()->...
умовні кроки str($x)->when($cond, fn ($s) => $s->append('...'))

Stringable - не рядок. Він приводиться до рядка в echo, Blade і конкатенації, але === з рядком дасть false, а функція з типом string у строгому режимі отримає об'єкт. Тому в кінці ланцюжка часто викликають ->toString() чи ->value():

$name = str($input)->trim()->title()->toString();

Кирилиця: методи Str враховують багатобайтові символи - str('Привіт')->length() повертає 6, а не 12, як strlen(). Але Str::slug() і Str::ascii() транслітерують за правилами мови, яку треба вказати явно, якщо результат має бути українською транслітерацією.

Перед тим як писати власну функцію для рядків варто переглянути список методів Str: там уже є between, excerpt, initials, wordCount, plural, squish, isUuid, isJson і десятки інших.

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

Клас Illuminate\Support\Number - обгортка над розширенням intl для форматування чисел з урахуванням локалі. Для більшості методів розширення intl обов'язкове.

use Illuminate\Support\Number;

Number::format(1234567.891, precision: 2, locale: 'uk');  // '1 234 567,89'
Number::currency(1500, in: 'UAH', locale: 'uk');          // '1 500,00 ₴'
Number::percentage(12.345, precision: 1);                 // '12.3%'
Number::fileSize(1536000, precision: 1);                  // '1.5 MB'
Number::abbreviate(1500000, precision: 1);                // '1.5M'
Number::forHumans(1500000);                               // '2 million'
Number::spell(42, locale: 'uk');                          // 'сорок два'
Number::ordinal(3);                                       // '3rd'
Number::clamp($value, min: 1, max: 100);

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

  • precision за замовчуванням 0 у fileSize, abbreviate і forHumans: Number::forHumans(1500000) округлює до '2 million'. Для дробової частини вказуйте precision;
  • fileSize рахує в степенях 1024, а не 1000;
  • forHumans і abbreviate не локалізовані: вони завжди пишуть англійською (million, M);
  • format, currency, percentage, spell приймають locale, а в українській локалі роздільник тисяч - нерозривний пробіл, десятковий - кома.

Локаль для всього застосунку чи фрагмента:

// у сервіс-провайдері
Number::useLocale('uk');
Number::useCurrency('UAH');

// тимчасово для одного блоку
Number::withLocale('de', fn () => Number::format(1234.5));   // '1.234,5'

Зворотна операція - розібрати введене користувачем число з урахуванням локалі:

Number::parseFloat('1 234,5', locale: 'uk');

Гроші: Number::currency() - лише для відображення. Рахувати гроші через float не можна: зберігайте суму в копійках цілим числом (або в decimal у базі) і форматуйте тільки при виводі:

Number::currency($order->total_cents / 100, in: 'UAH');

У Blade це зручно загорнути в accessor моделі чи Blade-компонент, щоб формат валюти й точність були однаковими на всьому сайті.

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

Вкладені масиви (конфігурація, JSON зі стороннього API, payload вебхука) зручно читати крапковою нотацією замість ланцюжка isset:

use Illuminate\Support\Arr;

$data = [
    'user' => ['profile' => ['name' => 'Olena']],
    'orders' => [['total' => 10], ['total' => 20]],
];

Arr::get($data, 'user.profile.name');          // 'Olena'
Arr::get($data, 'user.email', 'n/a');          // 'n/a' - значення за замовчуванням
Arr::has($data, 'user.profile.name');          // true

data_get - те саме, але для масивів і об'єктів разом і з підтримкою *:

data_get($data, 'orders.*.total');             // [10, 20]
data_get($request->json(), 'payment.status');
data_get($model, 'author.profile.city');       // працює й з Eloquent-моделями

Запис:

Arr::set($config, 'mail.from.address', 'hello@example.com');   // за посиланням
data_set($data, 'orders.*.paid', false);       // усім елементам
data_fill($data, 'meta.source', 'api');        // лише якщо ключа ще немає
Arr::forget($data, 'user.password');

Інші корисні методи:

Метод Що робить
Arr::only($a, ['name', 'email']) лише вказані ключі
Arr::except($a, ['password']) усі, крім вказаних
Arr::wrap($value) гарантує масив: null → [], 'a' → ['a']
Arr::dot($a) / Arr::undot($a) вкладений масив ↔ плаский з ключами a.b.c
Arr::pluck($a, 'user.name') значення поля з кожного елемента
Arr::first($a, fn ($v) => ...) перший елемент за умовою
Arr::string($a, 'key'), Arr::integer(...) значення з перевіркою типу, інакше виняток

Пастка крапкової нотації: якщо в ключі масиву справді є крапка ('version.major' => 1), Arr::get($a, 'version.major') спершу перевіряє точний ключ і знайде його, але data_get і запис через Arr::set трактуватимуть крапку як вкладеність. Для таких даних краще звичайний доступ $a['version.major'].

Arr::wrap особливо корисний для параметрів, що приймають «одне значення або масив»: foreach (Arr::wrap($recipients) as $email) - без перевірок типу.

Для колекцій є ті самі можливості: collect($data)->get(...), ->pluck('user.name'), ->dot(). Якщо дані обробляються ланцюжком перетворень - колекція зручніша, для одного звернення - Arr чи data_get.

Докладніше в документації: Хелпери: data_get

Laravel-пакет - це звичайний Composer-пакет, який інтегрується з фреймворком через сервіс-провайдер. Мінімальна структура:

acme/laravel-invoices/
├── composer.json
├── config/invoices.php
├── src/
│   ├── InvoicesServiceProvider.php
│   ├── InvoiceGenerator.php
│   └── Facades/Invoices.php
├── database/migrations/
├── resources/views/
└── tests/

Сервіс-провайдер - точка входу: реєструє класи в контейнері (register) і підключає конфігурацію, маршрути, представлення, міграції, команди (boot).

class InvoicesServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->mergeConfigFrom(__DIR__.'/../config/invoices.php', 'invoices');
        $this->app->singleton(InvoiceGenerator::class);
    }

    public function boot(): void
    {
        $this->loadViewsFrom(__DIR__.'/../resources/views', 'invoices');
    }
}

Автоматичне виявлення (package discovery): провайдер і фасади описуються в composer.json пакета:

{
    "name": "acme/laravel-invoices",
    "autoload": {
        "psr-4": { "Acme\\Invoices\\": "src/" }
    },
    "extra": {
        "laravel": {
            "providers": ["Acme\\Invoices\\InvoicesServiceProvider"],
            "aliases": { "Invoices": "Acme\\Invoices\\Facades\\Invoices" }
        }
    }
}

Після composer require скрипт php artisan package:discover (Laravel запускає його в post-autoload-dump) читає секції extra.laravel усіх встановлених пакетів і записує список у bootstrap/cache/packages.php. Користувачу не треба нічого додавати в bootstrap/providers.php.

Як вимкнути виявлення - у composer.json застосунку:

"extra": {
    "laravel": {
        "dont-discover": ["barryvdh/laravel-debugbar"]
    }
}

Наприклад, щоб підключати налагоджувальний пакет лише в локальному середовищі через власний провайдер.

Якщо пакет «не підхопився»: перевірити bootstrap/cache/packages.php, виконати php artisan package:discover і переконатися, що composer install не запускався з --no-scripts (тоді discover не виконується).

Залежності пакета: вимагати лише ті компоненти, які використовуються (illuminate/support, illuminate/database), а не весь laravel/framework - так пакет легше встановити й у застосунки на компонентах Laravel.

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

Коли пакет пишеться для конкретного застосунку (або виділяється з нього), незручно щоразу публікувати версію, щоб перевірити зміну. Path-репозиторій підключає пакет з локального каталогу.

~/code/
├── shop/                    # застосунок
└── packages/
    └── laravel-invoices/    # пакет

У composer.json застосунку:

{
    "repositories": [
        {
            "type": "path",
            "url": "../packages/laravel-invoices",
            "options": { "symlink": true }
        }
    ],
    "require": {
        "acme/laravel-invoices": "@dev"
    }
}
composer update acme/laravel-invoices

Composer створює символьне посилання vendor/acme/laravel-invoices → ../packages/laravel-invoices. Зміни в коді пакета видно в застосунку одразу, без повторного встановлення.

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

  • версія: @dev бере поточний стан каталогу. Якщо в пакеті є Git-теги чи поле version, можна вимагати й звичайне обмеження (^1.0);
  • автозавантаження: після додавання нових класів у пакет зазвичай достатньо symlink, але при зміні секції autoload у composer.json пакета - composer dump-autoload у застосунку;
  • package discovery спрацьовує як для звичайного пакета;
  • у Docker symlink на каталог поза проєктом не працюватиме, якщо цей каталог не змонтовано в контейнер. Часто пакет тримають усередині репозиторію (packages/ у корені) з "url": "packages/*";
  • symlink: false - копіювання замість посилання: стабільніше для CI чи образів, але зміни потребують composer update.

Не забути перед публікацією:

  • прибрати path-репозиторій з composer.json застосунку (чи тримати його лише локально) і вимагати опубліковану версію;
  • перевірити, що пакет встановлюється «з нуля» без застосунку - тести через Orchestra Testbench саме це й перевіряють.

Альтернатива для приватних пакетів: vcs-репозиторій (Git URL), Private Packagist чи Satis - тоді пакет встановлюється з тегів, як публічний.

Стартовий шаблон: замість ручного створення структури можна взяти spatie/package-skeleton-laravel - там уже налаштовані Testbench, Pest, PHPStan, GitHub Actions і публікація конфігурації.

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

Laravel AI SDK (laravel/ai) - офіційний пакет для роботи з великими мовними моделями та іншими AI-сервісами через єдиний API: OpenAI, Anthropic, Gemini, Mistral, DeepSeek, xAI, Ollama та інші. Код застосунку не залежить від конкретного провайдера - його можна змінити в конфігурації.

composer require laravel/ai

Ключі провайдерів задаються в .env (OPENAI_API_KEY, ANTHROPIC_API_KEY...), налаштування - у config/ai.php.

Агент - окремий PHP-клас, що інкапсулює все потрібне для однієї задачі: інструкції (системний промпт), контекст розмови, інструменти й схему відповіді.

php artisan make:agent SupportAssistant
namespace App\Ai\Agents;

use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Promptable;

class SupportAssistant implements Agent
{
    use Promptable;

    public function instructions(): string
    {
        return 'Ти асистент підтримки інтернет-магазину. Відповідай коротко й лише про замовлення та доставку.';
    }
}

Виклик:

$response = (new SupportAssistant)->prompt('Де моє замовлення №1042?');

return (string) $response;

SupportAssistant::make(...) створює агента через контейнер - з автоматичним впровадженням залежностей.

Налаштування через атрибути:

#[Provider(Lab::Anthropic)]
#[MaxTokens(1024)]
#[Temperature(0.2)]
#[Timeout(60)]
class SupportAssistant implements Agent

Що ще вміє SDK, крім текстових агентів:

  • структурована відповідь за JSON-схемою;
  • інструменти - функції застосунку, які модель може викликати;
  • стримінг відповіді й обробка в черзі;
  • збереження розмов у базі (RemembersConversations);
  • ембединги для семантичного пошуку (Str::of($text)->toEmbeddings(), whereVectorSimilarTo у Query Builder);
  • генерація зображень, аудіо, транскрибування;
  • резервні провайдери: provider: [Lab::OpenAI, Lab::Anthropic] - якщо перший недоступний чи вичерпав ліміт.

Чому агент - клас, а не просто виклик API: інструкції, інструменти й налаштування зібрані в одному місці, агента можна підробити в тестах (SupportAssistant::fake()) і перевикористати в контролері, команді чи черзі.

Докладніше в документації: AI SDK: агенти

MCP (Model Context Protocol) - відкритий протокол, через який AI-клієнт (Claude Code, Cursor, Codex, Copilot, Junie) отримує доступ до інструментів, ресурсів і промптів зовнішньої системи. Сервер MCP описує, що він уміє, а модель вирішує, коли цим скористатися.

Laravel Boost (laravel/boost) - пакет для розробки, що робить AI-асистента обізнаним саме про ваш застосунок і версії пакетів.

composer require laravel/boost --dev
php artisan boost:install

boost:install питає, якими агентами ви користуєтеся, і генерує для них файли:

  • конфігурацію MCP (.mcp.json тощо) - агент запускає php artisan boost:mcp;
  • настанови (guidelines) - файли на кшталт CLAUDE.md, AGENTS.md з правилами для вашої версії Laravel і встановлених пакетів (Livewire 4, Filament 5, Pest...);
  • навички (skills) - детальні інструкції, що завантажуються на вимогу під конкретну задачу.

Інструменти MCP-сервера Boost:

Інструмент Навіщо агенту
Application Info версії PHP, Laravel, пакетів, список моделей
Database Schema / Query структура таблиць і запити до бази
Search Docs документація саме для встановлених версій пакетів
Last Error / Read Log Entries останні помилки з логів
Browser Logs помилки з консолі браузера
Get Absolute URL правильна адреса сторінки застосунку

Чому це важливо: модель навчена на коді різних версій. Без контексту вона охоче напише код для Livewire 2 чи старий синтаксис маршрутів. Boost дає їй документацію й правила для того, що реально встановлено, і можливість перевірити схему бази замість вгадування.

Власні правила проєкту:

  • .ai/guidelines/*.md (чи .blade.php) - додаткові настанови, що потрапляють у згенеровані файли;
  • .ai/skills/{назва}/SKILL.md - власні навички.

Оновлення: php artisan boost:update - перегенерувати настанови й навички після оновлення пакетів (зручно додати в post-update-cmd Composer).

Безпека: Boost - залежність лише для розробки (--dev). Інструмент запитів до бази виконується з правами застосунку, тож підключати агента варто до локальної бази, а не до продакшену.

Докладніше в документації: Boost: MCP-сервер

Прапорець функції (feature flag) - перемикач, який вмикає чи вимикає частину функціональності без деплою. Код нової можливості вже на продакшені, але бачать її лише ті, кому прапорець увімкнено.

Навіщо це потрібно:

  • поступовий запуск: спершу 5% користувачів, потім 50%, потім усі;
  • trunk-based розробка: незавершену функцію можна зливати в main, бо вона прихована прапорцем;
  • швидкий «вимикач»: якщо нова функція ламає продакшен, її вимикають без відкату коду;
  • A/B-тести й бета-доступ для окремих клієнтів.

Laravel Pennant - офіційний пакет для прапорців. Прапорець оголошують у сервіс-провайдері:

use App\Models\User;
use Illuminate\Support\Lottery;
use Laravel\Pennant\Feature;

Feature::define('new-checkout', fn (User $user) => match (true) {
    $user->isInternal() => true,
    $user->isHighTrafficCustomer() => false,
    default => Lottery::odds(1 / 10),
});

Замикання отримує скоп - за замовчуванням автентифікованого користувача - і повертає результат.

Перевірка в коді й шаблонах:

if (Feature::active('new-checkout')) {
    return $this->newCheckout($request);
}
@feature('new-checkout')
    <x-checkout.new />
@else
    <x-checkout.legacy />
@endfeature

Для маршрутів є middleware EnsureFeaturesAreActive, що повертає помилку, якщо прапорець вимкнено.

Важлива деталь: драйвер за замовчуванням - database. Pennant зберігає обчислене значення в таблиці features, тож користувач, якому одного разу «випало» true в лотереї, і далі бачитиме нову версію. Це робить досвід стабільним, але означає, що зміна визначення не впливає на вже збережені значення.

Чого не робити: не залишати прапорці назавжди. Кожен прапорець - це дві гілки коду, які треба підтримувати й тестувати. Після повного запуску прапорець і стара гілка видаляються.

Докладніше в документації: Pennant: оголошення можливостей

Socialite реалізує протокол OAuth для входу через сторонніх провайдерів: Google, GitHub, Facebook, GitLab, LinkedIn, Slack, X та інших.

Як проходить вхід:

  1. користувач натискає «Увійти через GitHub» - застосунок перенаправляє його на GitHub;
  2. GitHub питає користувача, чи дозволити доступ;
  3. GitHub повертає користувача на ваш callback-маршрут з одноразовим кодом;
  4. Socialite обмінює код на токен доступу й отримує профіль користувача;
  5. застосунок знаходить чи створює користувача і входить від його імені.

Налаштування - ключі застосунку провайдера в config/services.php:

'github' => [
    'client_id' => env('GITHUB_CLIENT_ID'),
    'client_secret' => env('GITHUB_CLIENT_SECRET'),
    'redirect' => '/auth/github/callback',
],

Два маршрути:

use Laravel\Socialite\Socialite;

Route::get('/auth/github/redirect', fn () => Socialite::driver('github')->redirect());

Route::get('/auth/github/callback', function () {
    $githubUser = Socialite::driver('github')->user();

    $user = User::updateOrCreate(
        ['github_id' => $githubUser->getId()],
        ['name' => $githubUser->getName(), 'email' => $githubUser->getEmail()],
    );

    Auth::login($user, remember: true);

    return redirect('/dashboard');
});

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

  • шукати користувача за ID провайдера (github_id), а не за email: email у профілі можна змінити, а ID стабільний;
  • email може бути відсутнім - деякі провайдери не повертають його без додаткового скопу або якщо користувач приховав пошту;
  • скопи визначають, до чого застосунок отримає доступ: ->scopes(['read:user']). Просіть мінімум;
  • параметр state Socialite перевіряє сам - це захист від CSRF у процесі OAuth. Для API без сесії є stateless(), але тоді захист доведеться забезпечити інакше;
  • тести: Socialite можна підмінити фейком і не ходити до справжнього провайдера.

Для кількох провайдерів зручно зберігати зв'язки в окремій таблиці social_accounts (provider, provider_id, user_id), а не додавати колонку в users для кожного провайдера.

Докладніше в документації: Socialite: автентифікація та збереження

Питання з реальних технічних співбесід - 378 питань у 43 темах, розібраних із відповідями. Нижче - розбивка за рівнями та темами, якщо хочете звузити підготовку.

Рівні
Junior 101 Middle 147 Senior 130

Готуєтесь до співбесіди не просто так: зараз на сайті 147 відкритих вакансій Laravel і PHP. Переглянути вакансії