Ти помічав, що кожна Laravel-вакансія шукає когось, хто "проєктує масштабовані бекенди, оптимізує запити та будує підтримувані системи", а потім співбесіда починається з "що таке service container?"
Це типова картина. Senior-співбесіди поєднують базові запитання з питаннями про проєктування систем, і тобі потрібно бути готовим до обох.
Цей посібник — план підготовки, яким я сам користуюся. Він охоплює внутрішню будову Laravel, senior-теми, які справді перевіряють на співбесідах, та архітектурні питання, що відрізняють "я використовував Laravel" від "я запускав Laravel у промисловому середовищі".
Матеріал структурований так, що ти можеш розтягнути його на від одного до десяти днів — залежно від того, наскільки добре ти знаєш тему. Пробігайся по тому, що знаєш, і детально відпрацьовуй те, що ні.
Підказки для зображень. Поруч із кожною діаграмою є готовий prompt для GPT-зображення. Prompt для обкладинки знаходиться в самому кінці статті.
Як користуватися цим посібником
Сприймай кожен розділ як контрольну точку. Якщо ти можеш пояснити концепцію вголос, написати невеликий приклад і відповісти на питання "чому", — ти готовий рухатися далі.
Практичний ритм:
- Доступно 1-2 дні. Зосередься на life-cycle запиту, Eloquent + N+1, чергах, валідації та авторизації. Решту переглянь поверхово.
- Доступно 3-5 днів. Додай кешування, транзакції бази даних, тестування та проєктування API. Хоча б раз пройдися по senior-сценаріях у кінці.
- Доступно 6-10 днів. Охопи усе. Побудуй невеликий сайд-проєкт із чергами, подіями, задачами та policies — сам процес написання коду дасть тобі більше, ніж будь-який посібник.
Мета — не зазубрювання. Мета — вміти вільно говорити про компроміси.
Life-cycle запиту
Laravel-запит потрапляє через public/index.php. Далі — досить чітко регламентована послідовність. Знати її добре означає звучати як senior вже з перших хвилин.
require __DIR__.'/../vendor/autoload.php';
$app = require_once __DIR__.'/../bootstrap/app.php';
$app->handleRequest(Request::capture());
Шлях твого запиту:
- Autoloader та bootstrap. Запускається autoloader Composer, а
bootstrap/app.phpбудує контейнер додатку. - Реєстрація service providers. Спочатку виконується метод
register()кожного провайдера. Він лише додає прив'язки, залежності ще не вирішуються. - Завантаження service providers. Потім на кожному провайдері виконується
boot(). До цього моменту всі прив'язки вже існують, тому можна безпечно вирішувати залежності. - HTTP kernel обробляє запит. Виконується глобальний middleware (шифрування cookies, обрізання рядків, CSRF для веб-маршрутів).
- Вирішення маршруту. Router зіставляє URL, потім виконує middleware рівня маршруту (
auth,throttle,verified,signed,can:*). - Виконується контролер або дія маршруту. Залежності вводяться через service container.
- Повернення відповіді. Вона проходить назад через "after"-middleware і виходить до клієнта.
У Laravel 11+ bootstrap/app.php є твоїм центральним файлом налаштування: middleware, обробка винятків і маршрути конфігуруються там, а не в окремих класах Kernel.

