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

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

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

378 питань

Query Builder - плавний інтерфейс для побудови SQL-запитів без написання рядкового SQL. Працює з усіма підтримуваними СУБД і захищає від SQL-ін'єкцій через підготовлені вирази.

$users = DB::table('users')
    ->where('votes', '>', 100)
    ->orderBy('name')
    ->limit(10)
    ->get();

Повертає прості об'єкти stdClass. Eloquent побудований поверх Query Builder, тож ті самі методи (where, join, orderBy) доступні і на моделях.

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

Фасади надають зручний статичний інтерфейс до об'єктів із Service Container.

Cache::put('key', 'value', 60);
Route::get('/', fn () => view('home'));

Попри статичний синтаксис, це не справжні статичні методи: фасад через __callStatic() дістає реальний об'єкт із контейнера й викликає метод уже на ньому. Тому фасади тестовані - їх можна мокати:

Cache::shouldReceive('get')->once()->andReturn('value');

Кожен фасад має «accessor» - рядковий ключ сервісу в контейнері.

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

  • find($id) повертає модель за первинним ключем або null.
  • findOrFail($id) повертає модель або кидає ModelNotFoundException, яку Laravel автоматично перетворює на HTTP 404.
$post = Post::find($id);
if (! $post) { abort(404); } // ручна перевірка

$post = Post::findOrFail($id); // те саме одним рядком

findOrFail робить контролери чистішими. Аналогічна пара для запитів - first() / firstOrFail().

Докладніше в документації: Eloquent: не знайдено / findOrFail

Це «upsert»-методи, що позбавляють від ручних перевірок «існує / не існує».

// знайти за email; якщо нема - створити з усіма атрибутами
User::firstOrCreate(
    ['email' => $email],
    ['name' => $name]
);

// знайти за email; оновити name; якщо нема - створити
User::updateOrCreate(
    ['email' => $email],
    ['name' => $name]
);

Перший масив - умови пошуку, другий - значення для створення/оновлення. Споріднений firstOrNew() повертає незбережений екземпляр.

Докладніше в документації: Eloquent: upsert-методи

Зовнішній ключ завжди лежить у таблиці тієї моделі, що належить іншій. Звідси й вибір методу.

// posts.user_id посилається на users.id
class User extends Model
{
    public function posts(): HasMany
    {
        return $this->hasMany(Post::class);   // ключ у чужій таблиці
    }
}

class Post extends Model
{
    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class); // ключ у своїй таблиці
    }
}

Просте правило: у якій моделі колонка *_id, та модель і пише belongsTo. Інша сторона - hasOne чи hasMany.

Імена за замовчуванням:

  • hasMany шукає в posts колонку user_id - ім'я моделі-власника в snake_case плюс _id;
  • belongsTo бере ім'я методу плюс _id: метод author() шукатиме author_id, і тоді ключ доведеться передати явно, якщо колонка називається інакше.
public function author(): BelongsTo
{
    return $this->belongsTo(User::class, 'user_id');
}

Часта помилка новачків - написати hasOne там, де потрібен belongsTo, бо «в пості є один автор». Запит тоді шукатиме users.post_id, якого не існує.

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

Базовий layout оголошує «дірки» через @yield, дочірні шаблони їх заповнюють:

{{-- layouts/app.blade.php --}}
<html><body>
    <main>@yield('content')</main>
</body></html>
{{-- posts/show.blade.php --}}
@extends('layouts.app')

@section('content')
    <h1>{{ $post->title }}</h1>
@endsection

Навіщо: загальна розмітка (шапка, футер, підключення ассетів) описується один раз. Альтернатива - Blade-компоненти (<x-layout> зі слотами), які в нових проєктах часто витісняють @extends.

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

Blade-компоненти - багаторазові елементи UI, які підключаються тегом:

<x-alert type="error" :message="$error" />

Бувають:

  • Анонімні - лише файл resources/views/components/alert.blade.php.
  • Класові - php artisan make:component Alert, з PHP-класом для логіки.

Дані передаються атрибутами, контент - через слоти:

{{-- components/card.blade.php --}}
<div class="card">
    <h2>{{ $title }}</h2>
    {{ $slot }}
</div>

Чистіше за @include, бо мають явний інтерфейс (props) та інкапсуляцію.

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

{{ $value }} екранує вивід через htmlspecialchars, а {!! $value !!} виводить як є.

{{ $user->name }}      {{-- <b>Ада</b> покажеться як текст --}}
{!! $post->html !!}    {{-- теги спрацюють --}}

Чому це важливо: якщо користувач запише в ім'я <script>...</script>, {{ }} покаже це як текст, а {!! !!} виконає скрипт у браузері кожного, хто відкриє сторінку. Це XSS - викрадення сесії, дії від імені жертви.

Правила:

  • за замовчуванням - лише {{ }};
  • {!! !!} - для HTML, який згенерував сам застосунок (Markdown, перетворений безпечним рендерером, чи HTML, очищений HTML Purifier);
  • вміст від користувача в {!! !!} без очищення - ніколи.

Пастки, які екранування не закриває:

  • href="{{ $url }}" з javascript:alert(1) - екранування не забороняє схему, такі посилання валідують (url:http,https);
  • дані в <script> - для JavaScript беруть Js::from($data), а не {!! json_encode() !!}.

Для JavaScript-фреймворків, що теж використовують фігурні дужки, є @{{ name }} - Blade залишить вираз як є.

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

Іменований маршрут має псевдонім, за яким генерують URL - замість хардкоду шляху.

Route::get('/posts/{post}', [PostController::class, 'show'])->name('posts.show');
route('posts.show', $post); // у PHP
<a href="{{ route('posts.show', $post) }}">Деталі</a>

