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

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

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

378 питань

Достатньо викликати paginate() замість get() - Laravel сам читає номер сторінки з query-параметра ?page=:

$posts = Post::latest()->paginate(15);
@foreach ($posts as $post) ... @endforeach
{{ $posts->links() }}   {{-- кнопки навігації --}}
  • paginate() рахує загальну кількість (показує «сторінка X з Y»).
  • simplePaginate() - лише «вперед/назад», без COUNT(*), швидше на великих таблицях.
  • cursorPaginate() - курсорна пагінація, найефективніша для нескінченного скролу.

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

Найпростіше - повернути масив або модель: Laravel сам віддасть JSON із правильним заголовком.

public function show(Post $post)
{
    return $post;                       // JSON усієї моделі
}

public function stats()
{
    return ['total' => Post::count()];  // JSON масиву
}

Чому цього замало. Модель віддає всі свої поля, тож у відповідь потрапляє те, чого там бути не повинно, а перейменування колонки мовчки змінює формат API для всіх клієнтів.

API Resource - це шар між моделлю та JSON, який описує форму відповіді явно:

php artisan make:resource PostResource
class PostResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'title' => $this->title,
            'published_at' => $this->published_at?->toIso8601String(),
            'author' => new UserResource($this->whenLoaded('author')),
        ];
    }
}

Використання:

public function show(Post $post)
{
    return new PostResource($post);
}

public function index()
{
    return PostResource::collection(Post::paginate());
}

collection() разом із пагінацією сам додає блоки links і meta.

Що це дає: структура відповіді описана в одному місці, поля з бази не протікають назовні, дати мають єдиний формат, а whenLoaded() додає звʼязок лише тоді, коли його справді завантажили - і тим самим не створює N+1.

Докладніше в документації: Eloquent: API Resources

Route Model Binding автоматично резолвить параметр маршруту в екземпляр моделі - не треба вручну робити findOrFail().

Route::get('/posts/{post}', [PostController::class, 'show']);

// $post - вже знайдена модель
public function show(Post $post)
{
    return view('posts.show', compact('post'));
}

За замовчуванням пошук іде за id. Прив'язати за іншим стовпцем: {post:slug}. Якщо запис не знайдено - автоматично 404.

Докладніше в документації: Route Model Binding

Черга дозволяє відкласти повільну роботу: замість того щоб виконувати її під час запиту й тримати користувача, застосунок ставить завдання в чергу й одразу повертає відповідь. Окремий процес - воркер - виконує її згодом.

Створити завдання:

php artisan make:job SendWelcomeEmail
class SendWelcomeEmail implements ShouldQueue
{
    use Queueable;

    public function __construct(public User $user)
    {
    }

    public function handle(): void
    {
        Mail::to($this->user)->send(new WelcomeMail($this->user));
    }
}

Інтерфейс ShouldQueue - це і є те, що відправляє завдання в чергу, а не виконує одразу.

Поставити в чергу:

SendWelcomeEmail::dispatch($user);
SendWelcomeEmail::dispatch($user)->delay(now()->addMinutes(10));

Запустити воркер:

php artisan queue:work

Що варто виносити: надсилання листів і сповіщень, обробку зображень і відео, звернення до зовнішніх API, генерацію звітів та експорт, імпорт великих файлів - усе, що займає більше частки секунди й не потрібне для формування відповіді.

Що виносити не варто: те, результат чого користувач має побачити просто зараз. Черга - асинхронна, і відповідь її не дочекається.

Головна пастка новачка: у завдання передається модель, а Laravel серіалізує лише її ключ і перечитує запис у воркері. Якщо між постановкою й виконанням запис видалили, завдання впаде. Тому в чергу передають ідентифікатори або готові дані, а не громіздкі обʼєкти.

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

dispatch() ставить завдання в чергу: запит закінчується одразу, а роботу пізніше виконає воркер. dispatchSync() виконує handle() тут і зараз, у поточному процесі, черга не задіяна.

SendWelcomeEmail::dispatch($user);      // у чергу - відповідь не чекає листа
SendWelcomeEmail::dispatchSync($user);  // одразу - запит чекає, поки лист піде

