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

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

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

378 питань

Pulse - панель моніторингу продуктивності й використання застосунку. Вона показує агреговану картину за період: що повільне, що найчастіше використовується, де помилки.

Що показує Pulse (картки):

Картка Що видно
Сервери CPU, пам'ять, диск кожного сервера
Використання застосунку користувачі, що роблять найбільше запитів чи запускають найбільше завдань
Винятки найчастіші винятки й коли вони були востаннє
Черги пропускна здатність: поставлено, оброблено, невдалі
Повільні запити, завдання, SQL, вихідні HTTP що перевищує поріг (за замовчуванням 1000 мс)
Кеш влучання й промахи за ключами

Панель доступна за адресою /pulse, за замовчуванням лише в середовищі local. Для продакшену доступ відкривають через гейт viewPulse:

Gate::define('viewPulse', fn (User $user) => $user->isAdmin());

Чим відрізняється Telescope:

Pulse Telescope
що записує агреговані метрики кожен запит, запит до БД, завдання, лист окремо
питання, на яке відповідає «що загалом повільне й часто ламається?» «що саме сталося в цьому запиті?»
де використовувати продакшен переважно розробка й staging
навантаження помірне, з семплюванням значне на продакшені

Простіше кажучи: Pulse показує, де проблема (ендпойнт /reports повільний і викликається тисячу разів на годину), а Telescope - чому (у кожному запиті 300 SQL-запитів через N+1).

Чого Pulse не замінює:

  • зовнішній моніторинг доступності - якщо застосунок лежить, Pulse разом з ним;
  • трекер помилок (Sentry, Flare) з повними стеками й сповіщеннями;
  • централізовані логи для розслідувань.

Для серверних метрик на кожному сервері має працювати команда pulse:check під Supervisor.

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

Reverb - власний WebSocket-сервер Laravel, написаний на PHP. Він замінює сторонні сервіси на кшталт Pusher чи Ably: замість платити за кількість з'єднань і повідомлень, ви запускаєте сервер на своїй інфраструктурі.

Reverb реалізує протокол Pusher, тому фронтенд працює зі звичним Laravel Echo і клієнтом pusher-js, а перехід з Pusher на Reverb (чи назад) - це зміна змінних оточення.

Встановлення:

php artisan install:broadcasting --reverb

Команда встановлює пакети Composer і NPM, публікує конфігурацію, створює routes/channels.php і додає в .env ключі:

BROADCAST_CONNECTION=reverb
REVERB_APP_ID=...
REVERB_APP_KEY=...
REVERB_APP_SECRET=...
REVERB_HOST=localhost
REVERB_PORT=8080

Запуск сервера:

php artisan reverb:start
php artisan reverb:start --debug   # показувати вхідні й вихідні повідомлення

Подія для трансляції:

class OrderShipped implements ShouldBroadcast
{
    public function __construct(public Order $order) {}

    public function broadcastOn(): array
    {
        return [new PrivateChannel('orders.' . $this->order->user_id)];
    }
}

Прослуховування на фронтенді:

Echo.private(`orders.${userId}`).listen('OrderShipped', (e) => {
    console.log(e.order);
});

Що важливо пам'ятати:

  • ShouldBroadcast відправляє подію через чергу - без запущеного воркера повідомлення не надходять. ShouldBroadcastNow - одразу, без черги;
  • REVERB_SERVER_HOST/REVERB_SERVER_PORT - де слухає сам сервер, а REVERB_HOST/REVERB_PORT - куди Laravel надсилає повідомлення. На продакшені Reverb зазвичай слухає 0.0.0.0:8080, а назовні доступний через Nginx на 443;
  • Reverb - довгоживучий процес: після деплою його треба перезапустити (reverb:restart), а на сервері тримати під Supervisor.

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

Mailable - клас, що описує один тип листа: тему, відправника, шаблон, дані й вкладення. Створюється командою:

php artisan make:mail OrderShipped

Три основні методи:

use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\{Address, Attachment, Content, Envelope};

class OrderShipped extends Mailable
{
    use Queueable, SerializesModels;

    public function __construct(public Order $order) {}

