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?
answer
- register in a provider's boot()
- closure runs on every render
- @elseplan, @unlessplan, @endplan for free
- compiles to Blade::check(name, args)
- directive() handlers run once, at compile time
basics
~20 sBlade::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 sIn `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
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
Recall that Blade::if in a provider's boot() creates @name, @elsename, @unlessname and @endname directives.
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.
Diagnose the compiled-view bug where per-visitor logic inside a directive handler serves one user's answer to everyone, and keep conditions cheap.
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