Laravel N+1 Query: 101 Queries for 50 Articles
50 articles caused 101 queries and >1s load time.
20+ years shipping production PHP systems at scale. Everything here is grounded in real deployments.
- ✓Solid grasp of fundamentals
- ✓Comfortable reading code examples
- ✓Basic production concepts
- Laravel is a full-stack PHP framework using convention over configuration and inversion of control
- MVC request lifecycle wraps HTTP into predictable middleware → router → controller → response stages
- 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
Laravel is a PHP web application framework that implements the Model-View-Controller (MVC) architectural pattern, but its real power comes from the Service Container — a dependency injection container that manages class dependencies and performs automatic resolution. When you make a request to a Laravel app, it goes through a lifecycle: the public/index.php file bootstraps the framework, the HTTP kernel handles the request, middleware runs, the router dispatches to a controller, and the response is sent back.
This lifecycle is what makes Laravel predictable and testable, unlike the spaghetti code era where PHP developers mixed database queries, HTML, and business logic in single files.
Eloquent ORM is Laravel's ActiveRecord implementation for database interaction. Instead of writing raw SQL, you define models that extend Eloquent and use methods like User::where('active', true)->get(). Under the hood, Eloquent generates parameterized queries, preventing SQL injection.
The N+1 problem — which this article addresses — occurs when Eloquent lazily loads relationships, executing one query for the parent and one for each child. For 50 articles with comments, that's 51 queries instead of 2. Tools like Laravel Debugbar or Clockwork make this visible in development.
Blade is Laravel's templating engine that compiles views into plain PHP for caching. It provides control structures like @if, @foreach, and @section/@yield for layout inheritance, but doesn't let you run arbitrary PHP in templates — keeping views logic-light.
Laravel's built-in authentication system uses guards and providers: php artisan make:auth scaffolds login, registration, and password reset routes and views. For authorization, policies and gates let you define rules like "only the article author can edit." This system handles sessions, CSRF protection, and password hashing out of the box.
Laravel won the PHP framework war because it solved real pain points: Composer for dependency management, Artisan CLI for code generation, and conventions that reduce decision fatigue. Alternatives like Symfony are more modular but require more configuration; Slim is micro but lacks built-in ORM and auth.
Don't use Laravel for a single-page static site or a real-time WebSocket server (use React/Vue + Laravel Echo or Node.js instead). Prerequisites: PHP 8.1+, Composer, a database (MySQL/PostgreSQL/SQLite), and Node.js if you use Vite for asset bundling.
You'll also need a local server environment — Laravel Herd, Laravel Sail (Docker), or Valet for macOS.
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.
<?php // FILE: routes/web.php // This is where you map URLs to behaviour. // Laravel reads this file on every request. use Illuminate\Http\Request; use App\Http\Controllers\ArticleController; // A simple closure route — great for prototyping or tiny endpoints Route::get('/ping', function () { // Returns a JSON response — Laravel wraps the array automatically return response()->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@destroy Route::resource('articles'
DELETE /articles/5 calls ArticleController@destroy. Convention does the documentation for you.ArticleServiceInterface but the container doesn't know which concrete class to use, Laravel throws a BindingResolutionException. Always register bindings in AppServiceProvider::register().app()->singleton('MyService') ensures one instance per request. Use that judiciously to avoid memory leaks when storing mutable state.new for services that have their own dependencies.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.
<?php // FILE: app/Models/Article.php // One class = one table. Laravel pluralises 'Article' to 'articles' automatically. namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; class Article extends Model { // Only these fields can be mass-assigned via Article::create([...]) or fill() // This prevents mass-assignment vulnerabilities — always define this protected $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 convention public function author(): BelongsTo { return $this->belongsTo(User::class, 'author_id'); } // Relationship: an Article has many Comments public function comments(): HasMany { return $this->hasMany(Comment::class); } // A local scope — reusable query constraint // Usage: Article::published()->get() public function scopePublished($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 problem namespace App\Http\Controllers; use App\Models\Article; use Illuminate\Http\Request; class ArticleController extends Controller { public function index() { // 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 string return view('articles.index', [\n 'articles' => $publishedArticles,\n ]); } public function store(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 ])); return redirect()->route('articles.show', $article) ->with('success', 'Article published successfully.'); } }
$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.create() without $fillable. Define $fillable on every model before you create a record. Never trust user input for mass-assignment.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.
{{-- 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') — My Blog</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>Published Articles</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. Hardcoded URLs 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 Scheduler and monitored via Laravel Horizon or Telescope.\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 | TERMINAL SESSION — 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 }
public function down(): void
{
// Rollback: drop the table cleanly
Schema::dropIfExists('articles');
}
};
// 4. The generated Form Request — move validation OUT of the controller:
// FILE: app/Http/Requests/StoreArticleRequest.php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreArticleRequest extends FormRequest
{
// authorize() decides WHO can make this request
// Return false to send a 403 Forbidden automatically
public function authorize(): bool
{
return $this->user() !== null; // any logged-in user can create articles
}
// rules() defines validation — same rules as $request->validate() but reusable
public function rules(): array
{
return [
'title' => ['required', 'string', 'max:200'],
'body' => ['required', 'string', 'min:50'],
'published_at' => ['nullable', 'date', 'after_or_equal:today'],
];
}
// Optional: custom error messages
public function messages(): array
{
return [
'body.min' => 'Articles must be at least 50 characters long — give your readers something!',
];
}
}php artisan tinker and start experimenting.php artisan view:clear. This usually happens on production after a deploy if the cache isn't flushed.{!! !!} 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).{{ }} always, {!! !!} only when you fully trust the content.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.
<?php // FILE: app/Providers/AuthServiceProvider.php // Define Gates and register Policies use Illuminate\Support\Facades\Gate; use App\Models\Article; use App\Policies\ArticlePolicy; class AuthServiceProvider extends ServiceProvider { protected $policies = [ Article::class => ArticlePolicy::class, ]; public function boot() { $this->registerPolicies(); // Define a Gate directly (alternative to Policy class) Gate::define('update-article', function ($user, $article) { return $user->id === $article->author_id; }); } } // FILE: app/Policies/ArticlePolicy.php namespace App\Policies; use App\Models\User; use App\Models\Article; class ArticlePolicy { public function update(User $user, Article $article): bool {\n return $user->id === $article->author_id;\n } public function delete(User $user, Article $article): bool {\n return $user->id === $article->author_id;\n } public function create(User $user): bool { // Any authenticated user can create an article return true; } } // FILE: app/Http/Controllers/ArticleController.php // Authorize inside the controller namespace App\Http\Controllers; use App\Models\Article; use App\Http\Requests\StoreArticleRequest; class ArticleController extends Controller { public function edit(Article $article) { // This calls the update policy method $this->authorize('update', $article); return view('articles.edit', compact('article')); } public function destroy(Article $article) { $this->authorize('delete', $article); $article->delete(); return redirect()->route('articles.index') ->with('success', 'Article deleted.'); } }
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.auth()->guard('api') or use the auth:api middleware.$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.What Laravel Actually Is (And Why It Won Laravel's War Against PHP Spaghetti)
Laravel isn't just a framework. It's a philosophy that says 'I don't want to write SQL in my view files ever again.' Taylor Otwell started this in 2011 because PHP had a reputation problem — WordPress themes, raw PHP pages, and 2000-line index.php files that made you pray before deployments.
Laravel wins because it enforces structure. You get an MVC pattern whether you like it or not, and that pattern is backed by a Service Container that handles dependency injection without you thinking about it. Every request goes through a pipeline — middleware checks your auth, routes map to controllers, responses come back through Blade. No surprises. No magic autoload nonsense that breaks in production after a composer update.
What makes it special? The developer experience. Artisan CLI commands generate boilerplate in seconds. Eloquent makes queries readable. Migrations version your database schema so you don't need to manually run SQL on staging again. Compared to vanilla PHP or CodeIgniter, Laravel feels like having a senior engineer on your shoulder saying 'no, do it this way'.
// io.thecodeforge — php tutorial // Horrible pre-Laravel pattern — avoid this $db = mysqli_connect('localhost', 'root', '', 'users'); $result = mysqli_query($db, "SELECT * FROM posts WHERE user_id = $_SESSION['user_id']"); echo "<h1>Your Posts</h1>"; while ($row = mysqli_fetch_assoc($result)) { echo "<li>$row[title]</li>"; } // Laravel way — lean and testable Route::middleware('auth')->get('/posts', function () { $posts = Post::where('user_id', auth()->id())->get(); return view('posts.index', compact('posts')); });
Prerequisites — What You Need Before You Touch A Single Artisan Command
I've seen too many juniors jump into Laravel without knowing how PHP actually works. Don't be that person. You need solid HTML, Core PHP (closures, PDO, namespaces, autoloading), and Advanced PHP (traits, interfaces, dependency injection). If composer.json looks like a foreign language, stop. Go learn Composer first.
Why? Because Laravel abstracts the hell out of everything, and when something breaks — and it will break — you need to trace the stack back to PDO or the autoloader. You also need PHP >= 8.1 (for modern Laravel versions) and a web server like Nginx or Apache. Don't use XAMPP in production. Ever.
If you're coming from raw PHP, expect a shock: Laravel handles routing, CSRF protection, session management, and database connections out of the box. You don't write calls anymore. You write middleware. But you still need to understand HTTP verbs, headers, and database indexes. The framework handles the boring stuff; you handle the logic.header()
// io.thecodeforge — php tutorial // What your local environment must have $requiredPhpVersion = '8.1.0'; $currentPhpVersion = PHP_VERSION; if (version_compare($currentPhpVersion, $requiredPhpVersion, '<')) { die("Upgrade PHP. You're on $currentPhpVersion, need >= $requiredPhpVersion"); } echo "PHP version: $currentPhpVersion — OK\n"; echo "Composer: " . (shell_exec('which composer') ? 'installed' : 'missing') . "\n"; echo "Node/NPM: " . (shell_exec('which node') ? 'installed' : 'optional') . "\n"; // Output if everything is healthy // PHP version: 8.2.12 — OK // Composer: installed // Node/NPM: optional
Key Features That Actually Matter In Production (Not Buzzwords)
Marketing fluff will tell you Laravel has 'elegant syntax' and 'expressive code.' The real features that save your ass in production are different. Database migrations let you version your schema alongside your code — run php artisan migrate on deploy and you're done. No manual ALTER TABLE statements. No 'works on my machine' schema drift.
Unit testing is baked in with PHPUnit. Write tests before you write code. Laravel gives you RefreshDatabase trait that resets the DB between tests, so you never accidentally break production data from a CI pipeline. Validation is middleware-level — not scattered across controllers. You write rules once and apply them globally.
Caching is trivial: store a query result, an entire view, or config in Redis/Memcached with one method call. Eloquent's lazy loading is a footgun, so use ->with() to eager load relations. Everything else is sugar — Artisan scaffolding, event broadcasting, queues with Horizon. Focus on migrations, testing, and caching. Those three will keep you from getting paged at 2 AM.
// io.thecodeforge — php tutorial // Eager loading to avoid N+1 queries $posts = Post::with(['comments.user', 'tags'])->where('published', true)->get(); // Cache the result for 10 minutes $posts = Cache::remember('published_posts_with_comments', 600, function () { return Post::with(['comments.user', 'tags'])->where('published', true)->get(); }); // A migration that rolls back safely Schema::create('audit_logs', function (Blueprint $table) { $table->id(); $table->morphs('loggable'); // polymorphic relation $table->json('payload'); $table->timestamps(); });
->get() inside Blade loops. That's the N+1 problem. Always profile with Laravel Debugbar or Telescope before saying 'it's fast enough.'Validation That Doesn't Suck — Rules, Messages, and Request Classes
Most tutorials treat validation like a chore: write rules inline, pray, and move on. Laravel flips that. You define validation rules once, and the framework enforces them automatically before your controller logic ever runs. That's why form validation isn't an afterthought — it's a guardrail. Start with rule arrays: 'email' => 'required|email|unique:users'. But raw arrays get messy fast. Instead, pull validation into Form Request classes. Run php artisan make:request StoreUserRequest. Now your validation lives in a single class with a method. Custom error messages? Override rules(). Need authorization logic? Add an messages() method. The Request class also auto-redirects back with old input and errors on failure — no manual flash work. Production trap: never trust client-side validation alone. Always validate server-side. Laravel gives you the tools; don't skip them.authorize()
// io.thecodeforge — php tutorial // Example Laravel Form Request class <?php namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; class StoreUserRequest extends FormRequest { public function authorize(): bool { return true; // Or check user permissions } public function rules(): array { return [ 'email' => 'required|email|unique:users,email', 'password' => 'required|min:8|confirmed', ]; } public function messages(): array { return [ 'email.required' => 'We need your email address.', 'email.unique' => 'This email is already taken.', ]; } }
The N+1 Query That Killed the Article List Page
- 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.
create() without $fillable. Use $request->only(['title', 'body']) to be explicit.php artisan tinker>>> App\Models\User::find(1);composer require barryvdh/laravel-debugbar --devVisit any page in browser → Debugbar bottom toolbar shows query count, time, duplicatessudo chmod -R 775 storage bootstrap/cachesudo chown -R www-data:www-data storage bootstrap/cache (or your web server user)| Feature / Aspect | Raw PHP | Laravel |
|---|---|---|
| Routing | Parse $_SERVER['REQUEST_URI'] manually | Route::get('/path', Handler::class) — named, grouped, middleware-aware |
| Database access | PDO with manual prepared statements | Eloquent ORM + Query Builder — fluent, safe, relationship-aware |
| Input validation | Custom if/else logic per form field | Declarative rules array — auto 422 response with JSON error bag |
| Authentication | Sessions + password_hash() + manual checks | php artisan make:auth or Laravel Breeze/Jetstream — fully featured in minutes |
| Templating | echo + htmlspecialchars() everywhere | Blade — auto-escaping, inheritance, components, zero runtime overhead |
| Database migrations | Manual SQL scripts shared via Slack | Version-controlled PHP migration files run with php artisan migrate |
| Testing | PHPUnit wired up manually | PHPUnit + Pest pre-configured, HTTP test helpers, database factories built-in |
| Caching | Custom Redis/Memcached integration | Unified Cache facade — swap drivers (Redis, file, array) via .env config |
| Queue/Jobs | Cron jobs + custom queue tables | Queue facade — drivers for Redis, SQS, database; monitored by Horizon |
| File | Command / Code | Purpose |
|---|---|---|
| RequestLifecycleDemo.php | use Illuminate\Http\Request; | What Laravel Actually Is |
| ArticleEloquentExample.php | namespace App\Models; | Eloquent ORM |
| BladeTemplateSystem.blade.php | {{-- FILE: resources/views/layouts/app.blade.php --}} | Blade Templates |
| AuthExample.php | use Illuminate\Support\Facades\Gate; | Authentication and Authorization |
| MiddlewaresVsSpaghetti.php | $db = mysqli_connect('localhost', 'root', '', 'users'); | What Laravel Actually Is (And Why It Won Laravel's War Again |
| RequirementsCheck.php | $requiredPhpVersion = '8.1.0'; | Prerequisites |
| ProductionFeatures.php | $posts = Post::with(['comments.user', 'tags'])->where('published', true)->get(); | Key Features That Actually Matter In Production (Not Buzzwor |
| FormValidationExample.py | namespace App\Http\Requests; | Validation That Doesn't Suck |
Key takeaways
create() or fill().Common mistakes to avoid
4 patternsSkipping $fillable on Eloquent models
create() without either $fillable or explicitly picking fields with $request->only(['title', 'body']).Triggering the N+1 query problem
Caching config/routes in development
Not specifying the auth guard for API routes
Interview Questions on This Topic
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?
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?
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.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?
What is the difference between Laravel Breeze, Jetstream, and Fortify? When would you choose each?
Frequently Asked Questions
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.
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.
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.
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.
20+ years shipping production PHP systems at scale. Everything here is grounded in real deployments.
That's Laravel. Mark it forged?
6 min read · try the examples if you haven't