Типове питання на співбесіді
"В чому різниця між
register()іboot()у service provider?"
Сильна відповідь: register() лише прив'язує речі до контейнера. Не слід вирішувати інші сервіси з register(), оскільки контейнер може ще не мати всіх прив'язок. boot() виконується після того, як кожен провайдер зареєстрований, а отже, там безпечно вирішувати залежності, реєструвати обробники подій, публікувати конфіги або визначати шаблони маршрутів.
Маршрутизація та middleware
Маршрути — декларативні. Паттерни, які потрібно знати напам'ять:
Route::middleware(['auth:sanctum', 'throttle:60,1'])
->prefix('v1')
->group(function () {
Route::apiResource('orders', OrderController::class);
Route::post('orders/{order}/refund', [RefundController::class, 'store']);
});
Що таке middleware насправді
Middleware знаходиться між запитом і логікою твого додатку. Він може перевіряти запит, модифікувати його, скорочувати ланцюжок (повертати відповідь достроково), пропускати далі або модифікувати відповідь на виході.
Поширений Laravel middleware:
authтаauth:sanctum— автентифікація.throttle:60,1— обмеження частоти (60 запитів на хвилину).verified— перевірка підтвердження email.signed— перевірка підписаних URL.can:update,post— авторизація через policies.
Пастка, яку іноді ставлять інтерв'юери: "Де б ти розмістив rate limiting — у middleware, контролері чи сервісі?" Правильна відповідь — middleware. Це наскрізна інфраструктура, а не бізнес-логіка. Те саме стосується CSRF, локалі та автентифікації.
Route Model Binding
Функція Laravel, яку інтерв'юери люблять запитувати, бо вона показує, чи вийшов ти за межі базової маршрутизації.
// Implicit binding — Laravel resolves Order from {order} via the model's primary key
Route::get('orders/{order}', function (Order $order) {
return $order;
});
Користувацький ключ:
// Resolve by slug instead of id
Route::get('posts/{post:slug}', function (Post $post) {
return $post;
});
Scoped binding — коли одна модель належить іншій і ти хочеш відобразити це в URL:
// Will only resolve a comment that belongs to the given post
Route::get('posts/{post}/comments/{comment}', function (Post $post, Comment $comment) {
//
})->scopeBindings();
Також можна перевизначити вирішення для конкретної моделі:
public function resolveRouteBinding($value, $field = null): ?Model
{
return $this->where($field ?? 'slug', $value)
->whereNotNull('published_at')
->first();
}
Senior-сигнал тут: "Route model binding разом із scoped bindings означає, що ти переносиш валідацію зв'язків на рівень маршрутизації. Це тримає контролери тонкими та запобігає цілим категоріям помилок авторизації."
Service Container та Dependency Injection
Container — це мозок Laravel. Розберися в ньому, і ти одразу звучатимеш як senior.
public function register(): void
{
// New instance every resolve
$this->app->bind(PaymentGateway::class, StripePaymentGateway::class);
// One shared instance for the whole app lifecycle
$this->app->singleton(MetricsClient::class, fn () => new MetricsClient(
host: config('metrics.host'),
));
// One instance per request — important for Octane
$this->app->scoped(TenantContext::class, TenantContext::class);
}
Відмінність, яку перевіряють на співбесідах:
bind()— новий екземпляр при кожному вирішенні. Використовуй для стейтфул або дешевих сервісів.singleton()— один екземпляр на весь процес. Використовуй для дорогих або спільних сервісів, як-от HTTP-клієнт або зчитувач конфігурації.scoped()— один екземпляр на запит. Корисно у тривалих процесах, як-от Octane, деsingletonможе "протікати" станом між запитами.

Contextual binding
Коли двом класам потрібні різні реалізації одного інтерфейсу:
$this->app->when(VideoController::class)
->needs(Filesystem::class)
->give(fn () => Storage::disk('s3'));
$this->app->when(LogProcessor::class)
->needs(Filesystem::class)
->give(fn () => Storage::disk('local'));
Згадай це на співбесіді — і ти покажеш, що справді читав документацію.
Constructor injection на практиці
final class ReportController
{
public function __construct(
private readonly ReportService $reports,
private readonly Logger $logger,
) {}
public function index(): JsonResponse
{
return response()->json(
$this->reports->generateMonthly()
);
}
}
Method injection також працює: Laravel вирішує залежності за сигнатурами методів контролера через container.
Eloquent ORM
Саме тут більшість Laravel-співбесід проводять найбільше часу. Будьте готові до питань про зв'язки, пастки продуктивності та відмінність між Eloquent і Query Builder.
Моделі та зв'язки
final class Order extends Model
{
protected $fillable = ['user_id', 'status', 'total'];
protected function casts(): array
{
return [
'total' => 'decimal:2',
'placed_at' => 'immutable_datetime',
'metadata' => 'array',
];
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function items(): HasMany
{
return $this->hasMany(OrderItem::class);
}
public function tags(): MorphToMany
{
return $this->morphToMany(Tag::class, 'taggable');
}
}
Типи зв'язків, які варто знати напам'ять:
belongsTo,hasOne,hasMany,belongsToManyhasOneThrough,hasManyThroughmorphTo,morphOne,morphMany,morphToMany,morphedByMany(поліморфні)
У Laravel 11+ casts визначаються як метод, а не властивість — інтерв'юери можуть запитати, чому це важливо (це робить casts залежно ін'єктованими та ліниво обчислюваними).
Query scopes
Локальні scopes тримають логіку запитів у моделі:
public function scopeRecent(Builder $query, int $days = 30): void
{
$query->where('placed_at', '>=', now()->subDays($days));
}
// Usage
$recent = Order::recent(7)->get();
Глобальні scopes застосовуються до кожного запиту для моделі — зручно для soft deletes, мультитенантності або фільтрів "active". Будьте обережні: вони можуть приховати запити від колег, які про них не знають. Документуйте їх.
Observers
Observers переносять хуки life-cycle моделі за межі самої моделі:
final class OrderObserver
{
public function creating(Order $order): void
{
$order->reference ??= Str::uuid();
}
public function updated(Order $order): void
{
if ($order->wasChanged('status')) {
OrderStatusChanged::dispatch($order);
}
}
public function deleted(Order $order): void
{
$order->items()->delete();
}
}
Доступні хуки: creating, created, updating, updated, saving, saved, deleting, deleted, restoring, restored, forceDeleted, retrieved.
Коли інтерв'юери запитують "де б ти розмістив логіку, що виконується при кожному оновленні замовлення?", observers зазвичай є правильною відповіддю. Вони тримають моделі чистими та роблять поведінку life-cycle тестованою.
Senior-застереження: observers не спрацьовують при масових оновленнях, наприклад Order::query()->update([...]). Згадайте це — це класична "пастка", яку люблять перевіряти інтерв'юери.
Soft deletes
use SoftDeletes;
Soft deletes позначають рядок як видалений за допомогою мітки часу deleted_at замість фактичного видалення. Корисно для журналів аудиту, функціональності скасування та відновлення випадково видалених записів.
$post->delete(); // soft delete (sets deleted_at)
$post->restore(); // un-delete
$post->forceDelete(); // actually remove from the database
Post::withTrashed()->get(); // include soft-deleted
Post::onlyTrashed()->get(); // only soft-deleted
Не застосовуйте soft deletes бездумно — кожен запит отримує WHERE deleted_at IS NULL, що має наслідки для індексування та продуктивності. Використовуйте там, де бізнес справді потребує можливості відновлення.
Проблема N+1
Мабуть, найпоширеніше питання на Laravel-співбесідах.
// Bad: 1 query for orders + N queries for users
$orders = Order::all();
foreach ($orders as $order) {
echo $order->user->name;
}
// Good: 2 queries total
$orders = Order::with('user')->get();

Інструменти, які варто згадати:
with()— eager load під час початкового запиту.load()— eager load після факту.loadMissing()— eager load лише якщо ще не завантажено.withCount()— eager load кількості зв'язків.withExists()— завантажити булеве значення "є хоча б один пов'язаний рядок".Model::preventLazyLoading()— у non-production-середовищі кидає виняток при lazy loading. Змушує виправляти N+1 проблеми завчасно.
Eloquent vs Query Builder
// Eloquent — model objects, relationships, events, casts
$users = User::where('active', true)->get();
// Query Builder — raw rows, faster, no model overhead
$users = DB::table('users')->where('active', true)->get();
Використовуй Eloquent для бізнес-логіки. Переходь на Query Builder для звітів, аналітики або масових операцій, де накладні витрати моделей справді мають значення.
Обробка великих наборів даних
Коли ти обробляєш мільйони рядків, не використовуй ->get() для всього:
// Loads all rows into memory — bad for large tables
Order::all()->each(fn ($o) => $this->process($o));
// Better: chunk in batches, but careful when modifying records
Order::orderBy('id')->chunk(1000, fn ($orders) => $orders->each(...));
// Best for stable iteration when records are being modified
Order::lazyById(1000)->each(fn ($o) => $this->process($o));
chunkById() та lazyById() безпечніші за chunk(), якщо ти оновлюєш записи під час ітерації, бо вони пагінують за первинним ключем, а не зміщенням. З offset-пагінацією та оновленнями ти пропускаєш рядки.
Валідація та Form Requests
Вбудована валідація підходить для невеликих endpoints:
$data = $request->validate([
'email' => ['required', 'email'],
'age' => ['required', 'integer', 'min:18'],
]);
Для будь-чого серйознішого використовуй Form Requests:
final class StoreOrderRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()->can('create', Order::class);
}
public function rules(): array
{
return [
'product_id' => ['required', 'exists:products,id'],
'quantity' => ['required', 'integer', 'min:1', 'max:100'],
'shipping_address' => ['required', 'array'],
'shipping_address.street' => ['required', 'string', 'max:255'],
];
}
protected function prepareForValidation(): void
{
$this->merge([
'quantity' => (int) $this->quantity,
]);
}
}
Form Requests централізують валідацію, авторизацію та трансформацію вхідних даних. Завжди надавай перевагу $request->validated() перед $request->all(), щоб уникнути витоків масового присвоєння.
Автентифікація та авторизація
Sanctum vs Passport
Поширений prompt на співбесіді: "Що б ти вибрав для API?"
- Sanctum — для first-party SPA, мобільних додатків і простих token-based API. Легковагий. Використовує сесійні cookies для SPA та personal access tokens для мобільних/API клієнтів.
- Passport — повноцінний OAuth2-сервер. Використовуй, коли тобі справді потрібні OAuth2-потоки: сторонні клієнти, authorization codes, refresh tokens, scopes на рівні протоколу.
Якщо OAuth2 явно не потрібен, Sanctum — це сучасний вибір за замовчуванням. Назвати цей компроміс — senior-сигнал.
Policies та Gates
Policies обробляють авторизацію для конкретних моделей:
final class OrderPolicy
{
public function update(User $user, Order $order): bool
{
return $user->id === $order->user_id
|| $user->hasRole('admin');
}
}
// In a controller
$this->authorize('update', $order);
// In Blade
@can('update', $order) ... @endcan
// In a Form Request
public function authorize(): bool
{
return $this->user()->can('update', $this->route('order'));
}
Gates обробляють одиночну авторизацію, яка не прив'язана до моделі:
Gate::define('access-admin-panel', fn (User $user) => $user->is_admin);
Черги та задачі
Черги є обов'язковою частиною production Laravel. Все, що повільне або ненадійне, не повинно виконуватися в HTTP-запиті.
Анатомія задачі
final class ProcessPayment implements ShouldQueue
{
use Queueable;
public int $tries = 5;
public int $backoff = 30;
public int $timeout = 120;
public function __construct(public int $orderId) {}
public function handle(PaymentGateway $gateway): void
{
$order = Order::findOrFail($this->orderId);
$gateway->charge($order);
}
public function failed(Throwable $e): void
{
Log::error('Payment failed permanently', [
'order_id' => $this->orderId,
'error' => $e->getMessage(),
]);
}
}
Зверніть увагу: ми передаємо $orderId, а не $order. Задачі серіалізуються в чергу. Передача ідентифікаторів і повторне отримання даних у handle() тримає навантаження малим та уникає застарілих даних.

