Sheath rule
blade-unclosed-directives
#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
@sectionopened while one is still open. Each@sectioncompiles to astartSectioncall and a single@endsectiononly 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:
- Unclosed directive:
Unclosed @if directive. Missing @endif. - Orphaned directive:
Orphaned @endif directive. Missing matching @if. - 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. - 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
- blade-forelse-has-empty - @forelse should have @empty