Питання
Найпопулярніші питання з реальних Laravel/PHP співбесід для всіх рівнів
171 питань
Індекс - структура (зазвичай B-дерево), що пришвидшує пошук і сортування за стовпцем ціною уповільнення запису та додаткового місця.
$table->index('status'); // звичайний
$table->unique('email'); // унікальний
$table->index(['user_id', 'created_at']); // композитний
Правила:
- Індексуйте стовпці у
WHERE,JOIN,ORDER BY, зовнішні ключі. - Композитний індекс корисний за префіксом стовпців (порядок важливий).
EXPLAINпоказує, чи використовується індекс.- Зайві індекси шкодять записам - балансуйте.
На таблиці в кілька мільйонів рядків необережна міграція блокує запис на хвилини, і застосунок лежить.
Що блокує довго:
- Додавання колонки
NOT NULLзі значенням за замовчуванням у старих версіях MySQL переписує таблицю цілком. У MySQL 8 і PostgreSQL 11+ це вже метадані, але перевіряти варто саме свою версію. - Створення індексу звичайним
CREATE INDEXтримає таблицю на час побудови. - Зміна типу колонки майже завжди означає перезапис.
Безпечний порядок для нової колонки:
// 1. Спершу nullable - миттєво, без перезапису.
Schema::table('vacancies', function (Blueprint $table) {
$table->string('short_name')->nullable()->after('title');
});
Далі заповнити дані пачками - окремою командою, не в міграції:
Vacancy::whereNull('short_name')->chunkById(500, function ($vacancies) {
foreach ($vacancies as $vacancy) {
$vacancy->update(['short_name' => Str::limit($vacancy->title, 40, '')]);
}
});
І лише потім, якщо потрібно, робити колонку обовʼязковою - окремою міграцією, коли даних без значення вже немає.
Індекс без блокування:
// PostgreSQL
DB::statement('CREATE INDEX CONCURRENTLY vacancies_level_index ON vacancies (level)');
CONCURRENTLY не можна виконувати всередині транзакції, тож у міграції потрібно вимкнути обгортання:
public $withinTransaction = false;
Загальне правило: розділяйте зміну схеми й заповнення даних. Міграція має бути швидкою операцією над структурою, а перенесення даних - командою, яку можна запустити, зупинити й продовжити.
Chunking обробляє великі набори даних порціями, щоб не тримати всі рядки в пам'яті одразу.
Post::chunk(200, function ($posts) {
foreach ($posts as $post) { /* ... */ }
});
chunkById(200, ...)- безпечніший, коли під час обробки змінюються записи (нумерує заid, а не за offset).lazy()/cursor()- повертають LazyCollection: ще менше пам'яті, але один активний запит.
Без chunking Post::all() на мільйонній таблиці впаде з браку пам'яті.
Rate Limiting обмежує кількість запитів за період. Іменовані обмежувачі визначають через RateLimiter::for() (у Laravel 11+ зазвичай у bootstrap/app.php або AppServiceProvider::boot()):
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
Застосування до маршрутів:
Route::middleware('throttle:api')->group(...);
Route::post('/login', ...)->middleware('throttle:5,1'); // 5 за хвилину
При перевищенні - HTTP 429 із заголовками Retry-After та X-RateLimit-*.
Звичайне middleware працює навколо запиту: щось робить до $next($request), щось - після, але завжди до того, як відповідь піде користувачу.
Terminable middleware виконується після відправлення відповіді:
class LogRequestDuration
{
public function handle(Request $request, Closure $next): Response
{
return $next($request);
}
public function terminate(Request $request, Response $response): void
{
// Користувач уже отримав сторінку - це його не затримує.
RequestLog::create([
'path' => $request->path(),
'status' => $response->getStatusCode(),
'duration' => microtime(true) - LARAVEL_START,
]);
}
}
Метод terminate() викликається з $app->terminate() після send().
Важлива умова: це працює лише коли сервер уміє віддати відповідь і продовжити виконання - тобто на FastCGI з fastcgi_finish_request(). За іншої конфігурації користувач усе одно чекатиме.
Ще одна деталь: за замовчуванням у terminate() потрапляє новий екземпляр middleware. Якщо потрібен той самий - зареєструйте його синглтоном:
$this->app->singleton(LogRequestDuration::class);
Для чого доречно: запис аналітики, логування тривалості, дрібне прибирання - те, що не впливає на відповідь.
Для чого ні: будь-що довге. Воно все одно тримає PHP-воркер зайнятим, тож ця робота належить у чергу, а не в terminate().
View Composer прив'язує дані до шаблону щоразу, коли той рендериться - щоб не дублювати передачу спільних даних у багатьох контролерах.
View::composer('partials.sidebar', function ($view) {
$view->with('categories', Category::all());
});
Тепер будь-яке відображення partials.sidebar автоматично отримає $categories. Зручно для меню, лічильників, віджетів у layout. Реєструється у boot() сервіс-провайдера.
Це різні рівні автентифікації:
- Breeze - стартовий набір UI: реєстрація/вхід/скидання пароля на Blade+Livewire або React/Vue. Для швидкого старту.
- Fortify - headless-бекенд автентифікації (без UI): логіка реєстрації, 2FA, скидання пароля. Під ним працює Breeze/Jetstream.
- Sanctum - легка автентифікація для SPA (через cookie) та простих API-токенів. Дефолт для більшості API.
- Passport - повноцінний OAuth2-сервер: видача access/refresh токенів стороннім клієнтам. Обирають, коли потрібен саме OAuth2.
Правило: SPA чи мобільний застосунок → Sanctum; «увійти через наш сервіс» для третіх сторін → Passport.
Sanctum дає два режими:
1. API-токени - модель випускає токен, який клієнт шле в заголовку Authorization: Bearer ...:
$token = $user->createToken('mobile', ['posts:read'])->plainTextToken;
Route::middleware('auth:sanctum')->get('/user', fn (Request $r) => $r->user());
2. SPA-автентифікація - для односторінкових застосунків на тому ж домені використовує звичайні сесійні cookie + CSRF (без зберігання токенів у JS, що безпечніше).
Токени підтримують abilities (scopes): $user->tokenCan('posts:read'). Трейт HasApiTokens додає tokens()-зв'язок і createToken().
Notification - одне повідомлення, яке можна доставити кількома каналами одночасно.
class InvoicePaid extends Notification
{
public function via(object $notifiable): array
{
return ['mail', 'database', 'broadcast'];
}
public function toMail($notifiable): MailMessage { /* ... */ }
public function toArray($notifiable): array { /* для database */ }
}
$user->notify(new InvoicePaid($invoice));
Канали з коробки: mail, database (зберігає в notifications), broadcast (WebSockets), Vonage (SMS), Slack. Є community-канали (Telegram, push). Канал database зручний для «дзвіночка» сповіщень у UI; реалізувавши ShouldQueue, відправку виносять у чергу.
«Тонкий контролер» - це той, що лише координує: приймає запит, викликає потрібне й повертає відповідь. Усе інше живе деінде.
Що виносять із контролера і куди:
| Що | Куди |
|---|---|
| Правила валідації | Form Request |
| Перевірка прав | Policy або authorize() у Form Request |
| Бізнес-логіка | Service чи Action-клас |
| Складна вибірка | Локальний скоп на моделі |
| Формат відповіді | API Resource |
| Побічні ефекти (лист, індексація) | Слухач події або джоба в черзі |
Було:
public function store(Request $request)
{
$data = $request->validate([...]); // валідація
if (! $request->user()->can('create', Post::class)) { // права
abort(403);
}
$post = Post::create($data); // логіка
$post->tags()->sync($data['tags']);
Mail::to($post->author)->send(new PostCreated($post)); // побічний ефект
return response()->json([ // формат
'id' => $post->id,
'title' => $post->title,
]);
}
Стало:
public function store(StorePostRequest $request, CreatePost $createPost)
{
return new PostResource($createPost->handle($request->validated()));
}
Валідація й права спрацювали до входу в метод, логіка з побічними ефектами - усередині CreatePost, формат - у ресурсі.
Навіщо це насправді. Не заради краси: логіку з контролера не викликати ні з Artisan-команди, ні з черги, ні з тесту без HTTP-запиту. Щойно та сама операція знадобилась у другому місці, тонкий контролер перестає бути питанням смаку.
Де межа. Post::create($request->validated()) не потребує ні сервісу, ні ресурсу - шар заради шару лише додає файлів. Виносьте, коли з'явилася друга причина: кілька моделей в одній операції, виклик не з HTTP або контролер перестав вміщатися на екран.
LazyCollection використовує PHP-генератори, щоб тримати в пам'яті лише один елемент за раз - критично для величезних наборів.
LazyCollection::make(function () {
$handle = fopen('huge.csv', 'r');
while (($line = fgets($handle)) !== false) {
yield $line;
}
})->filter(...)->take(100)->each(...);
З Eloquent:
User::cursor()->each(function ($user) { /* по одному рядку */ });
На відміну від звичайної Collection, методи не матеріалізують увесь набір - обчислення «ліниві» й виконуються лише при ітерації.
Custom Cast інкапсулює логіку перетворення атрибута між форматом БД та об'єктом PHP.
class Money implements CastsAttributes
{
public function get($model, $key, $value, $attributes): MoneyValue
{
return new MoneyValue($value); // з БД → Value Object
}
public function set($model, $key, $value, $attributes): array
{
return ['price' => $value->cents]; // VO → у БД
}
}
protected $casts = ['price' => Money::class];
Застосування: робота з Value Objects, шифрування полів, JSON-структури. Вбудовані касти: array, encrypted, datetime, enum-класи, AsCollection.
Backed enum кастується в моделі, і колонка починає повертати обʼєкт замість рядка:
enum VacancyLevel: string
{
case Junior = 'junior';
case Middle = 'middle';
case Senior = 'senior';
public function label(): string
{
return match ($this) {
self::Junior => 'Junior',
self::Middle => 'Middle',
self::Senior => 'Senior',
};
}
}
class Vacancy extends Model
{
protected function casts(): array
{
return ['level' => VacancyLevel::class];
}
}
Тепер $vacancy->level - це enum, а не рядок:
$vacancy->level->label();
$vacancy->level === VacancyLevel::Senior;
$vacancy->update(['level' => VacancyLevel::Middle]); // у базу піде 'middle'
Переваги над константами класу:
- Обмежена множина. Значення поза переліком не існує, тоді як константа не заважає передати будь-який рядок.
- Типізація.
function assign(VacancyLevel $level)не прийме випадковий рядок, і редактор підкаже варіанти. - Поведінка поруч зі значенням. Enum має методи, тож підпис, колір чи іконка живуть там само, а не в розкиданих
matchпо шаблонах. matchбезdefaultпідсвітить пропущений випадок, коли додасте новий кейс.
Дві практичні деталі. У валідації є правило Rule::enum(VacancyLevel::class). А tryFrom() повертає null замість винятку - саме він потрібен, коли значення приходить від користувача чи з URL.
Laravel має тестування з коробки поверх PHPUnit; популярна надбудова - Pest із лаконічним синтаксисом.
it('creates a post', function () {
$user = User::factory()->create();
$response = $this->actingAs($user)->post('/posts', [
'title' => 'Hello',
]);
$response->assertRedirect();
$this->assertDatabaseHas('posts', ['title' => 'Hello']);
});
- Feature-тести перевіряють HTTP-флоу (більшість тестів); Unit - окремі класи ізольовано.
- Трейт
RefreshDatabaseізолює тести, відкочуючи зміни після кожного; дані генерують фабрики. actingAs($user)автентифікує користувача для захищених маршрутів.
Корисні асерції: assertStatus, assertSee, assertJson, assertDatabaseHas, assertRedirect, assertAuthenticated.
Мокінг - ізоляція від зовнішніх ефектів (API, листи, черги):
Mail::fake(); Mail::assertSent(OrderShipped::class);
Queue::fake(); Queue::assertPushed(ProcessPodcast::class);
Notification::fake(); Http::fake();
// мок сервісу через контейнер
$this->mock(PaymentGateway::class)
->shouldReceive('charge')->once()->andReturn(true);
Запуск: php artisan test, --filter=PostTest, --parallel (швидше).
Тест не повинен слати справжні листи - це повільно, ненадійно й іноді доходить до реальних людей.
Mail::fake() підміняє транспорт і запамʼятовує, що мало піти:
it('sends a welcome letter on registration', function () {
Mail::fake();
$this->post('/register', [
'email' => 'dev@example.com',
'password' => 'password',
]);
Mail::assertSent(WelcomeMail::class, function (WelcomeMail $mail) {
return $mail->hasTo('dev@example.com');
});
});
Є ще assertNotSent(), assertNothingSent() і assertSentCount().
Важлива пастка: якщо лист відправляється через сповіщення ($user->notify(...)), Mail::fake() його не побачить - потрібен Notification::fake() і assertSentTo(). Плутанина між цими двома - найчастіша причина «тест не бачить листа, хоча він точно йде».
Друга пастка: якщо mailable реалізує ShouldQueue, а в тесті стоїть Queue::fake(), лист не дійде до пошти взагалі - перевіряти треба постановку завдання.
Поза тестами для перегляду верстки зручні два інструменти. Драйвер log пише лист у storage/logs, а Mailpit чи Mailtrap ловлять пошту в локальний ящик - листи виглядають як справжні, але нікуди не йдуть.
Ще одна дрібниця: mailable можна відкрити прямо в браузері, повернувши його з маршруту - зручно для правки шаблону без повторних відправлень.
Питання з реальних співбесід Laravel і PHP - 171 питання у 41 темі, розібраних із відповідями. Нижче - розбивка за рівнями та темами, якщо хочете звузити підготовку.
- Теми
- Eloquent 19 Архітектура 12 Database 10 Performance 7 Routing 5 Безпека 5 Blade 5 API 5
Готуєтесь до співбесіди не просто так: зараз на сайті 167 відкритих вакансій Laravel і PHP. Переглянути вакансії