Навіщо: якщо URL зміниться (/posts → /articles), достатньо поправити маршрут - усі посилання оновляться автоматично. Також redirect()->route('posts.show', $post).

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

Розклад описується в PHP, а не в crontab - тож він лежить у git разом із кодом.

Опис завдання у routes/console.php:

use Illuminate\Support\Facades\Schedule;

Schedule::command('vacancies:parse')->twiceDaily(6, 18);
Schedule::command('reports:send')->dailyAt('09:00');
Schedule::job(new CleanupTempFiles)->hourly();

Що потрібно на сервері - один запис у cron:

* * * * * cd /path-to-project && php artisan schedule:run >> /dev/null 2>&1

Саме так: щохвилини й один рядок на весь застосунок. Cron будить Laravel щохвилини, а той сам вирішує, чиє зараз час. Новий пункт розкладу не потребує змін у cron.

Перевірити, не чекаючи часу:

php artisan schedule:list      # що і коли має виконатися
php artisan schedule:run       # виконати те, чому час зараз
php artisan schedule:work      # тримати планувальник локально, без cron

Типові помилки новачка:

  • Прописати кожне завдання окремим рядком у crontab - тоді розклад із коду не працює зовсім.
  • Забути cd у шлях проєкту: команда не знайде застосунок.
  • Запускати від імені іншого користувача, ніж веб-сервер, - права на storage не збігаються, і завдання падає на записі логу.
  • Не дивитися на вивід: без emailOutputOnFailure() чи логування падіння завдання непомітне.

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

Route::resource() одним рядком реєструє 7 маршрутів за RESTful-конвенцією:

Route::resource('posts', PostController::class);
Метод URL Дія
GET /posts index
GET /posts/create create
POST /posts store
GET /posts/{post} show
GET /posts/{post}/edit edit
PUT/PATCH /posts/{post} update
DELETE /posts/{post} destroy
  • apiResource() - те саме без create/edit (для API).
  • ->only([...]) / ->except([...]) обмежують набір.

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

Файл приходить через $request->file(); зберігають через фасад Storage:

$request->validate([
    'avatar' => ['required', 'image', 'max:2048'], // до 2 МБ
]);

$path = $request->file('avatar')->store('avatars', 'public');
// або з випадковим унікальним іменем - store() уже так робить

$user->update(['avatar_path' => $path]);
  • store() повертає шлях для збереження в БД.
  • Публічний URL - Storage::url($path) (потребує php artisan storage:link).
  • Валідатори image, mimes:pdf,docx, max: (в КБ) убезпечують від небажаних файлів.

Докладніше в документації: Зберігання файлів

Верифікація email підтверджує, що користувач справді володіє вказаною адресою. Laravel має це з коробки.

// 1. Модель реалізує контракт
class User extends Authenticatable implements MustVerifyEmail {}

// 2. Маршрути захищають middleware
Route::get('/dashboard', ...)->middleware(['auth', 'verified']);

Як працює: після реєстрації Laravel шле лист із підписаним URL (signed URL), перехід за яким ставить email_verified_at. Middleware verified не пускає непідтверджених користувачів. Подія Registered автоматично тригерить відправку листа.

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

Auth::attempt() перевіряє облікові дані й, якщо вони правильні, відкриває сесію.

public function login(LoginRequest $request): RedirectResponse
{
    if (! Auth::attempt($request->only('email', 'password'), $request->boolean('remember'))) {
        throw ValidationException::withMessages(['email' => __('auth.failed')]);
    }

    $request->session()->regenerate();

    return redirect()->intended('/dashboard');
}

Покроково:

  1. Провайдер гарда шукає користувача за всіма полями, крім пароля.
  2. Пароль порівнюється з хешем через Hash::check() - відкритий пароль ніде не зберігається й не порівнюється напряму.
  3. При успіху ID користувача записується в сесію, і наступні запити бачать його через auth()->user().

Що робиться після входу:

  • session()->regenerate() - новий ідентифікатор сесії, щоб підкинутий заздалегідь не дав зловмиснику увійти разом з жертвою (session fixation).
  • redirect()->intended() - повертає туди, куди користувач ішов до перенаправлення на логін.

Додаткові умови передають тим самим масивом: Auth::attempt([...$credentials, 'is_active' => true]) не пустить вимкнений акаунт. У стартових наборах Laravel 13 усе це вже зроблено пакетом Fortify.

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

Звичайна сесія закінчується через SESSION_LIFETIME хвилин бездіяльності. «Запам'ятати мене» дозволяє лишатися в системі довше - без повторного входу.

Auth::attempt($credentials, remember: true);

Як це влаштовано: Laravel генерує випадковий токен, зберігає його в колонці users.remember_token і ставить довгоживучу зашифровану cookie. Коли сесія вже закінчилась, а cookie є, користувача автентифікують за нею, і Auth::viaRemember() повертає true.

Ризики й що з ними роблять:

  • Вкрадена cookie = доступ на весь термін її дії. Тому cookie має HttpOnly і Secure (через HTTPS), а на чутливих діях пароль запитують ще раз - middleware password.confirm.
  • Спільний комп'ютер. Галочка на бібліотечному ПК лишить акаунт відкритим для наступного. Тому вона не має бути ввімкнена за замовчуванням.
  • Вихід скидає токен. Auth::logout() оновлює remember_token, і стара cookie перестає працювати - тож «вийти» справді виходить.

Якщо viaRemember() повертає true, розумно обмежити найнебезпечніші дії, доки людина не підтвердить пароль.

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

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

Рівні
Junior 101 Middle 147 Senior 130

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