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

Питання

Найпопулярніші питання з реальних Laravel/PHP співбесід для всіх рівнів

121 питань

  • 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, очищення пов'язаних ресурсів, аудиту.

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

  • 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 при відмові.

Докладніше в документації: Авторизація (Gates та Policies)

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().

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

Планувальник дозволяє описати періодичні задачі прямо в коді. У 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).

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

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 (зі списком учасників).

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

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().

Докладніше в документації: 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.

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

Dependency Injection - патерн, за якого клас отримує залежності ззовні (зазвичай через конструктор), а не створює їх сам. Це знижує зв'язування й полегшує тестування (можна підсунути мок).

class OrderController
{
    public function __construct(
        private PaymentGateway $gateway, // інжектується контейнером
    ) {}
}

У Laravel DI працює «з коробки»: контейнер читає type-hints і автоматично будує граф залежностей. Інжектити можна й у методи контролера (method injection), зокрема сам Request.

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

Індекс - структура (зазвичай B-дерево), що пришвидшує пошук і сортування за стовпцем ціною уповільнення запису та додаткового місця.

$table->index('status'); // звичайний
$table->unique('email'); // унікальний
$table->index(['user_id', 'created_at']); // композитний

Правила:

  • Індексуйте стовпці у WHERE, JOIN, ORDER BY, зовнішні ключі.
  • Композитний індекс корисний за префіксом стовпців (порядок важливий).
  • EXPLAIN показує, чи використовується індекс.
  • Зайві індекси шкодять записам - балансуйте.

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

Chunking обробляє великі набори даних порціями, щоб не тримати всі рядки в пам'яті одразу.

Post::chunk(200, function ($posts) {
    foreach ($posts as $post) { /* ... */ }
});
  • chunkById(200, ...) - безпечніший, коли під час обробки змінюються записи (нумерує за id, а не за offset).
  • lazy() / cursor() - повертають LazyCollection: ще менше пам'яті, але один активний запит.

Без chunking Post::all() на мільйонній таблиці впаде з браку пам'яті.

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

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-*.

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