Драйвери черг
- Redis — найпоширеніший вибір для production. Швидкий, підтримує відкладені задачі, гарна інтеграція з Horizon.
- Database — підходить для невеликих обсягів; конкуренція за блокування дається взнаки при масштабуванні.
- SQS / Beanstalkd — керовані альтернативи.
- Sync — виконується негайно. Корисно для тестування.
Проваленні задачі, повторні спроби, backoff
php artisan queue:failed
php artisan queue:retry 5
php artisan queue:retry all
php artisan queue:forget 5
Налаштовуйте поведінку повторних спроб у задачі ($tries, $backoff, $timeout) або у worker. Для ідемпотентних задач повторні спроби безпечні; для неідемпотентних (списання з картки, відправка emails) проєктуйте уважно — дивіться розділ про ідемпотентність нижче.
Пакети та ланцюжки
// Run sequentially, stop on failure
Bus::chain([
new ChargeCustomer($order),
new SendReceipt($order),
new UpdateInventory($order),
])->dispatch();
// Run in parallel, track collective progress
Bus::batch([
new ProcessImage($asset1),
new ProcessImage($asset2),
new ProcessImage($asset3),
])->then(fn (Batch $batch) => /* all succeeded */)
->catch(fn (Batch $batch, Throwable $e) => /* one failed */)
->dispatch();
Horizon
Horizon — це дашборд і supervisor для черг на базі Redis. Він надає метрики, інспекцію проваленних задач, графіки пропускної здатності та автобалансування workers. Якщо інтерв'юер запитує про моніторинг черг — це і є відповідь.
Події та слухачі
Події дозволяють відокремити логіку. Замовлення оплачене — і ти не хочеш, щоб контролер знав про листи, інвентар, аналітику та вебхуки.
final class OrderPaid
{
public function __construct(public Order $order) {}
}
final class SendReceipt implements ShouldQueue
{
public function handle(OrderPaid $event): void
{
Mail::to($event->order->user)
->send(new ReceiptMail($event->order));
}
}
// Fire it
OrderPaid::dispatch($order);
У сучасному Laravel слухачі автоматично виявляються, якщо ти розміщуєш їх у app/Listeners і type-hint подію в handle().

Слухачі, що реалізують ShouldQueue, виконуються в черзі. В іншому разі вони виконуються синхронно всередині запиту і сповільнюють його. Email та виклики вебхуків завжди мають бути в черзі.
Broadcasting
Broadcasting публікує події до клієнтів у реальному часі (Pusher, Reverb, Soketi). Якщо ти використовував WebSockets у Laravel — згадай це, це показує, що ти виходив за межі CRUD.
Notifications
Система сповіщень Laravel уніфікує відправку повідомлень по кількох каналах:
final class OrderShipped extends Notification implements ShouldQueue
{
use Queueable;
public function via(User $user): array
{
return ['mail', 'database', 'broadcast'];
}
public function toMail(User $user): MailMessage { /* ... */ }
public function toDatabase(User $user): array { /* ... */ }
public function toBroadcast(User $user): BroadcastMessage { /* ... */ }
}
// Usage
$user->notify(new OrderShipped($order));
Канали включають mail, database, broadcast, Vonage (SMS), Slack та будь-який власний драйвер. Database notifications живлять вбудовані поштові скриньки; broadcast notifications живлять оновлення UI в реальному часі. Якщо запитують "як би ти побудував центр сповіщень у додатку?" — це і є відповідь.
Кешування та Redis
$products = Cache::remember('homepage:featured', 3600, fn () =>
Product::where('featured', true)->take(10)->get()
);
Стратегії
- Read-through — стиль
Cache::remember(). Промах кешу, запит до БД, збереження. - Write-through — оновлення кешу разом із оновленням бази даних.
- Cache-aside — додаток явно інвалідує ключі кешу при записі.
- Tagged cache — групування пов'язаних записів кешу для пакетної інвалідації (лише Redis).
Cache::tags(['products', "user:{$user->id}"])
->remember('user-feed', 600, fn () => buildFeed($user));
// Later, blow away everything tagged 'products'
Cache::tags(['products'])->flush();

