All rules

Sheath rule

blade-unclosed-directives

Block directives should be properly paired with their closing directive.
Package
Core
Category
Blade
Default severity
error by default
Auto-fix
Manual fix

#Why

Blade compiles each directive as it meets it, so @if ($a) becomes <?php if($a): ?> whether or not an @endif ever arrives. A missing terminator leaves that block unterminated, and what surfaces is a PHP parse error pointing into a compiled view under storage/, several steps from the template that caused it.

Three pairings fail in quieter ways, and the rule covers those too:

  • Stray @endphp / @endverbatim. These pairs are extracted before compilation, so a terminator with no opener is not a Blade error. It remains in the page as literal text.
  • A second @section opened while one is still open. Each @section compiles to a startSection call and a single @endsection only closes the innermost one, so the first section's buffered content is silently lost at render time. Sections in mutually exclusive conditional branches, and genuinely nested sections that each have a terminator, are fine and are not reported.

#Examples

#Bad

<!-- Unclosed @if -->
@if($user->isAdmin())
<p>Admin panel</p>
<!-- Missing @endif -->
<!-- Unclosed @foreach -->
@foreach($items as $item)
<li>{{ $item }}</li>
<!-- Missing @endforeach -->
<!-- Orphaned closing directive: no matching @if -->
<div>
@endif
</div>
<!-- Mismatched directives: wrong closing directive -->
@if($condition)
<p>Content</p>
@endforeach
<!-- Stray @endphp: no @php opened, so the marker renders as literal text -->
<div>Total</div>
@endphp
<!-- Stray @endverbatim: same mechanic -->
<p>Done.</p>
@endverbatim
<!-- Second @section while 'header' is still open: its content is lost -->
@section('header')
<h1>Title</h1>
@section('content')
<p>Body</p>
@endsection

#Good

<!-- Properly closed @if -->
@if($user->isAdmin())
<p>Admin panel</p>
@endif
<!-- Properly closed @foreach -->
@foreach($items as $item)
<li>{{ $item }}</li>
@endforeach
<!-- Properly nested -->
@if($showUsers)
@foreach($users as $user)
<p>{{ $user->name }}</p>
@endforeach
@endif
<!-- All block directives closed -->
@auth
@can('edit', $post)
<a href="{{ route('posts.edit', $post) }}">Edit</a>
@endcan
@endauth
<!-- Raw blocks paired -->
@php
$total = $items->sum('price');
@endphp
@verbatim
<p>{{ handledByVue }}</p>
@endverbatim
<!-- Sections closed before the next one opens; the inline form never opens -->
@section('title', 'Home')
@section('header')
<h1>Title</h1>
@endsection
@section('content')
<p>Body</p>
@stop

#Block Directives Checked

The rule checks paired opening and closing directives in these categories.

Category Directives
Conditionals @if, @unless, @isset, @empty
Authentication @auth, @guest, @can, @cannot, @canany
Loops @for, @foreach, @forelse, @while, @switch
Content @verbatim, @push, @pushOnce, @prepend, @section, @component, @slot
Environment @production, @env
Other @error, @once

#Options

Use this option to check only selected block directives.

Option Type Default Description
directives array (see above) Narrow the check to a subset of the supported block directives

The option can only narrow the supported block directives listed above. Custom block directives such as @mydirective cannot be added through this option and are not checked.

The same option controls raw blocks and sections. Include php and verbatim to report stray terminators, and include section to report overlapping sections. All three are enabled by default. Omitting one disables its checks.

<?php
'blade-unclosed-directives' => ['error', [
// Only report these; other known block directives are left alone.
'directives' => ['if', 'foreach', 'forelse', 'auth', 'can'],
]],

#Error Messages

The rule produces four types of messages:

  1. Unclosed directive: Unclosed @if directive. Missing @endif.
  2. Orphaned directive: Orphaned @endif directive. Missing matching @if.
  3. Stray raw-block terminator: Orphaned @endphp directive. Missing matching @php. Blade only pairs @php ... @endphp before compilation, so a stray @endphp is left in the page as literal text.
  4. Overlapping sections: @section('two') opened while @section('one') is still open, and nothing ever closes @section('one'). At render time its buffered content is silently lost. Close it with @endsection or @stop before opening the next section.

#Notes

  • Nested directives must be closed in the correct order (LIFO)
  • Custom block directives are outside the supported pairing list

#Related Rules