    public function envelope(): Envelope
    {
        return new Envelope(
            from: new Address('shop@example.com', 'Acme Shop'),
            replyTo: [new Address('support@example.com')],
            subject: "Замовлення №{$this->order->number} відправлено",
        );
    }

    public function content(): Content
    {
        return new Content(
            view: 'mail.orders.shipped',
            text: 'mail.orders.shipped-text',
        );
    }

    public function attachments(): array
    {
        return [
            Attachment::fromStorageDisk('s3', $this->order->invoice_path)
                ->as('invoice.pdf')
                ->withMime('application/pdf'),
        ];
    }
}
Метод Що визначає
envelope() тема, відправник, reply-to, теги й метадані для провайдера
content() Blade-шаблон HTML-версії, текстова версія, Markdown-шаблон
attachments() вкладення з диска, сховища чи згенерованих даних

Дані для шаблону: усі публічні властивості класу автоматично доступні у шаблоні ($order). Для обчислених значень - параметр with у Content.

Надсилання:

Mail::to($order->customer)
    ->cc($order->manager)
    ->send(new OrderShipped($order));

to() приймає email, модель з полями email і name або колекцію.

Глобальний відправник: щоб не вказувати from у кожному листі - MAIL_FROM_ADDRESS і MAIL_FROM_NAME в .env.

Корисні звички:

  • текстова версія листа покращує доставлюваність і читання в простих клієнтах;
  • великі файли краще не вкладати, а давати посилання (наприклад, підписаний URL);
  • лист можна повернути з маршруту (return new OrderShipped($order);) і переглянути в браузері під час розробки.

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

Канал database зберігає сповіщення в таблиці notifications - так будують «дзвіночок» з лічильником непрочитаних в інтерфейсі.

Таблиця:

php artisan make:notifications-table
php artisan migrate

Таблиця містить id (UUID), type (клас сповіщення), поліморфний notifiable, data (JSON), read_at і мітки часу.

Сповіщення:

class InvoicePaid extends Notification
{
    public function __construct(public Invoice $invoice) {}

    public function via(object $notifiable): array
    {
        return ['mail', 'database'];
    }

    public function toArray(object $notifiable): array
    {
        return [
            'invoice_id' => $this->invoice->id,
            'amount' => $this->invoice->amount,
            'url' => route('invoices.show', $this->invoice),
        ];
    }
}

Масив з toArray (чи toDatabase) кодується в JSON і зберігається в колонці data. Зберігайте в ньому все потрібне для показу: ідентифікатори, короткий текст, посилання - а не лише ID, щоб для списку сповіщень не довелося підвантажувати десяток моделей.

Читання:

$user->notifications;          // усі, найновіші першими
$user->unreadNotifications;    // read_at = null
$user->unreadNotifications()->count();
@foreach (auth()->user()->unreadNotifications as $notification)
    <a href="{{ $notification->data['url'] }}">
        Рахунок на {{ $notification->data['amount'] }} оплачено
    </a>
@endforeach

Позначити прочитаними:

$notification->markAsRead();
$user->unreadNotifications->markAsRead();                     // колекцію
$user->unreadNotifications()->update(['read_at' => now()]);   // одним запитом

Останній варіант кращий для «позначити все»: не завантажує сповіщення в пам'ять.

Що варто врахувати:

  • зміна структури data: старі записи лишаються у старому форматі. Шаблон має переживати відсутні ключі, а метод databaseType дозволяє зберігати стабільний тип замість імені класу (зручно, якщо клас перейменують);
  • таблиця росте безкінечно - старі прочитані сповіщення варто видаляти за розкладом;
  • індекс на (notifiable_type, notifiable_id, read_at) прискорює підрахунок непрочитаних;
  • для показу в реальному часі додають канал broadcast.

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

Scout дає єдиний API пошуку для моделей (Post::search('laravel')->paginate()), а сам пошук виконує рушій: Meilisearch, Typesense, Algolia - або власна база даних.

Два рушії не потребують жодного зовнішнього сервісу.

Рушій database:

SCOUT_DRIVER=database
  • шукає прямо в таблицях через повнотекстові індекси MySQL/PostgreSQL та умови LIKE;
  • окремого індексування немає - дані завжди актуальні, scout:import не потрібен;
  • стратегію пошуку для колонок можна задати атрибутами:
use Laravel\Scout\Attributes\{SearchUsingFullText, SearchUsingPrefix};

#[SearchUsingPrefix(['id', 'email'])]
#[SearchUsingFullText(['title', 'body'])]
public function toSearchableArray(): array
{
    return ['id' => $this->id, 'title' => $this->title, 'body' => $this->body];
}

SearchUsingPrefix шукає текст% (може використати звичайний індекс), SearchUsingFullText - через повнотекстовий індекс, решта колонок - %текст%.

Рушій collection:

SCOUT_DRIVER=collection
  • завантажує всі записи моделі з бази й фільтрує їх у PHP;
  • працює з будь-якою базою, включно з SQLite, - зручно для тестів і прототипів;
  • для кількох сотень записів - нормально, для таблиці на сотні тисяч - пам'ять і час ростуть лінійно.

Порівняння:

collection database Meilisearch/Typesense
інфраструктура нічого нічого окремий сервер
обсяг даних сотні записів десятки-сотні тисяч мільйони
стійкість до опечаток ні ні так
ранжування, фасети, підсвічування ні обмежено так
актуальність миттєва миттєва після індексування

Практичний підхід: почати з database - код пошуку однаковий для всіх рушіїв, тож коли знадобиться стійкість до опечаток чи релевантність, достатньо змінити драйвер і проіндексувати дані. А collection - у тестах, щоб не піднімати пошуковий сервер.

Докладніше в документації: Scout: рушії database і collection

Плейсхолдери - змінні частини рядка позначаються двокрапкою:

// lang/uk/messages.php
'welcome' => 'Вітаємо, :name!',
__('messages.welcome', ['name' => $user->name]);

Регістр плейсхолдера впливає на регістр підстановки: :NAME дасть ОЛЕНА, :Name - Олена.

Множина. В англійській дві форми: «1 comment», «2 comments». В українській - три:

Число Форма
1, 21, 31, 101 1 коментар
2-4, 22-24 3 коментарі
0, 5-20, 25-30, 11-14 5 коментарів

Форми розділяють символом |, а функція trans_choice обирає потрібну за числом і правилами мови поточної локалі:

// lang/uk/comments.php
'count' => ':count коментар|:count коментарі|:count коментарів',
trans_choice('comments.count', 1);   // 1 коментар
trans_choice('comments.count', 3);   // 3 коментарі
trans_choice('comments.count', 11);  // 11 коментарів
trans_choice('comments.count', 21);  // 21 коментар

:count підставляється автоматично. Правила української (як і інших мов) уже вбудовані в Laravel - у класі MessageSelector, тож порядок форм для uk такий: одна, кілька, багато.

Явні діапазони - коли потрібен особливий текст для нуля чи межі:

'count' => '{0} Коментарів ще немає|{1} :count коментар|[2,4] :count коментарі|[5,*] :count коментарів',

Але явні діапазони не знають правил мови: [2,4] спрацює для 3, а для 23 - ні. Для українських текстів надійніше лишити три форми без діапазонів, а нуль обробити окремою умовою в коді чи шаблоні.

У Blade:

{{ trans_choice('comments.count', $post->comments_count) }}

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

  • склеювати рядок з частин: $count . ' ' . __('коментарів') - неправильна форма для половини чисел;
  • писати дві форми для української за англійським зразком;
  • перевіряти лише 1, 2 і 5, а не 11-14 і 21 - саме там помиляються найчастіше.

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

Коли APP_DEBUG=true, Laravel показує сторінку помилки з усім потрібним для розслідування. Головне - читати її в правильному порядку.

1. Клас винятку й повідомлення вгорі сторінки - найважливіше:

Що бачите Що це зазвичай означає
ModelNotFoundException findOrFail чи прив'язка моделі до маршруту не знайшли запис
QueryException помилка SQL: немає колонки, таблиці, порушено обмеження
ErrorException: Undefined array key звернення до ключа, якого немає
TypeError у функцію передано значення не того типу (часто null)
ViewException помилка всередині Blade-шаблону - справжня причина нижче в ланцюжку

