Skip to content
ExponaBLOG
PHP & LARAVEL DEVELOPMENT · DAY 048 / 100

Blade templates: PHP & Laravel best practices explained

Server-rendered views with components.

Expona Admin9 min read

Introduction

After 48 days of Laravel development, you've likely encountered Blade templates—but are you using them to their full potential? Blade templates: PHP & Laravel best practices explained will walk you through moving from legacy template patterns to modern, maintainable server-rendered views. We'll explore how components replace older inheritance models, how escaping protects your application, and how Livewire bridges the gap between server and client without abandoning your backend expertise.

By the end of this guide, you'll have concrete patterns you can apply to real projects, whether you're refactoring an existing codebase or building fresh features.

Layouts and Components: Moving Beyond @extends

For years, Blade developers relied on @extends and @section to build page hierarchies. While this approach works, it creates tight coupling and makes it harder to reason about reusable layout pieces. Modern Laravel encourages you to think in components instead—discrete, self-contained units with clear props and slots.

Consider a traditional layout pattern:

php
<!-- resources/views/layouts/app.blade.php -->
<!DOCTYPE html>
<html>
  <head>
    <title>@yield('title')</title>
  </head>
  <body>
    <nav>@include('partials.navbar')</nav>
    <main>
      @yield('content')
    </main>
    <footer>@include('partials.footer')</footer>
  </body>
</html>

<!-- resources/views/pages/dashboard.blade.php -->
@extends('layouts.app')

@section('title', 'Dashboard')

@section('content')
  <h1>Welcome</h1>
@endsection

This works, but the relationship between layout and content isn't explicit. You're relying on naming conventions and familiar patterns rather than clear contracts. Now compare this to a component-based approach:

php
<!-- resources/views/components/layout.blade.php -->
<!DOCTYPE html>
<html>
  <head>
    <title>{{ $title }}</title>
  </head>
  <body>
    <nav>
      <x-navbar />
    </nav>
    <main>
      {{ $slot }}
    </main>
    <footer>
      <x-footer />
    </footer>
  </body>
</html>

<!-- resources/views/pages/dashboard.blade.php -->
<x-layout title="Dashboard">
  <h1>Welcome</h1>
</x-layout>

Notice the clarity: the layout component declares its props ($title) upfront, and you pass data as attributes. The {{ $slot }} placeholder receives the component's content. This makes the relationship explicit and easier to test or refactor.

For more complex layouts, you can use named slots:

php
<!-- resources/views/components/card-layout.blade.php -->
<div class="card">
  <div class="card-header">
    {{ $header }}
  </div>
  <div class="card-body">
    {{ $slot }}
  </div>
  <div class="card-footer">
    {{ $footer }}
  </div>
</div>

<!-- Usage in a view -->
<x-card-layout>
  <x-slot:header>
    User Profile
  </x-slot:header>

  <p>Name: John Doe</p>

  <x-slot:footer>
    <button>Edit</button>
  </x-slot:footer>
</x-card-layout>

This pattern scales well. You're composing layouts from smaller, predictable pieces rather than managing a deep hierarchy. Teams find this easier to document and maintain because the intent is visible in the template itself.

Blade Components: Building Reusable UI with Props and Slots

Components are the heart of modern Blade. They're essentially PHP classes paired with a view, allowing you to encapsulate logic, validation, and rendering in one unit.

Let's build a practical example: a dropdown menu component that manages its own state:

php
<!-- app/View/Components/Dropdown.php -->
namespace App\View\Components;

use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;

class Dropdown extends Component
{
    public function __construct(
        public string $label = 'Menu',
        public string $triggerClass = '',
    ) {}

    public function render(): View|Closure|string
    {
        return view('components.dropdown');
    }
}

<!-- resources/views/components/dropdown.blade.php -->
<div class="dropdown">
  <button class="btn {{ $triggerClass }}">
    {{ $label }}
  </button>
  <div class="dropdown-menu">
    {{ $slot }}
  </div>
