skip to content

On a Laravel pricing page, how would you add a @plan('pro') conditional with Blade::if, and why is it safer than Blade::directive for a per-visitor check?

level: seniorimportance: should knowfreq 30%

answer

  1. register in a provider's boot()
  2. closure runs on every render
  3. @elseplan, @unlessplan, @endplan for free
  4. compiles to Blade::check(name, args)
  5. directive() handlers run once, at compile time

basics

~20 s

Blade::if('plan', fn (string $plan) => ...) registers @plan, @elseplan, @unlessplan and @endplan that call your closure through Blade::check() each render; a Blade::directive handler runs once at compile time, so per-visitor logic inside it gets frozen into the cached view.

solid answer

~40 s

In `AppServiceProvider::boot()`, `Blade::if('plan', fn (string $plan) => auth()->user()?->plan === $plan)` stores the closure and registers four directives: `@plan`, `@elseplan`, `@unlessplan` and `@endplan`. `@plan('pro')` compiles to `<?php if (\Illuminate\Support\Facades\Blade::check('plan', 'pro')): ?>`, so the closure runs on every render with the current visitor. `Blade::directive('plan', fn ($expression) => ...)` is different: its handler runs **when the template is compiled**, receives the argument as a raw string, and must return PHP source. Evaluating the user or the plan inside that handler bakes one visitor's answer into the compiled file cached in `storage/framework/views`, which then serves every visitor. Use `Blade::if` for conditions; reserve `Blade::directive` for generating code.

code

php · 15 lines
php
<?php

namespace App\Providers;

use Illuminate\Support\Facades\Blade;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        // Evaluated on every render via Blade::check('plan', ...)
        Blade::if('plan', fn (string $plan): bool => auth()->user()?->plan === $plan);
    }
}

go deeper

for a junior

Recall that Blade::if in a provider's boot() creates @name, @elsename, @unlessname and @endname directives.

for a middle

Explain that the directives compile to Blade::check() so the closure runs per render, while a Blade::directive handler returns PHP source at compile time.

for a senior

Diagnose the compiled-view bug where per-visitor logic inside a directive handler serves one user's answer to everyone, and keep conditions cheap.

for a principal

Decide which rules may live in view conditionals and which belong to gates, policies or view models, so templates stay presentational.

## The goal A pricing page shows different call-to-action blocks depending on the visitor's current plan: an "upgrade" button for Free users, "manage seats" for Team, nothing for Enterprise. Scattering `@if (auth()->user()?->plan === 'team')` across templates repeats the rule. A custom conditional names it once. ## Blade::if ```php Blade::if('plan', function (string $plan) { return auth()->user()?->plan === $plan; }); ``` Called in a service provider's `boot()` method, `Blade::if`: 1. Stores the closure under the name `plan` in the compiler's conditions list. 2. Registers four directives: `@plan(...)`, `@elseplan(...)`, `@unlessplan(...)` and `@endplan`. 3. Makes each compile to a call such as `<?php if (\Illuminate\Support\Facades\Blade::check('plan', 'team')): ?>`. `Blade::check()` looks up the stored closure and calls it with the arguments - **at render time, on every request**. The template then reads: ```html @plan('free') <a href="/upgrade">Upgrade</a> @elseplan('team') <a href="/seats">Manage seats</a> @else <span>Your plan is fully managed</span> @endplan ``` A condition with no argument works too: `@plan` compiles to `Blade::check('plan')`, and the closure is called without parameters. ## Blade::directive, and why it is the wrong tool here `Blade::directive('name', $handler)` registers a directive whose handler is a **compile-time code generator**. When Blade compiles a template it calls the handler once with the directive's argument as a **string** - for `@plan('team')`, the handler receives the source text `'team'`, outer parentheses stripped, not an evaluated value - and pastes the handler's returned PHP source into the compiled view. Blade caches compiled views on disk (the `storage/framework/views` path by default) and recompiles only when the template file changes. So: - A handler that returns `'<?php if (auth()->user()?->plan === '.$expression.'): ?>'` is **correct**: it generates code that runs later. - A handler that **calls** `auth()->user()` itself and returns `'<?php if (true): ?>'` or `'<?php if (false): ?>'` is **wrong**: the first visitor whose request compiled the view decides the output for everyone, until the template changes. When views are precompiled during a deploy there is no visitor at all. `Blade::if` removes that trap because it only ever generates a call to `Blade::check()`, and the logic stays in a closure evaluated per render. | | `Blade::if` | `Blade::directive` | |---|---|---| | Handler runs | on every render | once, at compile time | | Handler receives | evaluated arguments | the raw expression string | | Returns | a boolean | PHP source code | | Extra directives | `@else...`, `@unless...`, `@end...` | none | | Fits | conditions | new syntax, code generation | ## The built-in conditionals it complements Blade already ships environment conditionals: `@production ... @endproduction` compiles to `app()->environment('production')`, and `@env('staging')` or `@env(['staging', 'local'])` to `app()->environment(...)`. A pricing page might hide a "test card numbers" note behind `@env('local')`. Like `Blade::if`, these evaluate at render time. ## @once and @php: two more render-time tools `@once ... @endonce` renders its content only the first time it is reached in a render cycle. The compiler assigns the block a UUID when the template compiles (or uses an id you pass), and at render time the view factory records that id once rendered - so a plan card partial included ten times can emit its tooltip script once. It is usually paired with stacks, which are layout material. `@php ... @endphp` (or `@php($x = 1)`) embeds raw PHP. It is legitimate for a small local variable, but a pricing template that computes discounts or queries models inside `@php` has business logic in the wrong layer; move it to the controller, a view model or a `Blade::if` condition. ## Practical rules - Register conditions in a service provider's `boot()` method, as the Blade docs do. - Keep the closure cheap - it runs on every use, and a page may call it many times. Read the visitor's plan once (the authenticated user is already loaded) rather than querying per call. - Use type-hinted parameters so a typo like `@plan(pro)` fails loudly. - Remember that the directive **names** are compile-time: renaming a condition requires templates to be recompiled, which happens automatically when they change. - For authorisation questions ("may this user upgrade?"), a gate or policy is the right home, not a view conditional; that is covered in the authorisation material.

  • What does the handler of Blade::directive('plan', ...) receive for @plan('team')?
    A string containing the expression source, such as `'team'` with its quotes, not the evaluated value. The handler's job is to return PHP code that uses that source, for example `"<?php if (auth()->user()?->plan === {$expression}): ?>"`. Any PHP it executes itself runs once, when the view compiles.
  • How would you test a Blade::if condition?
    Call `Blade::check('plan', 'pro')` directly after acting as a user with that plan, and render a small Blade string with `Blade::render()` to confirm the branches. Because the closure runs at render time, no compiled-view clearing is needed between cases.

saying these in an interview costs you the question

  • Blade::directive handlers run on every request like Blade::if closures
  • Checking auth()->user() inside a Blade::directive handler is fine
  • Blade::if requires writing @elseplan and @endplan by hand
  • A Blade::if closure receives the raw expression string
  • @production checks APP_DEBUG rather than the environment name