# PHP Laravel Framework Interview Questions 2026: Eloquent, Queues and Architecture Patterns > Master Laravel 13 interview questions covering Eloquent ORM relationships, queue architecture, service container patterns, and real-world coding challenges that senior developers face in 2026. - Published: 2026-09-16 - Updated: 2026-09-16 - Author: Anthony Fillion-Maillet - Tags: laravel, php, interview, eloquent, queues, architecture - Reading time: 11 min --- PHP Laravel framework interview questions test more than syntax knowledge. Companies hiring in 2026 expect candidates to explain Eloquent relationship optimization, queue failure handling, and service container bindings with production-level detail. Laravel 13 introduces AI tooling, JSON:API resources, and vector search capabilities that add new dimensions to technical interviews. > **What interviewers evaluate** > > Senior Laravel positions focus on three areas: Eloquent performance (N+1 prevention, chunking, cursor pagination), queue reliability (retries, dead letter handling, job batching), and architectural decisions (service providers, facades vs injection, repository patterns). ## Eloquent ORM: Relationship Loading and Query Optimization Eloquent interview questions consistently target the N+1 problem. Candidates must demonstrate when to use `with()`, `load()`, and `loadMissing()`, plus explain the memory tradeoffs of eager loading large datasets. ```php // UserController.php // Eager load with constraints to avoid loading unnecessary data $users = User::query() ->with(['posts' => function (Builder $query) { $query->where('published', true) ->select('id', 'user_id', 'title', 'published_at'); }]) ->withCount('posts') ->paginate(25); // Bad: triggers N+1 when accessing posts in loop foreach ($users as $user) { $user->posts; // Each iteration queries the database } ``` The `withCount()` method adds a subquery that counts related records without loading them. This avoids memory overhead when displaying counts in listing pages. For relationships that might not be accessed, `loadMissing()` prevents redundant queries when the relationship was already eager loaded upstream. A common follow-up question asks about cursor pagination vs offset pagination. Cursor pagination uses `cursorPaginate()` and avoids the performance cliff that offset pagination hits on large tables. The tradeoff: cursor pagination cannot jump to arbitrary pages. ## Advanced Eloquent: Polymorphic Relationships and Query Scopes Polymorphic relationships require precise understanding of the morph map and index strategy. Interviewers often present a scenario with comments that belong to posts, videos, and products, then ask candidates to optimize the queries. ```php // Comment.php class Comment extends Model { // morphTo automatically resolves the parent type from commentable_type column public function commentable(): MorphTo { return $this->morphTo(); } } // AppServiceProvider.php // Morph map prevents class name exposure in database and improves query performance Relation::enforceMorphMap([ 'post' => Post::class, 'video' => Video::class, 'product' => Product::class, ]); ``` The morph map serves two purposes: it decouples database values from class names (enabling refactoring without migrations), and it creates shorter, indexable strings. Without a morph map, Laravel stores full class names like `App\Models\Post` in the `commentable_type` column. Query scopes demonstrate understanding of reusable query logic. Global scopes apply automatically, while local scopes require explicit invocation. A typical interview question asks when to use each. ```php // Post.php // Global scope: applies to all queries unless explicitly removed protected static function booted(): void { static::addGlobalScope('published', function (Builder $builder) { $builder->where('published', true); }); } // Local scope: invoked explicitly with ->popular() public function scopePopular(Builder $query, int $minViews = 1000): Builder { return $query->where('views', '>=', $minViews); } // Usage: Post::popular(5000)->get() // Remove global scope: Post::withoutGlobalScope('published')->get() ``` ## Queue Architecture: Job Design and Failure Handling Queue questions assess production readiness. Laravel 13.31 added `Queue::totalSize()` to count every job on a connection and a `JobInterrupted` event for graceful shutdown handling. Candidates should know the difference between sync, database, Redis, and SQS drivers, plus when each fits. For deeper coverage of Laravel queue patterns, see the [Queues & Jobs interview questions](/technologies/laravel/interview-questions/queues-jobs) module. ```php // ProcessOrder.php class ProcessOrder implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public int $tries = 3; // Maximum attempts before failing public int $backoff = 60; // Seconds between retries public int $timeout = 120; // Maximum execution time public int $maxExceptions = 2; // Fail after 2 unhandled exceptions public function __construct( public readonly Order $order ) {} public function handle(PaymentGateway $gateway): void { // SerializesModels refreshes the Order from database when job runs $gateway->charge($this->order); } public function failed(Throwable $exception): void { // Called after all retries exhausted - alert team, refund, etc. Log::critical('Order processing failed', [ 'order_id' => $this->order->id, 'exception' => $exception->getMessage(), ]); } } ``` The `SerializesModels` trait stores only the model ID in the queue payload, not the full model. When the job runs, Eloquent fetches a fresh instance. This prevents stale data but means deleted models cause `ModelNotFoundException`. Handle this with `deleteWhenMissingModels`. Job batching questions appear frequently. Batches allow grouping jobs and defining callbacks for completion, failure, or cancellation. ```php // OrderBatchController.php public function processBatch(array $orderIds): PendingBatch { $jobs = collect($orderIds) ->map(fn (int $id) => new ProcessOrder(Order::find($id))); return Bus::batch($jobs) ->name('Process Orders ' . now()->toDateString()) ->allowFailures() // Continue batch even if some jobs fail ->then(function (Batch $batch) { // All jobs completed successfully Notification::send(Admin::all(), new BatchCompleted($batch)); }) ->catch(function (Batch $batch, Throwable $e) { // First failure in batch Log::warning('Batch job failed', ['batch_id' => $batch->id]); }) ->finally(function (Batch $batch) { // Batch finished (success or failure) }) ->dispatch(); } ``` ## Service Container: Bindings, Contextual Injection, and Providers The [service container](/technologies/laravel/interview-questions/service-container-di) is Laravel's core. Interview questions test understanding of binding types, resolution order, and when to use each approach. ```php // AppServiceProvider.php public function register(): void { // Singleton: same instance throughout request lifecycle $this->app->singleton(PaymentGateway::class, function (Application $app) { return new StripeGateway( apiKey: config('services.stripe.secret'), logger: $app->make(LoggerInterface::class) ); }); // Bind: new instance on each resolution $this->app->bind(ReportGenerator::class, function (Application $app) { return new PdfReportGenerator($app->make(ViewFactory::class)); }); // Contextual binding: different implementation per consumer $this->app->when(PhotoController::class) ->needs(Filesystem::class) ->give(fn () => Storage::disk('photos')); $this->app->when(DocumentController::class) ->needs(Filesystem::class) ->give(fn () => Storage::disk('documents')); } ``` Contextual bindings solve the problem of injecting different implementations into different classes without creating separate interfaces. The container resolves `Filesystem` differently based on which class requests it. A senior-level question asks about deferred providers. Standard service providers register on every request, even if their services go unused. Deferred providers register only when one of their declared services is resolved. ```php // ReportingServiceProvider.php class ReportingServiceProvider extends ServiceProvider implements DeferrableProvider { public function register(): void { $this->app->singleton(ReportingService::class, function () { return new ReportingService(/* heavy initialization */); }); } // Container only loads this provider when ReportingService is requested public function provides(): array { return [ReportingService::class]; } } ``` ## Middleware Architecture: Request Pipeline and Terminable Middleware Middleware interview questions focus on the request/response lifecycle, priority ordering, and terminable middleware for post-response tasks. ```php // RateLimitApi.php class RateLimitApi { public function handle(Request $request, Closure $next): Response { $key = 'api:' . ($request->user()?->id ?? $request->ip()); if (RateLimiter::tooManyAttempts($key, maxAttempts: 60)) { $retryAfter = RateLimiter::availableIn($key); return response()->json( ['error' => 'Rate limit exceeded'], Response::HTTP_TOO_MANY_REQUESTS )->header('Retry-After', $retryAfter); } RateLimiter::hit($key, decayMinutes: 1); // Pass request to next middleware $response = $next($request); // Modify response before returning to client return $response->header( 'X-RateLimit-Remaining', RateLimiter::remaining($key, 60) ); } } ``` The `$next($request)` call passes the request down the middleware stack. Code before this call runs during the request phase; code after runs during the response phase. This symmetry enables logging, timing, and response modification. Terminable middleware executes after the response is sent to the client, useful for slow operations that should not delay the user. ```php // LogSlowRequests.php class LogSlowRequests implements TerminableMiddleware { public function handle(Request $request, Closure $next): Response { $request->attributes->set('start_time', microtime(true)); return $next($request); } public function terminate(Request $request, Response $response): void { $duration = microtime(true) - $request->attributes->get('start_time'); if ($duration > 1.0) { Log::warning('Slow request detected', [ 'url' => $request->fullUrl(), 'duration_ms' => round($duration * 1000), 'user_id' => $request->user()?->id, ]); } } } ``` ## Repository Pattern: When to Use and When to Avoid The repository pattern generates debate. Laravel's Eloquent already implements Active Record. Adding repositories creates abstraction that some teams find unnecessary. Interviewers ask candidates to justify their position. Arguments for repositories: - Decouples business logic from Eloquent, enabling database switches (rare in practice) - Simplifies testing by mocking the repository interface - Centralizes query logic when multiple controllers use similar queries Arguments against: - Adds boilerplate without clear benefit for CRUD operations - Eloquent's query builder is already expressive and testable - Laravel's dependency injection works directly with models ```php // OrderRepository.php (when repositories add value) class OrderRepository { public function __construct( private readonly Order $model ) {} public function findPendingForUser(User $user): Collection { return $this->model->query() ->where('user_id', $user->id) ->where('status', OrderStatus::Pending) ->with('items.product') ->orderByDesc('created_at') ->get(); } public function calculateRevenueForPeriod(CarbonPeriod $period): Money { $total = $this->model->query() ->whereBetween('completed_at', [$period->start, $period->end]) ->where('status', OrderStatus::Completed) ->sum('total_cents'); return Money::ofMinor($total, 'USD'); } } ``` A balanced answer: repositories add value when query logic is complex, shared across contexts, or when the team prioritizes testability through interface mocking. For simple CRUD, injecting the model directly keeps code concise. ## Testing: Feature Tests, Mocking, and Database Strategies Laravel testing questions verify candidates can write reliable, fast tests. The framework provides `RefreshDatabase`, `DatabaseTransactions`, and `LazilyRefreshDatabase` traits with different tradeoffs. ```php // OrderTest.php class OrderTest extends TestCase { use RefreshDatabase; // Migrates once per class, transactions per test public function test_user_can_place_order(): void { // Arrange $user = User::factory()->create(); $product = Product::factory()->create(['price_cents' => 2999]); // Act $response = $this->actingAs($user) ->postJson('/api/orders', [ 'items' => [ ['product_id' => $product->id, 'quantity' => 2], ], ]); // Assert $response->assertCreated() ->assertJsonPath('data.total_cents', 5998); $this->assertDatabaseHas('orders', [ 'user_id' => $user->id, 'status' => 'pending', ]); } public function test_order_dispatches_processing_job(): void { Queue::fake(); $user = User::factory()->create(); $order = Order::factory()->for($user)->create(); $this->actingAs($user) ->postJson("/api/orders/{$order->id}/process"); Queue::assertPushed(ProcessOrder::class, function ($job) use ($order) { return $job->order->id === $order->id; }); } } ``` The `RefreshDatabase` trait runs migrations once per test class and wraps each test in a transaction that rolls back. This balances speed with isolation. For large test suites, `LazilyRefreshDatabase` skips migration if the schema matches. Mocking external services prevents tests from hitting real APIs. ```php // PaymentTest.php public function test_payment_failure_returns_error(): void { $gateway = $this->mock(PaymentGateway::class); $gateway->shouldReceive('charge') ->once() ->andThrow(new PaymentFailedException('Card declined')); $user = User::factory()->create(); $order = Order::factory()->for($user)->create(); $response = $this->actingAs($user) ->postJson("/api/orders/{$order->id}/pay"); $response->assertStatus(402) ->assertJsonPath('error', 'Card declined'); } ``` ## Laravel 13 Features Likely to Appear in Interviews Laravel 13 shipped March 2026 with features that interviewers now expect candidates to know. The [official release notes](https://laravel.com/docs/13.x/releases) cover these in detail. **AI SDK**: Laravel 13 includes first-party AI tooling with a unified API for text generation, embeddings, and tool-calling agents. Questions may ask about integrating AI features into existing applications. **JSON:API Resources**: The new `JsonApiResource` class simplifies building JSON:API-compliant responses with relationships, sparse fieldsets, and compound documents. **Vector Search**: The `AsVector` Eloquent cast handles vector columns for semantic search. This works with PostgreSQL pgvector and MariaDB's binary vector format. **Cloud Facade**: `Cloud::hosted()` detects Laravel Cloud deployment, `Cloud::usesManagedQueues()` checks for managed queue connections. This enables environment-specific behavior without configuration checks. ## Key Takeaways for Laravel Interview Preparation - Explain N+1 prevention with `with()`, `loadMissing()`, and `withCount()`. Know the memory implications of eager loading large datasets. - Describe queue job design: retry strategies, `SerializesModels` behavior, batching callbacks, and when to use `deleteWhenMissingModels`. - Articulate service container binding types: singleton vs bind, contextual bindings, and deferred providers for performance. - Demonstrate middleware request/response flow and terminable middleware for post-response operations. - Take a position on the repository pattern with concrete tradeoffs rather than dogmatic rules. - Write tests that use `RefreshDatabase` appropriately, mock external services, and assert queue dispatches. - Stay current on Laravel 13 features, particularly AI SDK, JSON:API resources, and vector search capabilities. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/en/blog/laravel/php-laravel-framework-interview-questions-eloquent-queues-architecture