Changelog - codesaur/template
This file documents all notable changes to the codesaur/template package.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[4.1.1] - 2026-07-04
[4.1.1]: https://github.com/codesaur-php/Template/compare/v4.1.0...v4.1.1
Fixed
Expression parser left tokens unconsumed on short-circuited branches
- Bug: When the ternary (
? :), elvis (?:), null coalescing (??),and, andoroperators short-circuited, the parser pointer was not advanced past the skipped operand. Standalone expressions appeared to work (trailing text is ignored), but inside parentheses, function arguments, or array literals the leftover tokens corrupted parsing:{{ (a ? 'x' : 'y') ~ '!' }}renderedxinstead ofx!{% if (a and b) or c %}evaluated incorrectly (cwas never reached){{ max(a ? 1 : 2, 5) }}truncated the argument list and could raise aTypeError
- Fix: Branching operators now always consume both operands and select the
result afterwards (
pTernary,pNullCoalesce,pOr,pAnd). Note: operands are therefore always evaluated as well - this is safe because all accessors in the engine are null-safe and division is zero-guarded.
Unary minus
- Bug: Negative literals only worked by accident (
{{ -5 }}parsed asnull - 5), and failed at higher precedence:{{ 3 * -2 }}returned-2instead of-6. - Fix:
pPrimary()now supports a proper unary minus for numeric operands ({{ -5 }},{{ 3 * -2 }},{{ -price }}); non-numeric operands yield0.
Added
Tests
testTernaryInsideParentheses,testElvisInsideParentheses,testNullCoalescingInsideParentheses,testLogicalOperatorsInsideParentheses,testOrFollowedByTernary,testTernaryInsideFunctionArguments- parser pointer regression teststestUnaryMinus- negative literals, unary minus with multiplication and variables
Changed
- Removed the author's phone number from
composer.json.
[4.1.0] - 2026-06-19
[4.1.0]: https://github.com/codesaur-php/Template/compare/v4.0.2...v4.1.0
Added
- More Twig-compatible operators in the expression parser (
MemoryTemplate::pCompare()):in/not inmembership -{% if type in ['image', 'video'] %}. Works on arrays (in_array, loose), strings (str_containssubstring), andTraversable.ends withstring operator - complements the existingstarts with.matchesregex operator -{% if email matches '/^[^@]+@[^@]+$/' %}(usespreg_match, errors suppressed).is even/is oddnumber tests (also viais not) - e.g. alternating row styles in loops.- Note: the
for ... inloop is parsed at the tag level (buildFor), so the newincomparison operator does not conflict with it.
[4.0.2] - 2026-06-09
[4.0.2]: https://github.com/codesaur-php/Template/compare/v4.0.1...v4.0.2
Changed
- Property names no longer use a leading underscore (PSR-12 compliance)
MemoryTemplate:$_html->$html,$_vars->$varsFileTemplate:$_file->$filepath- Internal only - all properties are
protected, so the public API is unchanged.
[4.0.1] - 2026-04-27
[4.0.1]: https://github.com/codesaur-php/Template/compare/v4.0.0...v4.0.1
Fixed
{% for %}{% else %}{% endfor %} empty-iterable branch
- Bug: Templates using the Twig-style
{% for %}{% else %}{% endfor %}construct silently dropped everything after the{% endfor %}from the rendered output. The parser hit{% else %}, returned the loop body it had collected so far, but never consumed theelse/endfortokens - so the outerbuildTreesaw the orphanelseas an end-marker and returned immediately. Both theelsebranch and any content following the loop were lost. - Root cause:
buildFor()only looked for{% endfor %}and had no knowledge of the{% else %}separator. The for-node had noelseslot, andrenderFor()returned an empty string for non-iterable / empty collections instead of falling back to the alternate branch. - Fix:
buildFor()now optionally consumes an{% else %}block and attaches its body as a newelsekey on the for-node.renderFor()renders that else body when the iterable is missing, non-iterable, or empty - matching Twig's documented behavior.
Object method calls in expressions
- Bug: Expressions like
{{ user.can('edit') }}and{% if auth.is('admin') %}silently returnednull, causing permission-gated UI to be hidden even from authorized users. - Root cause: The
pPostfix()parser only handled the_self.macroName(...)(macro invocation) case when it encountered a method call with arguments, and silently set the result tonullfor all other cases - completely skipping object and array-of-callables method dispatch. - Fix:
pPostfix()now handles three distinct cases when a.name(args)postfix is parsed:_self.macro(...)->callMacro(unchanged)object.method(...)->$val->$method(...$args)(guarded bymethod_exists)array['callable'](...)-> invokes the callable array element directly- Otherwise ->
null(unchanged)
Added
Documentation
- Added "Method calls" line to the
MemoryTemplateclass docblock describing the newly supportedobject.method(args)andarray_of_callables.name(args)forms.
Tests
testForElseWithItems-{% else %}is skipped when the loop iteratestestForElseWithEmptyItems-{% else %}renders for empty arraystestForElseWithNonIterable-{% else %}renders for null / non-iterabletestObjectMethodCall- calling public methods on an object from expressionstestObjectMethodCallInIfBlock- using a method call as an{% if %}conditiontestArrayOfCallablesMethodCall- calling closures stored as array valuestestMissingMethodReturnsNull- non-existent methods safely returnnull
[4.0.0] - 2026-03-30
[4.0.0]: https://github.com/codesaur-php/Template/compare/v3.0.1...v4.0.0
Changed
Completely removed Twig dependency
- Removed
twig/twigpackage entirely from dependencies - During development, the necessary capabilities inspired by Twig's syntax and design patterns were reimplemented as our own standalone engine
- Added ext-mbstring to requirements (for capitalize, upper, lower, length, etc.)
Migrated full engine into MemoryTemplate
- The complete template engine (tokenizer, parser, renderer, expression evaluator) has been moved from FileTemplate into MemoryTemplate. MemoryTemplate now supports if/for/set/macro, filter chains, expression parser, ternary/null coalescing, and loop variables
- FileTemplate is now a thin wrapper that only reads files and passes them to MemoryTemplate's engine
New methods added to MemoryTemplate
addFilter(string, callable),addFunction(string, callable)- register custom filters/functions- 33 built-in filters (e, date, length, keys, slice, json_encode, json_decode, abs, trim, striptags, title, join, reverse, sort, unique, column, batch, values, replace, wordwrap, etc.)
- Built-in functions:
attribute,range,max,min
Removed
- Removed the TwigTemplate class (merged into FileTemplate)
twig/twigdependency (15+ files, ~50,000 lines of code removed)addGlobal(),getEnvironment()methods (Twig-specific){# comment #}template comment syntax- Removed the
stringify()protected method - replaced with(string)cast
[3.0.1] - 2026-03-05
[3.0.1]: https://github.com/codesaur-php/Template/compare/v3.0.0...v3.0.1
Changed
Documentation Cleanup
- Removed all Unicode emoji characters from documentation files
- Replaced Unicode arrow with ASCII arrow
[3.0.0] - 2026-01-08
[3.0.0]: https://github.com/codesaur-php/Template/compare/v2.0.0...v3.0.0
Stable Release
This version is the stable release of the codesaur/template package with complete features, full test coverage, and comprehensive documentation.
Added
Core Functionality
- MemoryTemplate - Lightweight template engine with simple {{key}} placeholders
- FileTemplate - File-based template loader (extends MemoryTemplate)
- TwigTemplate - Advanced renderer fully integrated with Twig engine (extends FileTemplate)
Testing
- Unit, Integration, Performance, Memory tests (70+ tests, 1200+ assertions)
- 100% line coverage, 100% method coverage
CI/CD
- GitHub Actions CI/CD pipeline (PHP 8.2)
Documentation
- README.md, API.md, REVIEW.md (Mongolian and English)
[2.0.0] - 2025-11-28
[2.0.0]: https://github.com/codesaur-php/Template/compare/v1.0...v2.0.0
Added
Core Improvements
- Enhanced template processing capabilities
- Improved error handling
- Better file template support
[1.0] - 2021-03-09
[1.0]: https://github.com/codesaur-php/Template/releases/tag/v1.0
Initial Release
- MemoryTemplate - Basic template engine with {{key}} placeholders
- FileTemplate - File-based template loader
- TwigTemplate - Twig engine integration