DEV Community

Cover image for The Blade Gotcha That 500s Your Page: Directives Inside Component Attributes
Nasrul Hazim
Nasrul Hazim

Posted on

The Blade Gotcha That 500s Your Page: Directives Inside Component Attributes

TL;DR

  • You can't put a Blade directive (@if/@endif) inside a component tag's attribute list. Blade compiles component tags before directives, so the view won't compile — the page 500s.
  • Fix: branch around the whole component, or compute the attribute value in a plain PHP expression.
  • The real lesson: this shipped unnoticed because the page sat behind a feature gate and no test ever rendered it. Touch a gated page → add a render test.

The trap

I wanted a button that only shows a confirm dialog when there's already a secret to rotate. So I reached for the obvious thing: drop an @if right into the attribute list.

<flux:button
    variant="{{ $hasToken ? 'danger' : 'primary' }}"
    wire:click="rotateToken"
    @if($hasToken) wire:confirm="Rotate the secret? Existing integrations will break." @endif
>
    {{ $hasToken ? 'Rotate Secret' : 'Generate Secret' }}
</flux:button>
Enter fullscreen mode Exit fullscreen mode

Looks harmless. It isn't. The page throws a 500 with:

syntax error, unexpected token "endif"
Enter fullscreen mode Exit fullscreen mode

Why it breaks

Blade doesn't evaluate a component tag the way your eyes read it. The compile order is the problem:

Blade source
   |
   v
[1] parse component tags   <flux:button ...>  ->  PHP that builds the component
   |                          (attributes captured as a literal list HERE)
   v
[2] compile directives     @if / @endif  ->  if(...) { } 
Enter fullscreen mode Exit fullscreen mode

By the time step 2 would turn @if/@endif into real PHP, step 1 has already swallowed everything between <flux:button and > as an attribute string. The directive never gets its closing half in a valid position, and the compiled view is broken PHP. Plain HTML tags tolerate this; component tags do not.

Three ways that actually work

Approach When to use
Branch around the whole component with @if ... @else ... @endif The conditional changes several attributes at once
Compute the value in an expression: wire:confirm="{{ $hasToken ? '...' : '' }}" One attribute, and an empty value is harmless
Bind a prop from the component's PHP class The logic is reusable or non-trivial

The branch-around version is what I shipped, because the confirm text and the variant both flip together:

@if($hasToken)
    <flux:button variant="danger" wire:click="rotateToken"
        wire:confirm="Rotate the secret? Existing integrations will break.">
        Rotate Secret
    </flux:button>
@else
    <flux:button variant="primary" wire:click="rotateToken">
        Generate Secret
    </flux:button>
@endif
Enter fullscreen mode Exit fullscreen mode

More markup, yes. But it compiles, and each branch reads as exactly one state.

The bigger miss: nothing rendered the page

Here's the part worth keeping. This bug didn't survive because it was subtle — it survived because the page lived behind a feature gate, so it was never hit in a test. A gate is a great way to ship a view that literally no one, including CI, ever renders.

The cheapest insurance is a render test. If a feature-gated page has one test that grants the feature and asserts a 200, this class of bug dies on the first run.

beforeEach(function () {
    $this->user = User::factory()->withOrganization()->create();
    $this->actingAs($this->user);
    grantFeature($this->user->currentOrganization, 'api-access'); // helper
});

it('renders the settings page without a secret', function () {
    $this->get(route('organization.api.index'))
        ->assertOk()
        ->assertSee('Generate Secret');
});

it('renders the settings page with a secret set', function () {
    $this->user->currentOrganization->update(['api_secret' => 'existing']);

    $this->get(route('organization.api.index'))
        ->assertOk()
        ->assertSee('Rotate Secret');
});
Enter fullscreen mode Exit fullscreen mode

Two assertions, both branches of the button, done. While I was there I also added a test that the generated secret is shown once and stored encrypted at rest — but that's a separate story.

Takeaway

Component tags compile before directives, so keep directives out of their attribute lists — branch around the component or compute the value. And treat every feature-gated page as untested until proven otherwise: a one-line render test is the difference between catching this in CI and catching it in production.

Top comments (0)