2. Перший кадр вашого коду. Стек викликів читається згори вниз: верхній кадр - де виняток кинуто. Часто це код фреймворку чи пакета, і він не винен. Сторінка позначає кадри з vendor/ окремо й згортає їх - шукайте перший кадр з app/, routes/ чи resources/: там рядок, який викликав проблему.

3. Попередні винятки. Якщо виняток обгорнуто (як ViewException), справжня причина - у «previous exception». Сторінка показує весь ланцюжок.

4. Контекст запиту: маршрут, контролер, middleware, параметри, заголовки, тіло запиту й виконані SQL-запити. Часто відповідь там: не той ідентифікатор у параметрі, порожнє поле форми.

Корисні дрібниці:

  • кнопка копіювання помилки в Markdown - зручно вставити в задачу чи в чат з AI-асистентом;
  • посилання на файли відкриваються в IDE, якщо вказати редактор у config/app.php: 'editor' => 'phpstorm';
  • те саме, що на сторінці, записано в storage/logs/laravel.log - для помилок у чергах і командах, де сторінки немає.

На продакшені APP_DEBUG має бути false: сторінка розкриває змінні оточення, запити й код. Користувач бачить загальну сторінку 500, а деталі - в логах і системі моніторингу помилок.

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

Дані запиту приходять з різних місць, і Laravel дає окремі методи для кожного.

input() - тіло запиту й рядок запиту разом:

$name = $request->input('name');
$city = $request->input('address.city');     // вкладене поле через крапку
$tags = $request->input('tags.*.name');      // усі значення з масиву
$page = $request->input('page', 1);          // значення за замовчуванням

Для форм (application/x-www-form-urlencoded, multipart/form-data) і JSON-тіла (application/json) працює однаково. Якщо поле є і в тілі, і в рядку запиту, перевагу має тіло.

query() - лише рядок запиту (?sort=name&page=2):

$sort = $request->query('sort', 'created_at');

Корисно, коли важливо, звідки прийшло значення: наприклад, параметр фільтра не повинен підмінятися полем у тілі POST-запиту.

json() - лише JSON-тіло:

$items = $request->json('items');

Параметри маршруту - частина URL, а не вхідні дані запиту:

Route::get('/users/{user}/posts/{post}', function (User $user, Post $post) { ... });

$request->route('post');   // з будь-якого місця, наприклад у middleware