Атомарні блокування
Для запобігання дублюванню операцій:
$lock = Cache::lock("payment:{$order->id}", 10);
if ($lock->get()) {
try {
$this->charge($order);
} finally {
$lock->release();
}
}
Це правильна відповідь, коли інтерв'юери запитують "як би ти запобіг подвійному списанню?"
Cache stampede
Коли популярний ключ кешу закінчується і 1000 запитів одночасно звертаються до бази даних. Засоби боротьби: атомарні блокування навколо регенерації кешу або Cache::flexible() (Laravel 11+), який повертає застарілі дані, поки один worker їх оновлює.
База даних
Міграції
return new class extends Migration
{
public function up(): void
{
Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('status')->index();
$table->decimal('total', 10, 2);
$table->timestamps();
$table->index(['user_id', 'status']);
});
}
public function down(): void
{
Schema::dropIfExists('orders');
}
};
Поради щодо міграцій для senior-співбесід:
- Завжди роби міграції зворотними.
- Додавай індекси обдумано, а не рефлекторно.
- Ніколи не редагуй задеплоєну міграцію — напиши нову.
- Для нульового простою розбивай ризиковані міграції: додати стовпець → заповнити дані → перемкнути читання → видалити старий стовпець. Не поєднуй ці кроки в одному релізі.
Транзакції
DB::transaction(function () use ($order) {
$order->update(['status' => 'paid']);
Inventory::decrement($order);
Receipt::create(['order_id' => $order->id]);
});
DB::transaction() повторює спробу при дедлоках (налаштовується). Для вкладених транзакцій Laravel використовує savepoints під капотом.
Песимістичне vs оптимістичне блокування
Песимістичне — блокування рядка в базі даних під час роботи з ним:
DB::transaction(function () {
$product = Product::lockForUpdate()->find(1);
$product->stock -= 1;
$product->save();
});
Оптимістичне — використання стовпця версії та перевірка при оновленні:
$updated = Product::where('id', $id)
->where('version', $currentVersion)
->update([
'stock' => $newStock,
'version' => $currentVersion + 1,
]);
if ($updated === 0) {
throw new StaleDataException();
}

Песимістичне простіше, але утримує блокування. Оптимістичне масштабується краще, але потребує логіки повторних спроб. Назвати обидва підходи та їхні компроміси — це те, що хочуть почути senior-інтерв'юери.
Індексування
Індекси, які варто знати:
- B-tree (за замовчуванням) — добре для рівності та діапазонів.
- Складені індекси — порядок стовпців має значення; правило лівого префікса.
- Унікальні індекси — також забезпечують цілісність даних.
- Часткові / функціональні індекси (Postgres) — індексування підмножини або виразу.
- Повнотекстові індекси — замінники
LIKE '%term%'.
Senior-погляд: індекси прискорюють читання, але сповільнюють запис. Профілюйте перед додаванням.
Пагінація та чому cursor pagination має значення
// Offset-based — simple, but slow on large tables
$users = User::paginate(50);
// Cursor-based — uses a key, not OFFSET. Fast even on millions of rows.
$users = User::orderBy('id')->cursorPaginate(50);
Чому це senior-питання: paginate() виконує SELECT COUNT(*) плюс LIMIT/OFFSET, що стає дорогим при сотнях тисяч рядків. cursorPaginate() пропускає підрахунок і використовує фільтр WHERE id > last_seen_id — константний час незалежно від позиції.
Компроміс: cursor pagination не підтримує "перейти на сторінку 47". Використовуй для стрічок з нескінченним прокручуванням, API endpoints та великих адмін-таблиць. Для малих наборів даних, де користувачам потрібні номери сторінок, використовуй offset-пагінацію.
Тестування
final class OrderControllerTest extends TestCase
{
use RefreshDatabase;
public function test_authenticated_user_creates_order(): void
{
$user = User::factory()->create();
$product = Product::factory()->create();
$response = $this->actingAs($user)->postJson('/api/orders', [
'product_id' => $product->id,
'quantity' => 2,
]);
$response->assertCreated();
$this->assertDatabaseHas('orders', [
'user_id' => $user->id,
'product_id' => $product->id,
]);
}
}

