π‘οΈ Blade Template Engine
Controllers gather your data; Blade turns it into HTML the browser can read. In this lesson you'll meet Laravel's compiled templating engine β how it renders views, escapes output to keep you safe, and gives you clean directives for the everyday work of looping, branching, and reusing markup.
π― Learning Objectives
By the end of this lesson, you will be able to:
- Explain how Blade compiles a
.blade.phpview into cached PHP and why that keeps rendering fast - Render views from routes and controllers and pass data into them
- Display data with
{{ }}and describe why Blade escapes output by default - Use control-flow directives (
@if,@foreach,@forelse) and the$loopvariable - Include sub-views, write Blade comments, and drop into raw PHP when you truly need it
Estimated Time: 30β40 minutes β’ Difficulty: BeginnerβIntermediate
Hands-on: Build a product-listing view that gracefully handles an empty list.
In This Lesson
What Is Blade?
Blade is the templating engine that ships with Laravel. A template is just a file of HTML with small placeholders where dynamic values go β the user's name, a list of products, whether a button should show. Blade gives you a tidy, readable syntax for those placeholders while never taking away your ability to write plain PHP when you need it.
π‘ A useful analogy: A Blade view is a fill-in-the-blanks form letter. The layout (Dear ____, your order ____ has shipped) is written once. Blade drops today's real values into the blanks for each visitor, then hands the finished page to the browser.
Blade files live under resources/views and end in .blade.php. The two things it does best are worth learning first: it compiles to fast PHP behind the scenes, and it escapes your output automatically so a stray <script> in your data can't hijack the page.
Storage & Compilation
When a view is requested for the first time, Blade reads your .blade.php file, translates every directive into equivalent PHP, and writes the result to a cache directory (storage/framework/views). On every later request Laravel runs that cached PHP directly β your original template isn't re-parsed unless you change it. The clever syntax you write costs essentially nothing at runtime.
This is the same idea as a modern JavaScript bundler: you author in an ergonomic syntax, and a build step produces something the machine runs efficiently. The difference is that Blade's "build" happens automatically and per-file the first time each view is hit.
Rendering Views & Passing Data
You return a view from a route or (more commonly) a controller using the view() helper. The first argument is the template name; the second is the data to hand it.
// routes/web.php β straight from a route
Route::get('/welcome', function () {
return view('welcome', ['name' => 'Ray']);
});
// A controller action β the more typical home for this
class UserController extends Controller
{
public function show(User $user)
{
// Route-model binding gives us $user already loaded
return view('users.profile', ['user' => $user]);
}
}
Notice the dot notation: users.profile maps to resources/views/users/profile.blade.php. Each dot is a subdirectory. Two other ways to pass data read a little more fluently:
// The fluent with() method β chainable
return view('users.profile')->with('user', $user);
// compact() builds the array from variable names
$user = User::findOrFail($id);
$posts = $user->posts;
return view('users.profile', compact('user', 'posts'));
π Key Terms
View: a .blade.php template that produces HTML.
Directive: a Blade command starting with @, such as @if or @foreach.
Escaping: converting characters like < and > into harmless HTML entities so data can't inject markup.
Displaying Data Safely
The workhorse of Blade is the double-brace echo. Whatever expression you put inside {{ }} is printed β and, crucially, run through PHP's htmlspecialchars() first.
{{-- Echo a variable (automatically escaped) --}}
Hello, {{ $name }}
{{-- Any PHP expression works, including a null-safe fallback --}}
Welcome, {{ $user->name ?? 'Guest' }}
{{-- Unescaped output β renders raw HTML. Use only on trusted content. --}}
{!! $trustedHtml !!}
β οΈ Why escaping matters
Imagine a user sets their display name to <script>stealCookies()</script>. With {{ $name }}, Blade prints it as literal text β harmless. With {!! $name !!}, the browser would execute it. That difference is a cross-site scripting (XSS) vulnerability. Reach for {!! !!} only when you produced the HTML yourself (say, from a Markdown converter you trust).
Think of {{ }} as a security checkpoint that every value passes through automatically β like a bank teller sealing cash in a tamper-proof envelope before it leaves the counter.
Control Structures
Blade wraps PHP's control structures in @ directives that are easier to scan inside markup than raw <?php if ... ?> tags.
Conditionals
@if ($records->count() === 1)
I have one record!
@elseif ($records->count() > 1)
I have multiple records!
@else
I don't have any records!
@endif
{{-- @unless is the inverse of @if --}}
@unless (Auth::check())
You are not logged in.
@endunless
{{-- Shorthand emptiness checks --}}
@isset($records)
{{-- $records is defined and not null --}}
@endisset
@empty($records)
{{-- $records is "empty" --}}
@endempty
Loops
@for ($i = 0; $i < 10; $i++)
The current value is {{ $i }}
@endfor
@foreach ($users as $user)
<p>This is user {{ $user->id }}</p>
@endforeach
{{-- @forelse loops, but falls through to @empty when the list is empty --}}
@forelse ($users as $user)
<li>{{ $user->name }}</li>
@empty
<p>No users found.</p>
@endforelse
β Reach for @forelse
@forelse combines a loop with a built-in empty-state β no separate @if (count(...)) guard. It's a smart delivery driver who already knows what to do when nobody's home, so you never ship a blank, confusing page.
The $loop Variable
Inside any @foreach, Blade quietly hands you a $loop object describing where you are in the iteration. No manual counters required.
@foreach ($users as $user)
@if ($loop->first)
<p>β Start of list β</p>
@endif
<p>User {{ $user->id }}
(row {{ $loop->iteration }} of {{ $loop->count }})</p>
@if ($loop->last)
<p>β End of list β</p>
@endif
@endforeach
| Property | Meaning |
|---|---|
$loop->index | 0-based position |
$loop->iteration | 1-based position |
$loop->count | Total items |
$loop->remaining | Items left after this one |
$loop->first / ->last | True on the first / last pass |
$loop->even / ->odd | Alternating rows (great for zebra striping) |
$loop->depth / ->parent | Nesting level and access to the outer loop |
For nested loops, $loop->parent reaches back out to the enclosing loop:
@foreach ($users as $user)
@foreach ($user->posts as $post)
@if ($loop->parent->first)
First user's post: {{ $post->title }}
@endif
@endforeach
@endforeach
Including Sub-Views
Repetition is the enemy of maintainable templates. @include pulls one view into another so shared markup lives in exactly one file.
{{-- Include a partial; it inherits the parent's data --}}
@include('shared.errors')
{{-- Include and pass extra data --}}
@include('shared.status', ['status' => 'complete'])
{{-- Only include if the view actually exists --}}
@includeIf('shared.promo', ['code' => 'SUMMER'])
{{-- Include based on a condition --}}
@includeWhen($showBanner, 'shared.banner')
@includeUnless($user->isPremium(), 'shared.upgrade-cta')
{{-- Use the first view that exists (fallback chain) --}}
@includeFirst(['custom.header', 'default.header'])
Included views are like prefabricated wall panels: build the panel once, then drop it wherever it's needed. Update the panel and every page that uses it updates too.
π‘ A note on the future
Modern Laravel increasingly favours Blade components (<x-alert />) over @include for reusable UI, because components take explicit inputs and encapsulate their own logic. You'll meet them in the Layout Inheritance and Components lesson. For simple shared snippets, @include is still perfectly good.
Comments & Raw PHP
Blade comments
Blade comments are stripped during compilation, so they never reach the browser β unlike HTML comments, which are visible in "View Source".
{{-- This note is for developers only; it won't appear in the HTML. --}}
<!-- This HTML comment WILL be visible in page source. -->
Dropping into PHP
When a directive doesn't exist for what you need, @php gives you a small escape hatch. Use it sparingly β heavy logic belongs in the controller, not the view.
@php
$total = collect($items)->sum('price');
@endphp
<p>Order total: {{ $total }}</p>
Protecting curly braces from Blade
If you use a front-end framework that also uses {{ }} (Vue, for example), tell Blade to leave those braces alone:
{{-- Blade processes this --}}
{{ $bladeValue }}
{{-- @ escapes the braces so Vue receives them untouched --}}
@{{ vueValue }}
{{-- Or protect a whole block --}}
@verbatim
<div>Hello, {{ vueValue }}</div>
@endverbatim
Worked Example: A Product Grid
Here's a realistic view that ties the pieces together β safe echoing, @forelse, conditional classes, an @include, and pagination links. In Laravel 11 the controller might pass a paginated collection like this:
// app/Http/Controllers/ProductController.php
public function index()
{
$products = Product::where('active', true)->paginate(12);
return view('products.index', compact('products'));
}
{{-- resources/views/products/index.blade.php --}}
<div class="product-grid">
@forelse ($products as $product)
<div class="product-card {{ $product->in_stock ? 'in-stock' : 'out-of-stock' }}">
<h3>{{ $product->name }}</h3>
@if ($product->discount_percent > 0)
<span class="discount-badge">{{ $product->discount_percent }}% OFF</span>
@endif
<p class="price">
@if ($product->original_price > $product->current_price)
<s>${{ number_format($product->original_price, 2) }}</s>
@endif
${{ number_format($product->current_price, 2) }}
</p>
@if ($product->in_stock)
<button class="add-to-cart">Add to Cart</button>
@else
<button class="notify-me">Notify Me</button>
@endif
@include('products.partials.wishlist-button', ['product' => $product])
</div>
@empty
<div class="no-products">
<p>No products match your filters.</p>
<a href="{{ route('products.index') }}">View all products</a>
</div>
@endforelse
</div>
{{-- Pagination links, only when there's more than one page --}}
@if ($products->hasPages())
<nav class="pagination">{{ $products->links() }}</nav>
@endif
What this view demonstrates
@forelsewith a friendly empty state- Conditional classes via a ternary inside
{{ }} - Nested
@iffor discount and stock states - A partial (
@include) for the wishlist button - The
route()helper and paginatorlinks()
Notice how the view worries only about presentation. The heavy lifting β querying and paginating $products β happened in the controller. Keeping that boundary crisp is the single most important habit for readable Blade.
Hands-on Exercise
ποΈ Build a Task List View
Objective: Render a list of tasks that behaves well whether there are ten tasks or none.
Instructions:
- Assume a controller passes
$tasks(a collection where each task hastitle,done, anddue_date). - Use
@forelseto loop; show a "You're all caught up!" message when the list is empty. - Give each row a
doneorpendingclass using a ternary. - Use the
$loopvariable to number each task (1.,2., β¦) and add aneven/oddclass. - Echo
{{ $task->title }}safely and display the due date.
π‘ Hint
Start the numbering from $loop->iteration (1-based). For the alternating class, {{ $loop->even ? 'even' : 'odd' }} does the whole job in one expression.
β Sample solution
{{-- resources/views/tasks/index.blade.php --}}
<ul class="task-list">
@forelse ($tasks as $task)
<li class="task {{ $task->done ? 'done' : 'pending' }} {{ $loop->even ? 'even' : 'odd' }}">
<span class="num">{{ $loop->iteration }}.</span>
<span class="title">{{ $task->title }}</span>
<time>{{ $task->due_date->format('M j, Y') }}</time>
</li>
@empty
<li class="empty">You're all caught up! π</li>
@endforelse
</ul>
π― Quick Quiz
Question 1: Why does {{ $name }} protect you against cross-site scripting?
Question 2: Which directive loops over a collection and provides a built-in empty state?
Question 3: A Blade view named users.profile lives at which path?
Summary & Quiz
π Key Takeaways
- Blade compiles
.blade.phpviews to cached PHP, so its friendly syntax is essentially free at runtime. - Return views with
view('name', $data); dot notation maps to subdirectories. {{ }}escapes output by default;{!! !!}does not β use it only on trusted HTML.- Directives like
@if,@foreach, and especially@forelsekeep control flow readable. - The
$loopvariable gives you position, counts, and first/last flags for free. @includeshares markup; keep real logic in the controller, not the view.
π Further Reading
π What's Next?
Next we go deeper into Blade's directive toolkit β authentication and authorization directives, form helpers like @csrf and @method, loop control, and how to write your own custom directives.
π Nice work!
You can now render safe, dynamic views. Let's sharpen your directive skills.