web php

Template Engines: Twig And Blade

Template engines give PHP applications a dedicated syntax for views. They make escaping, layouts, includes, loops, and components more consistent than hand-written PHP templates.

Twig is common in Symfony and many standalone PHP projects. Blade is Laravel's template engine. You do not need to master every directive on day one, but junior PHP roles often expect you to read and safely change both styles.

Escaped output

Both Twig and Blade are designed around escaping output by default when you use the normal echo syntax.

Twig:

<h1>{{ page_title }}</h1>
<p>{{ user.display_name }}</p>

Blade:

<h1>{{ $pageTitle }}</h1>
<p>{{ $user->display_name }}</p>

The normal {{ ... }} syntax is for text you want escaped. That should be the default for user names, titles, comments, search terms, and database content.

Raw output is dangerous

Both engines have a way to output raw HTML. Use it rarely.

Twig:

{{ trusted_html|raw }}

Blade:

{!! $trustedHtml !!}

Raw output should usually mean the value has already been sanitized and is intentionally allowed to contain HTML. It should not be used to "fix" escaped characters without understanding the source of the value.

Loops and empty states

Templates often render lists. Good templates handle the empty state as deliberately as the normal state.

Twig:

{% for invoice in invoices %}
    <li>{{ invoice.number }} - {{ invoice.total }}</li>
{% else %}
    <li>No invoices found.</li>
{% endfor %}

Blade:

@forelse ($invoices as $invoice)
    <li>{{ $invoice->number }} - {{ $invoice->total }}</li>
@empty
    <li>No invoices found.</li>
@endforelse

Layouts

Layouts keep shared page structure in one place.

Twig:

{% extends "base.html.twig" %}

{% block title %}Invoices{% endblock %}

{% block body %}
    <h1>Invoices</h1>
{% endblock %}

Blade:

@extends('layouts.app')

@section('title', 'Invoices')

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

The exact names vary by project. The idea is the same: the page fills named regions in a shared frame.

Includes, partials, and components

Twig includes smaller templates:

{% include "partials/_flash.html.twig" with { messages: flash_messages } %}

Blade supports includes and components:

@include('partials.flash', ['messages' => $messages])

<x-alert type="success" :message="$message" />

Use these for repeated UI that has a real responsibility. Avoid hiding simple markup behind too many layers.

Keep business logic out

Template engines have conditionals and loops, but that does not mean all logic belongs there. The controller should prepare view data. The template should display it.

This PHP example shows the preparation step:

PHP example
<?php

declare(strict_types=1);

$viewData = [
    'pageTitle' => 'Invoices',
    'invoices' => [
        ['number' => 'INV-1001', 'total' => '99.00'],
    ],
];

echo $viewData['pageTitle'] . ': ' . count($viewData['invoices']) . ' row' . PHP_EOL;

// Prints:
// Invoices: 1 row

The template should not need to calculate invoice totals, query the database, or decide whether the user is allowed to see the page.

What to check in a project

Check which engine the project uses and follow its conventions. Twig and Blade look similar in places, but their directives and escaping controls are different.

Check raw output carefully. |raw and {!! !!} should stand out during review.

Check variable names. Templates are easier to maintain when view data is named for display, such as pageTitle, primaryAction, or emptyMessage.

Check component boundaries. Components are useful when they reduce duplication and make state clearer, not when they hide basic HTML.

What you should be able to do

After this lesson, you should be able to read basic Twig and Blade templates, recognise escaped and raw output, render loops and empty states, understand layouts, and keep heavy application logic outside view files.

Practice

Task: Translate A Small Template

Write a short comparison that renders the same invoice list in Twig and Blade.

Requirements

  • Include escaped output for the page title.
  • Include a loop over invoices.
  • Include an empty state.
  • Show the Twig version.
  • Show the Blade version.
  • Add a short note identifying which syntax is raw output in each engine and why it should be rare.

Check Your Work

Read both examples and confirm they show the same behaviour: title, list rows, and empty state.

Show solution

Twig:

<h1>{{ page_title }}</h1>

<ul>
    {% for invoice in invoices %}
        <li>{{ invoice.number }} - {{ invoice.total }}</li>
    {% else %}
        <li>No invoices found.</li>
    {% endfor %}
</ul>

Blade:

<h1>{{ $pageTitle }}</h1>

<ul>
    @forelse ($invoices as $invoice)
        <li>{{ $invoice->number }} - {{ $invoice->total }}</li>
    @empty
        <li>No invoices found.</li>
    @endforelse
</ul>

Twig raw output is {{ value|raw }}. Blade raw output is {!! $value !!}. Both should be rare because raw output bypasses normal escaping.

Why This Works

Both versions use escaped output for ordinary values. Both versions handle the empty state in the template without needing the controller to build separate HTML.