Skip to content

Add %empty branch to %loop and harden loop directives - #91

Merged
papac merged 2 commits into
3.xfrom
fix-package
Oct 4, 2026
Merged

papac merged 2 commits into
3.xfrom
fix-package

Conversation

@papac

@papac papac commented Oct 4, 2026

Copy link
Copy Markdown
Member

Summary

Adds an %empty branch to %loop, equivalent to Blade's @forelse / @empty, fixes a greedy-regex bug in %stop(...) / %jump(...), and brings the loop directives under unit tests.

%loop($users as $user)
  Hello {{ $user->name }}
%empty
  No user yet.
%endloop

What changed

%empty loop branch (src/Lexique/CompileLoop.php, src/Compiler.php)

  • A bare %empty (no parentheses) between %loop and %endloop renders its content when the iterable is empty. %empty($x) with parentheses remains the existing conditional helper, even inside a loop. A bare %empty previously compiled to if (empty()), which is invalid PHP, so no working template changes meaning.
  • Compilation is line by line, so %loop cannot see whether an %empty follows. A pre-scan in Compiler::compile() walks %loop / %empty / %endloop tokens with a stack after comments and verbatim blocks are stripped, and records which loops own a branch and at what depth. The three loop compilers consume those queues.
  • Compiled shape, with a per-depth flag so nested loops do not clobber each other:
    <?php $__tintin_loop_1 = true; foreach ($users as $user): $__tintin_loop_1 = false; ?>
    ...
    <?php endforeach; if ($__tintin_loop_1): ?>
    ...
    <?php endif; ?>
  • A %loop without %empty compiles exactly as before, so existing templates and cached output are unchanged. %for and %while have no empty branch.

Fix: greedy %stop(...) / %jump(...) condition

  • The breaker regex used .+ for the condition, so %stop($b) ... ($y) on one line extended the condition to the last ) and produced broken PHP. It now uses the same balanced-paren matcher as the loop heads. Same class of bug as the condition_pattern fix already in the changelog.

Tests (tests/CompileLoopTest.php, 15 → 36 tests)

  • Unit tests for every loop directive head and end variant, %stop (previously untested), %jump, the bare %empty compiler, the pre-scan (nesting, helper and stray tokens ignored, state reset), and the stack runner.
  • End-to-end renders through the compiler for %loop key/value, %while, %for with %jump / %stop, and %empty with nested loops and after %stop.
  • Regression test for the greedy breaker condition.

Docs (docs/en.md, docs/fr.md, CHANGELOG.md)

  • New %empty section, key/value and single-line %loop examples, real %for / %while examples, note that %jump / %stop work in all loops, table of contents reordered to match, %esle → %else and %notendempty → %endnotempty typo fixes.

Verification

  • vendor/bin/phpunit: 124 tests, 302 assertions, all passing.
  • phpcs --standard=psr12 clean on the changed source files.
  • Every documentation example rendered through Tintin::render() with both populated and empty data.

papac added 2 commits October 3, 2026 22:40
A bare %empty between %loop and %endloop renders its content when the
iterable is empty, like Blade's @forelse / @empty. %empty($x) with
parentheses remains the conditional helper. A pre-scan in Compiler::compile
records which loops own a branch so each directive compiles correctly line
by line; nested loops get their own flag and a %loop without %empty still
compiles to a plain foreach.

Also fix the greedy condition in %stop(...) and %jump(...), which swallowed
any later parentheses on the same line, and add unit tests covering every
loop directive, the pre-scan, and the stack runner.
Document key/value and single-line %loop forms, note that %jump and %stop
compile to continue and break and work in every loop kind, give %for and
%while real examples, and fix the %else and %endnotempty typos in the
conditional helpers section. Table of contents now matches section order.
@papac
papac merged commit ace2b5c into 3.x Oct 4, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant