Eloquent ORM reduces 95% of queries to fluent PHP — but lazy loading triggers the N+1 trap
Blade templates compile to cached PHP with zero runtime overhead and auto-escaped output
Artisan CLI generates model+controller+migration in one command: php artisan make:model -mrc --requests
Production gotcha: missing $fillable causes MassAssignmentException; caching config on dev breaks new routes
Plain-English First
Imagine you're building a house. You could dig your own foundations, manufacture your own bricks, and wire the electricity yourself — or you could use a construction company that already has all those systems ready, tested, and standardised. Laravel is that construction company for PHP web applications. It hands you pre-built tools for the most common jobs — routing URLs, talking to databases, sending emails, handling logins — so you spend your time building the rooms, not inventing concrete.
PHP has been powering the web for nearly 30 years, but writing raw PHP for a modern web application is like navigating a city with only a hand-drawn map. You'll waste time, get lost, and ship something nobody else can read. Laravel changed that conversation. Since 2011, it's become the most starred PHP framework on GitHub and the default choice for teams who want production-quality applications without starting from scratch.
The real problem Laravel solves isn't 'PHP is hard' — it's 'web applications are complex'. Every app needs authentication, database access, validation, sessions, queued jobs, and a hundred cross-cutting concerns. Without a framework you either bolt these together inconsistently or copy-paste from Stack Overflow. Laravel gives you coherent, well-documented answers for all of them, following conventions your whole team agrees on.
By the end of this article you'll understand exactly what Laravel is, why it was built, how its MVC architecture maps to real requests, how to set up a project and write your first route, controller, and Blade view, and — most importantly — when to reach for Laravel's built-in tools instead of rolling your own. You'll also walk away knowing the gotchas that trip up developers migrating from plain PHP, and the questions interviewers love to ask.
What Laravel Actually Is — MVC, the Service Container, and the Request Lifecycle
Laravel is a full-stack PHP framework built on top of Composer packages and underpinned by two big ideas: convention over configuration and inversion of control.
Convention over configuration means Laravel makes sensible decisions for you. Put a model in app/Models, a controller in app/Http/Controllers, and a view in resources/views — the framework finds them automatically. You only configure the things that differ from the default.
Inversion of control is handled by Laravel's Service Container, which is essentially an intelligent object factory. Instead of calling new DatabaseConnection() deep in your code, you ask the container for a DatabaseConnection and it builds one — injecting its own dependencies automatically. This makes testing dramatically easier because you can swap real implementations for fakes.
The request lifecycle ties it together: an HTTP request hits public/index.php, gets wrapped in an Illuminate\Http\Request object, travels through global middleware (think authentication checks, CORS headers), hits the Router which matches the URL to a closure or controller method, passes through route-specific middleware, runs your controller logic, then returns an Illuminate\Http\Response — a Blade view, JSON, a redirect, or a file download. Every single step is predictable and overridable.
The container also auto-resolves dependencies in controller constructors and even methods if you type-hint. That's why you can write public function store(Request $request, ArticleService $service) and Laravel figures out the ArticleService from scratch. You don't manage object creation anymore — you just ask for what you need.
RequestLifecycleDemo.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php
// FILE: routes/web.php// This is where you map URLs to behaviour.// Laravel reads this file on every request.useIlluminate\Http\Request;
useApp\Http\Controllers\ArticleController;
// A simple closure route — great for prototyping or tiny endpointsRoute::get('/ping', function () {
// Returns a JSON response — Laravel wraps the array automaticallyreturnresponse()->json([
'status' => 'alive',
'version' => app()->version(), // pulls the Laravel version from the container
]);
});
// A resourceful route — maps 7 CRUD actions to one controller in one line// GET /articles -> ArticleController@index// GET /articles/{id} -> ArticleController@show// POST /articles -> ArticleController@store// PUT /articles/{id} -> ArticleController@update// DELETE /articles/{id} -> ArticleController@destroyRoute::resource('articles'
Output
GET /ping → HTTP 200
{
"status": "alive",
"version": "10.x"
}
GET /articles → Calls ArticleController@index
GET /dashboard (unauthenticated) → HTTP 302 redirect to /login
GET /dashboard (authenticated) → Renders resources/views/dashboard.blade.php
Why Resource Routes Matter:
Route::resource() is not just shorthand — it enforces RESTful naming conventions that your whole team understands instantly. When a new developer joins, they don't need to read your routing file to know that DELETE /articles/5 calls ArticleController@destroy. Convention does the documentation for you.
Production Insight
The most common production mistake here is forgetting to bind interfaces in a service provider. If your controller type-hints ArticleServiceInterface but the container doesn't know which concrete class to use, Laravel throws a BindingResolutionException. Always register bindings in AppServiceProvider::register().
The container handles singletons too — app()->singleton('MyService') ensures one instance per request. Use that judiciously to avoid memory leaks when storing mutable state.
Key Takeaway
Laravel's Service Container is not magic — it's a configurable map of interfaces to implementations.
Type-hint your dependencies and let the container wire them up. Never call new for services that have their own dependencies.
Test with fakes by binding a mock class in the container, not by modifying global state.
Eloquent ORM — Talking to Your Database Without Writing Raw SQL
Every web app needs a database. The question is how much of your time you spend fighting SQL strings versus building features. Laravel's Eloquent ORM answers that by treating each database table as a PHP class and each row as an object. You get a fluent, readable API that handles 95% of queries without a single line of SQL.
Eloquent is an ActiveRecord implementation — the model knows about the database and can save itself. An Article model represents the articles table. Call Article::all() and you get a Collection of Article objects. Call $article->save() and the row is written. Relationships are declared as methods (hasMany, belongsTo, belongsToMany) and loaded lazily or eagerly with with().
The piece most beginners miss is that Eloquent returns Illuminate Collections, not plain arrays. Collections come with 80+ higher-order methods — filter, map, groupBy, pluck, sortBy — all chainable, all lazy where possible. Learning Collections is arguably more valuable than memorising query builder syntax.
For schema changes, Eloquent works alongside Migrations. A migration is a version-controlled PHP file that describes a database change. Your whole team runs php artisan migrate and everyone's database matches — no more 'it works on my machine' database drift.
One more thing: Eloquent has global scopes, local scopes, accessors, mutators, and model events. You can hook into creating, created, updating, updated, etc. This keeps business logic in the model rather than scattered across controllers.
ArticleEloquentExample.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
<?php
// FILE: app/Models/Article.php// One class = one table. Laravel pluralises 'Article' to 'articles' automatically.namespaceApp\Models;
useIlluminate\Database\Eloquent\Model;
useIlluminate\Database\Eloquent\Relations\BelongsTo;
useIlluminate\Database\Eloquent\Relations\HasMany;
classArticleextendsModel
{
// Only these fields can be mass-assigned via Article::create([...]) or fill()// This prevents mass-assignment vulnerabilities — always define thisprotected $fillable = ['title', 'body', 'published_at', 'author_id'];
// Cast 'published_at' to a Carbon datetime object automatically// so you can call $article->published_at->diffForHumans() etc.protected $casts = [
'published_at' => 'datetime',
];
// Relationship: an Article belongs to one Author (User)// Laravel infers the foreign key is 'author_id' by conventionpublicfunctionauthor(): BelongsTo
{
return $this->belongsTo(User::class, 'author_id');
}
// Relationship: an Article has many Commentspublicfunctioncomments(): HasMany
{
return $this->hasMany(Comment::class);
}
// A local scope — reusable query constraint// Usage: Article::published()->get()publicfunctionscopePublished($query)
{
return $query->whereNotNull('published_at')
->where('published_at', '<=', now());
}
}
// -------------------------------------------------------// FILE: app/Http/Controllers/ArticleController.php// Real-world usage: eager loading to avoid the N+1 problemnamespaceApp\Http\Controllers;
useApp\Models\Article;
useIlluminate\Http\Request;
classArticleControllerextendsController
{
publicfunctionindex()
{
// with('author') eager-loads the author relationship in ONE extra query// instead of firing a new query per article (the N+1 problem)
$publishedArticles = Article::published()
->with('author') // eager load author
->withCount('comments') // adds comments_count column to each result
->latest('published_at') // ORDER BY published_at DESC
->paginate(15); // automatically handles ?page= query stringreturnview('articles.index', [\n 'articles' => $publishedArticles,\n ]);
}
publicfunctionstore(Request $request)
{
// Validate first — Laravel throws a 422 with JSON errors if this fails
$validatedData = $request->validate([
'title' => 'required|string|max:200',
'body' => 'required|string',
'published_at' => 'nullable|date',
]);
// merge the authenticated user's ID — never trust a user-supplied author_id
$article = Article::create(array_merge($validatedData, [\n 'author_id' => $request->user()->id,\n ]));
returnredirect()->route('articles.show', $article)
->with('success', 'Article published successfully.');
}
}
Output
// After visiting GET /articles:
// Eloquent fires exactly 3 queries:
// 1. SELECT COUNT(*) FROM articles WHERE published_at <= NOW() (pagination count)
// 2. SELECT * FROM articles WHERE published_at <= NOW() ORDER BY published_at DESC LIMIT 15
// 3. SELECT * FROM users WHERE id IN (1, 2, 5, 7 ...) (eager load — all authors in ONE query)
// Session flash: 'Article published successfully.'
Watch Out — The N+1 Query Problem:
If you loop over articles and access $article->author inside the loop without eager loading, Laravel fires one query per article. With 100 articles that's 101 queries. Always use with('author') in the controller when you know you'll need the relationship in the view. Install the Laravel Debugbar package in development — it shows you exactly how many queries each page fires.
Production Insight
The N+1 problem is the most common Eloquent performance killer. If you loop through 50 articles and access $article->author inside the loop, Eloquent fires 51 queries. Always add with('author') when you need the relationship in the view.
MassAssignmentException happens when you call create() without $fillable. Define $fillable on every model before you create a record. Never trust user input for mass-assignment.
Key Takeaway
Eloquent lazy loading is the biggest hidden performance trap. Fix it with ->with() before the view.
$fillable is not optional — it's your only defence against mass-assignment vulnerabilities.
Use withCount() over ->count() in loops to avoid extra queries.
Blade Templates — Logic-Light Views That Don't Fight You
Blade is Laravel's templating engine and it solves a real frustration: mixing raw PHP into HTML creates spaghetti that nobody wants to maintain. Blade gives you clean, readable directives — @if, @foreach, @auth, @include — that compile to plain PHP and get cached, so there's zero runtime overhead.
The killer feature is template inheritance. You define a master layout (layouts/app.blade.php) that contains your HTML skeleton, navigation, and footer. Every child view @extends that layout and fills in named @section blocks. Change the navigation once in the layout and every page updates. No copy-pasting. No include chains.
Blade also auto-escapes output by default. {{ $userInput }} runs through htmlspecialchars — you get XSS protection for free. The only time you bypass it is when you explicitly use {!! $trustedHtml !!}, which is a deliberate, visible decision rather than an easy accident.
Components (introduced in Laravel 7) take this further — they let you build reusable UI chunks like <x-alert type="success"> that compile to a Blade partial with its own logic class. Think of them as PHP-powered web components.
Another powerful feature:@stack and @push allow you to inject scripts or styles from child views into specific stacks in the layout. No more duplicating <script> tags or fighting with asset managers.
BladeTemplateSystem.blade.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
{{-- FILE: resources/views/layouts/app.blade.php --}}
{{-- The master layout — every page on the site extends this --}}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>@yield('page-title', 'TheCodeForge') — MyBlog</title>
</head>
<body>
<nav>
{{-- @auth only renders its contents if a user is logged in --}}
@auth
<span>Welcome, {{ auth()->user()->name }}</span>
<a href="{{ route('logout') }}">Logout</a>
@else
<a href="{{ route('login') }}">Login</a>
@endauth
</nav>
<main>
{{-- @yield marks the slot that child views fill in --}}
@yield('content')
</main>
</body>
</html>
{{-- FILE: resources/views/articles/index.blade.php --}}
{{-- Child view — it extends the layout and fills the 'content' slot --}}
@extends('layouts.app')
@section('page-title', 'Latest Articles')
@section('content')
<h1>PublishedArticles</h1>
{{-- Flash message from the controller's redirect()->with('success', ...) --}}
@if(session('success'))
<div class="alert alert-success">
{{ session('success') }}
</div>
@endif
@forelse($articles as $article)
<article>
<h2>
{{-- route() generates /articles/42 — no hardcoded URLs --}}
<a href="{{ route('articles.show', $article) }}">
{{ $article->title }} {{-- auto-escaped: safe against XSS --}}
</a>
</h2>
<p class="meta">
By {{ $article->author->name }}
·
{{-- Carbon's diffForHumans() gives '3 days ago' style output --}}
{{ $article->published_at->diffForHumans() }}
·
{{ $article->comments_count }} comment{{ $article->comments_count !== 1 ? 's' : '' }}
</p>
<p>{{ Str::limit($article->body, 200) }}</p>
</article>
@empty
{{-- @forelse's @empty block handles the zero-results state cleanly --}}\n <p>No articles published yet. <a href=\"{{ route('articles.create') }}\">Write one?</a></p>\n @endforelse\n\n {{-- $articles->links() renders Bootstrap/Tailwind pagination automatically --}}\n {{ $articles->links() }}\n@endsection",
"output": "<!-- Rendered HTML sent to the browser (abbreviated): -->\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <title>Latest Articles — My Blog</title>\n</head>\n<body>\n <nav>\n <a href=\"/login\">Login</a> <!-- unauthenticated user -->\n </nav>\n <main>\n <h1>Published Articles</h1>\n <article>\n <h2><a href=\"/articles/1\">Getting Started with Laravel</a></h2>\n <p class=\"meta\">By Jane Doe · 3 days ago · 4 comments</p>\n <p>Laravel is a web application framework with expressive, elegant syntax...</p>\n </article>\n <!-- ... 14 more articles ... -->\n <!-- Pagination: « Previous 1 2 3 Next » -->\n </main>\n</body>\n</html>"
},
"callout": {
"type": "tip",
"title": "Pro Tip — Use route() Everywhere:",
"text": "Never hardcode URLs in Blade like href=\"/articles/42\". Always use route('articles.show', $article). When you rename a URI in routes/web.php, every link in every template updates automatically. HardcodedURLs are the reason refactors become nightmares."
}
},
{
"heading": "Artisan CLI — The Command-Line Superpower You'll Use Every Day",
"content": "Every Laravel project ships with Artisan, a CLI tool that handles the repetitive parts of development so you don't have to. It's not just a code generator — it's your interface to the application itself.\n\nThe commands you'll use daily are `make:model`, `make:controller`, `make:migration`, and `make:request`. The `-mrc` flag on `make:model` generates the model, migration, resource controller, and form request all at once — four files with one command. That's a lot of boilerplate gone in seconds.\n\nBeyond generation, Artisan lets you run migrations (`migrate`, `migrate:rollback`, `migrate:fresh`), manage the cache (`cache:clear`, `config:cache`, `route:cache`), tail logs, run scheduled tasks, and drop into a REPL called Tinker. Tinker is a REPL that boots your entire Laravel app — you can query your database, test Eloquent queries, and call any service from the command line without writing a throwaway script.\n\nYou can also write your own Artisan commands. Any repetitive task — importing a CSV, recalculating stats, sending a digest email — can become a first-class `php artisan` command, schedulable via Laravel's Schedulerand monitored via LaravelHorizonorTelescope.\n\nOne hidden gem: `php artisan route:list` shows all registered routes along with their middleware and controller methods. Super useful when you're lost in a new codebase or debugging a 404.",
"production_insight": "Never run php artisan config:cache or route:cache on local development. These commands flatten all config into a single cached file, so changes to .env or routes won't reflect until you clear the cache. Use them only in production to improve performance. If you accidentally run them, use php artisan config:clear && php artisan route:clear && php artisan cache:clear.\nTinker is a powerful debugging tool — you can even interact with the database in production if you're careful. But never run Tinker in production without a read-only approach unless you understand the consequences.",
"key_takeaway": "Artisan's make:model -mrc --requests generates 5 files in one command — master this for speed.\nTinker is the fastest way to test Eloquent queries before writing them in a controller.\nCache config and routes only on production — never on local.",
"code": {
"language": "php",
"filename": "ArtisanWorkflow.php",
"code": "<?php\n\n/*\n |------------------------------------------------------------------\n | TERMINALSESSION — typical Laravel development workflow\n |------------------------------------------------------------------\n */\n\n// 1. Create a new Laravel project\n// composer create-project laravel/laravel blog-platform\n// cd blog-platform\n\n// 2. Generate the Article model + migration + resource controller + form request in one shot\n// php artisan make:model Article -mrc --requests\n//\n// This creates:\n// app/Models/Article.php\n// database/migrations/2024_01_15_create_articles_table.php\n// app/Http/Controllers/ArticleController.php (with index/create/store/show/edit/update/destroy stubs)\n// app/Http/Requests/StoreArticleRequest.php\n// app/Http/Requests/UpdateArticleRequest.php\n\n\n// 3. The generated migration — you fill in the columns:\n\n// FILE: database/migrations/2024_01_15_000000_create_articles_table.php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n public function up(): void\n {\n Schema::create('articles', function (Blueprint $table) {\n $table->id(); // BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY\n $table->foreignId('author_id') // BIGINT UNSIGNED\n ->constrained('users') // adds FOREIGN KEY referencing users.id\n ->cascadeOnDelete(); // delete articles when user is deleted\n $table->string('title', 200);\n $table->text('body');\n $table->timestamp('published_at')->nullable(); // null = draft\n $table->timestamps(); // adds created_at and updated_at\n });\n }publicfunctiondown(): void
{
// Rollback: drop the table cleanlySchema::dropIfExists('articles');
}
};
// 4. The generated Form Request — move validation OUT of the controller:// FILE: app/Http/Requests/StoreArticleRequest.phpnamespaceApp\Http\Requests;
useIlluminate\Foundation\Http\FormRequest;
classStoreArticleRequestextendsFormRequest
{
// authorize() decides WHO can make this request// Return false to send a 403 Forbidden automaticallypublicfunctionauthorize(): bool
{
return $this->user() !== null; // any logged-in user can create articles
}
// rules() defines validation — same rules as $request->validate() but reusablepublicfunctionrules(): array
{
return [
'title' => ['required', 'string', 'max:200'],
'body' => ['required', 'string', 'min:50'],
'published_at' => ['nullable', 'date', 'after_or_equal:today'],
];
}
// Optional: custom error messagespublicfunctionmessages(): array
{
return [
'body.min' => 'Articles must be at least 50 characters long — give your readers something!',
];
}
}
Before you write a complex Eloquent query in a controller, test it in Tinker first. You get instant feedback with your real database, real models, and real relationships. It's the fastest way to go from 'I think this query works' to 'I know this query works'. Run php artisan tinker and start experimenting.
Production Insight
Blade compiles down to PHP and caches those files in storage/framework/views. If you change a view and it doesn't reflect on the page, clear the compiled views with php artisan view:clear. This usually happens on production after a deploy if the cache isn't flushed.
The {!! !!} syntax should be used extremely sparingly. Any user-generated content passed through {!! !!} opens an XSS vulnerability. Use it only for trusted HTML you generated yourself (e.g., structured rich text from a secure WYSIWYG editor).
Key Takeaway
Blade's auto-escaping is your XSS defence — use {{ }} always, {!! !!} only when you fully trust the content.
Template inheritance via @extends and @section eliminates duplicated HTML.
Use @stack and @push for clean asset injection instead of inline scripts.
Authentication and Authorization — Laravel's Built-in User System
Laravel offers multiple authentication starter kits to get you running in minutes. Breeze is the minimal choice — login, registration, password reset, email verification, and simple Blade or Vue/React views. It uses Laravel's built-in authentication controllers under the hood. Jetstream is the full-featured option — adds two-factor authentication, team management, profile photos, and API tokens via Sanctum, with Livewire or Inertia.js as the frontend stack.
Under the hood, Laravel's authentication system is built on guards and providers. Guards define how users are authenticated for each request (session token, API token, etc.). Providers define how users are retrieved (from Eloquent, the database query builder, or any custom source). You can have multiple guards — for example, a web guard for browser sessions and an api guard for token-based API authentication.
Authorization uses Gates and Policies. A Gate is a closure that checks if a user is allowed to do something (e.g.
// If validation fails: returns HTTP 422 with JSON error bag
// If not authenticated: redirects to login
Watch Your Guards:
If you mix web and api guards, your authenticated user might be null on API routes. Always use the correct middleware: auth:api for token-based routes, auth (or auth:web) for session-based routes. Also remember to pass the guard name to helpers: auth()->user() uses the default guard, while auth('api')->user() explicitly uses the API guard.
Production Insight
The most common mistake with Laravel auth is using the default guard when you need a custom one. If you build an API and use the web guard by mistake, you get session-based authentication instead of token-based. This leads to 'Unauthenticated' errors on API endpoints. Always specify the guard: auth()->guard('api') or use the auth:api middleware.
Another trap: modifying the User model's $fillable but forgetting to allow the password field when registering. This causes MassAssignmentException on registration. Always include 'password' in $fillable — never put it in $guarded alongside everything.
Key Takeaway
Use Breeze for most projects — it's minimal and just works. Jetstream only when you need teams and 2FA.
Gates and Policies keep authorization logic out of controllers — define once, reuse everywhere.
Always specify the correct guard for your route group (web vs api).
● Production incidentPOST-MORTEMseverity: high
The N+1 Query That Killed the Article List Page
Symptom
The article index page takes >1 second to load with only 50 articles. TTFB (time to first byte) jumps as the database server CPU spikes because Eloquent fires one query per related model in a loop.
Assumption
The developer assumed Eloquent would automatically join the author table when calling $article->author in the view, because the relationship is defined in the model.
Root cause
Lazy loading: accessing $article->author inside a @foreach loop triggers a separate SELECT query for each article. With 50 articles, that's 1 query for articles + 50 queries for authors = 51 queries. But if you also access $article->comments()->count() lazily, it adds another 50 queries. The page becomes 101 queries.
Fix
Add eager loading in the controller: Article::with('author')->withCount('comments')->get(). This reduces to 3 queries: one for articles, one for all authors, one for comment counts. Install barryvdh/laravel-debugbar to catch these in development.
Key lesson
Always eager-load relationships you know you'll use in the view — use with() before passing to the template.
Eager-load count with withCount() instead of lazy loading ->count() inside a loop.
Install Laravel Debugbar in development — it shows every query per page load. Make it non-negotiable for every team member.
Production debug guideSymptom → Action guide for the most common Laravel performance issues in production4 entries
Symptom · 01
Page loads slowly, high database CPU, Debugbar shows many duplicate SELECT queries
→
Fix
Check for N+1: install Laravel Debugbar (barryvdh/laravel-debugbar) and look for the 'Queries' tab. If you see repeated queries with the same pattern, find the loop and add ->with() or withCount() to the Eloquent query.
Symptom · 02
Config changes or new routes don't take effect, even after restarting the server
→
Fix
Did someone run php artisan config:cache or route:cache on development? Clear them: php artisan config:clear && php artisan route:clear && php artisan cache:clear. These commands are production-only — never cache config locally.
Symptom · 03
MassAssignmentException when creating a model
→
Fix
Define $fillable on the model with the exact columns you want mass-assignable. Never pass $request->all() into create() without $fillable. Use $request->only(['title', 'body']) to be explicit.
Symptom · 04
Login fails but no errors shown; authentication redirects correctly
→
Fix
Check the session driver: if using file driver, ensure storage/framework/sessions is writable. Also verify the APP_KEY in .env — a missing APP_KEY will reset all sessions on every deploy.
★ Laravel Quick Debug Cheat SheetThree commands to diagnose 80% of Laravel production issues
Unexpected behaviour, hard to trace data flow−
Immediate action
Drop into Tinker CLI to test queries and services interactively
Commands
php artisan tinker
>>> App\Models\User::find(1);
Fix now
Test your Eloquent query in Tinker before writing it in a controller → ensures exact behaviour without refreshing pages
Slow page load, uncertain query count+
Immediate action
Enable Debugbar to inspect all queries and memory usage
Commands
composer require barryvdh/laravel-debugbar --dev
Visit any page in browser → Debugbar bottom toolbar shows query count, time, duplicates
Fix now
Identify N+1 queries; add ->with() in the appropriate controller method
Storage or cache issues, permissions errors+
Immediate action
Set correct permissions on storage and bootstrap/cache directories
Commands
sudo chmod -R 775 storage bootstrap/cache
sudo chown -R www-data:www-data storage bootstrap/cache (or your web server user)
Fix now
Run php artisan storage:link to create a public symlink for uploaded files
Queue facade — drivers for Redis, SQS, database; monitored by Horizon
Key takeaways
1
Laravel's convention-over-configuration approach means your project structure IS your documentation
any Laravel developer can navigate your codebase on day one without a guide.
2
Eloquent's $fillable is not optional
it's your first line of defence against mass-assignment attacks. Define it on every model before you call create() or fill().
3
Route::resource() maps seven RESTful actions to a controller in one line and enforces URL conventions your whole team shares
use it for any resource that has CRUD operations.
4
Artisan's make:model -mrc --requests command is a force multiplier
it generates the model, migration, controller, and form requests simultaneously, eliminating boilerplate so you can focus on business logic immediately.
5
Always eager-load relationships (->with()) when you need them in a view to avoid the N+1 query explosion. Install Debugbar to catch this in dev.
Common mistakes to avoid
4 patterns
×
Skipping $fillable on Eloquent models
Symptom
MassAssignmentException when calling Article::create($request->all()). Laravel throws a MassAssignmentException because the model doesn't have a $fillable or $guarded property defined.
Fix
Always define protected $fillable = ['title', 'body', ...] with the exact columns you want to allow mass assignment. Never pass $request->all() into create() without either $fillable or explicitly picking fields with $request->only(['title', 'body']).
×
Triggering the N+1 query problem
Symptom
Your article list page fires 51 queries when you have 50 articles, each one fetching the author. The page is visibly slow and Debugbar shows repeated SELECT queries on related tables.
Fix
Add ->with('author') to your Eloquent query in the controller. If you discover it after the fact, install barryvdh/laravel-debugbar in development — it shows every query on every page load so N+1 problems are impossible to miss.
×
Caching config/routes in development
Symptom
You add a new route or change a .env value and nothing happens. You restart the server, clear browser cache, still nothing — cached config or routes are overriding your changes.
Fix
Never run php artisan config:cache or php artisan route:cache on a local development machine. These commands are for production only. If you accidentally ran them, clear with php artisan config:clear && php artisan route:clear && php artisan cache:clear.
×
Not specifying the auth guard for API routes
Symptom
API endpoints return 401 Unauthenticated even though the token is present. The web guard is trying to authenticate via session, but the request has no session state.
Fix
Use the auth:api middleware on API routes. In RouteServiceProvider, add a group with middleware('api') and within that use auth:api. Also ensure the api guard is configured in config/auth.php.
INTERVIEW PREP · PRACTICE MODE
Interview Questions on This Topic
Q01SENIOR
Explain the Laravel request lifecycle from the moment a browser sends an...
Q02SENIOR
What is the N+1 query problem in Eloquent, can you give a concrete examp...
Q03SENIOR
What is the Laravel Service Container and how does dependency injection ...
Q04SENIOR
What is the difference between Laravel Breeze, Jetstream, and Fortify? W...
Q01 of 04SENIOR
Explain the Laravel request lifecycle from the moment a browser sends an HTTP request to the moment a response is returned. What are the key stages and what happens at each one?
ANSWER
The lifecycle starts when the web server (usually Nginx or Apache) directs the request to public/index.php. The bootstrap process loads Composer's autoloader, creates the Laravel application instance, and creates a kernel (HTTP or Console). Requests go through the HTTP kernel's middleware pipeline (global middleware like \TrustProxies, \EncryptCookies, \StartSession, etc.). Then the request reaches the Router, which matches the URI to a route definition (from routes/web.php or routes/api.php). The router executes route-specific middleware (e.g., auth, throttle). If matched, it calls the controller method or closure. The controller returns a response (view, JSON, redirect). The response travels back through middleware's terminate methods, is modified by middleware if needed, and is sent to the browser.
Q02 of 04SENIOR
What is the N+1 query problem in Eloquent, can you give a concrete example of when it occurs, and what are two different ways to solve it?
ANSWER
The N+1 problem happens when you retrieve a list of models and then access a related model inside a loop. For example, fetching 50 articles and then calling $article->author->name inside a Blade @foreach loop generates 1 query for articles + 50 queries for authors = 51 queries. Solution 1: Eager load with ->with('author') in the controller query, reducing to 2 queries. Solution 2: Use the lazy loading suppression technique with load() or loadMissing() if you need to conditionally load relationships after the query. Also consider using withCount() for aggregate columns to avoid separate count queries inside loops.
Q03 of 04SENIOR
What is the Laravel Service Container and how does dependency injection work in a controller constructor? Why is this preferable to manually instantiating dependencies with new?
ANSWER
The Service Container is a registry where you bind interfaces to concrete implementations. In a controller, you type-hint a dependency in the constructor and Laravel automatically resolves it. For example: public function __construct(ArticleService $service). The container examines the type-hint, sees that ArticleService has its own dependencies (e.g., a repository), recursively resolves all of them, and passes the fully constructed object to the controller. This is better than manual instantiation because: (1) you never need to know the dependency graph — the container handles it; (2) you can swap implementations by changing a binding in AppServiceProvider (e.g., swap ArticleService for MockArticleService in tests); (3) it promotes writing loosely coupled code that can be unit-tested easily.
Q04 of 04SENIOR
What is the difference between Laravel Breeze, Jetstream, and Fortify? When would you choose each?
ANSWER
Breeze is the minimal authentication starter — login, registration, password reset, email verification, and simple Blade or Vue/React views. It's the right choice for most projects where you want simple, customisable auth without extra features. Jetstream is the full-featured option — adds two-factor authentication, team management, profile photos, and API tokens via Sanctum, using Livewire or Inertia.js as the frontend. Choose it when the project needs teams and advanced profile management from the start. Fortify is the backend-only authentication layer that both Breeze and Jetstream sit on top of. You'd only use Fortify directly if you're building a headless API and want full control over the frontend — you implement your own UI and route Fortify's responses to your API endpoints.
01
Explain the Laravel request lifecycle from the moment a browser sends an HTTP request to the moment a response is returned. What are the key stages and what happens at each one?
SENIOR
02
What is the N+1 query problem in Eloquent, can you give a concrete example of when it occurs, and what are two different ways to solve it?
SENIOR
03
What is the Laravel Service Container and how does dependency injection work in a controller constructor? Why is this preferable to manually instantiating dependencies with new?
SENIOR
04
What is the difference between Laravel Breeze, Jetstream, and Fortify? When would you choose each?
SENIOR
FAQ · 4 QUESTIONS
Frequently Asked Questions
01
Do I need to know PHP well before learning Laravel?
You need a solid grasp of PHP fundamentals — arrays, functions, classes, interfaces, and namespaces — before Laravel will make sense. Laravel leans heavily on object-oriented PHP patterns like dependency injection and traits. If you're comfortable writing a PHP class with methods and understand what 'static' and 'new' do, you're ready. If not, spend a week on plain PHP OOP first — it will make Laravel click much faster.
Was this helpful?
02
What is the difference between Laravel Breeze, Jetstream, and Fortify?
Breeze is the minimal authentication starter — login, registration, password reset, email verification, simple Blade or Vue/React views. It's the right choice for most projects. Jetstream is the full-featured option — adds two-factor authentication, team management, profile photos, and API tokens via Sanctum, using Livewire or Inertia.js. Fortify is the backend-only authentication layer that both sit on top of — you'd only use it directly if you're building a headless API and want full control over the frontend.
Was this helpful?
03
When should I use a Form Request instead of calling $request->validate() directly in the controller?
Use $request->validate() for simple one-off validations with two or three rules. Use a Form Request class when validation logic is complex, reused across multiple controller methods, or needs a custom authorize() check. The practical rule: if your controller's store() and update() methods both validate the same fields, extract a Form Request. It also keeps controllers lean — a controller method should orchestrate, not validate.
Was this helpful?
04
How do I deploy a Laravel application in production?
Deploy Laravel by setting APP_ENV=production and APP_DEBUG=false in .env, then run php artisan config:cache, route:cache, view:cache. Ensure the web server points to the public/ directory. Set proper permissions on storage and bootstrap/cache (775 or 775 with www-data user). Use a process monitor like Supervisor for queue workers (php artisan queue:work). Enable OPcache for PHP performance. Use Composer with --no-dev for production. Consider deploying with Forge, Envoyer, or a CI/CD pipeline like GitLab CI or GitHub Actions.