Sheath rule
blade-prefer-forelse
@forelse over @if + @foreach when checking for empty collections.
as a dangerous fix when the block has an @else branch. See Auto-fix.
#Why
@if wrapping a @foreach states the emptiness check twice: once in the
condition and once in the loop that would not run anyway. @forelse is the
directive Blade provides for exactly this shape, and it puts the empty case next
to the loop it belongs to rather than a branch away.
They do not compile the same way. @forelse sets a flag inside the loop and
checks it afterwards, so it never asks the collection how many items it holds.
@if (count($rows) > 0) does, which is why @forelse works on a generator,
where count() raises a TypeError.
This is a stylistic preference, so it belongs to stylistic rather than
recommended.
#Examples
#Bad
<!-- Verbose: @if wrapping @foreach -->
@if(count($users) > 0)
@foreach($users as $user)
<p>{{ $user->name }}</p>
@endforeach
@else
<p>No users found.</p>
@endif
<!-- Also verbose with a collection emptiness check -->
@if($items->isNotEmpty())
@foreach($items as $item)
<li>{{ $item->title }}</li>
@endforeach
@else
<p>No items available.</p>
@endif
@if($products->count())
@foreach($products as $product)
<div>{{ $product->name }}</div>
@endforeach
@else
<p>No products.</p>
@endif
#Good
<!-- Clean: @forelse handles both cases -->
@forelse($users as $user)
<p>{{ $user->name }}</p>
@empty
<p>No users found.</p>
@endforelse
@forelse($items as $item)
<li>{{ $item->title }}</li>
@empty
<p>No items available.</p>
@endforelse
@forelse($products as $product)
<div>{{ $product->name }}</div>
@empty
<p>No products.</p>
@endforelse
#Pattern Detection
The following patterns are reported:
<!-- Pattern 1: count() > 0 -->
@if(count($items) > 0)
@foreach($items as $item)
@endforeach
@else
@endif
<!-- Pattern 2: ->isNotEmpty() -->
@if($items->isNotEmpty())
@foreach($items as $item)
@endforeach
@else
@endif
<!-- Pattern 3: ->count() -->
@if($items->count())
@foreach($items as $item)
@endforeach
@else
@endif
#Auto-fix
When the block has an @else branch, --fix rewrites it. Both bodies are carried
over unchanged apart from indentation: the loop body loses one level, because the
@foreach it sat inside is gone.
{{-- before --}}
@if (count($posts) > 0)
@foreach ($posts as $post)
<li>{{ $post->title }}</li>
@endforeach
@else
<p>Nothing yet.</p>
@endif
{{-- after --}}
@forelse ($posts as $post)
<li>{{ $post->title }}</li>
@empty
<p>Nothing yet.</p>
@endforelse
The indent unit is read from the file, so tabs stay tabs and a two-space project stays on two spaces.
The rewrite requires --dangerous. A valid object can implement both
Countable and IteratorAggregate while returning a count that differs from
the number of values it yields, so the original guard and @forelse are not
provably behavior-equivalent.
#When it reports but will not rewrite
This is a structural rewrite rather than an edit to one token, so it only runs when the shape is unambiguous. In these cases the finding is still reported, with no fix attached:
| Shape | Why |
|---|---|
Anything beside the @foreach in the @if branch |
It would be lost, or pulled inside the loop |
An @elseif branch |
@forelse has no third branch to hold it |
A bare @empty in the loop body |
It would become the @forelse branch separator |
No @else branch |
Nothing to carry into @empty; removing the @if is a different edit |
#Notes
- Only applies when checking the same variable for emptiness and iteration
- Bare
$itemsand! empty($items)guards are not treated as emptiness checks. PHP objects are truthy even when an iterable object such as a Laravel collection contains no items, so rewriting those guards would change output. - Nullsafe checks such as
$items?->isNotEmpty()are not treated as candidates: the guard is safe when$itemsis null, while@forelsemust iterate it. - The
@elseblock maps directly to@empty - This preference has
infoseverity and belongs to thestylisticpreset rather thanrecommended
#Related Rules
- blade-forelse-has-empty - @forelse should have @empty block