Interview Prep
Common Laravel interview questions with detailed answers.
Clean syntax
Laravel ke faide:
Clean aur readable syntax
Authentication built-in
Database handle karna easy
Security strong
API banana easy
Laravel ke faide: Clean aur readable syntax Authentication built-in Database handle karna easy Security strong API banana easy
MVC stands for Model, View, Controller.
User registration system:
Model → User data save karega
Controller → Validation karega
View → Registration form show karega
MVC ka matlab hai Model, View aur Controller. Model database se deal karta hai View frontend hota hai Controller logic handle karta hai Is se code clean aur organized rehta hai.
Laravel is a PHP framework used to build modern, secure, and scalable web applications. It follows the MVC (Model-View-Controller) architecture and provides built-in features like routing, authentication, middleware, and ORM (Eloquent).
Agar aap hostel management system bana rahe ho, Laravel: Login system ready deta hai Database easily connect karta hai Role-based access control deta hai
Query scopes are reusable query constraints defined as methods on Eloquent models, keeping query logic organized and DRY.
<?php
// Local scopes on Post model
public function scopePublished($query)
{
return $query->whereNotNull('published_at')
->where('published_at', '<=', now());
}
public function scopeInCategory($query, $categoryId)
{
return $query->where('category_id', $categoryId);
}
public function scopePopular($query)
{
return $query->where('views_count', '>=', 1000);
}
// Usage - clean, readable queries
$posts = Post::published()->inCategory(5)->popular()->get();
// Dynamic scoping
$query = Post::query();
if ($request->has('status')) {
$query->where('status', $request->status);
}
$posts = $query->get();
Show how scopes make queries readable: "Post::published()->popular()->latest()" is self-documenting compared to nested where() calls.
Laravel uses exception handling via the bootstrap/app.php handler. Exceptions are caught globally and rendered as responses or logged.
<?php
// Custom exception
class PaymentFailedException extends \RuntimeException
{
public function __construct(
string $message = 'Payment processing failed',
int $code = 422,
?\Throwable $previous = null
) {
parent::__construct($message, $code, $previous);
}
}
// Usage in service
public function processPayment(Payment $payment): void
{
try {
$this->gateway->charge($payment);
} catch (\Exception $e) {
Log::error('Payment failed', ['error' => $e->getMessage()]);
throw new PaymentFailedException($e->getMessage(), 422, $e);
}
}
// Handler
public function register(): void
{
$this->reportable(function (PaymentFailedException $e) {
// Custom reporting logic
});
}
// Custom error page
// resources/views/errors/422.blade.php
Discuss the difference between report() (logging) and render() (response). Mention custom exception pages and the abort() helper.
Sessions store data on the server (identified by a session ID cookie). Cookies store data directly in the browser, limited to ~4KB.
<?php
// Session usage
session(['key' => 'value']);
$value = session('key');
// Request session
$request->session()->put('cart', $cartItems);
$cart = $request->session()->get('cart');
$request->session()->forget('cart');
// Cookie usage
// Set cookie in response
return response($content)->cookie('name', 'value', 60);
// Read cookie
$value = $request->cookie('name');
// Encrypt cookies (automatic in Laravel)
// Cookie::queue(Cookie::make('name', 'value', 120));
// Session configuration (.env)
SESSION_DRIVER=redis
SESSION_LIFETIME=120
SESSION_ENCRYPT=true
Emphasize security: sessions are more secure because sensitive data stays on the server. Mention Laravel's session encryption and CSRF protection.
Model observers group model event listeners into dedicated classes, providing a clean way to hook into Eloquent lifecycle events.
<?php
// php artisan make:observer PostObserver --model=Post
class PostObserver
{
public function created(Post $post): void
{
// Auto-publish to social media
SocialMediaJob::dispatch($post);
// Clear cache
Cache::forget('posts:all');
}
public function updated(Post $post): void
{
if ($post->isDirty('published_at')) {
$post->user->notify(new PostPublishedNotification($post));
}
Cache::forget('posts:all');
}
public function deleted(Post $post): void
{
Cache::forget('posts:all');
Storage::disk('public')->delete($post->thumbnail);
}
}
// Register in AppServiceProvider
public function boot(): void
{
Post::observe(PostObserver::class);
}
Compare observers to events/listeners: "Observers are model-specific, events are application-wide. Use observers when the logic is tied to a specific model."
Events represent something that happened. Listeners react to events. This decouples the triggering code from the side effects.
<?php
// Event class
class OrderPlaced
{
use Dispatchable, SerializesModels;
public function __construct(
public readonly Order $order
) {}
}
// Listener
class SendOrderConfirmation
{
public function __construct(private Mailer $mailer) {}
public function handle(OrderPlaced $event): void
{
$this->mailer->to($event->order->user->email)
->send(new OrderConfirmationMail($event->order));
}
}
// EventServiceProvider
protected $listen = [
OrderPlaced::class => [
SendOrderConfirmation::class,
UpdateInventory::class,
NotifyAdmin::class,
],
];
event(new OrderPlaced($order)); // Triggers all 3 listeners
Explain that listeners can be queued: "A listener can implement ShouldQueue for background processing, like sending emails without slowing the request."
The service container (IoC) is Laravel's dependency injection container that manages class dependencies and performs automatic injection.
<?php
// Binding
$this->app->bind(PostRepositoryInterface::class, function ($app) {
return new EloquentPostRepository(new Post);
});
$this->app->singleton(Mailer::class, function ($app) {
return $app->make('mailer');
});
// Constructor injection - container auto-resolves
public function __construct(
private PostRepositoryInterface $repository,
private Mailer $mailer
) {}
// Method injection in controllers
public function store(Request $request, PostService $service)
{
$post = $service->create($request->validated());
}
// Manual resolution
$service = app(PostService::class);
$service = app()->make(PostService::class);
Explain that auto-resolution works without explicit binding when all constructor dependencies can be resolved. Binding is needed for interfaces or when you want custom behavior.
Dependency injection is a design pattern where a class receives its dependencies from outside rather than creating them internally.
<?php
// Without DI - hard to test, tightly coupled
class ReportService
{
public function generate()
{
$pdf = new PdfGenerator(); // Can't swap this
$mailer = new Mailer(); // Can't mock this
// ...
}
}
// With DI - testable, flexible
class ReportService
{
public function __construct(
private PdfGeneratorInterface $pdf,
private MailerInterface $mailer
) {}
public function generate(): Report
{
$report = $this->pdf->create($data);
$this->mailer->send($report);
return $report;
}
}
// Testing with DI
class ReportServiceTest extends TestCase
{
public function test_generate(): void
{
$pdf = Mockery::mock(PdfGeneratorInterface::class);
$mailer = Mockery::mock(MailerInterface::class);
$service = new ReportService($pdf, $mailer);
// Test with mocks
}
}
Give a concrete test example: "With DI, I can inject a mock Mailer that doesn't actually send emails during tests."
Secure APIs with HTTPS, authentication (Sanctum/Passport), rate limiting, input validation, CORS configuration, and proper error handling.
<?php
// Rate limiting
Route::middleware(['auth:sanctum', 'throttle:60,1'])->group(function () {
Route::apiResource('posts', PostController::class);
});
// Form Request validation
public function rules(): array
{
return [
'title' => ['required', 'string', 'max:255'],
'body' => ['required', 'string', 'max:5000'],
];
}
// Authorization
$this->authorize('update', $post);
// CORS configuration
// config/cors.php
'allowed_origins' => ['https://frontend.com'],
// Safe error handling
report() => Log::error($e);
render() => response()->json(['error' => 'Server error'], 500);
Discuss defense in depth: "No single security measure is enough. Use multiple layers: HTTPS + auth + validation + rate limiting + authorization."
Rate limiting restricts the number of requests a client can make in a given time period, protecting against abuse and DDoS attacks.
<?php
// In AppServiceProvider boot()
use Illuminate\Routing\RateLimiter;
use Illuminate\Http\Requests\Request;
use Illuminate\Support\Facades\Cache;
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by(
$request->user()?->id ?: $request->ip()
);
});
RateLimiter::for('login', function (Request $request) {
return Limit::perMinute(5)->by($request->input('email'));
});
// Apply to routes
Route::middleware('throttle:api')->group(function () {
Route::apiResource('posts', PostController::class);
});
Route::post('/login', [AuthController::class, 'login'])
->middleware('throttle:login');
// Response headers automatically included:
// X-RateLimit-Remaining: 59
// X-RateLimit-Limit: 60
// Retry-After: 60
Explain that rate limits should be per-user for authenticated routes and per-IP for public routes. Mention the Retry-After header.
Caching stores computed results to avoid repeated expensive operations. Strategies include read-through, write-through, cache-aside, and write-behind.
<?php
// Cache-Aside (most common)
$posts = Cache::remember('posts:published', 3600, function () {
return Post::published()->with('user')->get();
});
// Tag-based caching (Redis/Memcached)
Cache::tags(['posts', 'home-page'])->put('featured', $posts, 3600);
Cache::tags(['posts'])->flush(); // Clear all post caches
// Cache invalidation patterns
// 1. Time-based: TTL expires automatically
Cache::put('key', $value, now()->addHour());
// 2. Event-based: Clear on data change
Post::created(fn () => Cache::forget('posts:all'));
// 3. Manual: Clear specific cache
Cache::forget('posts:all');
// Multi-level caching
function getDashboardData()
{
return Cache::remember('dashboard', 3600, function () {
// Expensive computation here
return $this->computeDashboard();
});
}
Discuss cache invalidation: "There are two hard things in computer science: cache invalidation and naming things." Explain when each strategy is appropriate.
Deploy via Git pull, composer install, migrations, config caching, and queue worker restart. Use Supervisor for processes, Nginx for web server.
# Deployment script
#!/bin/bash
set -e
cd /var/www/laravelhub
git pull origin main
composer install --no-dev --optimize-autoloader
php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan storage:link
php artisan queue:restart
chown -R www-data:www-data storage bootstrap/cache
chmod -R 775 storage bootstrap/cache
# Supervisor config
[program:laravel-worker]
command=php artisan queue:work --sleep=3 --tries=3
user=www-data
autostart=true
autorestart=true
numprocs=2
# Cron
* * * * * cd /var/www/laravelhub && php artisan schedule:run
Mention deployment tools (Forge, Envoyer) and CI/CD pipelines. Emphasize --force on migrations and the importance of Supervisor for queue workers.
Supervisor is a process control system that keeps queue workers running 24/7, automatically restarting them if they crash or the server reboots.
# Install Supervisor
sudo apt-get install supervisor
# Create config
# /etc/supervisor/conf.d/laravel-worker.conf
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/laravelhub/artisan queue:work --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=www-data
numprocs=2
redirect_stderr=true
stdout_logfile=/var/www/laravelhub/storage/logs/worker.log
stopwaitsecs=3600
# Start workers
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start "laravel-worker:*"
# Monitor
sudo supervisorctl status
Explain why Supervisor is needed: "Without it, queue workers die when the terminal closes. Supervisor keeps them alive and restarts on failure."
Repository pattern adds an abstraction layer over Eloquent. Eloquent itself is already a repository-like Active Record implementation.
// Eloquent only (simple, effective)
class PostController extends Controller
{
public function index()
{
return Post::with('user')->latest()->paginate();
}
}
// Repository pattern (more structure)
class PostController extends Controller
{
public function __construct(
private PostRepositoryInterface $posts
) {}
public function index()
{
return $this->posts->paginated();
}
}
// Repository implementation
class EloquentPostRepository implements PostRepositoryInterface
{
public function paginated(int $perPage = 15): LengthAwarePaginator
{
return Post::with(['user', 'tags'])->latest()->paginate($perPage);
}
}
Be balanced: "Eloquent is sufficient for most applications. The repository pattern adds value when you need abstraction for testing or multiple data sources."
Service classes encapsulate business logic that doesn't belong in controllers or models, keeping controllers thin and models focused on data.
<?php
// Service class
class PostService
{
public function __construct(
private PostRepositoryInterface $posts,
private Mailer $mailer,
private Cache $cache
) {}
public function create(array $data): Post
{
$post = $this->posts->create($data);
// Business logic
if ($post->user->isVerified()) {
$this->mailer->to($post->user)->send(new PostPublishedMail($post));
}
$this->cache->forget('posts:all');
event(new PostCreated($post));
return $post;
}
public function publish(Post $post): Post
{
$post->update(['published_at' => now()]);
$this->cache->forget('posts:all');
event(new PostPublished($post));
return $post;
}
}
// Thin controller
class PostController extends Controller
{
public function store(StorePostRequest $request, PostService $service)
{
$post = $service->create($request->validated());
return redirect()->route('posts.show', $post);
}
}
Give an example of when a service is needed: "Creating an order involves creating the order, deducting inventory, sending emails, and processing payment — that's a service, not a controller or model."
Blade directives are shorthand syntax for PHP control structures in templates, starting with @ symbol: @if, @foreach, @extends, @yield, @csrf.
@extends('layouts.app')
@section('content')
@if($posts->isNotEmpty())
@foreach($posts as $post)
<article>
<h2>{{ $post->title }}</h2>
@if($post->is_featured)
<span class="badge">Featured</span>
@endif
@unless(auth()->guest())
@can('update', $post)
<a href="{{ route('posts.edit', $post) }}">Edit</a>
@endcan
@endunless
</article>
@endforeach
@else
<p>No posts found.</p>
@endif
{{ $posts->links() }}
@endsection
@error('title')
<span class="error">{{ $message }}</span>
@enderror
Mention that Blade compiles to plain PHP for performance. Show awareness of @once, @production, @env directives for environment-specific content.
Authentication verifies WHO you are (login). Authorization determines WHAT you can do (permissions).
// Authentication - verifying identity
if (auth()->attempt($credentials)) {
// User is authenticated
}
Route::middleware('auth')->group(function () {
// Only authenticated users
});
// Authorization - checking permissions
public function update(Post $post)
{
$this->authorize('update', $post); // Throws 403 if denied
// User is authorized
}
// Gate definition
Gate::define('edit-settings', function (User $user) {
return $user->isSuperAdmin();
});
// Blade authorization
@can('update', $post)
<button>Edit</button>
@endcan
// Check in code
if ($user->can('delete', $post)) {
// User can delete
}
This is a very common interview question. Keep it simple: "Authentication is login, authorization is permissions. Laravel handles both differently."
Factories generate test data with faker values. Seeders populate the database with specific known data for development.
// Factory
public function definition(): array
{
return [
'title' => fake()->sentence(),
'slug' => fake()->unique()->slug(),
'body' => fake()->paragraphs(3, true),
'status' => fake()->randomElement(['draft', 'published']),
];
}
public function published(): static
{
return $this->state(fn () => [
'status' => 'published',
'published_at' => fake()->dateTimeBetween('-1 year'),
]);
}
// Usage
Post::factory()->count(20)->published()->create();
// Seeder
public function run(): void
{
Role::create(['name' => 'Admin', 'slug' => 'admin']);
Role::create(['name' => 'Editor', 'slug' => 'editor']);
}
// Run
php artisan migrate:fresh --seed
Explain that factories are for test data (random, bulk) while seeders are for setup data (specific, known). Mention factory states for data variations.