Типи тестів
- Unit tests — чисті класи, без фреймворку, найшвидші.
- Feature tests — повний HTTP-цикл із тестовим клієнтом.
- Integration tests — база даних, черги, події, що працюють разом.
- Browser tests (Dusk) — справжня JavaScript-взаємодія.
Корисні трейти та інструменти
RefreshDatabase— обертає кожен тест у транзакцію; відкат після.DatabaseMigrations— перезапускає міграції для кожного тесту (повільніше, більш ізольовано).WithFaker— екземпляр faker для фікстур.- Mockery — мокінг залежностей.
Bus::fake(),Queue::fake(),Mail::fake(),Event::fake(),Notification::fake()— перевіряють dispatch без фактичного виконання.
Паралельне тестування
php artisan test --parallel
Прискорює великі набори тестів. Переконайся, що твої тести не розділяють стан і не покладаються на захардкоджені ID.
Проєктування API
Route::middleware(['auth:sanctum', 'throttle:api'])
->prefix('v1')
->group(function () {
Route::apiResource('orders', OrderController::class);
});
Resources
final class OrderResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'status' => $this->status,
'total' => (float) $this->total,
'items' => OrderItemResource::collection(
$this->whenLoaded('items')
),
'placed_at' => $this->placed_at?->toIso8601String(),
];
}
}
whenLoaded() — твій захист від N+1 на рівні API: включає зв'язок лише якщо він був eager-завантажений.
Версіонування
Два поширені підходи:
- URL versioning —
/api/v1/.... Просто та видно. - Header versioning —
Accept: application/vnd.app.v1+json. Чистіші URL, складніше дебажити.
URL versioning — це те, що використовує більшість команд. Обирай його, якщо немає вагомих причин.
Rate limiting
RateLimiter::for('api', function (Request $request) {
return $request->user()
? Limit::perMinute(120)->by($request->user()->id)
: Limit::perMinute(20)->by($request->ip());
});
Ключі мають значення. Обмежуйте за ID користувача для автентифікованих запитів, за IP — в іншому разі.
Кастомна обробка винятків
Senior-сигнал: твої API мають повертати передбачувані форми помилок, а не debug-сторінки Laravel.
->withExceptions(function (Exceptions $exceptions) {
$exceptions->render(function (ModelNotFoundException $e, Request $request) {
if ($request->expectsJson()) {
return response()->json([
'message' => 'Resource not found',
], 404);
}
});
$exceptions->render(function (ValidationException $e, Request $request) {
return response()->json([
'message' => 'Invalid input',
'errors' => $e->errors(),
], 422);
});
});
Визнач єдиний формат відповіді з помилками для свого API і дотримуйся його. Інтерв'юери поважають відповіді на кшталт "ми стандартизували відповіді з помилками по всіх сервісах, щоб фронтенд ніколи не мав обробляти особливі випадки".
Продуктивність та оптимізація
Чеклист для production
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache
composer install --optimize-autoloader --no-dev
OpCache має бути увімкнений. Використовуйте preload, якщо можете.
Octane
Octane запускає твій додаток на Swoole, RoadRunner або FrankenPHP, тримаючи фреймворк завантаженим між запитами. Масштабне прискорення, але потрібна обережність:
- Singletons живуть між запитами — витоки стану реальні.
- Статичні властивості зберігаються між запитами.
- Використовуй
scoped-прив'язки обдумано замістьsingleton. - Стеж за витоками пам'яті в тривалих процесах.
Якщо інтерв'юер запитує, як збільшити пропускну здатність у 5 разів без зміни інфраструктури — "Octane плюс ретельне управління станом" є правильною відповіддю.
Продуктивність бази даних
- Eager loading (
with,withCount). - Цільові індекси.
EXPLAINдля повільних запитів.- Уникай
SELECT *, використовуй->select(['id', 'name']). - Read replicas для навантажень із переважаючим читанням.
- Connection pooling (PgBouncer, ProxySQL) при масштабуванні.
Інструменти профілювання
- Laravel Telescope — інспекція запитів та запитів до БД у локальній розробці.
- Laravel Pulse — дашборд продуктивності для production.
- Clockwork — альтернативний профілювальник.
- Blackfire / Tideways / New Relic — профілювання production-рівня.
Безпека
Senior не має просто говорити "Laravel безпечний за замовчуванням." Покажи, що ти знаєш точки збою.
SQL-ін'єкція
Eloquent і Query Builder використовують prepared statements. Небезпека — у сирих запитах із інтерпольованими вхідними даними:
// Never
DB::select("select * from users where email = '{$email}'");
// Always
DB::select('select * from users where email = ?', [$email]);
Mass assignment
// Dangerous — accepts any field, including is_admin
User::create($request->all());
// Safe
User::create($request->validated());
Налаштовуй $fillable (список дозволених) або $guarded (список блокованих). Списки дозволених безпечніші, бо забуття захистити новий стовпець ніколи не приведе до його витоку.
XSS
Blade екранує виведення за замовчуванням із {{ }}. Тільки {!! !!} пропускає екранування — використовуй свідомо і ніколи для користувацьких даних.
CSRF
Директива Blade @csrf у формах. Stateless API endpoints не потребують CSRF, якщо використовують bearer tokens, але endpoints з автентифікацією через сесію — обов'язково.
Хешування паролів
bcrypt за замовчуванням; argon2id доступний. Ніколи не зберігай паролі у відкритому вигляді. Ніколи не реалізовуй власне хешування.
Підписані URL
Для публічних, але захищених від підробки посилань (скидання пароля, завантаження файлів, автентифікація за magic-link):
URL::temporarySignedRoute('download', now()->addMinutes(5), ['file' => $id]);
Інші senior-рівневі теми
- Валідація завантаження файлів (
image,mimes,max:розмір). - Примусове HTTPS та HSTS headers.
- Content Security Policy headers.
- Сканування залежностей (
composer audit). - Управління секретами — env файли поза репозиторієм, vault-рішення у production.
Архітектурні паттерни
Це senior-розділ. Більшість кандидатів знають основи; небагато знають, як насправді структурувати великий Laravel-додаток.

