Питання
Найпопулярніші питання з реальних Laravel/PHP співбесід для всіх рівнів
171 питань
- Interface - це контракт: перелік методів, які клас зобов'язаний реалізувати. Не містить реалізації.
- Trait - механізм повторного використання коду: набір готових методів, які «вмішуються» в клас (горизонтальне перевикористання).
interface Loggable { public function logChannel(): string; }
// реалізація для багатьох моделей
trait HasUuid
{
public static function bootHasUuid(): void { /* ... */ }
}
class Order extends Model implements Loggable
{
use HasUuid;
public function logChannel(): string { return 'orders'; }
}
У Laravel трейти всюди: SoftDeletes, HasFactory, Notifiable. Інтерфейси («контракти») дають змогу підміняти реалізації через контейнер.
Поліморфний зв'язок дозволяє моделі належати кільком різним типам моделей через один зв'язок.
class Comment extends Model
{
public function commentable(): MorphTo
{
return $this->morphTo();
}
}
class Post extends Model
{
public function comments(): MorphMany
{
return $this->morphMany(Comment::class, 'commentable');
}
}
Таблиця comments має commentable_id + commentable_type. Тож Comment може належати і Post, і Video без окремих таблиць. Бувають також many-to-many поліморфні зв'язки (morphToMany), напр. теги.
Єдиний API поверх драйверів для зберігання результатів важких обчислень чи запитів. Драйвери: database (за замовчуванням у нових застосунках), file, redis, memcached, dynamodb, array (для тестів). Задається через CACHE_STORE у .env.
remember - найпоширеніший патерн (дістати з кешу або обчислити й закешувати):
$users = Cache::remember('active_users', 3600, function () {
return User::where('active', true)->get();
});
Cache::put('key', $value, now()->addMinutes(10));
$value = Cache::get('key', 'default');
Cache::forget('key');
Теговане кешування (лише Redis/Memcached) - для групового скидання:
Cache::tags(['posts'])->put('post.1', $post, 600);
Cache::tags(['posts'])->flush(); // скинути всю групу
Атомарні блокування проти гонок (один процес у критичній секції):
Cache::lock('processing', 10)->get(function () {
// критична секція
});
Найскладніше - інвалідація: кеш скидають у подіях/обзерверах моделей при зміні даних. Застарілий кеш часто гірший за його відсутність.
Observer групує слухачів подій моделі (creating, created, updating, saved, deleting тощо) в один клас - замість роздування boot() моделі.
class PostObserver
{
public function creating(Post $post): void
{
$post->slug = Str::slug($post->title);
}
public function deleted(Post $post): void
{
$post->image()->delete();
}
}
Реєстрація - атрибутом #[ObservedBy(PostObserver::class)] на моделі або в Service Provider. Зручно для генерації slug, очищення пов'язаних ресурсів, аудиту.
- Gate - замикання для простих, не прив'язаних до моделі перевірок.
- Policy - клас, що групує правила авторизації навколо конкретної моделі.
// Gate
Gate::define('view-admin', fn (User $u) => $u->is_admin);
// Policy
class PostPolicy
{
public function update(User $user, Post $post): bool
{
return $user->id === $post->user_id;
}
}
Застосування:
$this->authorize('update', $post); // у контролері
@can('update', $post) ... @endcan // у Blade
$user->can('update', $post); // будь-де
Політики автоматично відкривають 403 при відмові.
Scopes інкапсулюють часто вживані умови запитів.
Local scope - викликається вручну:
public function scopePublished(Builder $query): Builder
{
return $query->where('is_published', true);
}
Post::published()->latest()->get();
Global scope - застосовується автоматично до всіх запитів моделі:
#[ScopedBy([TenantScope::class])]
class Invoice extends Model {}
SoftDeletes - приклад глобального scope (автоматично додає where deleted_at is null). Глобальний scope можна обійти через withoutGlobalScope().
Планувальник дозволяє описати періодичні завдання прямо в коді. У Laravel 11+ розклад визначається в routes/console.php через фасад Schedule:
use Illuminate\Support\Facades\Schedule;
Schedule::command('reports:send')->dailyAt('08:00');
Schedule::job(new PruneLogs)->weekly();
Schedule::call(fn () => Cache::flush())->hourly();
На сервері потрібен лише один cron-запис, що щохвилини викликає планувальник:
* * * * * cd /app && php artisan schedule:run >> /dev/null 2>&1
Корисні модифікатори: withoutOverlapping(), onOneServer(), runInBackground().
API Resource - шар трансформації між Eloquent-моделлю та JSON-відповіддю. Дає повний контроль над структурою API, відв'язуючи її від схеми БД.
class PostResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'author' => UserResource::make($this->whenLoaded('author')),
'createdAt' => $this->created_at->toIso8601String(),
];
}
}
return PostResource::collection($posts);
whenLoaded()додає зв'язок лише якщо він eager-завантажений (без N+1).- Resource Collections дозволяють додавати метадані (
meta,links).
Broadcasting транслює серверні події на клієнт через WebSockets - для оновлень у реальному часі (чати, нотифікації).
class MessageSent implements ShouldBroadcast
{
public function broadcastOn(): array
{
return [new PrivateChannel('chat.'.$this->roomId)];
}
}
На клієнті Laravel Echo підписується на канал:
Echo.private(`chat.${roomId}`)
.listen('MessageSent', (e) => console.log(e.message));
Сервер WebSockets - Laravel Reverb (офіційний), Pusher або Soketi. Канали бувають public, private (з авторизацією) і presence (зі списком учасників).
FormRequest - окремий клас, що містить правила валідації та авторизацію, виносячи їх із контролера.
class StorePostRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()->can('create', Post::class);
}
public function rules(): array
{
return ['title' => ['required', 'max:255']];
}
}
// $request - вже провалідовано
public function store(StorePostRequest $request)
{
Post::create($request->validated());
}
Переваги: тонкі контролери, перевикористання правил, метод prepareForValidation() для нормалізації вводу, власні повідомлення в messages().
Класична скарга: користувач вибрав фільтри, перейшов на другу сторінку - і фільтри злетіли. Причина в тому, що посилання пагінації за замовчуванням несуть лише page.
Додати поточні параметри:
$posts = Post::filter($request)->paginate(15)->withQueryString();
withQueryString() переносить усі параметри рядка запиту в посилання пагінації. Якщо потрібні конкретні - appends():
$posts->appends(['sort' => $request->input('sort')]);
Друга частина проблеми - сортування без унікального ключа. Якщо сортувати за колонкою з повторами, рядки з однаковим значенням база може віддати в різному порядку на різних сторінках - і один запис зʼявиться двічі, а інший зникне:
// хитко: багато записів з однаковою датою
Post::orderByDesc('published_at')->paginate(15);
// стабільно: дозволяємо ключем
Post::orderByDesc('published_at')->orderByDesc('id')->paginate(15);
Третя - сторінка поза межами. Після зміни фільтра запис може стати менше, ніж потрібно для сторінки 7, і користувач бачить порожньо. Скидання номера сторінки при зміні фільтра вирішує це; у Livewire для того є resetPage().
Для SEO варто памʼятати ще про одне: кожна комбінація фільтрів із номером сторінки - окрема URL. Якщо їх багато, сторінки з фільтрами зазвичай закривають від індексації, лишаючи в ній базовий список.
Валідацію можна писати просто в контролері:
public function store(Request $request)
{
$data = $request->validate([
'title' => ['required', 'string', 'max:255'],
'body' => ['required'],
]);
}
Для двох правил цього досить. Коли їх десяток, а та сама форма ще й редагується, контролер розпухає, а правила дублюються між store() і update().
Form Request виносить це в окремий клас:
php artisan make:request StorePostRequest
class StorePostRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()->can('create', Post::class);
}
/**
* @return array<string, list<string>>
*/
public function rules(): array
{
return [
'title' => ['required', 'string', 'max:255'],
'body' => ['required', 'string'],
'published_at' => ['nullable', 'date'],
];
}
public function messages(): array
{
return ['title.required' => 'Вкажіть заголовок вакансії.'];
}
}
Використання - тип у сигнатурі:
public function store(StorePostRequest $request)
{
// сюди виконання дійде лише з валідними даними
$post = Post::create($request->validated());
}
Що це дає: валідація й перевірка прав відбуваються до входу в метод; контролер лишається про свою справу; правила лежать в одному місці й перевикористовуються; prepareForValidation() дозволяє нормалізувати вхід (обрізати пробіли, привести формат) до перевірки.
Деталь, яку часто пропускають: authorize(), що повертає false, дає 403 - тобто Form Request закриває і валідацію, і доступ.
Транзакція гарантує атомарність: або всі операції виконуються, або жодна.
DB::transaction(function () use ($order) {
$order->save();
$order->items()->createMany($items);
Inventory::decrement($order->product_id, $order->qty);
});
При винятку всередині замикання Laravel автоматично робить rollBack(). Ручний контроль:
DB::beginTransaction();
try {
// ...
DB::commit();
} catch (Throwable $e) {
DB::rollBack();
throw $e;
}
Другий аргумент transaction($cb, 3) задає кількість повторів при deadlock.
migrate:rollback відкочує останній «батч» міграцій, викликаючи їхні методи down():
php artisan migrate:rollback # останній батч
php artisan migrate:rollback --step=1 # рівно одну міграцію
migrate:reset відкочує всі міграції по черзі. migrate:refresh - відкочує все й накатує заново. migrate:fresh - видаляє всі таблиці й накатує міграції з нуля, взагалі не заглядаючи в down().
Що з цього небезпечне в проді: усі чотири. Кожна знищує дані, а migrate:fresh робить це найшвидше й без шансу на down(). У проді припустимий лише php artisan migrate.
Laravel сам питає підтвердження в продакшн-середовищі, і саме тому --force не варто вписувати в скрипти «щоб не заважало».
Практика, що рятує:
- Перевіряйте відкат локально одразу після написання:
migrate→migrate:rollback --step=1→migrate. Половина міграцій має неробочийdown(), і виявляється це в найгірший момент. - Пишіть
down()чесно, а якщо відкат неможливий - хай кидає виняток, це краще за мовчазну порожню реалізацію. - Не редагуйте вже застосовану міграцію - додавайте нову. Виправлений файл не перезастосується там, де він уже відпрацював.
- Видалення колонки й перейменування - окремими релізами від коду, що їх читає, інакше деплой ламає працюючий застосунок у проміжку.
Dependency Injection - патерн, за якого клас отримує залежності ззовні (зазвичай через конструктор), а не створює їх сам. Це знижує зв'язування й полегшує тестування (можна підсунути мок).
class OrderController
{
public function __construct(
private PaymentGateway $gateway, // інжектується контейнером
) {}
}
У Laravel DI працює «з коробки»: контейнер читає type-hints і автоматично будує граф залежностей. Інжектити можна й у методи контролера (method injection), зокрема сам Request.
Питання з реальних співбесід Laravel і PHP - 171 питання у 41 темі, розібраних із відповідями. Нижче - розбивка за рівнями та темами, якщо хочете звузити підготовку.
- Теми
- Eloquent 19 Архітектура 12 Database 10 Performance 7 Routing 5 Безпека 5 Blade 5 API 5
Готуєтесь до співбесіди не просто так: зараз на сайті 167 відкритих вакансій Laravel і PHP. Переглянути вакансії