# 25 запитань на співбесіді з Laravel та PHP у 2026 році > 25 найпоширеніших запитань на співбесіді з Laravel: Service Container, Eloquent ORM, middleware, черги, безпека, тестування та архітектурні патерни з прикладами коду. - Published: 2026-01-30 - Updated: 2026-04-12 - Author: SharpSkill - Tags: laravel, php, interview, eloquent, technical interview - Reading time: 22 min --- Співбесіди з Laravel перевіряють володіння фреймворком, знання екосистеми PHP та здатність проєктувати масштабовані веб-додатки. Цей посібник охоплює 25 запитань, які найчастіше зустрічаються на технічних інтерв'ю — від основ фреймворку до оптимізації продуктивності та розгортання на продакшені. > **Порада для співбесіди** > > Інтерв'юери цінують кандидатів, які можуть пояснити архітектурні рішення Laravel. Розуміння того, чому фреймворк використовує певні конвенції (Convention over Configuration, Service Container), створює реальну відмінність на співбесіді. ## Основи Laravel ### 1. Що таке Service Container і яку роль він виконує в Laravel? Service Container — це серце фреймворку Laravel, потужний механізм управління залежностями класів та виконання dependency injection. Кожен додаток Laravel має єдиний контейнер, який реєструє та розв'язує зв'язки між інтерфейсами та їхніми конкретними реалізаціями. Завдяки цьому код залишається слабо зв'язаним і легким для тестування. ```php // AppServiceProvider.php use App\Contracts\PaymentGateway; use App\Services\StripePaymentGateway; public function register(): void { // Bind interface to concrete implementation $this->app->bind(PaymentGateway::class, StripePaymentGateway::class); // Singleton: resolved once, same instance reused $this->app->singleton(PaymentGateway::class, function ($app) { return new StripePaymentGateway( config('services.stripe.secret') ); }); } ``` ```php // OrderController.php class OrderController extends Controller { // Automatic injection via type-hinting public function __construct( private readonly PaymentGateway $payment ) {} public function store(Request $request): JsonResponse { $result = $this->payment->charge( $request->validated('amount') ); return response()->json($result); } } ``` Контейнер автоматично розв'язує залежності на основі type-hint'ів. Якщо клас залежить від інтерфейсу, зареєстрованого в контейнері, Laravel ін'єктує відповідну реалізацію без додаткової конфігурації. ### 2. Яка різниця між Service Provider та Service Container? Service Container — це механізм зберігання та розв'язання залежностей. Service Provider — це клас, відповідальний за реєстрацію зв'язків у контейнері та конфігурацію сервісів додатку. Кожен провайдер має два методи: `register()` для прив'язки сервісів та `boot()` для дій, що потребують вже зареєстрованих сервісів. ```php // AnalyticsServiceProvider.php class AnalyticsServiceProvider extends ServiceProvider { public function register(): void { // register() - only bind things into the container $this->app->singleton(AnalyticsTracker::class, function ($app) { return new AnalyticsTracker( $app->make(HttpClient::class), config('analytics.key') ); }); } public function boot(): void { // boot() - all providers are registered, safe to use them $tracker = $this->app->make(AnalyticsTracker::class); $tracker->registerDefaultEvents(); // Publish config for package users $this->publishes([ __DIR__.'/../config/analytics.php' => config_path('analytics.php'), ]); } } ``` Провайдер вказується в масиві `providers` у `config/app.php`. Laravel завантажує їх послідовно — спочатку викликає `register()` у всіх провайдерів, а потім `boot()`, що гарантує доступність усіх сервісів під час конфігурації. ### 3. Як працює життєвий цикл HTTP-запиту в Laravel? Кожен HTTP-запит у Laravel проходить через чітко визначену послідовність етапів. Файл `public/index.php` завантажує автозавантажувач Composer і створює екземпляр додатку. Далі запит проходить через HTTP kernel, який виконує глобальні middleware, а маршрутизатор зіставляє запит з відповідним маршрутом. Контролер обробляє логіку та повертає відповідь, яка знову проходить через middleware у зворотному порядку. ```php // Simplified request lifecycle // 1. public/index.php - Entry point $app = require_once __DIR__.'/../bootstrap/app.php'; // 2. HTTP Kernel handles the request $kernel = $app->make(Kernel::class); $response = $kernel->handle( $request = Request::capture() ); // 3. Inside Kernel: middleware pipeline // Global middleware -> Route middleware -> Controller // 4. Router matches the request Route::get('/orders/{order}', [OrderController::class, 'show']) ->middleware(['auth', 'verified']); // 5. Controller returns response // 6. Response passes back through middleware // 7. Response sent to browser $response->send(); $kernel->terminate($request, $response); ``` Метод `terminate()` в кінці циклу дозволяє виконувати операції після відправки відповіді — наприклад, запис логів чи закриття з'єднань. ### 4. Що таке Facade в Laravel і як вони працюють під капотом? Facade надають статичний синтаксис доступу до сервісів, зареєстрованих у Service Container. Проте це не справжні статичні класи — під капотом кожен фасад делегує виклик відповідному об'єкту, розв'язаному з контейнера. Метод `getFacadeAccessor()` повертає ключ прив'язки в контейнері. ```php // How facades work under the hood // Using the Cache facade Cache::put('user_123', $userData, 3600); // Is equivalent to: app('cache')->put('user_123', $userData, 3600); // The facade class: class Cache extends Facade { protected static function getFacadeAccessor(): string { return 'cache'; // Key in the service container } } // Real-time facades - any class can become a facade use Facades\App\Services\PaymentGateway; PaymentGateway::charge(1000); // Resolves from container ``` Facade полегшують тестування — Laravel дозволяє їх мокати за допомогою `Cache::shouldReceive()`, що робить модульні тести простими та зрозумілими. ## Eloquent ORM ### 5. Які відмінності між відношеннями hasOne, hasMany, belongsTo та belongsToMany? Eloquent визначає відношення як методи в моделях. `hasOne` означає відношення один-до-одного, де зовнішній ключ знаходиться у пов'язаній таблиці. `hasMany` — це відношення один-до-багатьох. `belongsTo` є зворотнім — модель містить зовнішній ключ. `belongsToMany` обслуговує відношення багато-до-багатьох з проміжною таблицею (pivot). ```php // User.php class User extends Model { // One-to-One: users.id -> profiles.user_id public function profile(): HasOne { return $this->hasOne(Profile::class); } // One-to-Many: users.id -> posts.user_id public function posts(): HasMany { return $this->hasMany(Post::class); } // Many-to-Many: users <-> role_user <-> roles public function roles(): BelongsToMany { return $this->belongsToMany(Role::class) ->withPivot('assigned_at') ->withTimestamps(); } } // Post.php class Post extends Model { // Inverse of hasMany public function user(): BelongsTo { return $this->belongsTo(User::class); } } ``` ```php // Usage examples $user = User::find(1); // Lazy loading $profile = $user->profile; // Single query $posts = $user->posts; // Collection of posts // Eager loading to avoid N+1 $users = User::with(['posts', 'roles'])->get(); // Querying relationships $activeAuthors = User::whereHas('posts', function ($query) { $query->where('published', true) ->where('created_at', '>=', now()->subMonth()); })->get(); ``` Вибір правильного відношення впливає на продуктивність запитів та читабельність коду. Eager loading за допомогою `with()` усуває проблему N+1. ### 6. Як працюють Eloquent Scopes і коли варто їх використовувати? Scopes дозволяють інкапсулювати повторювані умови запитів у методах моделі. Є два види: Local Scopes, що викликаються явно в ланцюжку запитів, та Global Scopes, що застосовуються автоматично до кожного запиту моделі. ```php // Post.php - Local Scopes class Post extends Model { // Local scope - called explicitly public function scopePublished(Builder $query): Builder { return $query->where('status', 'published') ->whereNotNull('published_at'); } public function scopeByAuthor(Builder $query, int $userId): Builder { return $query->where('user_id', $userId); } // Chainable scopes public function scopeRecent(Builder $query, int $days = 7): Builder { return $query->where('created_at', '>=', now()->subDays($days)); } } // Usage: clean, readable queries $posts = Post::published() ->recent(30) ->byAuthor($userId) ->paginate(15); ``` ```php // Global Scope - applied to ALL queries on the model class ActiveScope implements Scope { public function apply(Builder $builder, Model $model): void { $builder->where('is_active', true); } } // In the model class Subscription extends Model { protected static function booted(): void { static::addGlobalScope(new ActiveScope()); } } // All queries automatically filter active Subscription::all(); // WHERE is_active = true // Remove global scope when needed Subscription::withoutGlobalScope(ActiveScope::class)->get(); ``` Scopes покращують читабельність коду та запобігають дублюванню логіки фільтрації. Global Scopes слід застосовувати обережно — автоматична фільтрація може призводити до помилок, які важко виявити. ### 7. Що таке Eloquent Mutators та Casts і як вони впливають на атрибути моделі? Mutators та Casts контролюють спосіб запису та читання атрибутів моделі. Casts визначають автоматичне перетворення типів, а Mutators (accessor та mutator) дозволяють виконувати довільну трансформацію значень через методи `Attribute`. ```php // User.php - Attribute Casting and Mutators class User extends Model { // Automatic type casting protected function casts(): array { return [ 'email_verified_at' => 'datetime', 'preferences' => 'array', // JSON <-> array 'is_admin' => 'boolean', 'salary' => 'decimal:2', 'metadata' => AsCollection::class, 'secret' => 'encrypted', // Auto encrypt/decrypt ]; } // Modern accessor (Laravel 9+) protected function fullName(): Attribute { return Attribute::make( get: fn () => "{$this->first_name} {$this->last_name}", ); } // Accessor + Mutator combined protected function email(): Attribute { return Attribute::make( get: fn (string $value) => strtolower($value), set: fn (string $value) => strtolower(trim($value)), ); } } ``` ```php // Custom Cast class for complex transformations class MoneyCast implements CastsAttributes { public function get(Model $model, string $key, mixed $value, array $attributes): Money { return new Money( amount: $attributes[$key], currency: $attributes['{$key}_currency'] ?? 'USD' ); } public function set(Model $model, string $key, mixed $value, array $attributes): array { return [ $key => $value->amount, "{$key}_currency" => $value->currency, ]; } } // Usage in model protected function casts(): array { return [ 'price' => MoneyCast::class, ]; } ``` Casts усувають необхідність ручного перетворення типів у контролерах і view. Користувацькі класи Cast дозволяють моделювати складні типи значень з повною інкапсуляцією. ## Middleware та маршрутизація ### 8. Як працює middleware в Laravel і як створювати власні? Middleware — це фільтри, що обробляють HTTP-запит до або після потрапляння до контролера. Laravel розрізняє глобальні middleware (виконуються при кожному запиті), middleware груп маршрутів та middleware, призначені конкретним маршрутам. Порядок виконання є важливим — middleware можуть модифікувати запит, перервати його або змінити відповідь. ```php // EnsureUserIsSubscribed.php class EnsureUserIsSubscribed { public function handle(Request $request, Closure $next): Response { if (!$request->user()?->isSubscribed()) { if ($request->expectsJson()) { return response()->json([ 'message' => 'Subscription required.' ], 403); } return redirect()->route('subscribe'); } return $next($request); } } // After middleware - runs after the response class AddSecurityHeaders { public function handle(Request $request, Closure $next): Response { $response = $next($request); $response->headers->set('X-Content-Type-Options', 'nosniff'); $response->headers->set('X-Frame-Options', 'DENY'); $response->headers->set('Strict-Transport-Security', 'max-age=31536000'); return $response; } } ``` ```php // bootstrap/app.php (Laravel 11+) return Application::configure(basePath: dirname(__DIR__)) ->withMiddleware(function (Middleware $middleware) { // Global middleware $middleware->append(AddSecurityHeaders::class); // Named middleware for routes $middleware->alias([ 'subscribed' => EnsureUserIsSubscribed::class, ]); // Middleware groups $middleware->appendToGroup('api', [ ThrottleRequests::class.':api', ]); }) ->create(); // Usage in routes Route::middleware('subscribed')->group(function () { Route::get('/premium', [PremiumController::class, 'index']); }); ``` З Laravel 11 конфігурація middleware перенесена з класу Kernel до файлу `bootstrap/app.php`, що спрощує структуру додатку. ### 9. Як працюють групи маршрутів та RESTful ресурси в Laravel? Групи маршрутів дозволяють спільно використовувати атрибути (middleware, префікси, простори імен) між кількома маршрутами. Resource controllers автоматично генерують CRUD-маршрути відповідно до конвенцій REST. ```php // routes/web.php // Route grouping with shared attributes Route::prefix('admin') ->middleware(['auth', 'admin']) ->name('admin.') ->group(function () { Route::get('/dashboard', [AdminController::class, 'dashboard']) ->name('dashboard'); // Resource controller: 7 RESTful routes Route::resource('articles', ArticleController::class); // API resource: excludes create/edit (no forms) Route::apiResource('categories', CategoryController::class); }); // Nested resources Route::resource('posts.comments', CommentController::class) ->scoped() // Auto scope bindings ->shallow(); // Shallow nesting for simplicity // Route model binding with custom resolution Route::get('/users/{user:username}', [UserController::class, 'show']); ``` ```php // ArticleController.php - Resource Controller class ArticleController extends Controller { // GET /admin/articles public function index(): View { /* list */ } // GET /admin/articles/create public function create(): View { /* form */ } // POST /admin/articles public function store(StoreArticleRequest $request): RedirectResponse { $article = Article::create($request->validated()); return redirect()->route('admin.articles.show', $article); } // GET /admin/articles/{article} public function show(Article $article): View { /* display */ } // GET /admin/articles/{article}/edit public function edit(Article $article): View { /* form */ } // PUT /admin/articles/{article} public function update(UpdateArticleRequest $request, Article $article): RedirectResponse { $article->update($request->validated()); return redirect()->route('admin.articles.show', $article); } // DELETE /admin/articles/{article} public function destroy(Article $article): RedirectResponse { $article->delete(); return redirect()->route('admin.articles.index'); } } ``` Route model binding автоматично розв'язує моделі на основі параметрів URL. Параметр `{user:username}` шукає користувача за колонкою `username` замість `id`. ## Черги та завдання (Queues & Jobs) ### 10. Як працюють черги в Laravel і коли варто їх використовувати? Черги дозволяють відкласти часомісткі операції — надсилання електронних листів, обробку файлів, синхронізацію із зовнішніми API — щоб не блокувати HTTP-відповідь. Laravel підтримує кілька драйверів черг: Redis, Amazon SQS, базу даних. ```php // ProcessPodcastJob.php class ProcessPodcast implements ShouldQueue { use Queueable; public int $tries = 3; public int $backoff = 60; // seconds between retries public int $timeout = 120; public function __construct( private readonly Podcast $podcast ) {} public function handle(AudioProcessor $processor): void { $processed = $processor->process( $this->podcast->audio_path ); $this->podcast->update([ 'processed_path' => $processed->path, 'duration' => $processed->duration, 'status' => 'ready', ]); } public function failed(\Throwable $exception): void { // Notify admin of failure $this->podcast->update(['status' => 'failed']); Log::error('Podcast processing failed', [ 'podcast_id' => $this->podcast->id, 'error' => $exception->getMessage(), ]); } } ``` ```php // Dispatching jobs // Basic dispatch ProcessPodcast::dispatch($podcast); // Delayed dispatch ProcessPodcast::dispatch($podcast) ->delay(now()->addMinutes(10)); // Specific queue ProcessPodcast::dispatch($podcast) ->onQueue('audio-processing'); // Job chaining - sequential execution Bus::chain([ new ProcessPodcast($podcast), new OptimizeAudio($podcast), new PublishPodcast($podcast), ])->dispatch(); // Job batching - parallel with completion tracking Bus::batch([ new ProcessChapter($podcast, 1), new ProcessChapter($podcast, 2), new ProcessChapter($podcast, 3), ])->then(function (Batch $batch) { Log::info('All chapters processed!'); })->catch(function (Batch $batch, Throwable $e) { Log::error('Batch failed: '.$e->getMessage()); })->dispatch(); ``` Job chaining гарантує послідовне виконання завдань, тоді як batching дозволяє паралельну обробку з відстеженням завершення та помилок. ### 11. Що таке middleware черг і як керувати обмеженням швидкості завдань? Middleware черг дозволяють контролювати спосіб виконання завдань — обмежувати частоту, запобігати дублікатам чи відкладати у разі перевантаження. Вони визначаються в методі `middleware()` класу завдання. ```php // RateLimitedJob.php class CallExternalApi implements ShouldQueue { use Queueable; public function middleware(): array { return [ // Max 10 jobs per minute for this API new ThrottlesExceptions(10, 5), // Rate limiting by key (new RateLimited('external-api')) ->dontRelease(), // Don't retry if rate limited // Prevent overlapping - only one at a time new WithoutOverlapping($this->userId), ]; } public function handle(): void { // API call logic } public function retryUntil(): DateTime { return now()->addHours(1); } } ``` ```php // Define rate limiter in AppServiceProvider RateLimiter::for('external-api', function (object $job) { return Limit::perMinute(30); }); // Unique jobs - prevent duplicates in queue class UpdateSearchIndex implements ShouldQueue, ShouldBeUnique { use Queueable; // Unique for 1 hour public int $uniqueFor = 3600; public function uniqueId(): string { return $this->product->id; } public function handle(): void { // Only one job per product in queue at a time } } ``` Middleware `WithoutOverlapping` запобігає одночасному виконанню завдань з однаковим ключем, усуваючи конфлікти даних. `ShouldBeUnique` гарантує, що в черзі перебуває максимум одне завдання для певного ідентифікатора. ## Безпека та автентифікація ### 12. Як Laravel захищає від атак CSRF, XSS та SQL Injection? Laravel забезпечує багаторівневий захист від найпоширеніших веб-атак. Токени CSRF захищають форми, шаблонізатор Blade автоматично екранує вихідні дані (захист від XSS), а Eloquent з Query Builder використовують підготовлені запити (захист від SQL Injection). ```php // CSRF Protection - automatic in forms
@csrf {{-- Generates hidden token field --}} @method('PUT')
// XSS Protection - Blade auto-escapes {{ $userInput }} {{-- Escaped: <script> --}} {!! $trustedHtml !!} {{-- Raw: use only for trusted content --}} // SQL Injection Protection // Safe - parameterized query $users = User::where('email', $request->email)->get(); $users = DB::select('SELECT * FROM users WHERE email = ?', [$email]); // DANGEROUS - raw unescaped input $users = DB::select("SELECT * FROM users WHERE email = '$email'"); // NEVER DO THIS ``` ```php // Mass Assignment Protection class User extends Model { // Only these fields can be mass-assigned protected $fillable = ['name', 'email', 'avatar']; // Or protect specific fields protected $guarded = ['is_admin', 'role']; } // Rate limiting for API protection // bootstrap/app.php $middleware->appendToGroup('api', [ ThrottleRequests::class.':60,1', // 60 requests per minute ]); ``` Захист mass assignment через `$fillable` або `$guarded` запобігає несанкціонованій зміні чутливих полів моделі. Рекомендується використовувати `$fillable` як підхід типу whitelist. ### 13. Як працює система автентифікації в Laravel? Laravel пропонує повноцінну систему автентифікації з підтримкою кількох guard'ів та provider'ів. Guard'и визначають спосіб перевірки користувача в кожному запиті, а provider'и вказують джерело даних користувача. ```php // config/auth.php return [ 'defaults' => [ 'guard' => 'web', ], 'guards' => [ 'web' => [ 'driver' => 'session', 'provider' => 'users', ], 'api' => [ 'driver' => 'sanctum', 'provider' => 'users', ], 'admin' => [ 'driver' => 'session', 'provider' => 'admins', ], ], 'providers' => [ 'users' => [ 'driver' => 'eloquent', 'model' => App\Models\User::class, ], 'admins' => [ 'driver' => 'eloquent', 'model' => App\Models\Admin::class, ], ], ]; ``` ```php // Authentication in controllers class AuthController extends Controller { public function login(LoginRequest $request): RedirectResponse { $credentials = $request->validated(); if (Auth::attempt($credentials, $request->boolean('remember'))) { $request->session()->regenerate(); return redirect()->intended('dashboard'); } throw ValidationException::withMessages([ 'email' => __('auth.failed'), ]); } public function logout(Request $request): RedirectResponse { Auth::logout(); $request->session()->invalidate(); $request->session()->regenerateToken(); return redirect('/'); } } // Multi-guard authentication Auth::guard('admin')->attempt($credentials); Auth::guard('api')->user(); ``` `session()->regenerate()` після входу запобігає атакам session fixation. `session()->invalidate()` при виході повністю знищує сесію. ### 14. Як працює авторизація за допомогою Gates та Policies в Laravel? Gates — це замикання (closures), що визначають загальні дозволи, тоді як Policies — це класи, що групують логіку авторизації для конкретної моделі. Gates підходять для простих перевірок, а Policies — для CRUD-операцій. ```php // AuthServiceProvider or AppServiceProvider boot() Gate::define('manage-settings', function (User $user): bool { return $user->is_admin; }); Gate::define('update-post', function (User $user, Post $post): bool { return $user->id === $post->user_id; }); // Using gates if (Gate::allows('manage-settings')) { /* ... */ } Gate::authorize('update-post', $post); // Throws 403 if denied ``` ```php // PostPolicy.php class PostPolicy { // Runs before all other checks public function before(User $user, string $ability): ?bool { if ($user->is_super_admin) { return true; // Bypass all checks } return null; // Fall through to specific method } public function view(User $user, Post $post): bool { return $post->published || $user->id === $post->user_id; } public function update(User $user, Post $post): bool { return $user->id === $post->user_id; } public function delete(User $user, Post $post): bool { return $user->id === $post->user_id && $post->comments_count === 0; } } // Using policies in controllers public function update(Request $request, Post $post): RedirectResponse { $this->authorize('update', $post); // Auto-resolves PostPolicy $post->update($request->validated()); return redirect()->route('posts.show', $post); } // In Blade templates @can('update', $post) Edit @endcan ``` Метод `before()` у Policy дозволяє обійти всі перевірки для адміністраторів. Повернення `null` замість `false` дозволяє перейти до стандартної логіки авторизації. ## Валідація та форми ### 15. Як працює валідація в Laravel і як створювати власні правила? Laravel пропонує розгалужену систему валідації з десятками вбудованих правил. Form Requests ізолюють логіку валідації від контролерів, а користувацькі правила дозволяють визначати власні валідатори. ```php // StoreArticleRequest.php - Form Request class StoreArticleRequest extends FormRequest { public function authorize(): bool { return $this->user()->can('create', Article::class); } public function rules(): array { return [ 'title' => ['required', 'string', 'max:255', 'unique:articles'], 'slug' => ['required', 'string', 'regex:/^[a-z0-9-]+$/'], 'body' => ['required', 'string', 'min:100'], 'category_id' => ['required', 'exists:categories,id'], 'tags' => ['array', 'max:5'], 'tags.*' => ['string', 'max:30'], 'publish_at' => ['nullable', 'date', 'after:now'], 'cover_image' => ['nullable', 'image', 'max:2048', 'dimensions:min_width=800'], ]; } public function messages(): array { return [ 'title.unique' => 'An article with this title already exists.', 'body.min' => 'Article content must be at least 100 characters.', ]; } // Transform data before validation protected function prepareForValidation(): void { $this->merge([ 'slug' => Str::slug($this->title), ]); } } ``` ```php // Custom validation rule class StrongPassword implements ValidationRule { public function validate(string $attribute, mixed $value, Closure $fail): void { if (strlen($value) < 12) { $fail('The :attribute must be at least 12 characters.'); } if (!preg_match('/[A-Z]/', $value)) { $fail('The :attribute must contain an uppercase letter.'); } if (!preg_match('/[0-9]/', $value)) { $fail('The :attribute must contain a number.'); } } } // Usage 'password' => ['required', new StrongPassword()], // Conditional validation $request->validate([ 'role' => 'required|in:user,admin', 'admin_code' => 'required_if:role,admin|string', 'company' => Rule::requiredIf($request->role === 'admin'), ]); ``` Form Requests є кращим підходом у професійних проєктах — вони відокремлюють логіку валідації від контролера та можуть також містити логіку авторизації. ## Тестування ### 16. Як писати тести в Laravel з використанням PHPUnit та Pest? Laravel забезпечує відмінну інфраструктуру тестування. Feature tests перевіряють повні HTTP-запити, а unit tests тестують ізольовані класи. Фреймворк пропонує допоміжні методи для тестування запитів, бази даних, черг та багато іншого. ```php // Feature test - tests/Feature/ArticleTest.php class ArticleTest extends TestCase { use RefreshDatabase; public function test_user_can_create_article(): void { $user = User::factory()->create(); $response = $this->actingAs($user) ->post('/articles', [ 'title' => 'Test Article', 'body' => str_repeat('Content ', 20), 'category_id' => Category::factory()->create()->id, ]); $response->assertRedirect(); $this->assertDatabaseHas('articles', [ 'title' => 'Test Article', 'user_id' => $user->id, ]); } public function test_guest_cannot_create_article(): void { $response = $this->post('/articles', [ 'title' => 'Test', ]); $response->assertRedirect('/login'); } } ``` ```php // Same tests with Pest syntax uses(RefreshDatabase::class); it('allows authenticated users to create articles', function () { $user = User::factory()->create(); $response = $this->actingAs($user) ->post('/articles', [ 'title' => 'Test Article', 'body' => str_repeat('Content ', 20), 'category_id' => Category::factory()->create()->id, ]); $response->assertRedirect(); expect(Article::where('title', 'Test Article')->exists())->toBeTrue(); }); it('prevents guests from creating articles', function () { $this->post('/articles', ['title' => 'Test']) ->assertRedirect('/login'); }); // Testing with mocks it('sends welcome email after registration', function () { Mail::fake(); $this->post('/register', [ 'name' => 'John', 'email' => 'john@example.com', 'password' => 'SecurePass123!', ]); Mail::assertSent(WelcomeEmail::class, function ($mail) { return $mail->hasTo('john@example.com'); }); }); ``` Трейт `RefreshDatabase` відкочує транзакції після кожного тесту, забезпечуючи ізоляцію. `actingAs()` імітує автентифікацію без необхідності реального входу. ### 17. Як працюють Model Factories та Seeders у тестах? Factories генерують реалістичні тестові дані за допомогою бібліотеки Faker. Seeders заповнюють базу даних початковими даними — як для тестів, так і для ініціалізації середовищ. ```php // database/factories/ArticleFactory.php class ArticleFactory extends Factory { protected $model = Article::class; public function definition(): array { return [ 'user_id' => User::factory(), // Auto-creates related user 'category_id' => Category::factory(), 'title' => fake()->sentence(), 'slug' => fake()->slug(), 'body' => fake()->paragraphs(5, true), 'status' => 'draft', 'published_at' => null, ]; } // State methods for variations public function published(): static { return $this->state(fn (array $attributes) => [ 'status' => 'published', 'published_at' => fake()->dateTimeBetween('-1 year'), ]); } public function withComments(int $count = 3): static { return $this->has(Comment::factory()->count($count)); } } ``` ```php // Using factories in tests // Single model $article = Article::factory()->create(); // With specific attributes $article = Article::factory()->published()->create([ 'title' => 'Specific Title', ]); // With relationships $user = User::factory() ->has(Article::factory()->count(5)->published()) ->has(Article::factory()->count(2)) // drafts ->create(); // Complex seeder class DatabaseSeeder extends Seeder { public function run(): void { // Create admin User::factory()->create([ 'email' => 'admin@example.com', 'is_admin' => true, ]); // Create users with articles User::factory(10) ->has(Article::factory()->count(rand(1, 10))->published()) ->create(); $this->call([ CategorySeeder::class, TagSeeder::class, ]); } } ``` Станові методи (state methods) у factories дозволяють створювати різні варіанти моделі читабельно та зручно в підтримці. Метод `has()` автоматично створює пов'язані моделі. ## Архітектура та патерни ### 18. Що таке патерн Repository і коли варто його застосовувати в Laravel? Патерн Repository створює рівень абстракції між бізнес-логікою та рівнем доступу до даних. Хоча Eloquent сам по собі є реалізацією патерну Active Record, додавання Repository може бути виправданим у великих додатках, що потребують тестованості та можливості заміни джерела даних. ```php // Contracts/ArticleRepositoryInterface.php interface ArticleRepositoryInterface { public function findById(int $id): ?Article; public function findPublished(int $perPage = 15): LengthAwarePaginator; public function create(array $data): Article; public function update(int $id, array $data): Article; public function delete(int $id): bool; } // Repositories/EloquentArticleRepository.php class EloquentArticleRepository implements ArticleRepositoryInterface { public function __construct( private readonly Article $model ) {} public function findById(int $id): ?Article { return $this->model ->with(['author', 'category', 'tags']) ->find($id); } public function findPublished(int $perPage = 15): LengthAwarePaginator { return $this->model ->published() ->with(['author', 'category']) ->latest('published_at') ->paginate($perPage); } public function create(array $data): Article { return $this->model->create($data); } public function update(int $id, array $data): Article { $article = $this->model->findOrFail($id); $article->update($data); return $article->fresh(); } public function delete(int $id): bool { return $this->model->findOrFail($id)->delete(); } } ``` ```php // Bind in ServiceProvider $this->app->bind( ArticleRepositoryInterface::class, EloquentArticleRepository::class ); // Use in controller class ArticleController extends Controller { public function __construct( private readonly ArticleRepositoryInterface $articles ) {} public function index(): View { return view('articles.index', [ 'articles' => $this->articles->findPublished(), ]); } } ``` У невеликих проєктах Repository може бути зайвою абстракцією над Eloquent. У великих додатках він полегшує тестування (мокання інтерфейсу) та підтримку чистої архітектури. ### 19. Як реалізувати патерн Action в Laravel? Патерн Action ізолює окрему бізнес-операцію у виділеному класі з одним публічним методом. Це робить код більш тестованим, повторно використовуваним та читабельним — кожен клас має одне, чітко визначене завдання. ```php // Actions/CreateArticleAction.php class CreateArticleAction { public function __construct( private readonly ImageUploader $uploader, private readonly SlugGenerator $slugs ) {} public function execute(User $author, array $data): Article { $slug = $this->slugs->generate($data['title']); $coverPath = isset($data['cover_image']) ? $this->uploader->upload($data['cover_image'], 'articles') : null; $article = $author->articles()->create([ 'title' => $data['title'], 'slug' => $slug, 'body' => $data['body'], 'cover_image' => $coverPath, 'category_id' => $data['category_id'], 'status' => $data['publish'] ? 'published' : 'draft', 'published_at' => $data['publish'] ? now() : null, ]); if (!empty($data['tags'])) { $article->tags()->sync($data['tags']); } if ($article->status === 'published') { event(new ArticlePublished($article)); } return $article; } } ``` ```php // Usage in controller class ArticleController extends Controller { public function store( StoreArticleRequest $request, CreateArticleAction $action ): RedirectResponse { $article = $action->execute( $request->user(), $request->validated() ); return redirect()->route('articles.show', $article); } } // Reusable in Artisan commands class ImportArticleCommand extends Command { protected $signature = 'articles:import {file}'; public function handle(CreateArticleAction $action): int { $articles = json_decode( file_get_contents($this->argument('file')), true ); foreach ($articles as $data) { $action->execute($this->getSystemUser(), $data); } return Command::SUCCESS; } } ``` Actions особливо добре підходять для логіки, що використовується як у контролерах, так і в командах Artisan та завданнях черг. Кожну Action легко протестувати модульно. ### 20. Як працюють Events та Listeners в Laravel? Система подій Laravel реалізує патерн Observer. Events представляють факти, що відбулися в додатку, а Listeners реагують на ці події. Це відокремлює дію від її наслідків. ```php // Events/OrderPlaced.php class OrderPlaced { use SerializesModels; public function __construct( public readonly Order $order ) {} } // Listeners/SendOrderConfirmation.php class SendOrderConfirmation implements ShouldQueue { public function handle(OrderPlaced $event): void { Mail::to($event->order->user) ->send(new OrderConfirmationMail($event->order)); } } // Listeners/UpdateInventory.php class UpdateInventory { public function handle(OrderPlaced $event): void { foreach ($event->order->items as $item) { $item->product->decrement('stock', $item->quantity); } } } // Listeners/NotifyWarehouse.php class NotifyWarehouse implements ShouldQueue { public function handle(OrderPlaced $event): void { Http::post('https://warehouse.api/orders', [ 'order_id' => $event->order->id, 'items' => $event->order->items->toArray(), ]); } } ``` ```php // Event registration (EventServiceProvider or auto-discovery) protected $listen = [ OrderPlaced::class => [ SendOrderConfirmation::class, UpdateInventory::class, NotifyWarehouse::class, ], ]; // Dispatching events event(new OrderPlaced($order)); // Or using the static dispatch method OrderPlaced::dispatch($order); // Model observers - events for Eloquent models class OrderObserver { public function created(Order $order): void { event(new OrderPlaced($order)); } public function updated(Order $order): void { if ($order->wasChanged('status')) { event(new OrderStatusChanged($order)); } } } ``` Listeners, що реалізують `ShouldQueue`, автоматично потрапляють до черги, запобігаючи уповільненню HTTP-відповіді. Model Observers централізують обробку подій життєвого циклу моделі. ## Продуктивність та оптимізація ### 21. Як вирішити проблему N+1 запитів у Laravel? Проблема N+1 виникає, коли додаток виконує один запит для отримання колекції, а потім окремий запит для кожного елемента. Eager loading за допомогою `with()` розв'язує цю проблему, завантажуючи зв'язки в одному або двох запитах. ```php // N+1 Problem - 1 query for posts + N queries for authors $posts = Post::all(); foreach ($posts as $post) { echo $post->author->name; // Each access = 1 query } // Result: 1 + N queries (101 queries for 100 posts!) // Solution: Eager Loading $posts = Post::with('author')->get(); // 2 queries total // Nested eager loading $posts = Post::with([ 'author', 'comments.user', // Nested: comments AND their users 'tags', ])->get(); // Conditional eager loading $posts = Post::with(['comments' => function ($query) { $query->where('approved', true) ->latest() ->limit(5); }])->get(); ``` ```php // Prevent N+1 in development // AppServiceProvider boot() public function boot(): void { // Throws exception when lazy loading happens Model::preventLazyLoading(!app()->isProduction()); // Log instead of throwing Model::handleLazyLoadingViolationUsing(function ($model, $relation) { Log::warning("Lazy loading {$relation} on {$model::class}"); }); } // Lazy eager loading - when you already have the collection $posts = Post::all(); $posts->load('author', 'tags'); // Eager load after initial query // withCount for counting without loading $posts = Post::withCount('comments') ->having('comments_count', '>', 10) ->get(); echo $posts[0]->comments_count; // No extra query ``` `Model::preventLazyLoading()` — незамінний інструмент у середовищі розробки. Він виявляє проблему N+1 на етапі розробки, перш ніж вона потрапить на продакшен. ### 22. Як працює кешування в Laravel і які стратегії оптимізації існують? Laravel пропонує уніфіковане API для кешування з підтримкою кількох драйверів: Redis, Memcached, файл, база даних. Стратегія кешування охоплює як кеш додатку, так і оптимізації, специфічні для фреймворку. ```php // Basic caching operations Cache::put('user:123', $userData, now()->addHours(1)); $user = Cache::get('user:123'); $user = Cache::get('user:123', 'default value'); // Remember pattern - get or compute and store $articles = Cache::remember('popular-articles', 3600, function () { return Article::published() ->withCount('views') ->orderByDesc('views_count') ->limit(10) ->get(); }); // Cache tags for grouped invalidation Cache::tags(['articles', 'homepage'])->put('featured', $articles, 3600); Cache::tags('articles')->flush(); // Clear all article caches // Atomic locks for race condition prevention $lock = Cache::lock('processing-order-'.$orderId, 10); if ($lock->get()) { try { // Process order - guaranteed exclusive access $this->processOrder($orderId); } finally { $lock->release(); } } ``` ```php // Framework optimization commands // Cache configuration files php artisan config:cache // Cache routes php artisan route:cache // Cache views php artisan view:cache // Cache events php artisan event:cache // Optimize all at once php artisan optimize // Database query caching strategy class ArticleService { public function getFeatured(): Collection { return Cache::tags('articles') ->remember('articles:featured', 1800, function () { return Article::published() ->featured() ->with(['author', 'category']) ->latest('published_at') ->limit(6) ->get(); }); } public function clearCache(): void { Cache::tags('articles')->flush(); } } ``` Команди `config:cache` та `route:cache` повинні бути частиною скрипта розгортання. Кеш тегів працює лише з драйверами, що підтримують теги (Redis, Memcached). ## Розгортання та продакшен ### 23. Як виглядає процес розгортання Laravel-додатку на продакшен? Професійне розгортання Laravel включає оптимізацію автозавантажувача, кешування конфігурації та маршрутів, компіляцію ресурсів та міграцію бази даних. Інструменти на кшталт Laravel Forge, Envoyer або власні CI/CD-пайплайни автоматизують цей процес. ```bash # Production deployment script #!/bin/bash set -e # Pull latest code git pull origin main # Install dependencies (no dev) composer install --no-dev --optimize-autoloader # Run migrations php artisan migrate --force # Clear and rebuild caches php artisan optimize:clear php artisan optimize # Build frontend assets npm ci npm run build # Restart queue workers php artisan queue:restart # Reload PHP-FPM (zero downtime) sudo systemctl reload php8.3-fpm ``` ```php // config/database.php - Production database configuration 'pgsql' => [ 'driver' => 'pgsql', 'host' => env('DB_HOST', '127.0.0.1'), 'port' => env('DB_PORT', '5432'), 'database' => env('DB_DATABASE', 'forge'), 'username' => env('DB_USERNAME', 'forge'), 'password' => env('DB_PASSWORD', ''), 'charset' => 'utf8', 'prefix' => '', 'search_path' => 'public', // Connection pooling 'pool' => [ 'min' => 2, 'max' => 10, ], ], // Logging configuration for production 'channels' => [ 'stack' => [ 'driver' => 'stack', 'channels' => ['daily', 'slack'], ], 'daily' => [ 'driver' => 'daily', 'path' => storage_path('logs/laravel.log'), 'level' => 'warning', // Only warnings+ in production 'days' => 14, ], ], ``` Прапорець `--force` у `php artisan migrate` є обов'язковим у продакшен-середовищі, оскільки Laravel за замовчуванням блокує міграції на продакшені. `queue:restart` сигналізує воркерам про м'яке завершення після поточного завдання. ### 24. Як налаштовувати та моніторити черги на продакшені? Правильне налаштування черг на продакшені потребує менеджера процесів (Supervisor), стратегії обробки помилок та моніторингу. Laravel Horizon надає дашборд та розширену конфігурацію для черг Redis. ```bash # Supervisor configuration: /etc/supervisor/conf.d/laravel-worker.conf [program:laravel-worker] process_name=%(program_name)s_%(process_num)02d command=php /var/www/app/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600 autostart=true autorestart=true stopasgroup=true killasgroup=true user=www-data numprocs=4 redirect_stderr=true stdout_logfile=/var/www/app/storage/logs/worker.log stopwaitsecs=3600 ``` ```php // config/horizon.php - Laravel Horizon configuration 'environments' => [ 'production' => [ 'supervisor-default' => [ 'maxProcesses' => 10, 'balanceMaxShift' => 1, 'balanceCooldown' => 3, ], 'supervisor-emails' => [ 'connection' => 'redis', 'queue' => ['emails'], 'maxProcesses' => 3, ], 'supervisor-heavy' => [ 'connection' => 'redis', 'queue' => ['audio-processing', 'video-processing'], 'maxProcesses' => 5, 'timeout' => 600, ], ], ], // Failed job handling // config/queue.php 'failed' => [ 'driver' => 'database-uuids', 'database' => 'pgsql', 'table' => 'failed_jobs', ], // Retry failed jobs // php artisan queue:retry all // php artisan queue:retry {uuid} // php artisan queue:flush // Delete all failed ``` Supervisor гарантує автоматичний перезапуск воркерів у разі збою. Laravel Horizon додає автобалансування, метрики та інтерфейс керування чергами Redis. ### 25. Як керувати змінними середовища та конфігурацією в різних середовищах? Laravel використовує файли `.env` для конфігурації, специфічної для середовища. На продакшені критично важливе кешування конфігурації, безпечне зберігання секретів та належне розмежування налаштувань між середовищами. ```php // .env - Environment-specific values APP_NAME=MyApp APP_ENV=production APP_DEBUG=false APP_URL=https://myapp.com DB_CONNECTION=pgsql DB_HOST=db.internal DB_DATABASE=myapp_prod CACHE_DRIVER=redis QUEUE_CONNECTION=redis SESSION_DRIVER=redis MAIL_MAILER=ses AWS_ACCESS_KEY_ID=your-key AWS_SECRET_ACCESS_KEY=your-secret ``` ```php // Accessing configuration $appName = config('app.name'); $dbHost = config('database.connections.pgsql.host'); // With default fallback $timeout = config('services.api.timeout', 30); // NEVER use env() outside config files! // Bad - breaks with config:cache $key = env('API_KEY'); // Good - always use config() // config/services.php return [ 'payment' => [ 'key' => env('PAYMENT_API_KEY'), 'secret' => env('PAYMENT_API_SECRET'), 'sandbox' => env('PAYMENT_SANDBOX', false), ], ]; // Then use: $key = config('services.payment.key'); // Environment-specific configuration if (app()->isProduction()) { // Production-only logic } // Custom environment detection $environment = match(true) { app()->environment('production') => 'prod', app()->environment('staging') => 'staging', default => 'dev', }; ``` Після виконання `config:cache` функція `env()` повертає `null` поза файлами конфігурації. Тому `env()` слід викликати виключно в каталозі `config/`, а в коді додатку — завжди використовувати `config()`. Кожне з наведених 25 запитань потребує глибшого аналізу з використанням офіційної документації Laravel. Кандидати, які здатні пояснити не лише "як", а передусім "чому" фреймворк приймає певні архітектурні рішення, виділяються під час співбесід. > **Наступні кроки** > > Кожне запитання заслуговує на глибше дослідження з офіційною документацією Laravel. Інтерв'юери цінують кандидатів, які знають тонкощі фреймворку та можуть обґрунтувати свої технічні рішення. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/uk/blog/laravel/laravel-php-interview-questions