Service classes
Service тримає бізнес-логіку, яка не належить моделі:
final class OrderService
{
public function __construct(
private readonly PaymentGateway $payments,
private readonly InventoryService $inventory,
) {}
public function place(User $user, array $data): Order
{
return DB::transaction(function () use ($user, $data) {
$order = $user->orders()->create($data);
$this->inventory->reserve($order);
$this->payments->authorize($order);
return $order;
});
}
}
Action classes
Більш детальна альтернатива — один клас, одна операція:
final class PlaceOrder
{
public function execute(User $user, array $data): Order
{
// ...
}
}
Actions чудові для ясності. Services групують пов'язані actions, коли потрібен координуючий шар.
Repository pattern — зазвичай не потрібен у Laravel
Repository pattern — найбільш надмірно застосований паттерн у PHP. Laravel-моделі вже є репозиторіями. Додавання OrderRepository, що обгортає Order::query(), не дає жодної цінності і додає шар болю.
Коли це має сенс? Тільки коли:
- У тебе є нереляційне джерело даних (наприклад, remote API).
- Тобі справді потрібна можливість замінювати реалізації.
- Ти хочеш абстрагувати зберігання від доменної моделі, яка нічого не має знати про Eloquent.
В іншому випадку пиши service або action.
Domain-driven structure
Для великих додатків структуруйте код за доменом функціональності замість Laravel-конвенцій:
app/
├── Domain/
│ ├── Orders/
│ │ ├── Models/
│ │ ├── Actions/
│ │ ├── Events/
│ │ └── ValueObjects/
│ └── Billing/
└── Http/
Згадайте це, якщо запитують про масштабування Laravel-кодової бази понад десятки тисяч рядків.
Мультитенантність
Поширене архітектурне питання для SaaS-ролей. Три паттерни, які варто знати:
- Одна база даних, стовпець tenant_id. Найпростіше. Кожна модель несе
tenant_id. Глобальний scope фільтрує кожен запит. Легко в роботі, складніше забезпечити строгу ізоляцію тенантів. Ризик: забутий scope може призвести до витоку даних між тенантами. - База даних на тенанта. Кожен тенант отримує власну базу даних. Сильна ізоляція. Складніше в роботі (міграції по N базах даних) та дорожче при масштабуванні. Пакети на кшталт
stancl/tenancyавтоматизують перемикання з'єднань. - Schema на тенанта (Postgres). Одна база даних, окремі схеми для кожного тенанта. Компроміс між двома підходами.
Senior-розмова — про компроміси. "Ми починали зі стовпцем tenant_id і глобальним scope, і додали параноїдальні перевірки на рівні контролера для запобігання доступу між тенантами" — це хороша розповідь.
Коли залучати черги, події, services
Корисна ментальна модель:
- Controller — приймає вхідні дані, валідує, передає далі, повертає відповідь.
- Service / action — виконує бізнес-операцію.
- Event — сповіщає, що щось сталося.
- Listener — реагує на подію.
- Job — відкладає роботу, яку не потрібно виконувати зараз.
Контролер рідко має містити бізнес-логіку. Якщо містить, то цю логіку важко повторно використовувати, важко тестувати й неможливо викликати з CLI-команди або задачі в черзі.
Поширені Senior-сценарії
Ідемпотентність
Як переконатися, що платіжний endpoint не буде випадково викликаний двічі?
$idempotencyKey = $request->header('Idempotency-Key');
return Cache::lock("idempotency:{$idempotencyKey}", 60)
->block(5, function () use ($idempotencyKey, $request) {
$cached = Cache::get("response:{$idempotencyKey}");
if ($cached) {
return response()->json($cached);
}
$result = $this->orderService->place(...);
Cache::put("response:{$idempotencyKey}", $result, 86400);
return response()->json($result);
});
Race conditions
Два запити, той самий рядок. Використовуйте:
lockForUpdate()для песимістичного блокування всередині транзакції.Cache::lock()для координації на рівні додатку.- Унікальні індекси для конфліктів "неможливих за конструкцією".
Тривалі задачі
Тривалі задачі потребують:
- Обмеженого часу виконання (
$timeout). - Усвідомленості пам'яті (chunk, lazy collections).
- Безпечних повторних спроб (
$tries, ідемпотентність). - Звітування про прогрес (рядок у БД, ключ Redis або batch).
Проєктування системи з нуля
Якщо просять "спроєктуйте X з Laravel", розмірковуйте вголос про:
- Обмежені контексти та таблиці.
- Публічну поверхню API (маршрути та resources).
- Синхронний шлях (controller → service → DB → response).
- Асинхронний шлях (events → listeners → jobs).
- Точки збою та спостережуваність.
- Питання масштабування: кешування, черги, репліки.
DevOps та деплой
Deploy без простою
Класичний потік:
- Отримати новий код у директорію релізу.
composer install --no-dev.php artisan migrate --force(обережно).- Кешувати конфіг, маршрути, вигляди.
- Атомарно переключити симлінк
currentна новий реліз. - Перезавантажити PHP-FPM (або перезапустити Octane workers).
Інструменти: Envoyer, Forge, Deployer, GitHub Actions.
Безпека міграцій у production
- Додай стовпець nullable, заповни дані, потім застосуй обмеження.
- Уникай
dropColumnу тому ж релізі, де припинив його використовувати; зроби це в наступному релізі. - Індексуй великі таблиці одночасно (Postgres) або за допомогою low-impact інструментів (
pt-online-schema-changeдля MySQL).
Планування задач
Планувальник Laravel замінює стіну записів crontab кодом:
Schedule::command('reports:daily')->dailyAt('03:00');
Schedule::command('queue:prune-failed')->daily();
Schedule::job(new SyncInventoryJob)->hourly()->withoutOverlapping();
Один запис cron запускає php artisan schedule:run щохвилини, а Laravel вирішує, що запустити. Корисні прапорці: withoutOverlapping(), onOneServer(), runInBackground(), evenInMaintenanceMode(). Згадай onOneServer(), якщо у тебе кілька серверів — він гарантує, що задача виконується лише на одному з них.
Стек моніторингу
Згадайте:
- Horizon — черги.
- Telescope — локальне дебагування.
- Pulse — продуктивність у production.
- Sentry / Bugsnag — відстеження винятків.
- Логи — Stackdriver, CloudWatch, Datadog.
Поведінкові питання, які справді задають
Senior-співбесіди не лише технічні. Будь готовий до:
- "Розкажи про випадок, коли ти був відповідальним за систему, що впала в production."
- "Як ти вирішуєш, що важливіше — виправити технічний борг чи випустити фічі?"
- "Опиши code review, який змінив твою думку."
- "Яке Laravel-рішення твоєї команди ти не підтримував?"
- "Як ти вводиш junior-розробника у складну кодову базу?"
Підготуй одну-дві реальні історії для кожної категорії. Використовуй STAR (Situation, Task, Action, Result), але не звучи як робот — це розмови.
Питання до інтерв'юера
Ти також оцінюєш їх. Сильні питання:
- На якій версії Laravel ви зараз і що заважає перейти на наступну?
- Як ви обробляєте черги, проваленні задачі та фонову обробку?
- Як виглядає піраміда тестування — unit vs feature vs end-to-end?
- Як ви деплоїте і яка ваша стратегія відкату?
- Які найбільші вузькі місця продуктивності сьогодні?
- Як відстежується та пріоритизується технічний борг?
- Як виглядає успіх у перші 90 днів?
- Де в кодовій базі живе бізнес-логіка?
- Як структурована команда — feature teams, platform teams, обидва?
Правильні питання говорять інтерв'юеру, що ти відправляв справжнє програмне забезпечення.
Фінальний чеклист
За день до співбесіди ти маєш вміти пояснити вголос, без нотаток:
- Повний life-cycle запиту від
index.phpдо відповіді. - Різницю між
bind,singletonтаscoped. - Проблеми N+1 та щонайменше три способи їх виправити.
- Як працюють черги, що відбувається при збої задачі та як допомагає Horizon.
- Коли використовувати events, jobs і services, і як вони співвідносяться.
- Як запобігати race conditions за допомогою блокувань і транзакцій.
- Компроміси між Sanctum і Passport.
- Як спроєктувати чистий шар API із resources, версіонуванням та rate limiting.
- Щонайменше три реальні оптимізації продуктивності, які ти дійсно застосовував.
- Чеклист безпеки, довший за просто "використовуй Eloquent."
Завершальні поради
Найкращі Laravel-співбесіди — не тести на знання тривіальних фактів. Це розмови про те, як ти будуєш, масштабуєш та обслуговуєш реальні системи. Зазубрювання кожного параметра конфігурації не допоможе. А знання того, чому ти б вибрав singleton замість bind — допоможе.
Кілька останніх нагадувань:
- Говори про компроміси. Senior-сигнали проявляються, коли ти порівнюєш варіанти вголос.
- Використовуй реальні приклади зі своєї роботи. Конкретне завжди краще за абстрактне.
- Визнай, чого не знаєш. "Я не використовував це, але ось як би я міркував" — сильна відповідь.
- Задавай уточнювальні питання. Справжній інжиніринг починається з розуміння проблеми.
- Тримай код чистим під час live coding. Іменування, малі функції та помітні роздуми краще за хитромудрі однолайнери.
Ти відправляв Laravel-додатки. Говори відповідно. Бажаємо отримати оффер 👊