Їх отримують аргументами контролера (з прив'язкою до моделі), а не через input().

Інші джерела:

Метод Що повертає
$request->all() усі вхідні дані разом з файлами
$request->only(['name', 'email']) лише вказані поля
$request->except(['password']) усе, крім указаних
$request->header('X-Request-Id') заголовок
$request->cookie('theme') cookie (розшифрований)
$request->file('avatar') завантажений файл

Важливе правило: для збереження даних використовуйте не all() чи input(), а $request->validated() з Form Request - лише ті поля, які пройшли валідацію. Інакше клієнт може надіслати поля, яких ви не очікували.

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

Форма має відправляти файли як multipart/form-data:

<form method="POST" action="/profile/avatar" enctype="multipart/form-data">
    @csrf
    <input type="file" name="avatar">
</form>

Без enctype браузер відправить лише назву файлу.

Отримання файлу:

$file = $request->file('avatar');     // UploadedFile чи null
$request->hasFile('avatar');          // чи є файл у запиті
$file->isValid();                     // чи завантажився без помилок

Спершу - валідація:

$validated = $request->validate([
    'avatar' => ['required', 'image', 'mimes:jpg,png,webp', 'max:2048'],   // max у кілобайтах
    'documents.*' => ['file', 'mimes:pdf', 'max:10240'],
]);

Збереження:

$path = $request->file('avatar')->store('avatars', 's3');
// avatars/Xk9...q2.jpg - унікальна випадкова назва

$path = $request->file('avatar')->storeAs('avatars', "{$user->id}.webp", 'public');

store() сам генерує унікальну назву - це правильний варіант за замовчуванням. У базу зберігають шлях, а не сам файл.

Інформація про файл:

Метод Що повертає Чи можна довіряти
getClientOriginalName() назва з комп'ютера користувача ні - лише для відображення
getClientOriginalExtension() розширення з назви ні
extension() розширення за вмістом (MIME) так
getSize() розмір у байтах так
getMimeType() тип за вмістом так

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

  • зберігати файл під оригінальною назвою - перезапис чужих файлів, обхід шляху, shell.php;
  • перевіряти лише розширення з назви файлу;
  • забути про ліміти PHP: upload_max_filesize і post_max_size у php.ini (і client_max_body_size у Nginx). Якщо файл більший, запит приходить без файлу і без зрозумілої помилки валідації - виглядає, ніби поле порожнє;
  • завантажувати користувацькі файли в public/ поруч з кодом.

Множинне завантаження: <input type="file" name="photos[]" multiple> і $request->file('photos') повертає масив.

Докладніше в документації: Завантажені файли

paginate() робить два запити:

select count(*) as aggregate from posts where published = true;   -- загальна кількість
select * from posts where published = true order by id desc limit 15 offset 30;

Завдяки кількості він знає, скільки всього сторінок, і може показати навігацію «1 2 3 ... 48» та «Показано 31-45 з 712».

simplePaginate() робить один запит і бере на один запис більше:

select * from posts where published = true order by id desc limit 16 offset 30;

Якщо повернувся 16-й запис - наступна сторінка існує. Загальної кількості він не знає, тому показує лише «Попередня» і «Наступна».

$posts = Post::where('published', true)->latest('id')->simplePaginate(15);

Порівняння:

paginate() simplePaginate()
запитів 2 (з count) 1
номери сторінок так ні
загальна кількість $posts->total() недоступна
$posts->lastPage() так ні
швидкість на великих таблицях count(*) може бути дорогим швидше

Коли обирати simplePaginate():

  • стрічки й списки, де користувач гортає послідовно: новини, коментарі, історія дій;
  • кнопка «Завантажити ще»;
  • великі таблиці, де count(*) зі складними умовами займає помітний час;
  • API для мобільних застосунків, яким номери сторінок не потрібні.

Коли paginate(): таблиці в адмінці, результати пошуку з переходом на конкретну сторінку, місця, де потрібно показати загальну кількість.

Третій варіант - cursorPaginate(): теж без загальної кількості, але замість offset використовує умову за ключем сортування - швидкий навіть на далеких сторінках. Але не дозволяє перейти на довільну сторінку.

Відображення однакове: {{ $posts->links() }} для обох; для простої пагінації Laravel використовує окремий шаблон лише з двома кнопками.

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

На одному сервері багато речей «просто працює», бо все зберігається локально. Коли серверів кілька за балансувальником, кожен наступний запит користувача може потрапити на інший сервер. Тому все, що зберігається на диску чи в пам'яті одного сервера, треба винести в спільне сховище.

Чек-лист:

Що На одному сервері На кількох
сесії file database чи redis - інакше користувача розлогінює
кеш file redis, memcached чи database - інакше кожен сервер має свій кеш і скидання не працює
файли користувачів диск local/public S3, R2 чи інше об'єктне сховище
черги database чи sync redis, database чи SQS - спільні для всіх воркерів
блокування (Cache::lock, withoutOverlapping) будь-який кеш спільний кеш з атомарними блокуваннями
планувальник cron на сервері на одному сервері чи onOneServer() для задач
логи storage/logs централізований збір (stderr, Papertrail, сервіс логів)

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

  • однаковий APP_KEY на всіх серверах - інакше cookie й сесії, зашифровані одним сервером, не розшифрує інший;
  • довірені проксі (trustProxies) - щоб застосунок бачив справжню IP-адресу й HTTPS за балансувальником;
  • міграції запускаються один раз за деплой, а не на кожному сервері;
  • локальні шляхи в коді (storage_path() для тимчасових файлів між запитами) - тимчасовий файл, створений одним запитом, інший сервер не побачить;
  • кеш конфігурації й маршрутів формується на кожному сервері при деплої - це нормально.

Головна ідея - сервер застосунку без стану (stateless). Його можна будь-коли вимкнути, замінити чи додати ще один, і користувачі цього не помітять. Тоді масштабування - це просто збільшення кількості однакових серверів.

Корисно перевірити локально: запустити два екземпляри застосунку (наприклад, два контейнери) за простим балансувальником - проблеми з сесіями й файлами проявляться одразу.

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

Repository - патерн, що ховає доступ до даних за інтерфейсом: код працює з PostRepositoryInterface, не знаючи, звідки беруться записи.

interface PostRepositoryInterface
{
    public function published(): Collection;
}

class EloquentPostRepository implements PostRepositoryInterface
{
    public function published(): Collection
    {
        return Post::where('is_published', true)->get();
    }
}

Питання зі співбесіди зазвичай про те, чи це потрібно тут. Аргумент «щоб замінити ORM» на практиці не спрацьовує: Eloquent - це Active Record, його моделі пронизують увесь застосунок, і підміна сховища все одно означала б переписування. Заміни ORM у живому проєкті майже не трапляється.

Аргумент «щоб тестувати» теж слабкий у Laravel: є RefreshDatabase і фабрики, тож тест із реальною базою пишеться просто й перевіряє більше, ніж мок репозиторію.

Що при цьому втрачається: зникають скопи, ліниві зв'язки й with() у місці виклику - або репозиторій обростає методами на кожну комбінацію фільтрів, або починає повертати Builder, і абстракція протікає.

Коли Repository справді доречний:

  • Дані живуть не в Eloquent - зовнішнє API, Elasticsearch, кілька джерел за одним інтерфейсом.
  • Домен свідомо відокремлений від фреймворку (DDD), і моделі домену не є Eloquent-моделями.

Що зазвичай працює краще: локальні скопи для повторюваних вибірок і Action/Service-класи для операцій. Вони дають ту саму зібраність без прошарку, який дублює Eloquent.

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

Service Container (IoC-контейнер) керує залежностями класів і виконує dependency injection. Він уміє автоматично будувати об'єкти, рекурсивно резолвлячи їхні залежності через type-hints конструктора.

// біндинг інтерфейсу до реалізації (зазвичай у Service Provider)
$this->app->bind(PaymentGateway::class, StripeGateway::class);

// тепер усюди, де просять PaymentGateway, прийде StripeGateway
public function __construct(private PaymentGateway $gateway) {}
  • bind() - нова реалізація щоразу.
  • singleton() - один екземпляр на весь життєвий цикл запиту.
  • app(PaymentGateway::class) - ручне резолвлення.

Це серце Laravel: контролери, middleware, події - усе резолвиться через контейнер.

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

N+1 виникає, коли ви завантажуєте N моделей одним запитом, а потім у циклі звертаєтесь до їхнього зв'язку - це генерує ще N запитів.

$posts = Post::all(); // 1 запит
foreach ($posts as $post) {
    echo $post->author->name; // +1 запит на кожен пост → N запитів
}

Рішення - eager loading через with():

$posts = Post::with('author')->get(); // лише 2 запити загалом

Вкладені та умовні зв'язки:

Post::with(['author', 'comments.user'])->get();        // вкладений eager
Post::with(['comments' => fn ($q) => $q->latest()])->get(); // умовний

Лічильники без завантаження зв'язку - withCount() (без N+1 і без вантаження самих рядків):

$posts = Post::withCount('comments')->get(); // доступ через $post->comments_count

Виявлення: Model::preventLazyLoading(! app()->isProduction()) у boot() кидає виняток на ледачих завантаженнях поза продакшеном; також допомагають Telescope і Debugbar.

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

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

class AppServiceProvider extends ServiceProvider
{
    // лише біндинги в контейнер
    public function register(): void
    {
        $this->app->singleton(Parser::class);
    }

    // виконується після реєстрації всіх провайдерів
    public function boot(): void
    {
        Gate::define('admin', fn ($u) => $u->is_admin);
    }
}

Правило: у register() - лише біндинги (інші сервіси можуть бути ще не зареєстровані); у boot() - усе інше.

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

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

Рівні
Junior 101 Middle 147 Senior 130

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