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
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.