</div>

<!-- Usage -->
<x-dropdown label="Actions" triggerClass="btn-primary">
  <a href="/edit">Edit</a>
  <a href="/delete">Delete</a>
</x-dropdown>

Notice that Laravel automatically discovers components in app/View/Components and makes them available with the x- prefix. You don't need to manually register them in a service provider (though you can if you prefer more control).

For components with more sophisticated behavior, you can add methods to the class:

php
namespace App\View\Components;

use Illuminate\View\Component;

class UserCard extends Component
{
    public function __construct(
        public int $userId,
        public bool $showEmail = false,
    ) {}

    public function user()
    {
        return \App\Models\User::find($this->userId);
    }

    public function render()
    {
        return view('components.user-card', [
            'user' => $this->user(),
        ]);
    }
}

<!-- resources/views/components/user-card.blade.php -->
<div class="user-card">
  <h3>{{ $user->name }}</h3>
  @if ($showEmail)
    <p>{{ $user->email }}</p>
  @endif
  {{ $slot }}
</div>

This pattern keeps your controller lean: rather than fetching the user and passing it to the view, the component handles its own data dependencies. Teams find this clearer because logic lives closer to where it's rendered.

You can also use anonymous components for simple, view-only pieces:

php
<!-- resources/views/components/alert.blade.php (no PHP class needed) -->
<div class="alert alert-{{ $type ?? 'info' }}">
  {{ $slot }}
</div>

<!-- Usage -->
<x-alert type="warning">
  This action cannot be undone.
</x-alert>

Anonymous components are fast to write and perfect for stateless UI elements. Reserve class-based components for cases where you need data fetching, conditional rendering based on application state, or method-level logic.

Escaping: Automatic Protection Against XSS

One of Blade's most important features is automatic HTML escaping by default. This protects your application from cross-site scripting (XSS) attacks without requiring constant vigilance.

By default, any data you output with double curly braces is escaped:

php
<?php
// In a controller or component
$userInput = '<script>alert("XSS")</script>';

// In the Blade template
{{ $userInput }}

// Renders as:
// <script>alert("XSS")</script>
// The browser treats this as text, not executable code.

The key rule: always use {{ }} for user-generated content. If you absolutely must render raw HTML (for example, when a trusted admin writes Markdown-to-HTML content), use the @html directive or {!! !!}—but do this only when you're certain the content is safe:

php
<!-- Safe: user input is escaped -->
<p>{{ $comment->body }}</p>

<!-- Use with caution: raw HTML -->
<div>
  {!! $article->htmlContent !!}
</div>

<!-- Modern alternative (Laravel 8.38+) -->
<div>
  @html($article->htmlContent)
</div>

In production systems handling payments, user data, or sensitive information, make escaping a habit. Many XSS vulnerabilities slip through because developers occasionally forget or assume content is "safe." Blade's default behavior saves you from that class of bug.

If you're passing data to JavaScript, use the @json directive to safely serialize PHP values:

php
<script>
  const user = @json($user);
  console.log(user.name);
</script>

This ensures that strings with quotes, special characters, and Unicode are properly escaped for JavaScript context.

Livewire: Adding Interactivity Without a Separate Frontend

By day 48, you've probably faced the question: "Do we need a JavaScript framework?" Livewire offers a pragmatic answer—yes, but keep your backend at the center. It lets you build interactive features (form validation, live search, real-time updates) without writing separate API endpoints or managing a React/Vue build pipeline.

Here's a practical example: a live search component for finding users.

php
<!-- app/Livewire/UserSearch.php -->
namespace App\Livewire;

use App\Models\User;
use Livewire\Attributes\Url;
use Livewire\Component;

class UserSearch extends Component
{
    #[Url]
    public string $query = '';

    public function render()
    {
        $results = $this->query
            ? User::where('name', 'like', "%{$this->query}%")
                ->limit(10)
                ->get()
            : collect();

        return view('livewire.user-search', [
            'results' => $results,
        ]);
    }
}

