Laravel Fundamentals: Routing, Controllers, Middleware, Requests, Blade, and Eloquent
Build the core mental model of a Laravel application: define routes, write controllers, apply and create middleware, validate incoming requests, render views with Blade, and read/write data through Eloquent models and relationships.
Laravel is, by a wide margin, the most popular PHP framework, and for good reason: it takes the concepts from the previous lesson — routing, middleware, DI, validation, an ORM, templating — and implements each of them with an unusually polished, well-documented, and consistent API. This lesson walks through the pieces you'll touch on nearly every Laravel feature you build: how a URL becomes a controller method, how that method gets clean validated input, how it renders a view with Blade, and how it reads and writes data through Eloquent models. By the end, you should be able to trace a request from routes/web.php all the way to a rendered page backed by real database data.
Routing: web.php, Parameters, and Route Model Binding
Every Laravel route is registered in a routes file — routes/web.php for browser-facing routes (which include session state and CSRF protection by default) and routes/api.php for stateless API routes. A route pairs an HTTP verb and a URI pattern with something to run: a closure for something trivial, or far more commonly, a controller class and method, written as an array like [ProductController::class, 'show']. Route parameters are declared with curly braces in the URI — Route::get('/products/{id}', ...) — and Laravel passes the matched segment as an argument to the closure or controller method in the same position.
Route model binding is one of Laravel's most convenient features: instead of manually querying Product::find($id) at the top of every method that receives a {product} parameter, you type-hint the parameter as the model class itself — public function show(Product $product) — and Laravel automatically looks the row up by its route key (the primary key by default) before your method even runs, returning a 404 automatically if no matching row exists. This eliminates an entire category of repetitive 'find or fail' boilerplate.
Routes can be grouped to share attributes — a URL prefix, a middleware stack, or a name prefix — using Route::middleware([...])->group(function () { ... }) or Route::prefix('admin')->group(...). Named routes (->name('products.show')) let you generate URLs elsewhere in the app with route('products.show', $product) instead of hardcoding path strings, so a URL structure can change in one place without breaking links throughout the codebase.
• Route::get/post/put/patch/delete($uri, $action) registers a route for a given HTTP verb. • {param} in a URI captures a segment and passes it as a method argument. • Route model binding: type-hint a model class as the parameter and Laravel fetches the row (or 404s) automatically. • Route::middleware()->group() and Route::prefix()->group() share attributes across many routes at once. • Named routes (->name(...)) enable stable URL generation via route() instead of hardcoded strings.
Controllers: Organizing Request-Handling Logic
A controller groups related request-handling methods into a single class, keeping routing files short and business logic organized by resource. A typical resource controller (php artisan make:controller ProductController --resource) generates seven conventional methods — index, create, store, show, edit, update, destroy — matching the standard CRUD operations, which you then register in one line with Route::resource('products', ProductController::class) instead of defining all seven routes by hand.
For an action that doesn't fit neatly into CRUD — like handling a checkout, or generating a report — Laravel supports single-action controllers: a class with just one __invoke() method, registered with Route::get('/checkout', CheckoutController::class). This keeps a controller focused on exactly one responsibility rather than accumulating unrelated methods, which is a common code-smell in older, larger controllers.
Controllers should generally stay thin: their job is to receive a request, delegate to models/services for the actual logic, and return a response (a view, a redirect, or JSON) — not to contain large blocks of business logic themselves. As an application grows, logic that doesn't belong to a specific model (like sending a multi-step notification, or coordinating several models together) is usually extracted into a dedicated service class that the controller calls into, keeping the controller itself easy to read at a glance.
• Resource controllers: index/create/store/show/edit/update/destroy, registered via Route::resource(). • Single-action controllers: one __invoke() method for a focused, non-CRUD action. • Controllers should stay thin — delegate real logic to models or service classes. • php artisan make:controller Name --resource scaffolds a full resource controller's method stubs.
Middleware: Built-in and Custom
Laravel ships several middleware out of the box that you use constantly without writing them yourself: auth rejects unauthenticated users (redirecting them to a login route), guest does the opposite (blocking already-authenticated users from pages like the login form), throttle:60,1 rate-limits a route to 60 requests per minute per user/IP, and verified requires a confirmed email address. These are applied to a route or group with ->middleware('auth') or chained: ->middleware(['auth', 'verified']).
Writing custom middleware is straightforward: php artisan make:middleware EnsureUserIsAdmin generates a class with a single handle(Request $request, Closure $next) method. Inside it, you inspect the request (or the currently authenticated user) and either call return $next($request); to let the request continue down the pipeline, or short-circuit it by returning a response of your own (a redirect, an abort, a JSON error) without calling $next at all.
A newly created middleware must be registered before it can be referenced by a short alias name — in modern Laravel this is done in bootstrap/app.php via the ->withMiddleware() callback, mapping an alias like 'admin' to the middleware's class name, after which it can be applied to any route the same way as a built-in one: Route::get('/admin', ...)->middleware('admin').
• Built-in middleware: auth, guest, throttle:60,1, verified, and more, applied via ->middleware(...). • Custom middleware: handle(Request $request, Closure $next) — call $next($request) to continue, or return a response to short-circuit. • php artisan make:middleware Name scaffolds a new middleware class. • Register a custom middleware's alias (in bootstrap/app.php) before referencing it by name on a route.
Requests: The Request Object, validate(), and Form Requests
Every controller method can type-hint Laravel's Illuminate\Http\Request object to access everything about the incoming request: $request->input('title') or the magic property $request->title for form/query data, $request->all() for everything, $request->file('avatar') for an uploaded file, and $request->user() for the currently authenticated user. This object wraps $_GET/$_POST/$_FILES/$_SERVER behind a single, consistent, testable API instead of reading PHP's raw superglobals directly.
The quickest way to validate input inside a controller is $request->validate([...]), which runs the given rules and — if any fail — automatically redirects back to the previous page with the errors and the user's old input flashed to the session, ready for a Blade view to redisplay; if validation passes, it returns just the validated data as an array, safe to pass straight to Model::create().
For more complex or reusable validation, a Form Request class (php artisan make:request StoreProductRequest) moves the rules (and an authorize() check for whether this user is even allowed to make the request) out of the controller entirely. Type-hinting the Form Request class instead of the plain Request in a controller method causes Laravel to run that validation automatically before the method body executes at all — keeping controllers focused purely on what happens once the input is already known to be valid.
• Request object: $request->input(), magic properties, $request->all(), $request->file(), $request->user(). • $request->validate([...]) — inline validation; auto-redirects with errors on failure, returns validated data on success. • Form Requests (php artisan make:request) move validation rules and authorization checks into their own class. • Type-hinting a Form Request in a controller method runs its validation automatically before the method body runs.
Blade Templating
Blade is Laravel's templating engine, compiled down to plain PHP and cached, so it costs essentially nothing at runtime. Blade files use the .blade.php extension and offer directives that replace common raw-PHP control structures: @if/@elseif/@else/@endif, @foreach/@endforeach, @for/@endfor, and @while/@endwhile mirror their PHP equivalents but read more cleanly inside HTML markup.
Layout inheritance avoids repeating the surrounding HTML (head, nav, footer) on every page. A parent layout defines named placeholders with @yield('content'), and a child view declares @extends('layouts.app') followed by @section('content') ... @endsection to fill that placeholder. Reusable chunks of UI that take their own data can be built as Blade components (@component('alert', ['type' => 'error']) or the tag syntax <x-alert type="error">Message</x-alert>), which behave like small, composable templates with their own props.
Output escaping is Blade's most important security feature: {{ $value }} automatically runs the value through htmlspecialchars()-equivalent escaping, so a value containing <script> renders as harmless text rather than executing. The raw-output syntax {!! $value !!} skips that escaping entirely and prints the value exactly as given — it exists for genuinely trusted HTML (like content from a WYSIWYG editor you control) and must never be used on unsanitized user input, since doing so reopens the exact cross-site scripting hole the framework otherwise closes by default.
• Directives: @if/@elseif/@else/@endif, @foreach/@endforeach, @for, @while — compiled to plain PHP. • Layout inheritance: @extends, @section/@endsection, and a parent's @yield('name') placeholder. • Components: <x-alert type="error">...</x-alert> or @component for reusable, prop-driven UI chunks. • {{ $value }} escapes output automatically; {!! $value !!} prints raw, unescaped HTML — use only for trusted content.
Eloquent ORM: Models, Relationships, and Mass Assignment
An Eloquent model is a plain PHP class extending Illuminate\Database\Eloquent\Model that represents one database table — by convention, a model named Post maps to a posts table (snake_case, plural) unless you override it with a protected $table property. Basic reads and writes feel like working with ordinary objects: Post::find(1), Post::where('published', true)->get(), $post->title = 'New title'; $post->save();, and Post::create([...]) for inserting a new row in one call.
Relationships are declared as methods on the model, and Eloquent uses foreign-key naming conventions to build the underlying query automatically. hasMany describes a one-to-many relationship from the 'one' side (User::posts() returns all posts belonging to that user); belongsTo is its inverse on the 'many' side (Post::user() returns the single owning user); belongsToMany describes a many-to-many relationship through a pivot table (Post::tags() and Tag::posts()). Once declared, a relationship is accessed like a property ($user->posts — note no parentheses — to get the loaded collection) or queried further as a method ($user->posts()->where(...)).
Mass assignment — passing a whole array of fields into create() or fill() at once — is a convenience that also opens a serious vulnerability if left unguarded: if a form unexpectedly includes an is_admin field and nothing stops it, mass assignment could let an attacker set it. Eloquent guards against this by requiring every model to declare either protected $fillable = [...] (an explicit allow-list of mass-assignable fields) or protected $guarded = [...] (fields to always exclude — $guarded = [] disables the protection entirely and should be avoided). Declaring $fillable deliberately, and never including sensitive fields like is_admin or role in it, is one of the most important small habits in real Laravel development.
• A model maps to a table by convention: Post → posts (snake_case, plural). • hasMany (one-to-many, on the 'one' side), belongsTo (inverse, on the 'many' side), belongsToMany (many-to-many via pivot table). • Access a relationship as a property ($user->posts) for loaded results, or as a method ($user->posts()->where(...)) to query further. • $fillable (allow-list) or $guarded (deny-list) is required to protect against unsafe mass assignment. • Never mass-assign sensitive fields (e.g. is_admin) directly from user-submitted input.
A Minimal Request Lifecycle: Route → Controller → Model → Blade
Post $post in the method signature is route model binding — Laravel queries the posts table for you before show() runs. $fillable explicitly whitelists which columns can be set via mass assignment (e.g. Post::create($data)).
<?php
// routes/web.php
use App\Http\Controllers\PostController;
Route::get('/posts/{post}', [PostController::class, 'show'])->name('posts.show');
// app/Http/Controllers/PostController.php
class PostController extends Controller
{
public function show(Post $post) // route model binding: auto-fetched or 404
{
return view('posts.show', ['post' => $post]);
}
}
// app/Models/Post.php
class Post extends Model
{
protected $fillable = ['title', 'body', 'user_id'];
public function author()
{
return $this->belongsTo(User::class, 'user_id');
}
}Code Examples
The title, author name, and each comment body use {{ }} because they are plain user-supplied text that must be escaped. $post->rendered_html deliberately uses {!! !!} because it's assumed to already be sanitized, trusted HTML (e.g. produced by a Markdown renderer on the server) — using {!! !!} on raw user input instead would reintroduce an XSS vulnerability.
Type-hinting StorePostRequest runs authorize() and rules() automatically before store() executes — if either fails, the controller body never runs at all. $request->user()->posts()->create(...) creates the post through the relationship, which automatically sets the correct user_id.
$post = $request->user()->posts()->create($request->validated());
{{-- resources/views/posts/show.blade.php --}}
@extends('layouts.app')
@section('content')
<article>
<h1>{{ $post->title }}</h1>
<p class="byline">By {{ $post->author->name }}</p>
<div class="body">
{!! $post->rendered_html !!}
</div>
@if ($post->comments->isEmpty())
<p>No comments yet.</p>
@else
<ul>
@foreach ($post->comments as $comment)
<li>{{ $comment->body }}</li>
@endforeach
</ul>
@endif
</article>
@endsection<?php
// app/Http/Requests/StorePostRequest.php
class StorePostRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user() !== null;
}
public function rules(): array
{
return [
'title' => 'required|string|max:255',
'body' => 'required|string',
];
}
}
// app/Http/Controllers/PostController.php
public function store(StorePostRequest $request)
{
$post = $request->user()->posts()->create($request->validated());
return redirect()->route('posts.show', $post);
}Anatomy of a Laravel Request
Each stage hands off to the next: routing → middleware → controller → model → view.
ourl routes/web.php Middleware Controller Eloquent Blade
┌────────────┐ ┌──────────┐ ┌────────────┐ ┌───────────┐ ┌────────┐
GET → │ Route match │ ────▶ │ auth, etc.│ ────▶ │ show() │──▶│ Post::find│──▶ │ view() │──▶ HTML
└────────────┘ └──────────┘ └────────────┘ └───────────┘ └────────┘Important Rules
Always declare $fillable (preferred) or $guarded on every Eloquent model — never leave mass assignment unguarded.
Use {{ }} for all user-supplied or otherwise untrusted output in Blade; reserve {!! !!} for content you've explicitly sanitized.
Prefer route model binding over manually calling Model::findOrFail($id) at the top of every method.
Keep controllers thin — push real business logic into models or dedicated service classes.
Use a Form Request instead of inline $request->validate() once validation rules get long or need to be reused.
Blade Components in a Real Design System
Larger Laravel applications typically build a small library of Blade components — <x-button>, <x-card>, <x-modal> — so that visual consistency and accessibility markup live in one place, and a designer-approved change to a button's styling updates every page in the application from a single file instead of dozens of copy-pasted HTML snippets.
protected $guarded = []; // disables mass-assignment protection entirely protected $fillable = ['title', 'body', 'user_id'];
An empty $guarded array means every column, including sensitive ones like is_admin or role, becomes mass-assignable. Explicitly listing $fillable columns ensures only intended fields can ever be set via create() or fill().
{!! $comment->body !!} {{ $comment->body }} User-submitted comment text must be escaped with {{ }}; using the raw {!! !!} output on untrusted input allows an attacker to inject a <script> tag that executes in every visitor's browser — a stored XSS vulnerability.
public function show($id) { $post = Post::where('id', $id)->firstOrFail();... } public function show(Post $post) {... }
Manually querying for a model by its route parameter duplicates what route model binding already does automatically, including the 404 response when no matching row exists — type-hinting the model class is shorter and less error-prone.
What does route model binding do when a controller method is written as public function show(Post $post)? A. It creates a new empty Post instance every time B. Laravel automatically looks up the Post matching the route parameter and injects it, or returns a 404 if not found C. It has no special effect; $post is always null unless manually set D. It deletes the Post matching the route parameter
B. Laravel automatically looks up the Post matching the route parameter and injects it, or returns a 404 if not found — Route model binding type-hints the model class itself, and Laravel resolves the route parameter into an actual model instance automatically, throwing a 404 response when no matching row exists.
In Blade, what is the key difference between {{ $value }} and {!! $value !!}? A. They are functionally identical, just different syntax preferences B. {{ }} escapes the output automatically; {!! !!} prints it raw and unescaped C. {!! !!} is faster but only works with numbers D. {{ }} only works inside @foreach loops
B. {{ }} escapes the output automatically; {!! !!} prints it raw and unescaped — {{ }} runs the value through automatic HTML escaping to prevent XSS; {!! !!} bypasses that escaping entirely and should only be used on content you have already sanitized or fully trust.
Why does Eloquent require declaring $fillable or $guarded on a model? A. To make queries run faster B. To control which columns can be set via mass assignment (e.g. Model::create($data)), preventing unintended fields like is_admin from being set by user input C. To define the table name D. It's purely optional and has no security implication
B. To control which columns can be set via mass assignment (e.g. Model::create($data)), preventing unintended fields like is_admin from being set by user input — Without an allow-list ($fillable) or deny-list ($guarded), mass assignment could let unexpected fields in submitted data set columns the developer never intended to expose, such as an admin flag.
What relationship method would you use on a Post model to fetch its single owning User, where the posts table has a user_id column? A. hasMany(User::class) B. belongsTo(User::class) C. belongsToMany(User::class) D. hasOne(User::class, 'post_id')
B. belongsTo(User::class) — belongsTo is used on the side of the relationship holding the foreign key (posts.user_id), representing that each post belongs to exactly one user.
Design the routes, controller method, Eloquent model relationship, and a Blade snippet needed to display a list of a single Author's Books at the URL /authors/{author}/books, where each Book belongsTo an Author.
// routes/web.php Route::get('/authors/{author}/books', [AuthorController::class, 'books']); // AuthorController.php public function books(Author $author) { return view('authors.books', ['author' => $author]); } // Author model public function books() { return $this->hasMany(Book::class); } // authors/books.blade.php @foreach ($author->books as $book) <li>{{ $book->title }}</li> @endforeach
Use route model binding for {author}, an Author::books() hasMany() relationship, and @foreach in the Blade view to list $author->books.