Skip to content
ExponaBLOG
PHP & LARAVEL DEVELOPMENT · DAY 073 / 100

Laravel interview questions in PHP & Laravel Development

Questions that reveal real experience.

Expona Admin7 min read

Laravel interview questions in PHP & Laravel Development

When you're sitting across from a candidate or preparing for an interview yourself, technical questions reveal whether someone understands Laravel's architecture or just knows syntax. We've spent years hiring and training engineers, and the questions that matter most are those that separate the engineers who've shipped code at scale from those who've only read the documentation.

In this guide, we'll walk through four core Laravel interview questions that consistently expose real production experience. These aren't gotchas—they're the decisions your team makes every day.

How does the service container work? — Auto-resolution, bindings and service providers

The Laravel service container is one of the framework's most powerful and misunderstood features. It's a tool for dependency injection and object resolution, but many developers treat it as magic. Understanding it deeply changes how you structure applications.

The container auto-resolves classes by inspecting their constructor parameters. If you type-hint a dependency, Laravel tries to instantiate it and inject it automatically. But this only works for concrete classes—interfaces need explicit bindings.

Here's a real scenario: you have a payment processor and you want to swap providers between Stripe and PayPal depending on environment.

php
interface PaymentProvider {
    public function charge($amount, $token);
}

class StripeProvider implements PaymentProvider {
    public function charge($amount, $token) {
        // Stripe API call
    }
}

class PayPalProvider implements PaymentProvider {
    public function charge($amount, $token) {
        // PayPal API call
    }
}

class PaymentService {
    public function __construct(private PaymentProvider $provider) {}
    
    public function processPayment($amount, $token) {
        return $this->provider->charge($amount, $token);
    }
}

Without bindings, Laravel doesn't know which implementation to inject. You define this in a service provider:

php
class PaymentServiceProvider extends ServiceProvider {
    public function register() {
        $this->app->bind(PaymentProvider::class, function ($app) {
            if (config('services.payment.driver') === 'paypal') {
                return new PayPalProvider();
            }
            return new StripeProvider();
        });
    }
}

Now whenever you type-hint PaymentProvider, the container resolves it based on configuration. This is how real applications stay maintainable—you're not hardcoding dependencies; you're declaring contracts.

Service providers also run boot logic. The register method defines bindings; the boot method runs after all services are registered. We use this to publish migrations, load routes, or run initialization.

A good follow-up question during interviews: "When would you use singleton() instead of bind()?" The answer reveals whether they understand instance reuse. Singletons persist across requests; regular bindings create new instances each time.

How do you avoid N+1 queries? — Eager loading with with() and preventLazyLoading

This is where we separate engineers who've debugged slow endpoints from those who haven't. N+1 queries are one of the most common performance killers in production, and they're easy to introduce accidentally.

Lazy loading happens when you access a relationship without preloading it. Each access triggers a new query:

php
$users = User::all();

foreach ($users as $user) {
    echo $user->profile->bio; // 1 query to fetch user, then N queries for profiles
}

With 1,000 users, that's 1,001 queries. Here's the fix—eager loading:

php
$users = User::with('profile')->get();

foreach ($users as $user) {
    echo $user->profile->bio; // 2 queries total: users + profiles
}

Laravel loads all related profiles in a single query, dramatically reducing overhead. Nested relationships work too:

php
$posts = Post::with('author.profile', 'comments.author')->get();

But here's where real experience shows: how do you prevent lazy loading in development? Laravel 8+ has preventLazyLoading():

php
if (! app()->isProduction()) {
    Model::preventLazyLoading();
}

Add this to your AppServiceProvider boot method. Now if you access an unloaded relationship, Laravel throws an exception in development. This catches N+1 bugs before they hit production.

There's nuance here. Sometimes you need conditional eager loading with when() or loadMissing(). If you've already loaded a relationship, loadMissing() doesn't re-fetch it. Real applications juggle these patterns daily.

How do queues work? — Jobs serialized to a driver, processed by workers with retries

Queues decouple work from requests. Instead of running a slow task synchronously (sending an email, generating a PDF), you dispatch it to a queue and a worker processes it later.