<!-- resources/views/livewire/user-search.blade.php -->
<div>
  <input
    type="text"
    wire:model.live="query"
    placeholder="Search users..."
    class="form-control"
  >

  @if ($results->isNotEmpty())
    <ul class="search-results">
      @foreach ($results as $user)
        <li>
          <a href="/users/{{ $user->id }}">
            {{ $user->name }}
          </a>
        </li>
      @endforeach
    </ul>
  @endif
</div>

<!-- In your main view -->
<livewire:user-search />

Notice the magic: wire:model.live="query" binds the input field to the Livewire component's property. Every keystroke updates $query on the server, re-renders the component, and sends back only the changed HTML. No manual API calls, no JSON parsing in JavaScript.

Livewire handles more complex interactions too. Here's a form with real-time validation:

php
<!-- app/Livewire/CreatePost.php -->
namespace App\Livewire;

use App\Models\Post;
use Livewire\Attributes\Validate;
use Livewire\Component;

class CreatePost extends Component
{
    #[Validate('required|min:3')]
    public string $title = '';

    #[Validate('required|min:10')]
    public string $body = '';

    public function save()
    {
        $this->validate();

        Post::create([
            'title' => $this->title,
            'body' => $this->body,
            'user_id' => auth()->id(),
        ]);

        $this->redirect('/posts', navigate: true);
    }

    public function render()
    {
        return view('livewire.create-post');
    }
}

<!-- resources/views/livewire/create-post.blade.php -->
<form wire:submit="save">
  <div class="form-group">
    <label>Title</label>
    <input
      type="text"
      wire:model="title"
      class="form-control @error('title') is-invalid @enderror"
    >
    @error('title')
      <span class="invalid-feedback">{{ $message }}</span>
    @enderror
  </div>

  <div class="form-group">
    <label>Body</label>
    <textarea
      wire:model="body"
      class="form-control @error('body') is-invalid @enderror"
    ></textarea>
    @error('body')
      <span class="invalid-feedback">{{ $message }}</span>
    @enderror
  </div>

  <button type="submit" class="btn btn-primary">
    Save Post
  </button>
</form>

The validation runs server-side, so you get the security guarantees of your Laravel rules. Users see errors instantly without a page reload. This pattern scales to pagination, filtering, multi-step wizards, and more.

The trade-off: Livewire requires an active WebSocket connection or falls back to polling over HTTP, which adds latency. For highly interactive dashboards or admin panels, this is negligible. For pages requiring sub-100ms response times, you might still need a traditional SPA.

Common Mistakes

  • Over-escaping. Using {!! !!} for user input "just to be safe" from Blade's escaping. This defeats the purpose—always escape user content by default.
  • Component logic in views. Placing database queries or complex conditionals directly in Blade templates instead of pushing them into component classes or controllers. This makes testing harder and couples views to your data layer.
  • Ignoring slot documentation. Forgetting to document which slots a component accepts. Future developers (including you) will struggle to use components effectively. Add a comment at the top of component classes listing available props and slots.
  • Livewire for everything. Using Livewire for simple, one-time interactions (like filtering a static table once per page load) when plain HTML forms or a lightweight Alpine.js component would be faster and simpler.
  • Mixing inheritance and components. Keeping old @extends patterns while introducing components. Pick one approach and commit to it for clarity.

Key Takeaways

  • Replace @extends with component-based layouts for clearer, more composable templates.
  • Use props and slots to make components self-documenting and easier to reason about.
  • Rely on Blade's default escaping with {{ }}—only use raw output ({!! !!} or @html()) when content is trusted.
  • Livewire bridges the gap between server-rendered views and interactive experiences without requiring a separate frontend codebase.
  • Keep logic out of views: favor component classes and controllers for anything beyond simple conditionals and loops.
  • Test components in isolation by rendering them with test data, just as you'd test a controller or model.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles