Laravel interview questions covering routing, controllers, middleware, Eloquent, migrations, validation, auth, queues, events, testing, and deployment.
Laravel is a modern PHP framework for building web applications and APIs. It provides routing, controllers, Blade templates, Eloquent ORM, migrations, queues, events, validation, authentication, authorization, caching, testing tools, and Artisan commands. A strong interview answer should mention that Laravel emphasizes developer productivity while still supporting serious production architecture.
Laravel follows the MVC pattern: models represent data and domain behavior, views render UI, and controllers handle HTTP requests and coordinate work. In practical apps, controllers should stay thin and delegate business logic to services, actions, jobs, or model methods where appropriate.
Routes map HTTP methods and URLs to closures or controller actions. They are usually defined in routes/web.php for browser routes and routes/api.php for stateless API routes. Named routes make redirects and links easier to maintain.
use App\Http\Controllers\UserController;
use Illuminate\Support\Facades\Route;
Route::get('/users/{user}', [UserController::class, 'show'])
->name('users.show');
web.php routes use the web middleware group, which includes session state, cookies, and CSRF protection. api.php routes are typically stateless, use API-focused middleware, and are commonly prefixed with /api. Use web.php for server-rendered pages and api.php for JSON APIs consumed by clients.
Route model binding automatically resolves route parameters into Eloquent models. If the model is not found, Laravel returns a 404. It reduces repetitive lookup code and keeps controllers cleaner.
Route::get('/users/{user}', function (User $user) {
return $user;
});
A controller groups request handling logic for related routes. It receives a request, validates or delegates validation, calls application logic, and returns a response. Controllers should not become huge classes containing queries, payment logic, email logic, and rendering all at once.
class UserController extends Controller
{
public function show(User $user)
{
return view('users.show', ['user' => $user]);
}
}
Middleware filters HTTP requests before or after they reach controllers. It is used for authentication, authorization checks, rate limiting, localization, request logging, CORS, and maintenance mode behavior. Middleware should handle cross-cutting request concerns rather than business workflows.
class EnsureUserIsAdmin
{
public function handle($request, Closure $next)
{
abort_unless($request->user()?->is_admin, 403);
return $next($request);
}
}
The service container is Laravel dependency injection system. It resolves classes, injects dependencies, and manages bindings for interfaces, factories, singletons, and contextual implementations. It helps keep code testable because dependencies can be swapped with fakes or mocks.
Bind interfaces in a service provider so controllers and services depend on abstractions instead of concrete classes. This is useful when switching payment gateways, storage providers, notification channels, or repository implementations.
public function register(): void
{
$this->app->bind(
PaymentGateway::class,
StripePaymentGateway::class
);
}
Service providers bootstrap framework and application services. They register container bindings, event listeners, macros, policies, routes, and package configuration. register() should bind services, while boot() should run logic after all providers have been registered.
Facades provide a static-looking interface to services resolved from the container, such as Cache, DB, Queue, Log, and Route. They are convenient and testable through Laravel helpers, but overusing facades in domain logic can hide dependencies and make design harder to reason about.
Eloquent is Laravel active record ORM. Each model usually maps to a database table, and model instances represent rows. Eloquent provides relationships, query scopes, casts, accessors, mutators, events, factories, soft deletes, and eager loading.
class User extends Model
{
protected $fillable = ['name', 'email'];
}
$users = User::where('active', true)->get();
Mass assignment lets you create or update models from an array of attributes. To prevent users from writing unsafe columns such as is_admin, Laravel requires fillable or guarded configuration. Always define which attributes can be mass assigned.
class User extends Model
{
protected $fillable = ['name', 'email', 'password'];
}
User::create($request->validated());
Relationships describe how models connect, such as hasOne, hasMany, belongsTo, belongsToMany, morphMany, and morphTo. They let Laravel build relationship queries and load related records in an expressive way.
class User extends Model
{
public function posts()
{
return $this->hasMany(Post::class);
}
}
Eager loading loads related models up front to avoid the N+1 query problem. If you loop through posts and access each post author lazily, Laravel may run one query per post. with() loads the relationship in fewer queries.
$posts = Post::with('author')->latest()->paginate(20);
foreach ($posts as $post) {
echo $post->author->name;
}
Query scopes are reusable query constraints defined on a model. Local scopes keep common filters readable and consistent, such as active users, published posts, or orders visible to a tenant.
class Post extends Model
{
public function scopePublished($query)
{
return $query->whereNotNull('published_at');
}
}
$posts = Post::published()->latest()->get();
Accessors transform values when reading model attributes, while mutators transform values when writing them. They are useful for formatting, normalization, encryption, and value objects. Avoid hiding expensive queries or business workflows inside accessors.
Casts convert database values to useful PHP types, such as booleans, arrays, dates, enums, encrypted values, or custom cast classes. They reduce repetitive parsing and make model attributes safer to use.
class User extends Model
{
protected $casts = [
'email_verified_at' => 'datetime',
'settings' => 'array',
'is_admin' => 'boolean',
];
}
Migrations are version-controlled database schema changes. They let teams create, modify, and roll back tables consistently across environments. Production migrations should be reviewed for locks, indexes, data volume, and rollback strategy.
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('title');
$table->timestamps();
});
Factories generate fake model data for tests and local development. Seeders insert known data, such as admin users, lookup tables, roles, or sample records. Do not run destructive seeders blindly in production.
Form requests move validation and authorization out of controllers into dedicated classes. This keeps controllers clean and makes validation rules reusable and testable.
class StorePostRequest extends FormRequest
{
public function rules(): array
{
return [
'title' => ['required', 'string', 'max:255'],
'body' => ['required', 'string'],
];
}
}
Laravel validation checks request data against rules and returns structured errors on failure. Rules can validate required fields, formats, uniqueness, file size, nested arrays, dates, enums, and custom constraints. For APIs, validation failures usually return JSON with 422 status.
Blade is Laravel templating engine. It supports layouts, sections, components, slots, conditionals, loops, escaping, and directives. Blade escapes output by default with double curly braces, which helps reduce XSS risk when displaying user content.
@extends('layouts.app')
@section('content')
<h1>{{ $post->title }}</h1>
@endsection
API resources transform models into JSON responses. They help control API shape, hide internal fields, format relationships, and keep response logic out of controllers. Resource collections are useful for lists and pagination.
class UserResource extends JsonResource
{
public function toArray($request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
];
}
}
Gates define authorization logic as closures or callbacks, often for simple checks. Policies organize authorization around a model, such as update, delete, or view actions for Post. Use policies when authorization is tied to resources and gates for broader application abilities.
Policies can compare the authenticated user with the model being accessed. Controllers can call authorize(), or routes can use can middleware. This keeps authorization rules centralized instead of scattered through views and controllers.
class PostPolicy
{
public function update(User $user, Post $post): bool
{
return $user->id === $post->user_id;
}
}
$this->authorize('update', $post);
Laravel authentication uses guards and providers. Guards define how users are authenticated for each request, such as session or token auth. Providers define how users are retrieved, usually from Eloquent. Starter kits such as Breeze, Jetstream, and Fortify provide common auth flows.
Sanctum provides token authentication for APIs and cookie-based SPA authentication. It is lighter than Passport and works well for first-party SPAs and simple API tokens. For SPAs, configure stateful domains, CSRF, cookies, and CORS carefully.
CSRF protection prevents a malicious site from submitting authenticated requests on behalf of a user. Laravel web routes include CSRF middleware by default. Forms should include @csrf, and APIs should use an auth strategy appropriate for stateless clients.
<form method="POST" action="/posts">
@csrf
<input name="title">
<button type="submit">Save</button>
</form>
Queues move slow work out of the request cycle, such as sending emails, processing images, syncing data, or calling third-party APIs. Jobs are pushed to a queue and processed by workers. Production systems need retries, timeouts, failed job monitoring, and worker supervision.
SendWelcomeEmail::dispatch($user);
A job is a class that contains work to be performed now or later. If it implements ShouldQueue, Laravel sends it to the configured queue. Jobs should be idempotent when possible because retries can run the same job more than once.
class SendWelcomeEmail implements ShouldQueue
{
public function handle(): void
{
Mail::to($this->user)->send(new WelcomeMail($this->user));
}
}
Events announce that something happened, and listeners respond to that event. They decouple side effects from main workflows. For example, after an order is paid, listeners can send a receipt, update analytics, and notify fulfillment.
Notifications send messages through channels such as mail, database, Slack, SMS, or broadcast. They are useful for user-facing alerts and system messages. Notifications can also be queued to avoid slowing down HTTP requests.
Laravel cache supports drivers such as file, database, Redis, Memcached, DynamoDB, and array. Use caching for expensive or frequently requested data, but define invalidation rules carefully. Incorrect cache keys can leak tenant or user-specific data.
$users = Cache::remember('active-users', 300, function () {
return User::where('active', true)->get();
});
The scheduler defines recurring tasks in code instead of many separate cron entries. A single cron entry runs schedule:run every minute, and Laravel decides which tasks should execute. Use withoutOverlapping for jobs that must not run concurrently.
protected function schedule(Schedule $schedule): void
{
$schedule->command('reports:daily')
->daily()
->withoutOverlapping();
}
Artisan commands are CLI commands for maintenance, imports, reports, cleanup jobs, and developer tooling. Custom commands should validate inputs, write clear output, return meaningful exit codes, and avoid assuming they only run manually.
Transactions group database operations so they commit together or roll back together. Use transactions for workflows like creating an order, reserving stock, and recording payment state. Avoid making slow external API calls inside long database transactions.
DB::transaction(function () use ($request) {
$order = Order::create($request->validated());
$order->items()->createMany($request->items);
});
Soft deletes mark a row as deleted using deleted_at instead of physically removing it. They are useful for restore workflows and audit-friendly systems. Remember that unique indexes, privacy requirements, and storage growth still need design attention.
use Illuminate\Database\Eloquent\SoftDeletes;
class Post extends Model
{
use SoftDeletes;
}
Laravel can paginate query results with paginate(), simplePaginate(), or cursorPaginate(). Use cursor pagination for large tables or infinite scroll where offset pagination becomes slow or inconsistent under frequent writes.
$posts = Post::latest()->paginate(15);
return PostResource::collection($posts);
Laravel handles uploaded files through the request object and Storage facade. Validate file type and size, store files on a configured disk, and avoid trusting original filenames for security-sensitive workflows.
$path = $request->file('avatar')->store('avatars', 'public');
$user->update(['avatar_path' => $path]);
Laravel filesystem abstraction supports local disks, public disks, S3-compatible storage, and custom drivers through Flysystem. Use Storage instead of hard-coded file paths so environments can switch storage backends cleanly.
Laravel supports feature tests for HTTP behavior, unit tests for isolated logic, database refresh helpers, model factories, fakes for Mail, Queue, Event, Notification, and Storage, and assertions for responses and database state.
public function test_user_can_view_posts(): void
{
$user = User::factory()->create();
$this->actingAs($user)
->get('/posts')
->assertOk();
}
Fakes replace external side effects during tests. Mail::fake(), Queue::fake(), Event::fake(), Notification::fake(), and Storage::fake() let tests assert that something would have happened without sending real emails, dispatching real jobs, or writing to real cloud storage.
.env stores environment-specific values, while config files read those values. In production, php artisan config:cache compiles configuration for speed. After config is cached, direct env() calls outside config files can behave unexpectedly, so application code should use config().
A production deployment should install optimized dependencies, run tests, run migrations safely, cache config/routes/views/events where appropriate, restart queue workers, clear stale caches when needed, set correct permissions, and verify logs, scheduler, queues, and health checks.
php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan queue:restart
Horizon is a dashboard and configuration system for Redis queues. It shows throughput, runtime, failed jobs, retries, and worker balancing. It is useful in production when queue behavior is important to user experience or business workflows.
Improve performance by eager loading relationships, adding proper database indexes, caching expensive reads, using queues for slow work, optimizing config/routes/views, reducing middleware overhead on hot routes, using OPcache, monitoring slow queries, and measuring p95 response time rather than guessing.
Common mistakes include disabling CSRF without a reason, using raw queries with user input, missing authorization checks, mass-assigning sensitive fields, leaking debug pages in production, storing secrets in source control, weak password hashing, broad CORS with credentials, and exposing private files from public storage.
Common anti-patterns include fat controllers, business logic hidden in Blade views, huge Eloquent models with unrelated behavior, unbounded eager loading, using facades everywhere instead of explicit dependencies, running slow work inside requests, and writing migrations that lock large production tables without planning.
A clean CRUD endpoint uses routes, a controller, form request validation, route model binding, Eloquent, authorization where needed, and API resources for JSON responses. The example shows the basic store and show shape.
Route::post('/posts', [PostController::class, 'store']);
Route::get('/posts/{post}', [PostController::class, 'show']);
class PostController extends Controller
{
public function store(StorePostRequest $request)
{
$post = Post::create($request->validated());
return new PostResource($post);
}
public function show(Post $post)
{
return new PostResource($post);
}
}
Explore 500+ free tutorials across 20+ languages and frameworks.