Коли що:

  • dispatch() - для всього, що може зачекати й не потрібне користувачу в цій відповіді: листи, обробка зображень, звернення до зовнішніх API.
  • dispatchSync() - у консольних командах і сідерах, де черга нічого не дає, і коли результат потрібен негайно.

Важливо: якщо клас не реалізує ShouldQueue, навіть dispatch() виконає його одразу. А в Laravel 13 є ще проміжний варіант - підключення deferred: завдання виконається в тому самому процесі, але після відправки відповіді користувачу, без воркера.

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

Найчастіше - бо ніхто їх не виконує. Черга лише зберігає завдання; виконує їх окремий процес-воркер:

php artisan queue:work

У свіжому застосунку Laravel підключення за замовчуванням - database (QUEUE_CONNECTION=database), тож завдання просто накопичуються в таблиці jobs, доки воркер не запущено.

Що ще перевірити:

  • Інша черга. Завдання з ->onQueue('emails') не візьме воркер, що слухає лише default: потрібен --queue=emails,default.
  • Інше підключення. Код пише в redis, а воркер слухає database.
  • Старий код. queue:work тримає застосунок у пам'яті й не бачить змін, доки його не перезапустити (php artisan queue:restart).

На сервері воркер має працювати постійно, тож його запускають під Supervisor чи systemd, які піднімуть процес після падіння. Для розробки є queue:listen: він повільніший, зате перечитує код на кожне завдання.

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

Згенерувати клас і описати логіку в handle():

php artisan make:command SendReports
class SendReports extends Command
{
    protected $signature = 'reports:send {--month=}';
    protected $description = 'Розіслати місячні звіти';

    public function handle(): int
    {
        $this->info('Відправка...');
        return self::SUCCESS;
    }
}

Аргументи й опції описуються в $signature. Команду можна запускати вручну або ставити в розклад через Schedule::command('reports:send')->monthly().

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

Factory (фабрика) генерує фейкові дані моделей через Faker. Незамінна в тестах і сідерах.

class PostFactory extends Factory
{
    public function definition(): array
    {
        return [
            'title' => fake()->sentence(),
            'body'  => fake()->paragraphs(3, true),
        ];
    }
}
Post::factory()->count(10)->create(); // 10 записів у БД
Post::factory()->published()->make(); // стан + без збереження

«Стани» (states) дозволяють описати варіації, наприклад ->published() чи ->trashed().

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

Різниця в тому, скільки застосунку бере участь у тесті.

Feature-тест (tests/Feature) піднімає весь Laravel: маршрути, middleware, базу, контейнер. Він перевіряє поведінку так, як її бачить користувач:

it('creates an order', function () {
    $user = User::factory()->create();

    $this->actingAs($user)
        ->post('/orders', ['product_id' => 1])
        ->assertRedirect('/orders');

    expect($user->orders)->toHaveCount(1);
});

Unit-тест (tests/Unit) перевіряє один клас ізольовано й не завантажує застосунок - отже, там немає фасадів, конфігурації й бази. Він підходить для чистої логіки:

it('calculates the discount', function () {
    expect((new PriceCalculator)->withDiscount(1000, 10))->toBe(900);
});

Як обирати: у Laravel-застосунку більшість цінних тестів - feature: вони ловлять помилки на стиках, де їх найбільше, і не ламаються від рефакторингу внутрішніх класів. Unit-тести - для алгоритмів, розрахунків і класів без залежностей від фреймворку, де вони швидкі й точно вказують на поломку.

Тест створюють командою php artisan make:test, з --unit для unit-тесту.

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

Скидати базу між тестами. Laravel дає для цього трейти.

RefreshDatabase - стандартний вибір. Він мігрує базу один раз на прогін, а кожен тест загортає в транзакцію й відкочує її наприкінці. Швидко, бо таблиці не перестворюються.

// tests/Pest.php
pest()->extend(Tests\TestCase::class)
    ->use(Illuminate\Foundation\Testing\RefreshDatabase::class)
    ->in('Feature');

LazilyRefreshDatabase - те саме, але мігрує лише тоді, коли тест справді звернувся до бази. Тести без бази стають швидшими.

DatabaseMigrations і DatabaseTruncation скидають базу повністю: перестворюють чи очищують таблиці. Значно повільніше - потрібні, коли код сам керує транзакціями чи з'єднаннями, і обгортка RefreshDatabase йому заважає.

Що ще ділять тести, крім бази:

  • кеш і сесію - у тестах їх тримають на драйвері array, що живе один запит;
  • статичні властивості й синглтони, які пережили тест;
  • файли - тому для дисків беруть Storage::fake().

Ознака проблеми: тести проходять поодинці, але падають разом чи в іншому порядку.

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

Локалізація дає багатомовність. Рядки перекладів лежать у lang/{locale} (наприклад, lang/uk/messages.php або JSON-файли).

// lang/uk/messages.php
return ['welcome' => 'Ласкаво просимо, :name'];
{{ __('messages.welcome', ['name' => $user->name]) }}
  • app()->setLocale('uk') перемикає мову (зазвичай у middleware).
  • trans_choice() обирає форму за кількістю (1 яблуко / 2 яблука / 5 яблук).

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

Collection - обгортка над масивом із плавним, ланцюжковим API. Eloquent-запити повертають саме колекції.

$names = collect($users)
    ->filter(fn ($u) => $u->active)
    ->sortBy('name')
    ->map(fn ($u) => $u->name)
    ->values();

Десятки методів: map, filter, reduce, groupBy, pluck, each, sum. Код читається зрозуміліше за вкладені цикли й array_*-функції. Для дуже великих наборів є LazyCollection (на генераторах).

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

Усі три повертають нову колекцію (чи значення) і не змінюють вихідну.

$prices = collect([100, 250, 40]);

$withVat = $prices->map(fn (int $p) => $p * 1.2);          // [120, 300, 48]
$expensive = $prices->filter(fn (int $p) => $p > 50);      // [0 => 100, 1 => 250]
$total = $prices->reduce(fn (int $sum, int $p) => $sum + $p, 0);   // 390

$prices->all();   // [100, 250, 40] - без змін
  • map() - перетворює кожен елемент, кількість та сама;
  • filter() - залишає елементи, що пройшли умову (без замикання - прибирає «порожні»);
  • reduce() - зводить колекцію до одного значення будь-якого типу.

Пастка filter(): ключі зберігаються - вийде [0 => 100, 1 => 250], а після видалення середнього елемента було б [0 => ..., 2 => ...]. У JSON такий масив стане об'єктом. Перед відповіддю викликають values().

Мутуючі методи теж є, і їх варто знати, щоб не здивуватися: transform(), push(), put(), pop(), shift(), forget() змінюють саму колекцію.

Для простих сум є готові sum(), avg(), max() - reduce() потрібен для складнішого накопичення.

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

Model::get() повертає Illuminate\Database\Eloquent\Collection - вона успадковує звичайну колекцію й додає методи для моделей.

$users = User::where('active', true)->get();

$users->find(7);                       // модель за первинним ключем, без запиту
$users->modelKeys();                   // [1, 7, 12]
$users->load('posts');                 // жадібно завантажити зв'язок для всіх
$users->loadCount('orders');
$users->contains($someUser);           // порівнює моделі за ключем
$users->fresh();                       // перечитати з бази
$users->toQuery()->update([...]);      // один запит за ключами колекції
$users->except([1, 7]);                // без моделей з цими ключами

Що важливо розуміти:

  • методи колекції працюють у пам'яті. $users->where('role', 'admin') фільтрує вже завантажене, а User::where('role', 'admin') - робить запит. Перше доречне для невеликої вибірки, друге - коли рядків багато;
  • деякі методи (map, pluck) повертають звичайну колекцію, якщо результат уже не моделі;
  • load() на колекції - спосіб прибрати N+1, коли моделі отримали без with().

Для власних методів ($orders->totalPaid()) моделі можна дати свою колекцію атрибутом #[CollectedBy].

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

Лист описується класом Mailable, шаблон - Blade або Markdown.

php artisan make:mail WelcomeEmail --markdown=emails.welcome
Mail::to($user)->send(new WelcomeEmail($user));

Налаштування транспорту (SMTP, Mailgun, SES, log) - у config/mail.php та .env. Реалізувавши ShouldQueue на Mailable, відправку можна винести в чергу, щоб не блокувати запит.

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

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

Рівні
Junior 101 Middle 147 Senior 130

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