Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Building Web Apps with Laravel  Module 4 – Blade, components and the front end

Blade templates and safe output

Print values in Blade so a <script> in a title lands on the page as text, keep raw echoes rare, and send JSON into a script tag with Js::from.

  • Beginner
  • 10 minutes
  • Examples run with PHP 8.5.11, Composer 2.10.3, Laravel 13.34.0 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 10 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Explain how {{ $value }} compiles and when {!! !!} is acceptable
  • Trace a stored URL through Blade and the url validation rule
  • Use the main Blade directives for loops, auth, errors and includes
  • Pass data into a <script> tag with Js::from
  • Compile every view ahead of time with php artisan view:cache

Before you start

What you’ll learn · Before you start

What you will learn

  • Explain how {{ $value }} compiles and when {!! !!} is acceptable
  • Trace a stored URL through Blade and the url validation rule
  • Use the main Blade directives for loops, auth, errors and includes
  • Pass data into a <script> tag with Js::from
  • Compile every view ahead of time with php artisan view:cache

Before you start

On this page

A visitor saves a notice titled <script>alert(1)</script> and the page prints it as the words <script>alert(1)</script>, not as a running script. That is Blade doing its first job, which is to turn every value a template prints into HTML-safe text, so a stored field cannot sneak code into the page. The default is cheap and silent: Blade compiles {{ $value }} into <?php echo e($value); ?>, and e() runs PHP’s htmlspecialchars with ENT_QUOTES on.

The default is not the whole story. A bad URL in an href attribute passes through it untouched, and a value placed inside a <script> tag needs an encoder of its own. Every case below runs on Laravel 13.34 and shows its real output: the HTML a value becomes, the PHP each everyday directive compiles to, and the files php artisan view:cache writes before the first request.

Blade escapes a value by default, and the raw sink stays off

Blade reads a template and rewrites each marker into PHP. The A short placeholder in a Blade template that the compiler rewrites into a PHP echo of the given expression, HTML-escaping its value by default. {{ $value }} becomes <?php echo e($value); ?>, and the raw echo: An unescaped print, written as {!! … !!} in Blade; the value is sent to the browser as HTML without a call to the escape helper. {!! $value !!} becomes <?php echo $value; ?>. The helper e() returns htmlspecialchars($value ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8', $doubleEncode), so the five characters HTML uses for its own grammar leave the page as character references: < becomes &lt;, > becomes &gt;, & becomes &amp;, " becomes &quot; and ' becomes the numeric reference &#039;. The HTML standard defines both kinds, named and numeric, so every browser renders them back to text.

The probe prints the two sinks side by side, with a safe title and the attack title in the same list:

escape-probe/escape.php: render twice and show the compiled PHP PHP · escape-probe/escape.php
<?php

// Renders two Blade templates with the same notice: one with {{ }} auto-escaping and one with the raw sink {!! !!}.
// The last block uses Blade's compiler to show exactly what PHP each echo compiles to.
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\View;

require __DIR__.'/vendor/autoload.php';
$app = require __DIR__.'/bootstrap/app.php';
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();

$notices = [
    ['title' => 'Spoken English evening batch', 'body' => 'Open & affordable'],
    ['title' => '<script>alert(1)</script>', 'body' => 'An attacker tried this one'],
];

echo "{{ \$notice['title'] }} output (every notice):\n";
foreach ($notices as $n) {
    echo '  ', trim(View::make('escape.safe', ['notice' => $n])->render()), "\n";
}

echo "\n{!! \$notice['title'] !!} output (same input, raw sink):\n";
foreach ($notices as $n) {
    echo '  ', trim(View::make('escape.raw', ['notice' => $n])->render()), "\n";
}

echo "\nWhat Blade compiles each echo to:\n";
echo "  {{ \$notice['title'] }}  ->  ", trim(Blade::compileString("{{ \$notice['title'] }}")), "\n";
echo "  {!! \$notice['title'] !!}  ->  ", trim(Blade::compileString("{!! \$notice['title'] !!}")), "\n";
Blade escaped output and raw-sink output on the same data Python · escaping_session.py
from session import run

# Render two Blade templates on the same notices: {{ }} auto-escaping and {!! !!} raw-sink, then print what Blade compiled.
run(
    """
php escape.php
""",
    app=True,
    overlay=("escape-probe",),
)

Output

$ php escape.php
{{ $notice['title'] }} output (every notice):
  <strong>Spoken English evening batch</strong>: Open &amp; affordable
  <strong>&lt;script&gt;alert(1)&lt;/script&gt;</strong>: An attacker tried this one

{!! $notice['title'] !!} output (same input, raw sink):
  <strong>Spoken English evening batch</strong>: Open & affordable
  <strong><script>alert(1)</script></strong>: An attacker tried this one

What Blade compiles each echo to:
  {{ $notice['title'] }}  ->  <?php echo e($notice['title']); ?>
  {!! $notice['title'] !!}  ->  <?php echo $notice['title']; ?>

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 escaping_session.py

The first block printed the escaped titles. Open & affordable became Open &amp; affordable, so a browser renders it as Open & affordable. The malicious title left the server as &lt;script&gt;alert(1)&lt;/script&gt; and lands in the page as text. The second block ran the same values through {!! !!} and sent the script tag through untouched; a browser given that line would run alert(1). The last two lines show the compiler at work: {{ $notice['title'] }} compiled to <?php echo e($notice['title']); ?>, and {!! $notice['title'] !!} compiled to a bare <?php echo $notice['title']; ?>.

Key idea

{{ }} is safe because Blade wraps the value in e(). {!! !!} prints the value unchanged, so it is an XSS sink: A place in a response where untrusted text is written in a way the browser can read as code: an unescaped echo, an attribute whose value is code, or an unescaped script string. and is acceptable only for HTML your server itself produced and sanitised, such as a Markdown document rendered with a trusted converter.

The OWASP XSS Prevention Cheat Sheet asks for output encoding as close as possible to where the data is rendered, in the encoding of that context, and for an HTML-body echo that is exactly what e() does. A raw echo belongs to code you own entirely, and even then a plain string that users wrote never goes through it.

When a raw echo really is right

A notice body in Markdown, converted on the server with raw HTML stripped and unsafe links refused, is HTML your code produced from text your code read, so a raw echo prints it. Set both options yourself: Laravel’s Str::markdown lets raw HTML through unless html_input is strip. A link a visitor typed, a comment, a filename or a title is text and goes through {{ }}.

Interactive · message by message From the echo marker to the page Follow the title `<script>alert(1)</script>` through Blade's two sinks and see what the browser receives.
One echo, from Blade to the page, step 4 of 4. Messages so far: Blade file to Compiler: pass the expression; Compiler to Browser: &lt;script&gt;… as text; Compiler to Browser: <script>alert(1)</script>.Blade file.blade.phpCompilerecho e(...)Browserscript runspass the expression&lt;script&gt;… as text<script>alert(1)</script>

The drawing shows the whole scene; its 4 steps are listed below.

Every step as text
  1. The echo marker is read. The template reads {{ $notice["title"] }}. Blade sees two curly braces and treats the inside as a PHP expression that should become an HTML-safe echo.
  2. The compiler wraps it in e(). Blade rewrites {{ $notice["title"] }} as <?php echo e($notice["title"]); ?>. The helper e() runs htmlspecialchars with ENT_QUOTES, so each of the five HTML characters leaves as a character reference.
  3. The page sends the escaped text. The server sends &lt;script&gt;alert(1)&lt;/script&gt;. The browser reads the five references and shows the text to the visitor; no element is created and no script runs.
  4. The raw sink skips e(). If the template had {!! $notice["title"] !!}, Blade would compile it to <?php echo $notice["title"]; ?>. The browser then sees a real <script> and runs alert(1), which is why a raw echo is for HTML your own code produced.

Checkpoint

Check: Blade auto-escaping and the raw sink

  1. One question A controller passes $title = 'Open & affordable'. The template prints {{ $title }}. What does the browser receive, and what does it render?

    Choose one answer.

    Show the answer: One question

    Answer: Open &amp; affordable arrives, and the browser renders Open & affordable.

    {{ }} compiles to echo e($value), which runs htmlspecialchars with ENT_QUOTES. The browser reverses the named references for display.

Escaping stops HTML, not a dangerous scheme in an attribute

Escaping fixes HTML characters, so a value with no HTML-special characters leaves {{ }} unchanged. That is a problem the moment the value lands inside an attribute whose grammar lets the browser run code. The classic case is a link whose href the visitor controls:

escape-probe/href_scheme.php: five URLs, one href template, and the url rule PHP · escape-probe/href_scheme.php
<?php

// Renders a view with a user-supplied link inside href="{{ $url }}" and prints what Blade produced for each URL.
// Shows that {{ }} escapes HTML but still happily passes an attacker-chosen scheme through to the browser.
use Illuminate\Support\Facades\Blade;

require __DIR__.'/vendor/autoload.php';
$app = require __DIR__.'/bootstrap/app.php';
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();

$cases = [
    '/batches/english-evening',
    'https://partner.example.com/offer',
    'javascript:alert(1)',
    'JAVASCRIPT:alert(1)',
    'data:text/html,<script>alert(1)</script>',
];

$template = Blade::compileString('<a href="{{ $url }}">Open</a>');

foreach ($cases as $url) {
    $html = (function (array $data) use ($template) {
        extract($data, EXTR_SKIP);
        ob_start();
        eval('?>'.$template);
        return trim(ob_get_clean());
    })(['url' => $url]);
    echo str_pad($url, 42), ' -> ', $html, "\n";
}

echo "\nValidator::make([...], ['url' => 'url:https,http']) result for each URL:\n";
foreach ($cases as $url) {
    $v = \Illuminate\Support\Facades\Validator::make(['url' => $url], ['url' => 'url:https,http']);
    echo str_pad($url, 42), ' -> ', $v->passes() ? 'passes' : 'fails: '.$v->errors()->first('url'), "\n";
}
A javascript: URL in href={{ $url }} and the server-side guard Python · href_scheme_session.py
from session import run

# Renders <a href="{{ $url }}"> for five chosen URLs; then validates each with 'url:https,http' to show the server-side guard.
run(
    """
php href_scheme.php
""",
    app=True,
    overlay=("escape-probe",),
)

Output

$ php href_scheme.php
/batches/english-evening                   -> <a href="/batches/english-evening">Open</a>
https://partner.example.com/offer          -> <a href="https://partner.example.com/offer">Open</a>
javascript:alert(1)                        -> <a href="javascript:alert(1)">Open</a>
JAVASCRIPT:alert(1)                        -> <a href="JAVASCRIPT:alert(1)">Open</a>
data:text/html,<script>alert(1)</script>   -> <a href="data:text/html,&lt;script&gt;alert(1)&lt;/script&gt;">Open</a>

Validator::make([...], ['url' => 'url:https,http']) result for each URL:
/batches/english-evening                   -> fails: The url field must be a valid URL.
https://partner.example.com/offer          -> passes
javascript:alert(1)                        -> fails: The url field must be a valid URL.
JAVASCRIPT:alert(1)                        -> fails: The url field must be a valid URL.
data:text/html,<script>alert(1)</script>   -> fails: The url field must be a valid URL.

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 href_scheme_session.py

The first five lines show what Blade produced for each URL. javascript:alert(1) and JAVASCRIPT:alert(1) left the server as valid href values; a click would run the script. The data:text/html,<script>...</script> payload had its < and > replaced, but a browser decodes those references when it reads the attribute, so the link still points at an HTML page with a script in it. Modern browsers refuse to open a data: URL as a top-level page, which stops this click, yet the same value in an iframe’s src would load: the escaping did its job on HTML and ignored the scheme.

The server has to validate the link itself. The second block runs the same list through Laravel’s url:https,http rule, which validates the value as a URL and refuses any scheme the list does not name. Every dangerous input failed, and /batches/english-evening failed too, because the plain url rule needs a scheme. A relative path needs its own rule: in practice a form stores two fields, a kind (absolute, path) and the value, and the rule changes with the kind. For an app that accepts user-supplied links, the OWASP cheat sheet asks for a allowlist: A list of exactly what is accepted, so anything absent from it is refused. The opposite of a denylist, and the safer default for input. More in “Validation rules and error messages” such as https and http, and that is what the rule does.

Context matters inside attributes

Blade’s default is for text, which fits an element body. For an attribute whose values can be code, the HTML escape is not enough: validate the value on the server before you store it, and refuse anything outside the schemes you allow.

Checkpoint

Check: Context inside attributes, and the url rule

  1. One question A notice holds $link = 'javascript:alert(1)'. The template prints <a href="{{ $link }}">Open</a>. A visitor clicks the link. What happens, and what fixes it on the server?

    Choose one answer.

    Show the answer: One question

    Answer: The browser runs alert(1); fix it by validating the stored value with url:https,http before saving.

    Context inside attributes matters. HTML escaping does not stop dangerous schemes; validate on the server with url:https,http.

Directives cover the loops and context a page needs

Beyond echoes, Blade has one more small language of its own: Blade directive: A compile-time keyword starting with @ in a Blade template; the view compiler replaces it with PHP before the template runs.. Each starts with @ and compiles into a block of PHP. The probe reads each directive and prints what Blade produces:

escape-probe/directives.php: run each directive through Blade::compileString PHP · escape-probe/directives.php
<?php

// Prints what Blade compiles for the directives this lesson covers, so the text shows exactly which PHP each one becomes.
use Illuminate\Support\Facades\Blade;

require __DIR__.'/vendor/autoload.php';
$app = require __DIR__.'/bootstrap/app.php';
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();

$cases = [
    "@foreach (\$notices as \$n) ... @endforeach",
    "@forelse (\$notices as \$n) ... @empty ... @endforelse",
    "@auth ... @endauth",
    "@can('update', \$notice) ... @endcan",
    "@env('production') ... @endenv",
    "@error('title') ... @enderror",
    "@csrf",
    "@method('PUT')",
    "@once ... @endonce",
    "@push('scripts') ... @endpush",
    "@include('partials.header', ['title' => 'Notices'])",
    "@each('partials.notice', \$notices, 'notice')",
];

foreach ($cases as $template) {
    echo str_pad($template, 56), ' -> ', str_replace(["\n", "\r"], ' ', trim(Blade::compileString($template))), "\n";
}
Twelve directives and the PHP they compile to Python · directives_session.py
from session import run

# Print what Blade's compileString produces for the directives this lesson covers.
# @once generates a fresh UUID per compile; the session mask replaces it so two runs match.
run(
    """
php directives.php
""",
    app=True,
    overlay=("escape-probe",),
    masks=[
        (r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", "<uuid>"),
    ],
)

Output

$ php directives.php
@foreach ($notices as $n) ... @endforeach                -> <?php $__currentLoopData = $notices; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $n): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?> ... <?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
@forelse ($notices as $n) ... @empty ... @endforelse     -> <?php $__empty_1 = true; $__currentLoopData = $notices; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $n): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); $__empty_1 = false; ?> ... <?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); if ($__empty_1): ?> ... <?php endif; ?>
@auth ... @endauth                                       -> <?php if(auth()->guard()->check()): ?> ... <?php endif; ?>
@can('update', $notice) ... @endcan                      -> <?php if (app(\Illuminate\Contracts\Auth\Access\Gate::class)->check('update', $notice)): ?> ... <?php endif; ?>
@env('production') ... @endenv                           -> <?php if(app()->environment('production')): ?> ... <?php endif; ?>
@error('title') ... @enderror                            -> <?php $__errorArgs = ['title']; $__bag = $errors->getBag($__errorArgs[1] ?? 'default'); if ($__bag->has($__errorArgs[0])) : if (isset($message)) { $__messageOriginal = $message; } $message = $__bag->first($__errorArgs[0]); ?> ... <?php unset($message); if (isset($__messageOriginal)) { $message = $__messageOriginal; } endif; unset($__errorArgs, $__bag); ?>
@csrf                                                    -> <?php echo csrf_field(); ?>
@method('PUT')                                           -> <?php echo method_field('PUT'); ?>
@once ... @endonce                                       -> <?php if (! $__env->hasRenderedOnce('<uuid>')): $__env->markAsRenderedOnce('<uuid>'); ?> ... <?php endif; ?>
@push('scripts') ... @endpush                            -> <?php $__env->startPush('scripts'); ?> ... <?php $__env->stopPush(); ?>
@include('partials.header', ['title' => 'Notices'])      -> <?php echo $__env->make('partials.header', ['title' => 'Notices'], array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?>
@each('partials.notice', $notices, 'notice')             -> <?php echo $__env->renderEach('partials.notice', $notices, 'notice'); ?>

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 directives_session.py

The directives split into four groups.

Loops. @foreach compiles into a foreach that also builds $loop, so the template can read $loop->first, $loop->last, $loop->iteration and $loop->count without a counter of its own. @forelse adds an @empty branch for the “no notices” case in one place, with no outer if around a loop.

Context. @auth and its sibling @guest compile to if(auth()->guard()->check()) and its negation, so a view shows a sign-in link to a guest and a profile menu to a signed-in visitor. @can('update', $notice) resolves the authorization gate from the container and checks the ability on a model, so the Edit button appears only to a user whose policy says yes. @env('production') and @error('title') give the page a cheap way to say “only in production” and “when this field has an error”.

Forms. @csrf compiles into csrf_field(), which prints the hidden <input name="_token"> the CSRF token: A secret value held in the visitor's session and expected back in every state-changing form, so a request a different site sent with the visitor's cookie is refused. More in “Your first CRUD feature with Eloquent” middleware expects, and @method('PUT') prints a hidden <input name="_method" value="PUT"> so an HTML form, which can send only GET and POST, can stand in for a PUT or DELETE.

Composition. @include and @each call the view engine to render another template, with its own data. @once runs its body once per rendering cycle, which is how a component that is used twice on a page prints its <script> tag only once. @push('scripts') and the matching @stack('scripts') in the layout let a child view add to a named region of the parent.

The table sets six of these directives against five jobs a template does. Answer its three questions about a template of your own, and it marks the rows that do not fit, with the reason on each:

Interactive · answer and compare Picking the right directive for a job Answer the three questions about your template; the rows that do not fit are marked, with the reason on each.
Is the job to print a list of items, such as twelve notices?
Can the list be empty, with a message such as "No notices today" to show then?
Must the output appear once per page, however many times the template code runs?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

Six directives against five jobs a page template does
Directive Loop over a list Show an empty state Reuse a partial view Print a tag only once Add a script to the layout
@foreach What it compiles to: a foreach that also builds $loop Good the default loop; $loop gives first, last and iteration Mixed needs an outer @if (count($items)) check Mixed works with an @include inside the body Poor runs its body on every iteration Poor not its job: that is @push
@forelse What it compiles to: a foreach plus an @empty branch Good the same loop as @foreach, with $loop Good built in: the @empty block holds the no-items case Mixed works with an @include inside the body Poor runs its body on every iteration Poor not its job: that is @push
@each What it compiles to: one partial rendered per item Mixed renders a partial per item, with no $loop in it Good a fourth argument names the view for an empty list Good one call renders the partial for every item Poor renders once for every item Poor not its job: that is @push
@include What it compiles to: one partial, rendered where it stands Poor renders one partial, not a list Poor knows nothing about a list being empty Good the plain way to reuse a partial view Poor renders every time it is reached Poor prints in place, not in the layout
@once What it compiles to: a block that runs once per page render Poor not a loop Poor not for empty states Mixed works, but @include is clearer without the once rule Good its body runs the first time it is reached in a render Mixed wraps a @push so the stack gets one copy
@push What it compiles to: an append to a named stack in the layout Poor not a loop Poor not for empty states Poor adds to a stack; @include reuses a partial Mixed appends each time it runs; pair it with @once Good a page adds its script to the layout stack
How the answers decide
  • “Is the job to print a list of items, such as twelve notices?” No, a block or a tag: rules out @foreach, @forelse and @each. With no list to print, a loop directive has nothing to repeat; @include, @once or @push does the job.
  • “Is the job to print a list of items, such as twelve notices?” Yes, a list: rules out @once and @push and adds a caveat to @include. A list needs a loop directive; @include still works inside a loop body, and @each makes that one call.
  • “Can the list be empty, with a message such as "No notices today" to show then?” Yes, it can be empty: adds a caveat to @foreach. @foreach needs an outer @if for the empty case; @forelse has @empty built in, and @each takes an empty view.
  • “Must the output appear once per page, however many times the template code runs?” Yes, one copy per page: rules out @foreach, @forelse, @each and @include and adds a caveat to @push. Loops and @include run every time they are reached; @once runs its body only the first time, and a @push needs @once around it.

Checkpoint

Check: Blade directives for loops, context and composition

  1. Question 1 of 2 You render a list of notices and want "No notices today" when it is empty, without an outer @if. Which directive fits?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: @forelse ... @empty ... @endforelse

    @forelse is @foreach plus @empty, so a template can state the empty case in the same directive.

  2. Question 2 of 2 Inside a @foreach ($notices as $n) loop body, which of these are set by Blade's $loop object?

    Choose every answer that is right.

    Show the answer: Question 2 of 2

    Answer:

    • $loop->iteration (one-based position)
    • $loop->last (true on the last iteration)
    • $loop->first (true on the first iteration)

    $loop->first, $loop->last and $loop->iteration are the common ones; $loop->count and $loop->depth also exist.

Js::from sends a value safely into a <script> tag

A page sometimes has to hand data to the browser’s JavaScript: a bootstrap object, a translation table, the current user id. Writing <script>const page = {{ json_encode($data) }}; breaks the script, because {{ }} HTML-escapes and every quote arrives as &quot;, which a script element never decodes. Writing <script>const page = {!! json_encode($data) !!}; works only by luck: PHP escapes / by default, so a </script> in the data arrives as <\/script>, but add JSON_UNESCAPED_SLASHES to keep URLs readable and a value that contains </script> ends the tag, so the characters after it are read as new HTML.

Laravel’s helper encodes the value and the The risk that a string printed inside a <script> element contains </script>, which ends the element early so the browser reads the rest as HTML. at once. For an array or an object, Js::from($value) emits a JSON.parse('…') call whose string is HEX-escaped so no <, >, &, ' or " survives, and then wraps the whole string once more so it is a valid single-quoted JavaScript literal; a plain string becomes a single-quoted literal with the same escapes, and a number stays a number. In a template the call sits inside the ordinary echo, const page = {{ Js::from($data) }};: the Js class is Htmlable, and e() prints an Htmlable value as it is. The probe runs the same payload four ways:

escape-probe/js_from.php: Js::from vs json_encode, including a </script> payload PHP · escape-probe/js_from.php
<?php

// Shows how Js::from($data) encodes a PHP value for safe inclusion inside <script>...</script>, including a </script> payload
// and a Hindi string; how plain json_encode() keeps < and > and escapes only the slash; and how JSON_UNESCAPED_SLASHES,
// added for readable URLs, puts a bare </script> back into the output.
use Illuminate\Support\Js;

require __DIR__.'/vendor/autoload.php';
$app = require __DIR__.'/bootstrap/app.php';
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();

$pageData = [
    'studentName' => 'मीरा',
    'batch' => "Maths & science",
    'payload' => "</script><script>alert('taken over')</script>",
    'url' => "/batches/weekend?q=who's there",
];

echo "Js::from(\$pageData)\n";
echo "  ", Js::from($pageData), "\n";

echo "\njson_encode(\$pageData) (no flags)\n";
echo "  ", json_encode($pageData), "\n";

echo "\njson_encode(\$pageData, JSON_HEX_TAG|JSON_HEX_APOS|JSON_HEX_QUOT|JSON_HEX_AMP|JSON_UNESCAPED_UNICODE)\n";
echo "  ", json_encode($pageData, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP | JSON_UNESCAPED_UNICODE), "\n";

echo "\njson_encode(\$pageData, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE)\n";
echo "  ", json_encode($pageData, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE), "\n";
Encoding a page payload safely for a <script> tag Python · js_from_session.py
from session import run

# Encode a page payload (Hindi text, &, a </script> attack string) four ways: Js::from, plain json_encode, json_encode
# with the HEX_* flags, and json_encode with JSON_UNESCAPED_SLASHES, so the lesson can read what each one puts inside a
# <script> tag.
run(
    """
php js_from.php
""",
    app=True,
    overlay=("escape-probe",),
)

Output

$ php js_from.php
Js::from($pageData)
  JSON.parse('{\u0022studentName\u0022:\u0022मीरा\u0022,\u0022batch\u0022:\u0022Maths \\u0026 science\u0022,\u0022payload\u0022:\u0022\\u003C\\\/script\\u003E\\u003Cscript\\u003Ealert(\\u0027taken over\\u0027)\\u003C\\\/script\\u003E\u0022,\u0022url\u0022:\u0022\\\/batches\\\/weekend?q=who\\u0027s there\u0022}')

json_encode($pageData) (no flags)
  {"studentName":"\u092e\u0940\u0930\u093e","batch":"Maths & science","payload":"<\/script><script>alert('taken over')<\/script>","url":"\/batches\/weekend?q=who's there"}

json_encode($pageData, JSON_HEX_TAG|JSON_HEX_APOS|JSON_HEX_QUOT|JSON_HEX_AMP|JSON_UNESCAPED_UNICODE)
  {"studentName":"मीरा","batch":"Maths \u0026 science","payload":"\u003C\/script\u003E\u003Cscript\u003Ealert(\u0027taken over\u0027)\u003C\/script\u003E","url":"\/batches\/weekend?q=who\u0027s there"}

json_encode($pageData, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE)
  {"studentName":"मीरा","batch":"Maths & science","payload":"</script><script>alert('taken over')</script>","url":"/batches/weekend?q=who's there"}

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 js_from_session.py

The Js::from line printed JSON.parse('{\u0022studentName\u0022:\u0022मीरा\u0022,…}'). The Hindi string survived as text, so the page source stays readable to humans, while every quote, slash and tag became an escape, so neither the string literal nor the <script> tag can be broken. The plain json_encode line escaped the slash and nothing else: the payload’s <script> and its quotes arrived as they were, and the tag held only because the closing sequence read <\/script>. The last line, with JSON_UNESCAPED_SLASHES, printed a bare </script>, where a browser would end the script element. The third line used the four JSON_HEX_* flags with JSON_UNESCAPED_UNICODE: those are the flags Js::from always adds before it wraps the result in JSON.parse('…').

The three places JSON goes

Inside a <script> tag, use Js::from. In a data-page='{"…"}' attribute, call json_encode and let {{ }} escape the attribute. For a response body, return the array or model and let Laravel send JSON.

Checkpoint

Check: Js::from for a safe <script> payload

  1. One question You pass ['payload' => '</script><script>alert(1)</script>'] into a page's <script> tag with Js::from. What does the output look like, and why is it safe?

    Choose one answer.

    Show the answer: One question

    Answer: JSON.parse('{\u0022payload\u0022:\u0022\u003C\/script\u003E\u003Cscript\u003E...\u003C\/script\u003E\u0022}'); every < and > is HEX-escaped and every / is escaped, so the </script> never ends the surrounding tag.

    The helper wraps a HEX-escaped JSON string in a JSON.parse('…') call. No character inside breaks either the string literal or the <script> tag.

view:cache compiles every view ahead of a request

A Blade template is read, compiled and stored in storage/framework/views/, in a PHP file named after an xxh128 hash of the template’s path. On the first render Blade compiles the file. On every later render Blade reads the mtime of the source: if the compiled file is newer than the source, the cached PHP is included; otherwise Blade compiles again. On a hot path that cost is already small, but a deploy sees a cold cache, so the first visitor to every page pays for its compile.

php artisan view:cache warms the cache by compiling every view at deploy time. php artisan view:clear empties the folder. The probe starts in a fresh project, so the folder is empty:

Compile every view, then clear the cache Python · view_cache_session.py
from session import run

# Run view:cache and view:clear and show how many compiled files land under storage/framework/views.
run(
    """
ls storage/framework/views/ | wc -l
php artisan view:cache
ls storage/framework/views/*.php 2>/dev/null | wc -l
php artisan view:clear
ls storage/framework/views/*.php 2>/dev/null | wc -l
""",
    app=True,
)

Output

$ ls storage/framework/views/ | wc -l
       0
$ php artisan view:cache
   INFO  Blade templates cached successfully.
$ ls storage/framework/views/*.php 2>/dev/null | wc -l
      62
$ php artisan view:clear
   INFO  Compiled views cleared successfully.
$ ls storage/framework/views/*.php 2>/dev/null | wc -l
       0

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 view_cache_session.py

The folder started with zero files. After view:cache the project held 62 compiled PHP files. Only one of them, welcome.blade.php, is in the skeleton’s resources/views; view:cache also compiles every view the framework registers: 9 pagination views, the notification email, 8 error pages with their 2 layouts and, because the project runs with APP_DEBUG=true, the 41 views of the debug error page. view:clear left the folder empty again.

Run view:cache at deploy, leave it alone in dev

A deploy script runs php artisan config:cache, php artisan route:cache and php artisan view:cache before serving traffic. On a developer machine nothing is needed: Blade sees that a saved template is newer than its compiled file and compiles it again, and view:clear is there for the day you want an empty folder.

Checkpoint

Check: php artisan view:cache

  1. One question You run php artisan view:cache in a fresh skeleton. The lesson's probe prints how many compiled files land under storage/framework/views/?

    Type a number.

    Show the answer: One question

    Answer: 62 files

    The skeleton has one view of its own, welcome.blade.php. view:cache also compiles the 61 views the framework registers (pagination, the notification email, the error pages and, with APP_DEBUG=true, the debug error page), and view:clear empties the folder again.

Practise

Try these on your own Pathshala project:

  1. Add a notice to the seeder whose title is <img src=x onerror="alert(1)">, visit the notices page, and view the source. The title must appear as text; nothing must run.
  2. Add a link field to the notice form validated with url:https,http or starts_with:/ (two kinds the form accepts) and prove each case with a feature test that posts a dangerous value and asserts an error for link with assertSessionHasErrors (a request sent with postJson gets 422 instead).
  3. Add a tiny bootstrap object with Js::from(['currentUser' => auth()->id(), 'csrfToken' => csrf_token()]) to the layout. Open DevTools, read the parsed object, and confirm nothing in it would break the <script> tag.
  4. Run php artisan view:cache and look under storage/framework/views. Change one .blade.php file and reload the page: the stale compiled file is replaced.

Then work through the self-check.

Practice that adapts to you

Practise this lesson

10 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 10 Which function does Blade's {{ }} call on the value?

    Choose one answer.

    Show the answer: Question 1 of 10

    Answer: htmlspecialchars($value ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8', true), through the e() helper

    e() is htmlspecialchars with ENT_QUOTES; it replaces <, >, & and " with named references and ' with the numeric reference &#039;.

  2. Question 2 of 10 A page shows a notice body that the controller converted from Markdown with raw HTML stripped and unsafe links refused. Which Blade echo fits?

    Choose one answer.

    Show the answer: Question 2 of 10

    Answer: {!! $notice->bodyHtml !!}, because the controller already produced HTML

    Markdown produced on the server is HTML your code made from text you read; the raw echo prints it. A field a user typed never goes through it.

  3. Question 3 of 10 A form stores a link, which may be an absolute URL or a path on your site. Which validation rule fits both without accepting javascript:?

    Choose one answer.

    Show the answer: Question 3 of 10

    Answer: link is one of two kinds, with a rule each: url:https,http for absolute, starts_with:/ for a path

    The storage format decides the rule; a path and an absolute URL have different rules.

  4. Question 4 of 10 Why does {{ $url }} inside href="…" not stop a javascript: URL?

    Choose one answer.

    Show the answer: Question 4 of 10

    Answer: Because escaping fixes HTML-special characters, and javascript: contains none of them

    e() targets HTML grammar. Attribute values can be code, and HTML escaping does not know about schemes.

  5. Question 5 of 10 What is the PHP that Blade generates for @forelse ($notices as $n) ... @empty ... @endforelse?

    Choose one answer.

    Show the answer: Question 5 of 10

    Answer: A foreach with an "empty" flag set to false inside each iteration; after the loop, an if ($__empty_1): block runs the @empty body.

    The compiler sets $__empty_1 = true before the loop, flips it to false if the body runs, and the trailing if fires the empty branch when nothing ran.

  6. Question 6 of 10 What does @csrf compile to, and why does an HTML form need it?

    Choose one answer.

    Show the answer: Question 6 of 10

    Answer: <?php echo csrf_field(); ?>, which prints <input name="_token" value="…"> for the CSRF middleware to compare with the session token on a POST, PUT, PATCH or DELETE

    @csrf is csrf_field(), which prints the hidden _token input, so the form carries proof that it came from the same session.

  7. Question 7 of 10 Why does the lesson prefer Js::from($data) over {!! json_encode($data) !!} inside a <script> tag?

    Choose one answer.

    Show the answer: Question 7 of 10

    Answer: json_encode keeps < and > and holds the tag only while / stays escaped, so with JSON_UNESCAPED_SLASHES a value containing </script> ends the surrounding tag; Js::from HEX-escapes < and > whatever flags you pass.

    Js::from encodes a value for a JavaScript single-quoted string literal, so no character in the payload can end the surrounding tag or escape the string.

  8. Question 8 of 10 What is the shape of Js::from(['x' => 1]) output?

    Choose one answer.

    Show the answer: Question 8 of 10

    Answer: JSON.parse('{\u0022x\u0022:1}')

    For a non-empty array or object, Js::from emits JSON.parse('…'), where the string inside is HEX-escaped JSON; a plain string becomes a single-quoted literal and a number stays a number.

  9. Question 9 of 10 What does php artisan view:cache do, and when do you run it?

    Choose one answer.

    Show the answer: Question 9 of 10

    Answer: Compiles every view into a PHP file under storage/framework/views/; run it at deploy time so no visitor pays the compile cost.

    Compiled templates are reused on every request as long as the source has not changed; view:cache makes sure the cache is warm on the first request after a deploy.

  10. Question 10 of 10 You run php artisan view:cache on a developer machine and edit a template. What happens on the next request?

    Choose one answer.

    Show the answer: Question 10 of 10

    Answer: Blade sees the source is newer than the compiled file and recompiles it.

    Blade compares mtimes; a changed source always recompiles. Developers call view:clear to clear the folder when they want to force the next compile.

Interview questions

Warm-up: what does Blade’s {{ }} do that Twig’s {{ }} does not?

The syntax looks the same, but the compilers differ. Blade’s {{ $value }} is one echo wrapped in e(), which runs htmlspecialchars with ENT_QUOTES on every value, and no block in a template turns that escaping off. Twig escapes by default too, but its autoescape setting can change per environment and per block, and it offers filters such as |raw or |e('js') that choose a context (HTML, JS, CSS, URL). Blade keeps one echo for text and a second one, {!! !!}, for raw HTML; context-specific encoding for a script tag is Js::from, and context-specific encoding for an attribute is still {{ }} plus server-side validation of what the attribute carries. Blade also compiles its own directives (@foreach, @auth, @error), while Twig has its own tag language, so a view’s shape differs even though the escape rule is the same.

Two longer questions follow:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Fresher to mid

    A visitor saves a notice whose link field holds javascript:alert(1). The Blade view renders <a href="{{ $link }}"> Open</a>. Is Blade's auto-escaping enough? If not, what do you add on the server, and how do you prove it?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Fresher to senior

    A page shows notice bodies that your controller renders from Markdown with a trusted converter. Can you use {!! !!}, and what would you still check on the server before you print the result?

    Model answer, Pro. First step and follow-ups

Flashcards · 5 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. What does Blade compile a double-brace echo into, and what does that escape?

    A call to e(), which runs htmlspecialchars with ENT_QUOTES. The five HTML-special characters (less-than, greater-than, ampersand and both quote marks) leave as character references, and the browser shows them as text.

  2. When is Blade's raw brace-bang echo acceptable?

    Only for HTML your own server produced and sanitised, such as a Markdown document rendered by a converter you trust. A value a user typed never goes through it: printed raw, it can inject markup or script, which makes it an XSS sink.

  3. Why does escaping not make a user's link safe in an href attribute?

    Escaping stops HTML, not a dangerous scheme: a javascript: URL in href still runs when clicked, and a data: URL still carries a page. Validate URLs on the server with the url rule and an allowlist of schemes such as https.

  4. How do you pass a PHP value safely into a script tag from Blade?

    With Js::from($value). It encodes the value as JSON.parse with every quote and angle bracket hex-escaped and every slash escaped, so a closing script tag inside the data cannot end the surrounding tag.

  5. What does php artisan view:cache do at a deploy?

    It compiles every Blade view into storage/framework/views ahead of time, so no visitor pays the compile cost on a cold cache. view:clear empties that folder on a development machine.

Key takeaways

  • Blade compiles {{ $value }} into <?php echo e($value); ?>, which calls htmlspecialchars with ENT_QUOTES, so the five HTML-special characters leave the page as character references, which browsers render back to text.
  • {!! $value !!} is a raw sink and is acceptable only for HTML your own code produced from text your own code read, such as a sanitised Markdown document; a value a user typed never goes through it.
  • Blade’s HTML escape fixes HTML, not an attribute whose values can be code: a javascript: URL in href still runs in the browser and a data: URL still carries its page, so validate URLs on the server with the url rule and a scheme allowlist.
  • Js::from($value) encodes a value as JSON.parse('…') with every quote and angle bracket HEX-escaped and every slash escaped, so a payload with a </script> string does not close the surrounding tag, whatever flags you pass.
  • php artisan view:cache compiles every view into storage/framework/views at deploy time, so no visitor pays the compile cost on a cold cache, and view:clear empties the folder on development.

Terms in this lesson

Blade directive
A compile-time keyword starting with @ in a Blade template; the view compiler replaces it with PHP before the template runs.
raw echo
An unescaped print, written as {!! … !!} in Blade; the value is sent to the browser as HTML without a call to the escape helper.
XSS sink
A place in a response where untrusted text is written in a way the browser can read as code: an unescaped echo, an attribute whose value is code, or an unescaped script string.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.