Here's how it flows: your code dispatches a job, Laravel serializes it to a driver (Redis, database, etc.), a worker picks it up, unserializes it, and runs it. If it fails, it can retry.

A practical example: a user signs up, and you want to send a welcome email asynchronously.

php
class UserRegistered {
    public function __construct(public User $user) {}
}

event(new UserRegistered($user));

Then a listener dispatches a job:

php
class SendWelcomeEmail implements ShouldQueue {
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
    
    public function __construct(public User $user) {}
    
    public function handle() {
        Mail::to($this->user->email)->send(new WelcomeEmail($this->user));
    }
}

The ShouldQueue interface tells Laravel to queue this job. The queue driver determines storage—Redis is faster and more robust than the database driver. You run a worker to process jobs:

bash
php artisan queue:work redis --queue=default --tries=3 --timeout=90

The worker polls Redis, grabs jobs, executes them. If an exception is thrown, it retries up to three times. The timeout prevents runaway jobs.

For production reliability, use failed job tables to track dead letters, implement exponential backoff for retries, and monitor queue depth. In interviews, ask: "How do you handle a job that fails permanently?" Good answers mention dead letter queues and monitoring dashboards.

How do you secure a Laravel API? — Sanctum, policies, validation and rate limiting

Building an API means thinking about authentication, authorization, input validation, and request throttling. Each layer protects your application differently.

Authentication with Sanctum: Laravel Sanctum is the go-to for SPA and mobile authentication. It issues tokens per device:

php
Route::post('/login', function (Request $request) {
    $user = User::where('email', $request->email)->firstOrFail();
    
    if (! Hash::check($request->password, $user->password)) {
        throw ValidationException::withMessages(['email' => 'Invalid credentials']);
    }
    
    return $user->createToken('api-token')->plainTextToken;
});

The client stores this token and sends it in the Authorization header. Sanctum validates it on each request:

php
Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
    return $request->user();
});

Authorization with policies: Authentication tells you who someone is; policies tell you what they can do. A policy is a class that authorizes actions:

php
class PostPolicy {
    public function update(User $user, Post $post) {
        return $user->id === $post->user_id;
    }
}

// In your controller:
$this->authorize('update', $post);

If the policy returns false, Laravel throws an exception. Use this pattern consistently—it's far cleaner than scattering authorization logic throughout controllers.

Validation: Never trust input. Validate everything, even if the client does:

php
$validated = $request->validate([
    'email' => 'required|email|unique:users',
    'password' => 'required|min:8|confirmed',
    'role' => 'required|in:user,admin',
]);

$user = User::create($validated);

The confirmed rule ensures password and password_confirmation match. The in rule prevents injection of invalid roles. Validate early.

Rate limiting: Protect endpoints from abuse with throttle middleware:

php
Route::middleware('throttle:60,1')->group(function () {
    Route::post('/login', LoginController::class);
});

Route::middleware('throttle:1000,1')->group(function () {
    Route::get('/posts', PostController::class . '@index');
});

This allows 60 login attempts per minute and 1,000 general requests per minute. In production, use Redis for distributed throttling across multiple servers.

Common mistakes

  • Forgetting to eager load relationships: Developers often discover N+1 queries in production. Make preventLazyLoading() a habit in development.
  • Binding to concrete classes instead of interfaces: This couples your code to implementations. Always bind to interfaces for flexibility.
  • Storing sensitive data in jobs: Jobs are serialized to storage. Never pass passwords or API keys directly; use job IDs and fetch fresh data in the handler.
  • Skipping authorization checks: Authentication doesn't equal authorization. A logged-in user shouldn't access another user's data without policy checks.
  • Validating only client-side: JavaScript validation is for UX, not security. Always validate server-side.

Key takeaways

  • The service container uses type-hinting to auto-resolve dependencies; bind interfaces to implementations in service providers for flexibility.
  • Lazy loading causes N+1 queries; use with() for eager loading and preventLazyLoading() in development to catch bugs early.
  • Queues deserialize jobs asynchronously; use workers with retries and failed job tables for production reliability.
  • Secure APIs with Sanctum for authentication, policies for authorization, validation for input safety, and rate limiting to prevent abuse.
  • These patterns aren't advanced—they're foundational. Engineers who've shipped code at scale have internalized them.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles