Version in base suite: 3.20.0-2 Base version: php-twig_3.20.0-2 Target version: php-twig_3.26.0-0+deb13u1 Base file: /srv/ftp-master.debian.org/ftp/pool/main/p/php-twig/php-twig_3.20.0-2.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/p/php-twig/php-twig_3.26.0-0+deb13u1.dsc .gitattributes | 8 .github/workflows/ci.yml | 2 .github/workflows/fabbot.yml | 14 .php-cs-fixer.dist.php | 15 CHANGELOG | 88 + bin/generate_operators_precedence.php | 109 + composer.json | 3 debian/changelog | 40 debian/clean | 11 debian/control | 2 debian/gbp.conf | 2 debian/patches/0002-Skip-failing-test-without-composer-install.patch | 4 debian/patches/0004-Switch-to-static-data-provides-for-integration-tests.patch | 8 debian/patches/0005-PHPUnit-11.patch | 572 +++++- debian/patches/0007-Twig-4-Add-PHPUnit-as-dev-dependency.patch | 121 - debian/patches/0008-Compatibility-with-recent-PHPUnit-12.patch | 324 +++ debian/patches/0009-Drop-redundant-require_once-in-tests.patch | 21 debian/patches/series | 1 debian/rules | 52 doc/_build/build.php | 15 doc/advanced.rst | 187 +- doc/api.rst | 33 doc/coding_standards.rst | 7 doc/deprecated.rst | 153 + doc/filters/escape.rst | 34 doc/filters/filter.rst | 2 doc/filters/format_datetime.rst | 23 doc/filters/format_number.rst | 40 doc/filters/html_attr_merge.rst | 141 + doc/filters/html_attr_type.rst | 123 + doc/filters/index.rst | 3 doc/filters/inky_to_html.rst | 2 doc/filters/number_format.rst | 14 doc/filters/reduce.rst | 10 doc/filters/replace.rst | 2 doc/filters/singular.rst | 6 doc/filters/slug.rst | 10 doc/filters/spaceless.rst | 10 doc/filters/u.rst | 2 doc/functions/country_timezones.rst | 4 doc/functions/date.rst | 2 doc/functions/enum.rst | 16 doc/functions/enum_cases.rst | 3 doc/functions/html_attr.rst | 202 ++ doc/functions/html_cva.rst | 31 doc/functions/include.rst | 6 doc/functions/index.rst | 1 doc/functions/random.rst | 5 doc/functions/template_from_string.rst | 10 doc/index.rst | 1 doc/operators_precedence.rst | 204 ++ doc/recipes.rst | 18 doc/sandbox.rst | 29 doc/tags/block.rst | 2 doc/tags/cache.rst | 2 doc/tags/extends.rst | 10 doc/tags/index.rst | 2 doc/tags/set.rst | 10 doc/templates.rst | 267 ++- doc/tests/index.rst | 2 extra/cache-extra/.gitattributes | 1 extra/cache-extra/Tests/FunctionalTest.php | 2 extra/cache-extra/TokenParser/CacheTokenParser.php | 9 extra/cache-extra/composer.json | 4 extra/cssinliner-extra/.gitattributes | 1 extra/cssinliner-extra/CssInlinerExtension.php | 6 extra/cssinliner-extra/Resources/functions.php | 4 extra/cssinliner-extra/Tests/FunctionalTest.php | 47 extra/html-extra/.gitattributes | 1 extra/html-extra/Cva.php | 2 extra/html-extra/HtmlAttr/AttributeValueInterface.php | 31 extra/html-extra/HtmlAttr/InlineStyle.php | 70 extra/html-extra/HtmlAttr/MergeableInterface.php | 55 extra/html-extra/HtmlAttr/SeparatedTokenList.php | 68 extra/html-extra/HtmlExtension.php | 144 + extra/html-extra/Tests/Fixtures/html_attr.test | 40 extra/html-extra/Tests/Fixtures/html_attr_merge.test | 10 extra/html-extra/Tests/HtmlAttrMergeTest.php | 223 ++ extra/html-extra/Tests/HtmlAttrTest.php | 319 +++ extra/html-extra/composer.json | 2 extra/inky-extra/.gitattributes | 1 extra/inky-extra/InkyExtension.php | 6 extra/inky-extra/Resources/functions.php | 4 extra/inky-extra/Tests/FunctionalTest.php | 33 extra/intl-extra/.gitattributes | 1 extra/intl-extra/IntlExtension.php | 8 extra/intl-extra/Tests/Fixtures/country_timezones.test | 4 extra/intl-extra/Tests/Fixtures/script_names.test | 8 extra/intl-extra/Tests/IntlExtensionTest.php | 31 extra/intl-extra/composer.json | 2 extra/markdown-extra/.gitattributes | 1 extra/markdown-extra/DefaultMarkdown.php | 3 extra/markdown-extra/ErusevMarkdown.php | 6 extra/markdown-extra/LeagueMarkdown.php | 3 extra/markdown-extra/MarkdownExtension.php | 4 extra/markdown-extra/Tests/Fixtures/html_to_markdown_escaping.test | 12 extra/markdown-extra/Tests/FunctionalTest.php | 25 extra/markdown-extra/composer.json | 2 extra/string-extra/.gitattributes | 1 extra/string-extra/composer.json | 2 extra/twig-extra-bundle/.gitattributes | 1 extra/twig-extra-bundle/DependencyInjection/Compiler/MissingExtensionSuggestorPass.php | 33 extra/twig-extra-bundle/DependencyInjection/Configuration.php | 12 extra/twig-extra-bundle/DependencyInjection/TwigExtraExtension.php | 26 extra/twig-extra-bundle/Extensions.php | 6 extra/twig-extra-bundle/Tests/DependencyInjection/TwigExtraExtensionTest.php | 6 extra/twig-extra-bundle/Tests/Fixture/Kernel.php | 9 extra/twig-extra-bundle/Tests/IntegrationTest.php | 9 extra/twig-extra-bundle/TwigExtraBundle.php | 25 extra/twig-extra-bundle/composer.json | 6 splitsh.json | 16 src/AbstractTwigCallable.php | 8 src/Attribute/AsTwigFilter.php | 58 src/Attribute/AsTwigFunction.php | 54 src/Attribute/AsTwigTest.php | 50 src/Compiler.php | 12 src/Environment.php | 33 src/Error/Error.php | 123 - src/ExpressionParser.php | 834 +--------- src/ExpressionParser/AbstractExpressionParser.php | 35 src/ExpressionParser/ExpressionParserDescriptionInterface.php | 17 src/ExpressionParser/ExpressionParserInterface.php | 36 src/ExpressionParser/ExpressionParserType.php | 33 src/ExpressionParser/ExpressionParsers.php | 155 + src/ExpressionParser/Infix/ArgumentsTrait.php | 83 src/ExpressionParser/Infix/ArrowExpressionParser.php | 53 src/ExpressionParser/Infix/AssignmentExpressionParser.php | 66 src/ExpressionParser/Infix/BinaryOperatorExpressionParser.php | 80 src/ExpressionParser/Infix/ConditionalTernaryExpressionParser.php | 62 src/ExpressionParser/Infix/DotExpressionParser.php | 108 + src/ExpressionParser/Infix/FilterExpressionParser.php | 85 + src/ExpressionParser/Infix/FunctionExpressionParser.php | 90 + src/ExpressionParser/Infix/IsExpressionParser.php | 84 + src/ExpressionParser/Infix/IsNotExpressionParser.php | 33 src/ExpressionParser/Infix/SquareBracketExpressionParser.php | 91 + src/ExpressionParser/InfixAssociativity.php | 18 src/ExpressionParser/InfixExpressionParserInterface.php | 27 src/ExpressionParser/PrecedenceChange.php | 42 src/ExpressionParser/Prefix/GroupingExpressionParser.php | 88 + src/ExpressionParser/Prefix/LiteralExpressionParser.php | 233 ++ src/ExpressionParser/Prefix/UnaryOperatorExpressionParser.php | 72 src/ExpressionParser/PrefixExpressionParserInterface.php | 25 src/Extension/AbstractExtension.php | 5 src/Extension/AttributeExtension.php | 176 ++ src/Extension/CoreExtension.php | 240 +- src/Extension/EscaperExtension.php | 29 src/Extension/ExtensionInterface.php | 11 src/Extension/SandboxExtension.php | 22 src/Extension/StringLoaderExtension.php | 3 src/ExtensionSet.php | 119 + src/FileExtensionEscapingStrategy.php | 2 src/Lexer.php | 79 src/Loader/ChainLoader.php | 4 src/Markup.php | 9 src/Node/CheckToStringNode.php | 7 src/Node/CoercesChildrenToStringInterface.php | 40 src/Node/DeprecatedNode.php | 16 src/Node/EmbedNode.php | 6 src/Node/Expression/ArrayExpression.php | 124 - src/Node/Expression/ArrowFunctionExpression.php | 25 src/Node/Expression/AssignNameExpression.php | 2 src/Node/Expression/Binary/AbstractBinary.php | 4 src/Node/Expression/Binary/AddBinary.php | 3 src/Node/Expression/Binary/AndBinary.php | 3 src/Node/Expression/Binary/BitwiseAndBinary.php | 3 src/Node/Expression/Binary/BitwiseOrBinary.php | 3 src/Node/Expression/Binary/BitwiseXorBinary.php | 3 src/Node/Expression/Binary/ConcatBinary.php | 9 src/Node/Expression/Binary/DivBinary.php | 3 src/Node/Expression/Binary/EndsWithBinary.php | 3 src/Node/Expression/Binary/EqualBinary.php | 9 src/Node/Expression/Binary/FloorDivBinary.php | 3 src/Node/Expression/Binary/GreaterBinary.php | 9 src/Node/Expression/Binary/GreaterEqualBinary.php | 9 src/Node/Expression/Binary/HasEveryBinary.php | 5 src/Node/Expression/Binary/HasSomeBinary.php | 5 src/Node/Expression/Binary/InBinary.php | 3 src/Node/Expression/Binary/LessBinary.php | 9 src/Node/Expression/Binary/LessEqualBinary.php | 9 src/Node/Expression/Binary/MatchesBinary.php | 17 src/Node/Expression/Binary/ModBinary.php | 3 src/Node/Expression/Binary/MulBinary.php | 3 src/Node/Expression/Binary/NotEqualBinary.php | 9 src/Node/Expression/Binary/NotInBinary.php | 3 src/Node/Expression/Binary/NotSameAsBinary.php | 23 src/Node/Expression/Binary/ObjectDestructuringSetBinary.php | 77 src/Node/Expression/Binary/OrBinary.php | 3 src/Node/Expression/Binary/PowerBinary.php | 3 src/Node/Expression/Binary/RangeBinary.php | 9 src/Node/Expression/Binary/SameAsBinary.php | 23 src/Node/Expression/Binary/SequenceDestructuringSetBinary.php | 67 src/Node/Expression/Binary/SetBinary.php | 44 src/Node/Expression/Binary/SpaceshipBinary.php | 9 src/Node/Expression/Binary/StartsWithBinary.php | 3 src/Node/Expression/Binary/SubBinary.php | 3 src/Node/Expression/Binary/XorBinary.php | 3 src/Node/Expression/BlockReferenceExpression.php | 22 src/Node/Expression/CallExpression.php | 35 src/Node/Expression/ConstantExpression.php | 6 src/Node/Expression/EmptyExpression.php | 33 src/Node/Expression/Filter/DefaultFilter.php | 2 src/Node/Expression/Filter/RawFilter.php | 2 src/Node/Expression/FilterExpression.php | 11 src/Node/Expression/FunctionExpression.php | 23 src/Node/Expression/FunctionNode/EnumCasesFunction.php | 9 src/Node/Expression/FunctionNode/EnumFunction.php | 9 src/Node/Expression/GetAttrExpression.php | 93 + src/Node/Expression/ListExpression.php | 41 src/Node/Expression/MacroReferenceExpression.php | 27 src/Node/Expression/MethodCallExpression.php | 9 src/Node/Expression/NameExpression.php | 11 src/Node/Expression/NullCoalesceExpression.php | 4 src/Node/Expression/ReturnArrayInterface.php | 16 src/Node/Expression/ReturnBoolInterface.php | 16 src/Node/Expression/ReturnNumberInterface.php | 16 src/Node/Expression/ReturnPrimitiveTypeInterface.php | 16 src/Node/Expression/ReturnStringInterface.php | 16 src/Node/Expression/SupportDefinedTestDeprecationTrait.php | 44 src/Node/Expression/SupportDefinedTestInterface.php | 24 src/Node/Expression/SupportDefinedTestTrait.php | 27 src/Node/Expression/TempNameExpression.php | 4 src/Node/Expression/Ternary/ConditionalTernary.php | 7 src/Node/Expression/Test/DefinedTest.php | 44 src/Node/Expression/Test/DivisiblebyTest.php | 6 src/Node/Expression/Test/EvenTest.php | 6 src/Node/Expression/Test/NullTest.php | 8 src/Node/Expression/Test/OddTest.php | 6 src/Node/Expression/Test/SameasTest.php | 6 src/Node/Expression/Test/TrueTest.php | 40 src/Node/Expression/TestExpression.php | 22 src/Node/Expression/Unary/AbstractUnary.php | 2 src/Node/IfNode.php | 9 src/Node/ImportNode.php | 12 src/Node/IncludeNode.php | 33 src/Node/ModuleNode.php | 35 src/Node/Node.php | 3 src/Node/PrintNode.php | 7 src/Node/SetNode.php | 2 src/Node/TypesNode.php | 9 src/NodeVisitor/EscaperNodeVisitor.php | 2 src/NodeVisitor/OptimizerNodeVisitor.php | 2 src/NodeVisitor/SafeAnalysisNodeVisitor.php | 13 src/NodeVisitor/SandboxNodeVisitor.php | 69 src/NodeVisitor/YieldNotReadyNodeVisitor.php | 2 src/OperatorPrecedenceChange.php | 24 src/Parser.php | 227 ++ src/Profiler/Dumper/HtmlDumper.php | 9 src/Resources/core.php | 43 src/Resources/debug.php | 4 src/Resources/string_loader.php | 4 src/Runtime/EscaperRuntime.php | 46 src/Sandbox/SecurityPolicy.php | 14 src/Template.php | 44 src/Test/IntegrationTestCase.php | 24 src/Token.php | 56 src/TokenParser/AbstractTokenParser.php | 29 src/TokenParser/ApplyTokenParser.php | 11 src/TokenParser/AutoEscapeTokenParser.php | 2 src/TokenParser/BlockTokenParser.php | 2 src/TokenParser/DeprecatedTokenParser.php | 7 src/TokenParser/DoTokenParser.php | 2 src/TokenParser/EmbedTokenParser.php | 2 src/TokenParser/ExtendsTokenParser.php | 2 src/TokenParser/ForTokenParser.php | 4 src/TokenParser/FromTokenParser.php | 2 src/TokenParser/GuardTokenParser.php | 10 src/TokenParser/IfTokenParser.php | 4 src/TokenParser/ImportTokenParser.php | 2 src/TokenParser/IncludeTokenParser.php | 4 src/TokenParser/MacroTokenParser.php | 4 src/TokenParser/SandboxTokenParser.php | 6 src/TokenParser/SetTokenParser.php | 18 src/TokenParser/TypesTokenParser.php | 2 src/TokenParser/UseTokenParser.php | 2 src/TokenParser/WithTokenParser.php | 2 src/TwigCallableInterface.php | 2 src/Util/CallableArgumentsExtractor.php | 4 src/Util/DeprecationCollector.php | 2 src/Util/TemplateDirIterator.php | 4 tests/Cache/ChainTest.php | 9 tests/Cache/FilesystemTest.php | 9 tests/Cache/ReadOnlyFilesystemTest.php | 9 tests/CompilerTest.php | 27 tests/ContainerRuntimeLoaderTest.php | 9 tests/CustomExtensionTest.php | 13 tests/DeprecatedCallableInfoTest.php | 13 tests/DummyBackedEnum.php | 9 tests/DummyUnitEnum.php | 9 tests/EnvironmentTest.php | 49 tests/ErrorTest.php | 211 ++ tests/ExpressionParserTest.php | 256 ++- tests/Extension/AttributeExtensionTest.php | 172 ++ tests/Extension/CoreTest.php | 50 tests/Extension/EscaperTest.php | 28 tests/Extension/Fixtures/ExtensionWithAttributes.php | 136 + tests/Extension/Fixtures/FilterWithoutValue.php | 22 tests/Extension/Fixtures/TestWithoutValue.php | 22 tests/Extension/SandboxTest.php | 608 +++++++ tests/FactoryRuntimeLoaderTest.php | 9 tests/FileExtensionEscapingStrategyTest.php | 9 tests/FilesystemHelper.php | 9 tests/Fixtures/errors/extends/include.twig | 4 tests/Fixtures/errors/extends/index.twig | 1 tests/Fixtures/errors/no_line_and_context_exception.twig | 3 tests/Fixtures/errors/no_line_and_context_exception_include_line_1.twig | 1 tests/Fixtures/errors/no_line_and_context_exception_include_line_5.twig | 5 tests/Fixtures/exceptions/strict_comparison_operator.test | 6 tests/Fixtures/exceptions/syntax_error_in_reused_template.test | 4 tests/Fixtures/expressions/array.test | 5 tests/Fixtures/expressions/dot_as_concatenation.test | 2 tests/Fixtures/expressions/postfix.test | 4 tests/Fixtures/expressions/sameas.test | 8 tests/Fixtures/expressions/set.test | 61 tests/Fixtures/expressions/spread_ternary_precedence.test | 14 tests/Fixtures/filters/arrow_reserved_names.test | 2 tests/Fixtures/filters/escape_html_attr_relaxed.test | 15 tests/Fixtures/filters/spaceless_pre_escape.legacy.test | 22 tests/Fixtures/operators/contat_vs_add_sub.legacy.test | 4 tests/Fixtures/operators/minus_vs_pipe.legacy.test | 10 tests/Fixtures/operators/not_precedence.legacy.test | 2 tests/Fixtures/operators/not_precedence.test | 2 tests/Fixtures/regression/4701-block-inheritance-issue.test | 21 tests/Fixtures/regression/markup_test.test | 18 tests/Fixtures/tags/guard/basic.test | 17 tests/Fixtures/tags/guard/throwing_handler.test | 18 tests/Fixtures/tests/null_coalesce.legacy.test | 20 tests/IntegrationTest.php | 40 tests/LexerTest.php | 69 tests/Loader/ArrayTest.php | 9 tests/Loader/ChainTest.php | 9 tests/Loader/FilesystemTest.php | 9 tests/Node/AutoEscapeTest.php | 9 tests/Node/BlockReferenceTest.php | 9 tests/Node/BlockTest.php | 12 tests/Node/DeprecatedTest.php | 12 tests/Node/DoTest.php | 9 tests/Node/EmbedTest.php | 91 + tests/Node/Expression/ArrayTest.php | 9 tests/Node/Expression/Binary/AddTest.php | 9 tests/Node/Expression/Binary/AndTest.php | 9 tests/Node/Expression/Binary/ConcatTest.php | 9 tests/Node/Expression/Binary/DivTest.php | 9 tests/Node/Expression/Binary/FloorDivTest.php | 9 tests/Node/Expression/Binary/ModTest.php | 9 tests/Node/Expression/Binary/MulTest.php | 9 tests/Node/Expression/Binary/NullCoalesceTest.php | 9 tests/Node/Expression/Binary/OrTest.php | 9 tests/Node/Expression/Binary/SubTest.php | 9 tests/Node/Expression/CallTest.php | 10 tests/Node/Expression/ConditionalTest.php | 9 tests/Node/Expression/ConstantTest.php | 9 tests/Node/Expression/Filter/RawTest.php | 9 tests/Node/Expression/FilterTest.php | 29 tests/Node/Expression/FilterTestExtension.php | 9 tests/Node/Expression/FunctionTest.php | 11 tests/Node/Expression/GetAttrTest.php | 27 tests/Node/Expression/MacroReferenceTest.php | 41 tests/Node/Expression/NullCoalesceTest.php | 9 tests/Node/Expression/ParentTest.php | 9 tests/Node/Expression/Ternary/ConditionalTernaryTest.php | 9 tests/Node/Expression/TestTest.php | 11 tests/Node/Expression/Unary/NegTest.php | 9 tests/Node/Expression/Unary/NotTest.php | 9 tests/Node/Expression/Unary/PosTest.php | 9 tests/Node/Expression/Variable/AssignContextVariableTest.php | 9 tests/Node/Expression/Variable/ContextVariableTest.php | 15 tests/Node/ForTest.php | 9 tests/Node/IfTest.php | 9 tests/Node/ImportTest.php | 11 tests/Node/IncludeTest.php | 22 tests/Node/MacroTest.php | 15 tests/Node/ModuleTest.php | 48 tests/Node/NodeTest.php | 20 tests/Node/PrintTest.php | 9 tests/Node/SandboxTest.php | 9 tests/Node/SetTest.php | 15 tests/Node/TextTest.php | 9 tests/Node/TypesTest.php | 9 tests/NodeVisitor/OptimizerTest.php | 11 tests/NodeVisitor/SandboxTest.php | 72 tests/ParserTest.php | 34 tests/Profiler/Dumper/BlackfireTest.php | 12 tests/Profiler/Dumper/HtmlTest.php | 32 tests/Profiler/Dumper/ProfilerTestCase.php | 13 tests/Profiler/Dumper/TextTest.php | 12 tests/Profiler/ProfileTest.php | 9 tests/Resources/LegacyCoreTest.php | 195 ++ tests/Runtime/EscaperRuntimeTest.php | 36 tests/TemplateTest.php | 60 tests/TemplateWrapperTest.php | 9 tests/TokenParser/GuardTokenParserTest.php | 11 tests/TokenParser/TypesTokenParserTest.php | 9 tests/TokenStreamTest.php | 9 tests/Util/CallableArgumentsExtractorTest.php | 9 tests/Util/DeprecationCollectorTest.php | 9 395 files changed, 11052 insertions(+), 2227 deletions(-) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpr89xozaa/php-twig_3.20.0-2.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpr89xozaa/php-twig_3.26.0-0+deb13u1.dsc: no acceptable signature found diff -Nru php-twig-3.20.0/.gitattributes php-twig-3.26.0/.gitattributes --- php-twig-3.20.0/.gitattributes 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/.gitattributes 2026-05-20 07:31:59.000000000 +0000 @@ -1,9 +1,11 @@ -/.github/ export-ignore +/bin/ export-ignore /doc/ export-ignore /extra/ export-ignore /tests/ export-ignore /.editorconfig export-ignore -/.gitattributes export-ignore -/.gitignore export-ignore +/.git* export-ignore /.php-cs-fixer.dist.php export-ignore /phpunit.xml.dist export-ignore +/phpstan.neon.dist export-ignore +/phpstan-baseline.neon export-ignore +/splitsh.json export-ignore diff -Nru php-twig-3.20.0/.github/workflows/ci.yml php-twig-3.26.0/.github/workflows/ci.yml --- php-twig-3.20.0/.github/workflows/ci.yml 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/.github/workflows/ci.yml 2026-05-20 07:31:59.000000000 +0000 @@ -104,7 +104,7 @@ - name: "PHPUnit version" run: vendor/bin/simple-phpunit --version - - name: "Prevent installing symfony/translation-contract 3.0" + - name: "Prevent installing symfony/translation-contracts 3.0" if: "matrix.extension == 'twig-extra-bundle'" working-directory: extra/${{ matrix.extension }} run: "composer require --no-update 'symfony/translation-contracts:^1.1|^2.0'" diff -Nru php-twig-3.20.0/.github/workflows/fabbot.yml php-twig-3.26.0/.github/workflows/fabbot.yml --- php-twig-3.20.0/.github/workflows/fabbot.yml 1970-01-01 00:00:00.000000000 +0000 +++ php-twig-3.26.0/.github/workflows/fabbot.yml 2026-05-20 07:31:59.000000000 +0000 @@ -0,0 +1,14 @@ +name: CS + +on: + pull_request: + +permissions: + contents: read + +jobs: + call-fabbot: + name: Fabbot + uses: symfony-tools/fabbot/.github/workflows/fabbot.yml@main + with: + package: Twig diff -Nru php-twig-3.20.0/.php-cs-fixer.dist.php php-twig-3.26.0/.php-cs-fixer.dist.php --- php-twig-3.20.0/.php-cs-fixer.dist.php 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/.php-cs-fixer.dist.php 2026-05-20 07:31:59.000000000 +0000 @@ -1,6 +1,10 @@ setRules([ '@Symfony' => true, '@Symfony:risky' => true, @@ -11,11 +15,12 @@ 'no_unreachable_default_argument_value' => false, 'braces' => ['allow_single_line_closure' => true], 'heredoc_to_nowdoc' => false, + 'single_line_throw' => false, 'ordered_imports' => true, 'phpdoc_types_order' => ['null_adjustment' => 'always_last', 'sort_algorithm' => 'none'], - // TODO: Remove once the "compiler_optimized" set includes "sprintf" - 'native_function_invocation' => ['include' => ['@compiler_optimized', 'sprintf'], 'scope' => 'all'], - ]) + 'no_superfluous_phpdoc_tags' => ['allow_mixed' => true], + ]) ->setRiskyAllowed(true) - ->setFinder((new PhpCsFixer\Finder())->in(__DIR__)) + ->setParallelConfig(ParallelConfigFactory::detect()) + ->setFinder((new Finder())->in(__DIR__)) ; diff -Nru php-twig-3.20.0/CHANGELOG php-twig-3.26.0/CHANGELOG --- php-twig-3.20.0/CHANGELOG 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/CHANGELOG 2026-05-20 07:31:59.000000000 +0000 @@ -1,3 +1,89 @@ +# 3.26.0 (2026-05-20) + + * Document that the sandbox doesn't protect against resource exhaustion + * Document `template_from_string` caveats when used in a sandboxed environment + * Add docs on `Markup` about the goal of this class in the context of a sandbox + * Pre-escape HTML input on the `spaceless` filter + * Pre-escape HTML input on `inline_css` and `inky_to_html` filters + * Fix XSS by adjusting `is_safe` annotation on HTML-emitting filters + * [Profiler] Escape template and profile names in `HtmlDumper` + * Fix unbounded memoisation of `IntlDateFormatter` / `NumberFormatter` + * Fix sandbox bypass in the "column" filter + * Fix sandbox bypass in the `{% sandbox %}` tag when including a preloaded template + * Fix sandbox bypass: PHP code injection via `{% use %}` template name + * Fix sandbox bypass: PHP code injection via `_self` / import macro reference + * Fix sandbox bypass in object destructuring assignment + * Fix sandbox bypass: propagate `Source` to `checkArrow` for source-policy sandboxing + * Encode single quotes as `\x27` in `Compiler::string()` as a defense-in-depth measure + * Fix sandbox `__toString` bypasses + * Add `Twig\Node\CoercesChildrenToStringInterface` to let nodes declare which of their child nodes will be string-coerced at runtime so the sandbox wraps them with a `__toString` check + +# 3.25.0 (2026-05-17) + + * Add a `needs_is_sandboxed` option for filters, functions, and tests + * Use deterministic suffixes for generated embed classes + * Lazy-load `EscaperRuntime` in `EscaperExtension` + +# 3.24.0 (2026-03-17) + + * Deprecate not implementing the `getOperatorTokens()` method in `ExpressionParserInterface` implementations + * Deprecate passing a non-`AbstractExpression` node to `Twig\Node\Expression\Binary\MatchesBinary` constructor + * Deprecate passing a non-`AbstractExpression` node to `Parser::setParent()` + * Add support for renaming variables in object destructuring (`{name: userName} = user`) + * Add `html_attr_relaxed` escaping strategy that preserves :, @, [, and ] for front-end framework attribute names + * Add support for short-circuiting in null-safe operator chains + * Add the `html_attr` function and `html_attr_merge` as well as `html_attr_type` filters + +# 3.23.0 (2026-01-23) + + * Add `=` assignment operator (allows to set variables in expression or to replace the short-form of the set tag) + * Add sequence, mapping, and object destructuring + * Add `?.` null-safe operator + * Add `===` and `!==` operators (equivalent to the `same as` and `not same as` tests) + * Fix opcache preload warning for unlinked anonymous class + * Fix spread operator behavior + +# 3.22.2 (2025-12-14) + + * Fix "cycle" with non-countable ArrayAccess + Traversable objects + * Use "getShareDir" as an indicator of Symfony version in Symfony bundle + * Fix escaper compatibility with PHP 8.5 + +# 3.22.1 (2025-11-16) + + * Add support for Symfony 8 + +# 3.22.0 (2025-10-29) + + * Add support for two words test in guard tag + * Add `Environment::registerUndefinedTestCallback()` + * Fix compatibility with Symfony 8 + * Fix accessing arrays with stringable objects as key + * Avoid errors when failing to guess the template info for an error + * Fix expression parser compatibility layer + * Fix compiling 'index' with repr (not string) in EmbedNode + * Update configuration keys + allow extra keys for CommonMark extensions + * Allow usage of other Markdown converters than CommonMark in LeagueMarkdown + +# 3.21.1 (2025-05-03) + + * Fix ExtensionSet usage of BinaryOperatorExpressionParser + +# 3.21.0 (2025-05-02) + + * Fix wrong array index + * Deprecate `Template::loadTemplate()` + * Fix testing and expression when it evaluates to an instance of `Markup` + * Add `ReturnPrimitiveTypeInterface` (and sub-interfaces for number, boolean, string, and array) + * Add `SupportDefinedTestInterface` for expression nodes supporting the `defined` test + * Deprecate using the `|` operator in an expression with `+` or `-` without using parentheses to clarify precedence + * Deprecate operator precedence outside of the [0, 512] range + * Introduce expression parser classes to describe operators and operands provided by extensions + instead of arrays (it comes with many deprecations that are documented in + the ``deprecated`` documentation chapter) + * Deprecate the `Twig\ExpressionParser`, and `Twig\OperatorPrecedenceChange` classes + * Add attributes `AsTwigFilter`, `AsTwigFunction`, and `AsTwigTest` to ease extension development + # 3.20.0 (2025-02-13) * Fix support for ignoring syntax errors in an undefined handler in guard @@ -25,6 +111,7 @@ # 3.18.0 (2024-12-29) + * Support for invoking closures * Fix unary operator precedence change * Ignore `SyntaxError` exceptions from undefined handlers when using the `guard` tag * Add a way to stream template rendering (`TemplateWrapper::stream()` and `TemplateWrapper::streamBlock()`) @@ -33,7 +120,6 @@ * Fix the null coalescing operator when the test returns null * Fix the Elvis operator when used as '? :' instead of '?:' - * Support for invoking closures # 3.17.0 (2024-12-10) diff -Nru php-twig-3.20.0/bin/generate_operators_precedence.php php-twig-3.26.0/bin/generate_operators_precedence.php --- php-twig-3.20.0/bin/generate_operators_precedence.php 1970-01-01 00:00:00.000000000 +0000 +++ php-twig-3.26.0/bin/generate_operators_precedence.php 2026-05-20 07:31:59.000000000 +0000 @@ -0,0 +1,109 @@ +getExpressionParsers() as $expressionParser) { + if (!$seen->offsetExists($expressionParser)) { + $expressionParsers[] = $expressionParser; + $seen->offsetSet($expressionParser, true); + $descriptionLength = max($descriptionLength, $expressionParser instanceof ExpressionParserDescriptionInterface ? strlen($expressionParser->getDescription()) : ''); + } +} + +fwrite($output, "\n+------------+------------------+---------+---------------+".str_repeat('-', $descriptionLength + 2)."+\n"); +fwrite($output, '| Precedence | Operator | Type | Associativity | Description'.str_repeat(' ', $descriptionLength - 11)." |\n"); +fwrite($output, '+============+==================+=========+===============+'.str_repeat('=', $descriptionLength + 2).'+'); + +usort($expressionParsers, static fn ($a, $b) => $b->getPrecedence() <=> $a->getPrecedence()); + +$previous = null; +foreach ($expressionParsers as $expressionParser) { + if (null !== $previous) { + fwrite($output, "\n+------------+------------------+---------+---------------+".str_repeat('-', $descriptionLength + 2).'+'); + } + $precedence = $expressionParser->getPrecedence(); + $previousPrecedence = $previous ? $previous->getPrecedence() : \PHP_INT_MAX; + $associativity = $expressionParser instanceof InfixExpressionParserInterface ? (InfixAssociativity::Left === $expressionParser->getAssociativity() ? 'Left' : 'Right') : 'n/a'; + $previousAssociativity = $previous ? ($previous instanceof InfixExpressionParserInterface ? (InfixAssociativity::Left === $previous->getAssociativity() ? 'Left' : 'Right') : 'n/a') : 'n/a'; + if ($previousPrecedence !== $precedence) { + $previous = null; + } + $operatorName = '``'.$expressionParser->getName().'``'; + if ($expressionParser->getAliases()) { + $operatorName .= ', ``'.implode('``, ``', $expressionParser->getAliases()).'``'; + } + fwrite($output, rtrim(sprintf("\n| %-10s | %-16s | %-7s | %-13s | %-{$descriptionLength}s |\n", + (!$previous || $previousPrecedence !== $precedence ? $precedence : '').($expressionParser->getPrecedenceChange() ? ' => '.$expressionParser->getPrecedenceChange()->getNewPrecedence() : ''), + $operatorName, + !$previous || ExpressionParserType::getType($previous) !== ExpressionParserType::getType($expressionParser) ? ExpressionParserType::getType($expressionParser)->value : '', + !$previous || $previousAssociativity !== $associativity ? $associativity : '', + $expressionParser instanceof ExpressionParserDescriptionInterface ? $expressionParser->getDescription() : '', + ))); + $previous = $expressionParser; +} +fwrite($output, "\n+------------+------------------+---------+---------------+".str_repeat('-', $descriptionLength + 2)."+\n"); +fwrite($output, "\nWhen a precedence will change in 4.0, the new precedence is indicated by the arrow ``=>``.\n"); + +fwrite($output, "\nHere is the same table for Twig 4.0 with adjusted precedences:\n"); + +fwrite($output, "\n+------------+------------------+---------+---------------+".str_repeat('-', $descriptionLength + 2)."+\n"); +fwrite($output, '| Precedence | Operator | Type | Associativity | Description'.str_repeat(' ', $descriptionLength - 11)." |\n"); +fwrite($output, '+============+==================+=========+===============+'.str_repeat('=', $descriptionLength + 2).'+'); + +usort($expressionParsers, static function ($a, $b) { + $aPrecedence = $a->getPrecedenceChange() ? $a->getPrecedenceChange()->getNewPrecedence() : $a->getPrecedence(); + $bPrecedence = $b->getPrecedenceChange() ? $b->getPrecedenceChange()->getNewPrecedence() : $b->getPrecedence(); + + return $bPrecedence - $aPrecedence; +}); + +$previous = null; +foreach ($expressionParsers as $expressionParser) { + if (null !== $previous) { + fwrite($output, "\n+------------+------------------+---------+---------------+".str_repeat('-', $descriptionLength + 2).'+'); + } + $precedence = $expressionParser->getPrecedenceChange() ? $expressionParser->getPrecedenceChange()->getNewPrecedence() : $expressionParser->getPrecedence(); + $previousPrecedence = $previous ? ($previous->getPrecedenceChange() ? $previous->getPrecedenceChange()->getNewPrecedence() : $previous->getPrecedence()) : \PHP_INT_MAX; + $associativity = $expressionParser instanceof InfixExpressionParserInterface ? (InfixAssociativity::Left === $expressionParser->getAssociativity() ? 'Left' : 'Right') : 'n/a'; + $previousAssociativity = $previous ? ($previous instanceof InfixExpressionParserInterface ? (InfixAssociativity::Left === $previous->getAssociativity() ? 'Left' : 'Right') : 'n/a') : 'n/a'; + if ($previousPrecedence !== $precedence) { + $previous = null; + } + $operatorName = '``'.$expressionParser->getName().'``'; + if ($expressionParser->getAliases()) { + $operatorName .= ', ``'.implode('``, ``', $expressionParser->getAliases()).'``'; + } + fwrite($output, rtrim(sprintf("\n| %-10s | %-16s | %-7s | %-13s | %-{$descriptionLength}s |\n", + !$previous || $previousPrecedence !== $precedence ? $precedence : '', + $operatorName, + !$previous || ExpressionParserType::getType($previous) !== ExpressionParserType::getType($expressionParser) ? ExpressionParserType::getType($expressionParser)->value : '', + !$previous || $previousAssociativity !== $associativity ? $associativity : '', + $expressionParser instanceof ExpressionParserDescriptionInterface ? $expressionParser->getDescription() : '', + ))); + $previous = $expressionParser; +} +fwrite($output, "\n+------------+------------------+---------+---------------+".str_repeat('-', $descriptionLength + 2)."+\n"); + +fclose($output); diff -Nru php-twig-3.20.0/composer.json php-twig-3.26.0/composer.json --- php-twig-3.20.0/composer.json 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/composer.json 2026-05-20 07:31:59.000000000 +0000 @@ -32,7 +32,8 @@ "require-dev": { "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0", "psr/container": "^1.0|^2.0", - "phpstan/phpstan": "^2.0" + "phpstan/phpstan": "^2.0@stable", + "php-cs-fixer/shim": "^3.0@stable" }, "autoload": { "files": [ diff -Nru php-twig-3.20.0/debian/changelog php-twig-3.26.0/debian/changelog --- php-twig-3.20.0/debian/changelog 2025-02-19 06:20:19.000000000 +0000 +++ php-twig-3.26.0/debian/changelog 2026-05-26 11:43:06.000000000 +0000 @@ -1,3 +1,43 @@ +php-twig (3.26.0-0+deb13u1) trixie-security; urgency=medium + + [ Fabien Potencier ] + * Fix sandbox bypass: propagate sandbox state to checkArrow for source-policy + sandboxing [CVE-2026-24425] + * Fix sandbox `__toString` bypasses [CVE-2026-47732] + * Pre-escape HTML input on the `spaceless` filter [CVE-2026-46628] + * Document template_from_string caveats when used in a sandboxed env + [CVE-2026-46634] + * Document that the sandbox doesn't protect against resource exhaustion + [CVE-2026-46627] + * Update CHANGELOG + * Prepare the 3.26.0 release + + [ Alexandre Daubois ] + * Fix sandbox bypass in object destructuring assignment [CVE-2026-46639] + * Fix unbounded memoisation of `IntlDateFormatter` / `NumberFormatter` + [CVE-2026-46629] + * Fix sandbox bypass: PHP code injection via {% use %} template name + [CVE-2026-46633] + * Fix sandbox bypass in the `{% sandbox %}` tag when including a preloaded + template [CVE-2026-46638] + * Fix sandbox bypass: PHP code injection via _self / import macro reference + [CVE-2026-46640] + * Fix sandbox bypass in the "column" filter [CVE-2026-46635] + + [ Nicolas Grekas ] + * Fix XSS by adjusting `is_safe` annotation on HTML-emitting filters + [CVE-2026-46637] + * Pre-escape HTML input on `inline_css` and `inky_to_html` filters + * [Profiler] Escape template and profile names in HtmlDumper [CVE-2026-47730] + + [ David Prévot ] + * Track debian/trixie branch + * Refresh patches + * Make phpab tolerant + * Update build for related path + + -- David Prévot Tue, 26 May 2026 13:43:06 +0200 + php-twig (3.20.0-2) unstable; urgency=medium * Restore autoload template building diff -Nru php-twig-3.20.0/debian/clean php-twig-3.26.0/debian/clean --- php-twig-3.20.0/debian/clean 2025-02-19 05:49:38.000000000 +0000 +++ php-twig-3.26.0/debian/clean 2026-05-26 11:43:06.000000000 +0000 @@ -4,9 +4,18 @@ debian/autoloaders/ debian/build-tmp/ debian/packages_to_build/ +DeepCopy/ +extra/*/.phpunit.cache/ extra/*/.phpunit.result.cache extra/*/autoload.php extra/twig-extra-bundle/var/ +PharIo/ +PHPUnit/ +Psr/ +SebastianBergmann/ src/autoload.php -Twig/ +src/Extra/ +staabm/ +Symfony/ +Twig vendor/ diff -Nru php-twig-3.20.0/debian/control php-twig-3.26.0/debian/control --- php-twig-3.20.0/debian/control 2025-02-19 06:20:15.000000000 +0000 +++ php-twig-3.26.0/debian/control 2026-05-26 07:00:49.000000000 +0000 @@ -30,7 +30,7 @@ re2c Homepage: https://twig.symfony.com Vcs-Browser: https://salsa.debian.org/php-team/pear/twig -Vcs-Git: https://salsa.debian.org/php-team/pear/twig.git +Vcs-Git: https://salsa.debian.org/php-team/pear/twig.git -b debian/trixie Rules-Requires-Root: no Package: php-twig diff -Nru php-twig-3.20.0/debian/gbp.conf php-twig-3.26.0/debian/gbp.conf --- php-twig-3.20.0/debian/gbp.conf 2024-11-08 08:42:18.000000000 +0000 +++ php-twig-3.26.0/debian/gbp.conf 2026-05-26 06:59:51.000000000 +0000 @@ -1,5 +1,5 @@ [DEFAULT] -debian-branch = debian/latest +debian-branch = debian/trixie pristine-tar = True filter = [ '.gitattributes' ] upstream-vcs-tag = v%(version%~%-)s diff -Nru php-twig-3.20.0/debian/patches/0002-Skip-failing-test-without-composer-install.patch php-twig-3.26.0/debian/patches/0002-Skip-failing-test-without-composer-install.patch --- php-twig-3.20.0/debian/patches/0002-Skip-failing-test-without-composer-install.patch 2025-02-15 14:52:01.000000000 +0000 +++ php-twig-3.26.0/debian/patches/0002-Skip-failing-test-without-composer-install.patch 2026-05-26 07:00:53.000000000 +0000 @@ -7,10 +7,10 @@ 1 file changed, 2 insertions(+) diff --git a/extra/twig-extra-bundle/Tests/IntegrationTest.php b/extra/twig-extra-bundle/Tests/IntegrationTest.php -index 04cbbee..c06eeee 100644 +index df62da2..7a72310 100644 --- a/extra/twig-extra-bundle/Tests/IntegrationTest.php +++ b/extra/twig-extra-bundle/Tests/IntegrationTest.php -@@ -8,6 +8,8 @@ class IntegrationTest extends KernelTestCase +@@ -17,6 +17,8 @@ class IntegrationTest extends KernelTestCase { public function testCommonMarkRendering() { diff -Nru php-twig-3.20.0/debian/patches/0004-Switch-to-static-data-provides-for-integration-tests.patch php-twig-3.26.0/debian/patches/0004-Switch-to-static-data-provides-for-integration-tests.patch --- php-twig-3.20.0/debian/patches/0004-Switch-to-static-data-provides-for-integration-tests.patch 2025-02-19 05:49:38.000000000 +0000 +++ php-twig-3.26.0/debian/patches/0004-Switch-to-static-data-provides-for-integration-tests.patch 2026-05-26 07:00:53.000000000 +0000 @@ -9,7 +9,7 @@ 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Test/IntegrationTestCase.php b/src/Test/IntegrationTestCase.php -index f4a5dc7..7bc0cd2 100644 +index d995c6a..24abb8e 100644 --- a/src/Test/IntegrationTestCase.php +++ b/src/Test/IntegrationTestCase.php @@ -11,6 +11,9 @@ @@ -22,7 +22,7 @@ use PHPUnit\Framework\TestCase; use Twig\Environment; use Twig\Error\Error; -@@ -114,6 +117,7 @@ abstract class IntegrationTestCase extends TestCase +@@ -122,6 +125,7 @@ abstract class IntegrationTestCase extends TestCase * * @return void */ @@ -30,7 +30,7 @@ public function testIntegration($file, $message, $condition, $templates, $exception, $outputs, $deprecation = '') { $this->doIntegrationTest($file, $message, $condition, $templates, $exception, $outputs, $deprecation); -@@ -126,17 +130,13 @@ abstract class IntegrationTestCase extends TestCase +@@ -134,17 +138,13 @@ abstract class IntegrationTestCase extends TestCase * * @return void */ @@ -50,7 +50,7 @@ { try { $fixturesDir = static::getFixturesDirectory(); -@@ -188,14 +188,14 @@ abstract class IntegrationTestCase extends TestCase +@@ -196,14 +196,14 @@ abstract class IntegrationTestCase extends TestCase return $tests; } diff -Nru php-twig-3.20.0/debian/patches/0005-PHPUnit-11.patch php-twig-3.26.0/debian/patches/0005-PHPUnit-11.patch --- php-twig-3.20.0/debian/patches/0005-PHPUnit-11.patch 2025-02-19 05:49:38.000000000 +0000 +++ php-twig-3.26.0/debian/patches/0005-PHPUnit-11.patch 2026-05-26 07:00:53.000000000 +0000 @@ -7,41 +7,47 @@ --- .gitignore | 3 ++- extra/cache-extra/composer.json | 3 --- - extra/cache-extra/phpunit.xml.dist | 32 +++++++++++++++-------- + extra/cache-extra/phpunit.xml.dist | 32 ++++++++++++++++-------- extra/cssinliner-extra/composer.json | 3 --- - extra/cssinliner-extra/phpunit.xml.dist | 32 +++++++++++++++-------- + extra/cssinliner-extra/phpunit.xml.dist | 32 ++++++++++++++++-------- extra/html-extra/Tests/CvaTest.php | 9 +++---- + extra/html-extra/Tests/HtmlAttrMergeTest.php | 5 ++-- + extra/html-extra/Tests/HtmlAttrTest.php | 5 ++-- extra/html-extra/composer.json | 3 --- - extra/html-extra/phpunit.xml.dist | 32 +++++++++++++++-------- + extra/html-extra/phpunit.xml.dist | 32 ++++++++++++++++-------- extra/inky-extra/composer.json | 3 --- - extra/inky-extra/phpunit.xml.dist | 32 +++++++++++++++-------- + extra/inky-extra/phpunit.xml.dist | 32 ++++++++++++++++-------- extra/intl-extra/composer.json | 3 --- - extra/intl-extra/phpunit.xml.dist | 32 +++++++++++++++-------- + extra/intl-extra/phpunit.xml.dist | 32 ++++++++++++++++-------- extra/markdown-extra/Tests/FunctionalTest.php | 5 ++-- - extra/markdown-extra/phpunit.xml.dist | 32 +++++++++++++++-------- + extra/markdown-extra/phpunit.xml.dist | 32 ++++++++++++++++-------- extra/string-extra/composer.json | 3 --- - extra/string-extra/phpunit.xml.dist | 32 +++++++++++++++-------- - extra/twig-extra-bundle/composer.json | 1 - + extra/string-extra/phpunit.xml.dist | 32 ++++++++++++++++-------- extra/twig-extra-bundle/phpunit-bootstrap.php | 8 ++++++ - extra/twig-extra-bundle/phpunit.xml.dist | 32 +++++++++++++++-------- + extra/twig-extra-bundle/phpunit.xml.dist | 32 ++++++++++++++++-------- tests/Cache/ChainTest.php | 5 ++-- tests/Cache/FilesystemTest.php | 4 +-- tests/Cache/ReadOnlyFilesystemTest.php | 4 +-- - tests/CustomExtensionTest.php | 5 ++-- - tests/ErrorTest.php | 5 ++-- - tests/ExpressionParserTest.php | 25 +++++------------- - tests/Extension/CoreTest.php | 37 ++++++++------------------- - tests/Extension/SandboxTest.php | 13 +++------- + tests/CustomExtensionTest.php | 2 ++ + tests/DeprecatedCallableInfoTest.php | 4 +-- + tests/ErrorTest.php | 9 +++---- + tests/ExpressionParserTest.php | 34 ++++++++----------------- + tests/Extension/AttributeExtensionTest.php | 12 +++------ + tests/Extension/CoreTest.php | 34 ++++++++----------------- + tests/Extension/EscaperTest.php | 6 ++--- + tests/Extension/SandboxTest.php | 17 +++++-------- tests/FileExtensionEscapingStrategyTest.php | 5 ++-- - tests/LexerTest.php | 13 +++------- + tests/LexerTest.php | 36 ++++++++------------------- tests/Loader/FilesystemTest.php | 13 +++------- - tests/Node/NodeTest.php | 17 ++++-------- + tests/Node/Expression/MacroReferenceTest.php | 4 +-- + tests/Node/NodeTest.php | 17 ++++--------- tests/NodeVisitor/OptimizerTest.php | 5 ++-- tests/ParserTest.php | 13 +++------- tests/Runtime/EscaperRuntimeTest.php | 9 +++---- - tests/TemplateTest.php | 29 ++++++--------------- + tests/TemplateTest.php | 33 +++++++----------------- tests/TokenParser/TypesTokenParserTest.php | 3 ++- - 36 files changed, 256 insertions(+), 249 deletions(-) + tests/Util/CallableArgumentsExtractorTest.php | 4 +-- + 42 files changed, 281 insertions(+), 301 deletions(-) create mode 100644 extra/twig-extra-bundle/phpunit-bootstrap.php diff --git a/.gitignore b/.gitignore @@ -58,12 +64,12 @@ -.phpunit.result.cache +.phpunit.cache diff --git a/extra/cache-extra/composer.json b/extra/cache-extra/composer.json -index 4ae0621..38c7ffc 100644 +index 8b21310..ce285de 100644 --- a/extra/cache-extra/composer.json +++ b/extra/cache-extra/composer.json @@ -19,9 +19,6 @@ - "symfony/cache": "^5.4|^6.4|^7.0", - "twig/twig": "^3.19|^4.0" + "symfony/cache": "^5.4|^6.4|^7.0|^8.0", + "twig/twig": "^3.21|^4.0" }, - "require-dev": { - "symfony/phpunit-bridge": "^6.4|^7.0" @@ -209,12 +215,58 @@ public function testAdditionalClasses(string|array $base, array|string $additionals, string $expected) { $cva = new Cva($base); +diff --git a/extra/html-extra/Tests/HtmlAttrMergeTest.php b/extra/html-extra/Tests/HtmlAttrMergeTest.php +index d129a5a..3f6c71e 100644 +--- a/extra/html-extra/Tests/HtmlAttrMergeTest.php ++++ b/extra/html-extra/Tests/HtmlAttrMergeTest.php +@@ -11,6 +11,7 @@ + + namespace Twig\Extra\Html\Tests; + ++use PHPUnit\Framework\Attributes\DataProvider; + use PHPUnit\Framework\TestCase; + use Twig\Error\RuntimeError; + use Twig\Extra\Html\HtmlAttr\MergeableInterface; +@@ -18,9 +19,7 @@ use Twig\Extra\Html\HtmlExtension; + + class HtmlAttrMergeTest extends TestCase + { +- /** +- * @dataProvider htmlAttrProvider +- */ ++ #[DataProvider('htmlAttrProvider')] + public function testMerge(array $expected, array $inputs) + { + $result = HtmlExtension::htmlAttrMerge(...$inputs); +diff --git a/extra/html-extra/Tests/HtmlAttrTest.php b/extra/html-extra/Tests/HtmlAttrTest.php +index 41f704c..c87a688 100644 +--- a/extra/html-extra/Tests/HtmlAttrTest.php ++++ b/extra/html-extra/Tests/HtmlAttrTest.php +@@ -11,6 +11,7 @@ + + namespace Twig\Extra\Html\Tests; + ++use PHPUnit\Framework\Attributes\DataProvider; + use PHPUnit\Framework\TestCase; + use Twig\Environment; + use Twig\Error\RuntimeError; +@@ -21,9 +22,7 @@ use Twig\Loader\ArrayLoader; + + class HtmlAttrTest extends TestCase + { +- /** +- * @dataProvider htmlAttrProvider +- */ ++ #[DataProvider('htmlAttrProvider')] + public function testPrintingAttributes(string $expected, array $inputs) + { + $result = HtmlExtension::htmlAttr(new Environment(new ArrayLoader()), ...$inputs); diff --git a/extra/html-extra/composer.json b/extra/html-extra/composer.json -index 55555a0..d31ca13 100644 +index db43e18..6cf6915 100644 --- a/extra/html-extra/composer.json +++ b/extra/html-extra/composer.json @@ -20,9 +20,6 @@ - "symfony/mime": "^5.4|^6.4|^7.0", + "symfony/mime": "^5.4|^6.4|^7.0|^8.0", "twig/twig": "^3.13|^4.0" }, - "require-dev": { @@ -330,12 +382,12 @@ + diff --git a/extra/intl-extra/composer.json b/extra/intl-extra/composer.json -index b728753..40a7235 100644 +index 21e3956..919c980 100644 --- a/extra/intl-extra/composer.json +++ b/extra/intl-extra/composer.json @@ -19,9 +19,6 @@ "twig/twig": "^3.13|^4.0", - "symfony/intl": "^5.4|^6.4|^7.0" + "symfony/intl": "^5.4|^6.4|^7.0|^8.0" }, - "require-dev": { - "symfony/phpunit-bridge": "^6.4|^7.0" @@ -390,7 +442,7 @@ + diff --git a/extra/markdown-extra/Tests/FunctionalTest.php b/extra/markdown-extra/Tests/FunctionalTest.php -index 72b277e..40449b3 100644 +index 58fd729..ff7ea8b 100644 --- a/extra/markdown-extra/Tests/FunctionalTest.php +++ b/extra/markdown-extra/Tests/FunctionalTest.php @@ -11,6 +11,7 @@ @@ -459,7 +511,7 @@ + diff --git a/extra/string-extra/composer.json b/extra/string-extra/composer.json -index 6b366e1..ce60ee7 100644 +index 27a0f34..2affd7c 100644 --- a/extra/string-extra/composer.json +++ b/extra/string-extra/composer.json @@ -20,9 +20,6 @@ @@ -518,18 +570,6 @@ + + -diff --git a/extra/twig-extra-bundle/composer.json b/extra/twig-extra-bundle/composer.json -index 88ee810..b37e76e 100644 ---- a/extra/twig-extra-bundle/composer.json -+++ b/extra/twig-extra-bundle/composer.json -@@ -22,7 +22,6 @@ - }, - "require-dev": { - "league/commonmark": "^1.0|^2.0", -- "symfony/phpunit-bridge": "^6.4|^7.0", - "twig/cache-extra": "^3.0", - "twig/cssinliner-extra": "^3.0", - "twig/html-extra": "^3.0", diff --git a/extra/twig-extra-bundle/phpunit-bootstrap.php b/extra/twig-extra-bundle/phpunit-bootstrap.php new file mode 100644 index 0000000..d896a46 @@ -591,10 +631,10 @@ + diff --git a/tests/Cache/ChainTest.php b/tests/Cache/ChainTest.php -index 3120ab1..48cd1c1 100644 +index 4383e60..ad6114e 100644 --- a/tests/Cache/ChainTest.php +++ b/tests/Cache/ChainTest.php -@@ -11,6 +11,7 @@ namespace Twig\Tests\Cache; +@@ -20,6 +20,7 @@ namespace Twig\Tests\Cache; * file that was distributed with this source code. */ @@ -602,7 +642,7 @@ use PHPUnit\Framework\TestCase; use Twig\Cache\ChainCache; use Twig\Cache\FilesystemCache; -@@ -203,9 +204,7 @@ class ChainTest extends TestCase +@@ -212,9 +213,7 @@ class ChainTest extends TestCase $this->assertSame(0, $this->cache->getTimestamp($this->key)); } @@ -614,10 +654,10 @@ { $cache = new ChainCache([]); diff --git a/tests/Cache/FilesystemTest.php b/tests/Cache/FilesystemTest.php -index e880562..ae4b7d0 100644 +index 85b1e97..74dd31b 100644 --- a/tests/Cache/FilesystemTest.php +++ b/tests/Cache/FilesystemTest.php -@@ -11,6 +11,7 @@ namespace Twig\Tests\Cache; +@@ -20,6 +20,7 @@ namespace Twig\Tests\Cache; * file that was distributed with this source code. */ @@ -625,7 +665,7 @@ use PHPUnit\Framework\TestCase; use Twig\Cache\FilesystemCache; use Twig\Tests\FilesystemHelper; -@@ -162,9 +163,8 @@ class FilesystemTest extends TestCase +@@ -171,9 +172,8 @@ class FilesystemTest extends TestCase /** * Test file cache is tolerant towards trailing (back)slashes on the configured cache directory. @@ -637,10 +677,10 @@ { $cache = new FilesystemCache($input); diff --git a/tests/Cache/ReadOnlyFilesystemTest.php b/tests/Cache/ReadOnlyFilesystemTest.php -index 34bc14f..aa2eeab 100644 +index d67276b..9e77f43 100644 --- a/tests/Cache/ReadOnlyFilesystemTest.php +++ b/tests/Cache/ReadOnlyFilesystemTest.php -@@ -11,6 +11,7 @@ namespace Twig\Tests\Cache; +@@ -20,6 +20,7 @@ namespace Twig\Tests\Cache; * file that was distributed with this source code. */ @@ -648,7 +688,7 @@ use PHPUnit\Framework\TestCase; use Twig\Cache\ReadOnlyFilesystemCache; use Twig\Tests\FilesystemHelper; -@@ -100,9 +101,8 @@ class ReadOnlyFilesystemTest extends TestCase +@@ -109,9 +110,8 @@ class ReadOnlyFilesystemTest extends TestCase /** * Test file cache is tolerant towards trailing (back)slashes on the configured cache directory. @@ -660,10 +700,10 @@ { $cache = new ReadOnlyFilesystemCache($input); diff --git a/tests/CustomExtensionTest.php b/tests/CustomExtensionTest.php -index 40be3b3..a38fe75 100644 +index 9f1fbd3..ba2b3a0 100644 --- a/tests/CustomExtensionTest.php +++ b/tests/CustomExtensionTest.php -@@ -11,6 +11,7 @@ namespace Twig\Tests; +@@ -20,6 +20,7 @@ namespace Twig\Tests; * file that was distributed with this source code. */ @@ -671,30 +711,42 @@ use PHPUnit\Framework\TestCase; use Twig\Environment; use Twig\Extension\ExtensionInterface; -@@ -18,9 +19,7 @@ use Twig\Loader\ArrayLoader; +@@ -32,6 +33,7 @@ class CustomExtensionTest extends TestCase + * + * @dataProvider provideInvalidExtensions + */ ++ #[DataProvider('provideInvalidExtensions')] + public function testGetInvalidOperators(ExtensionInterface $extension, $expectedExceptionMessage) + { + $env = new Environment(new ArrayLoader()); +diff --git a/tests/DeprecatedCallableInfoTest.php b/tests/DeprecatedCallableInfoTest.php +index 166eb9b..4a24827 100644 +--- a/tests/DeprecatedCallableInfoTest.php ++++ b/tests/DeprecatedCallableInfoTest.php +@@ -25,9 +25,7 @@ use Twig\DeprecatedCallableInfo; - class CustomExtensionTest extends TestCase + class DeprecatedCallableInfoTest extends TestCase { - /** -- * @dataProvider provideInvalidExtensions +- * @dataProvider provideTestsForTriggerDeprecation - */ -+ #[DataProvider('provideInvalidExtensions')] - public function testGetInvalidOperators(ExtensionInterface $extension, $expectedExceptionMessage) ++ #[DataProvider('provideTestsForTriggerDeprecation')] + public function testTriggerDeprecation($expected, DeprecatedCallableInfo $info) { - $env = new Environment(new ArrayLoader()); + $info->setType('function'); diff --git a/tests/ErrorTest.php b/tests/ErrorTest.php -index b7da2d5..b21081b 100644 +index 1dc145f..37d8fca 100644 --- a/tests/ErrorTest.php +++ b/tests/ErrorTest.php -@@ -11,6 +11,7 @@ namespace Twig\Tests; +@@ -20,6 +20,7 @@ namespace Twig\Tests; * file that was distributed with this source code. */ +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; - use Twig\Environment; - use Twig\Error\Error; -@@ -124,9 +125,7 @@ EOHTML, + use Twig\Attribute\YieldReady; + use Twig\Compiler; +@@ -141,9 +142,7 @@ EOHTML, } } @@ -705,11 +757,22 @@ public function testTwigExceptionAddsFileAndLine($templates, $name, $line) { $loader = new ArrayLoader($templates); +@@ -269,9 +268,7 @@ EOHTML, + } + } + +- /** +- * @dataProvider getErrorWithoutLineAndContextData +- */ ++ #[DataProvider('getErrorWithoutLineAndContextData')] + public function testErrorWithoutLineAndContext(LoaderInterface $loader, bool $debug, bool $addDebugInfo, bool $exceptionWithLineAndContext, int $errorLine) + { + $twig = new Environment($loader, ['debug' => $debug, 'cache' => false]); diff --git a/tests/ExpressionParserTest.php b/tests/ExpressionParserTest.php -index d3887e9..b49d9e9 100644 +index 1bd32ac..56a6a81 100644 --- a/tests/ExpressionParserTest.php +++ b/tests/ExpressionParserTest.php -@@ -11,6 +11,7 @@ namespace Twig\Tests; +@@ -20,6 +20,7 @@ namespace Twig\Tests; * file that was distributed with this source code. */ @@ -717,7 +780,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Twig\Attribute\FirstClassTwigCallableReady; -@@ -38,9 +39,7 @@ class ExpressionParserTest extends TestCase +@@ -53,9 +54,7 @@ class ExpressionParserTest extends TestCase { use ExpectDeprecationTrait; @@ -728,7 +791,7 @@ public function testCanOnlyAssignToNames($template) { $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); -@@ -68,9 +67,7 @@ class ExpressionParserTest extends TestCase +@@ -83,9 +82,7 @@ class ExpressionParserTest extends TestCase ]; } @@ -739,7 +802,7 @@ public function testSequenceExpression($template, $expected) { $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); -@@ -81,9 +78,7 @@ class ExpressionParserTest extends TestCase +@@ -96,9 +93,7 @@ class ExpressionParserTest extends TestCase $this->assertEquals($expected, $parser->parse($stream)->getNode('body')->getNode('0')->getNode('expr')); } @@ -750,18 +813,37 @@ public function testSequenceSyntaxError($template) { $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); -@@ -220,9 +215,7 @@ class ExpressionParserTest extends TestCase - $parser->parse($stream); +@@ -247,6 +242,7 @@ class ExpressionParserTest extends TestCase + /** + * @dataProvider getTestsForString + */ ++ #[DataProvider('getTestsForString')] + public function testStringExpression($template, $expected) + { + $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false, 'optimizations' => 0]); +@@ -301,9 +297,7 @@ class ExpressionParserTest extends TestCase + ]; } - /** -- * @dataProvider getTestsForString +- * @dataProvider getTestsForNullSafeOperator - */ -+ #[DataProvider('getTestsForString')] - public function testStringExpression($template, $expected) ++ #[DataProvider('getTestsForNullSafeOperator')] + public function testNullSafeOperator($template, $data, $expected) { - $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false, 'optimizations' => 0]); -@@ -288,9 +281,7 @@ class ExpressionParserTest extends TestCase + $env = new Environment(new ArrayLoader(['template' => $template]), ['strict_variables' => true]); +@@ -373,9 +367,7 @@ class ExpressionParserTest extends TestCase + ]; + } + +- /** +- * @dataProvider getTestForInvalidNullSafeOperatorShortCircuiting +- */ ++ #[DataProvider('getTestForInvalidNullSafeOperatorShortCircuiting')] + public function testInvalidNullSafeOperatorShortCircuiting(string $template, array $data, string $expectedMessage) + { + $env = new Environment(new ArrayLoader(['template' => $template]), ['strict_variables' => true]); +@@ -411,9 +403,7 @@ class ExpressionParserTest extends TestCase $parser->parse($env->tokenize(new Source('{% macro foo("a") %}{% endmacro %}', 'index'))); } @@ -772,7 +854,7 @@ public function testMacroDefinitionDoesNotSupportNonConstantDefaultValues($template) { $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); -@@ -310,9 +301,7 @@ class ExpressionParserTest extends TestCase +@@ -433,9 +423,7 @@ class ExpressionParserTest extends TestCase ]; } @@ -783,30 +865,75 @@ public function testMacroDefinitionSupportsConstantDefaultValues($template) { $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); +@@ -713,9 +701,7 @@ class ExpressionParserTest extends TestCase + $this->expectNotToPerformAssertions(); + } + +- /** +- * @dataProvider getBindingPowerTests +- */ ++ #[DataProvider('getBindingPowerTests')] + public function testBindingPower(string $expression, string $expectedExpression, mixed $expectedResult, array $context = []) + { + $env = new Environment(new ArrayLoader([ +diff --git a/tests/Extension/AttributeExtensionTest.php b/tests/Extension/AttributeExtensionTest.php +index 41b215c..6e24f56 100644 +--- a/tests/Extension/AttributeExtensionTest.php ++++ b/tests/Extension/AttributeExtensionTest.php +@@ -25,9 +25,7 @@ use Twig\TwigTest; + + class AttributeExtensionTest extends TestCase + { +- /** +- * @dataProvider provideFilters +- */ ++ #[DataProvider('provideFilters')] + public function testFilter(string $name, string $method, array $options) + { + $extension = new AttributeExtension(ExtensionWithAttributes::class); +@@ -54,9 +52,7 @@ class AttributeExtensionTest extends TestCase + yield 'pattern' => ['pattern_*_filter', 'patternFilter', []]; + } + +- /** +- * @dataProvider provideFunctions +- */ ++ #[DataProvider('provideFunctions')] + public function testFunction(string $name, string $method, array $options) + { + $extension = new AttributeExtension(ExtensionWithAttributes::class); +@@ -83,9 +79,7 @@ class AttributeExtensionTest extends TestCase + yield 'deprecated' => ['deprecated_function', 'deprecatedFunction', ['deprecation_info' => new DeprecatedCallableInfo('foo/bar', '1.2')]]; + } + +- /** +- * @dataProvider provideTests +- */ ++ #[DataProvider('provideTests')] + public function testTest(string $name, string $method, array $options) + { + $extension = new AttributeExtension(ExtensionWithAttributes::class); diff --git a/tests/Extension/CoreTest.php b/tests/Extension/CoreTest.php -index bbea3d5..4e83a17 100644 +index 2828d29..6ffadec 100644 --- a/tests/Extension/CoreTest.php +++ b/tests/Extension/CoreTest.php -@@ -11,6 +11,7 @@ namespace Twig\Tests\Extension; +@@ -20,6 +20,7 @@ namespace Twig\Tests\Extension; * file that was distributed with this source code. */ +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; + use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Twig\Environment; - use Twig\Error\RuntimeError; -@@ -22,9 +23,7 @@ use Twig\Sandbox\SecurityPolicy; - - class CoreTest extends TestCase - { -- /** -- * @dataProvider provideCycleCases -- */ +@@ -37,6 +38,7 @@ class CoreTest extends TestCase + /** + * @dataProvider provideCycleCases + */ + #[DataProvider('provideCycleCases')] public function testCycleFunction($values, $position, $expected) { $this->assertSame($expected, CoreExtension::cycle($values, $position)); -@@ -45,9 +44,7 @@ class CoreTest extends TestCase +@@ -57,9 +59,7 @@ class CoreTest extends TestCase ]; } @@ -817,7 +944,7 @@ public function testCycleFunctionThrowRuntimeError($values, mixed $position = null) { $this->expectException(RuntimeError::class); -@@ -63,9 +60,7 @@ class CoreTest extends TestCase +@@ -75,9 +75,7 @@ class CoreTest extends TestCase ]; } @@ -828,7 +955,7 @@ public function testRandomFunction(array $expectedInArray, $value1, $value2 = null) { for ($i = 0; $i < 100; ++$i) { -@@ -163,9 +158,7 @@ class CoreTest extends TestCase +@@ -175,9 +173,7 @@ class CoreTest extends TestCase $this->assertEquals($output, 'éÄ'); } @@ -839,7 +966,7 @@ public function testTwigFirst($expected, $input) { $this->assertSame($expected, CoreExtension::first('UTF-8', $input)); -@@ -184,9 +177,7 @@ class CoreTest extends TestCase +@@ -196,9 +192,7 @@ class CoreTest extends TestCase ]; } @@ -850,7 +977,7 @@ public function testTwigLast($expected, $input) { $this->assertSame($expected, CoreExtension::last('UTF-8', $input)); -@@ -205,9 +196,7 @@ class CoreTest extends TestCase +@@ -217,9 +211,7 @@ class CoreTest extends TestCase ]; } @@ -861,7 +988,7 @@ public function testArrayKeysFilter(array $expected, $input) { $this->assertSame($expected, CoreExtension::keys($input)); -@@ -228,9 +217,7 @@ class CoreTest extends TestCase +@@ -240,9 +232,7 @@ class CoreTest extends TestCase ]; } @@ -872,7 +999,7 @@ public function testInFilter($expected, $value, $compare) { $this->assertSame($expected, CoreExtension::inFilter($value, $compare)); -@@ -256,9 +243,7 @@ class CoreTest extends TestCase +@@ -268,9 +258,7 @@ class CoreTest extends TestCase ]; } @@ -883,7 +1010,7 @@ public function testSliceFilter($expected, $input, $start, $length = null, $preserveKeys = false) { $this->assertSame($expected, CoreExtension::slice('UTF-8', $input, $start, $length, $preserveKeys)); -@@ -285,9 +270,7 @@ class CoreTest extends TestCase +@@ -297,9 +285,7 @@ class CoreTest extends TestCase ]; } @@ -894,11 +1021,39 @@ public function testCompare($expected, $a, $b) { $this->assertSame($expected, CoreExtension::compare($a, $b)); +diff --git a/tests/Extension/EscaperTest.php b/tests/Extension/EscaperTest.php +index 09aa4ca..8f0a835 100644 +--- a/tests/Extension/EscaperTest.php ++++ b/tests/Extension/EscaperTest.php +@@ -30,10 +30,9 @@ use Twig\RuntimeLoader\FactoryRuntimeLoader; + class EscaperTest extends TestCase + { + /** +- * @dataProvider provideCustomEscaperCases +- * + * @group legacy + */ ++ #[DataProvider('provideCustomEscaperCases')] + public function testCustomEscaper($expected, $string, $strategy) + { + $twig = new Environment(new ArrayLoader()); +@@ -52,10 +51,9 @@ class EscaperTest extends TestCase + } + + /** +- * @dataProvider provideCustomEscaperCases +- * + * @group legacy + */ ++ #[DataProvider('provideCustomEscaperCases')] + public function testCustomEscaperWithoutCallingSetEscaperRuntime($expected, $string, $strategy) + { + $twig = new Environment(new ArrayLoader()); diff --git a/tests/Extension/SandboxTest.php b/tests/Extension/SandboxTest.php -index 3fb1f65..fd655f7 100644 +index 07423f2..6cd278c 100644 --- a/tests/Extension/SandboxTest.php +++ b/tests/Extension/SandboxTest.php -@@ -11,6 +11,7 @@ namespace Twig\Tests\Extension; +@@ -20,6 +20,7 @@ namespace Twig\Tests\Extension; * file that was distributed with this source code. */ @@ -906,7 +1061,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Twig\Environment; -@@ -76,9 +77,7 @@ class SandboxTest extends TestCase +@@ -86,9 +87,7 @@ class SandboxTest extends TestCase ]; } @@ -917,7 +1072,19 @@ public function testSandboxForCoreTags(string $tag, string $template) { $twig = $this->getEnvironment(true, [], self::$templates, []); -@@ -279,9 +278,7 @@ class SandboxTest extends TestCase +@@ -123,10 +122,9 @@ class SandboxTest extends TestCase + } + + /** +- * @dataProvider getSandboxedForExtendsAndUseTagsTests +- * + * @group legacy + */ ++ #[DataProvider('getSandboxedForExtendsAndUseTagsTests')] + public function testSandboxForExtendsAndUseTags(string $tag, string $template) + { + $this->expectDeprecation(\sprintf('Since twig/twig 3.12: The "%s" tag is always allowed in sandboxes, but won\'t be in 4.0, please enable it explicitly in your sandbox policy if needed.', $tag)); +@@ -289,9 +287,7 @@ class SandboxTest extends TestCase } } @@ -927,23 +1094,31 @@ + #[DataProvider('getSandboxUnallowedToStringTests')] public function testSandboxUnallowedToString($template) { - $twig = $this->getEnvironment(true, [], ['index' => $template], [], ['upper', 'join', 'replace'], ['Twig\Tests\Extension\FooObject' => 'getAnotherFooObject'], [], ['random']); -@@ -324,9 +321,7 @@ class SandboxTest extends TestCase - ]; + $twig = $this->getEnvironment(true, [], ['index' => $template], ['if', 'do', 'for', 'set'], ['upper', 'join', 'replace', 'format', 'split'], ['Twig\Tests\Extension\FooObject' => 'getAnotherFooObject'], [], ['random', 'range', 'my_func']); +@@ -493,6 +489,7 @@ class SandboxTest extends TestCase + /** + * @dataProvider getSandboxAllowedToStringTests + */ ++ #[DataProvider('getSandboxAllowedToStringTests')] + public function testSandboxAllowedToString($template, $output) + { + $twig = $this->getEnvironment(true, [], ['index' => $template], ['set', 'do'], [], ['Twig\Tests\Extension\FooObject' => ['foo', 'getAnotherFooObject']]); +@@ -959,9 +956,7 @@ EOF + $twig->load('1_basic')->render([]); } - /** -- * @dataProvider getSandboxAllowedToStringTests +- * @dataProvider provideSourcePolicyArrowBlockedTemplates - */ -+ #[DataProvider('getSandboxAllowedToStringTests')] - public function testSandboxAllowedToString($template, $output) ++ #[DataProvider('provideSourcePolicyArrowBlockedTemplates')] + public function testSourcePolicyBlocksNonClosureCallableInArrow(string $template) { - $twig = $this->getEnvironment(true, [], ['index' => $template], ['set'], [], ['Twig\Tests\Extension\FooObject' => ['foo', 'getAnotherFooObject']]); + $sourcePolicy = new class implements \Twig\Sandbox\SourcePolicyInterface { diff --git a/tests/FileExtensionEscapingStrategyTest.php b/tests/FileExtensionEscapingStrategyTest.php -index ed5ce06..411f9da 100644 +index 8b05810..5fa835c 100644 --- a/tests/FileExtensionEscapingStrategyTest.php +++ b/tests/FileExtensionEscapingStrategyTest.php -@@ -11,14 +11,13 @@ namespace Twig\Tests; +@@ -20,14 +20,13 @@ namespace Twig\Tests; * file that was distributed with this source code. */ @@ -961,10 +1136,10 @@ { $this->assertSame($strategy, FileExtensionEscapingStrategy::guess($filename)); diff --git a/tests/LexerTest.php b/tests/LexerTest.php -index 806b655..7ae9985 100644 +index 04f0770..a6962a4 100644 --- a/tests/LexerTest.php +++ b/tests/LexerTest.php -@@ -11,6 +11,7 @@ namespace Twig\Tests; +@@ -20,6 +20,7 @@ namespace Twig\Tests; * file that was distributed with this source code. */ @@ -972,7 +1147,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Twig\Environment; -@@ -181,9 +182,7 @@ class LexerTest extends TestCase +@@ -180,9 +181,7 @@ class LexerTest extends TestCase $this->assertEquals('922337203685477580700', $node->getValue()); } @@ -983,7 +1158,18 @@ public function testStringWithEscapedDelimiter(string $template, string $expected) { $lexer = new Lexer(new Environment(new ArrayLoader())); -@@ -484,9 +483,7 @@ bar +@@ -256,9 +255,8 @@ class LexerTest extends TestCase + + /** + * @group legacy +- * +- * @dataProvider getStringWithEscapedDelimiterProducingDeprecation + */ ++ #[DataProvider('getStringWithEscapedDelimiterProducingDeprecation')] + public function testStringWithEscapedDelimiterProducingDeprecation(string $template, string $expected, string $expectedDeprecation) + { + $this->expectDeprecation($expectedDeprecation); +@@ -533,9 +531,7 @@ bar $this->addToAssertionCount(1); } @@ -994,7 +1180,7 @@ public function testErrorsAtTheEndOfTheStream(string $template) { $lexer = new Lexer(new Environment(new ArrayLoader())); -@@ -507,9 +504,7 @@ bar +@@ -556,9 +552,7 @@ bar yield ['{{ ..']; } @@ -1005,11 +1191,66 @@ public function testStrings(string $expected) { $template = '{{ "'.$expected.'" }}'; +@@ -594,9 +588,7 @@ bar + $this->assertTrue($stream->isEOF()); + } + +- /** +- * @dataProvider getTemplateForInlineCommentsForVariable +- */ ++ #[DataProvider('getTemplateForInlineCommentsForVariable')] + public function testInlineCommentForVariable(string $template) + { + $lexer = new Lexer(new Environment(new ArrayLoader())); +@@ -627,9 +619,7 @@ bar + }}']; + } + +- /** +- * @dataProvider getTemplateForInlineCommentsForBlock +- */ ++ #[DataProvider('getTemplateForInlineCommentsForBlock')] + public function testInlineCommentForBlock(string $template) + { + $lexer = new Lexer(new Environment(new ArrayLoader())); +@@ -665,9 +655,7 @@ bar + %}me{% endif %}']; + } + +- /** +- * @dataProvider getTemplateForInlineCommentsForComment +- */ ++ #[DataProvider('getTemplateForInlineCommentsForComment')] + public function testInlineCommentForComment(string $template) + { + $lexer = new Lexer(new Environment(new ArrayLoader())); +@@ -682,9 +670,7 @@ bar + #}']; + } + +- /** +- * @dataProvider getTemplateForUnclosedBracketInExpression +- */ ++ #[DataProvider('getTemplateForUnclosedBracketInExpression')] + public function testUnclosedBracketInExpression(string $template, string $bracket) + { + $lexer = new Lexer(new Environment(new ArrayLoader())); +@@ -703,9 +689,7 @@ bar + yield ['{{ (([1]) + 3 }}', '(']; + } + +- /** +- * @dataProvider getTemplateForUnexpectedBracketInExpression +- */ ++ #[DataProvider('getTemplateForUnexpectedBracketInExpression')] + public function testUnexpectedBracketInExpression(string $template, string $bracket) + { + $lexer = new Lexer(new Environment(new ArrayLoader())); diff --git a/tests/Loader/FilesystemTest.php b/tests/Loader/FilesystemTest.php -index c7315ea..62b0645 100644 +index f4c0b97..945072a 100644 --- a/tests/Loader/FilesystemTest.php +++ b/tests/Loader/FilesystemTest.php -@@ -11,6 +11,7 @@ namespace Twig\Tests\Loader; +@@ -20,6 +20,7 @@ namespace Twig\Tests\Loader; * file that was distributed with this source code. */ @@ -1017,7 +1258,7 @@ use PHPUnit\Framework\TestCase; use Twig\Environment; use Twig\Error\LoaderError; -@@ -26,9 +27,7 @@ class FilesystemTest extends TestCase +@@ -35,9 +36,7 @@ class FilesystemTest extends TestCase $this->assertEquals(realpath($path.'/errors/index.html'), realpath($loader->getSourceContext('errors/index.html')->getPath())); } @@ -1028,7 +1269,7 @@ public function testSecurity($template) { $loader = new FilesystemLoader([__DIR__.'/../Fixtures']); -@@ -71,9 +70,7 @@ class FilesystemTest extends TestCase +@@ -80,9 +79,7 @@ class FilesystemTest extends TestCase ]; } @@ -1039,7 +1280,7 @@ public function testPaths($basePath, $cacheKey, $rootPath) { $loader = new FilesystemLoader([$basePath.'/normal', $basePath.'/normal_bis'], $rootPath); -@@ -206,9 +203,7 @@ class FilesystemTest extends TestCase +@@ -215,9 +212,7 @@ class FilesystemTest extends TestCase ]; } @@ -1050,11 +1291,26 @@ public function testArrayInheritance(string $templateName) { $loader = new FilesystemLoader([]); +diff --git a/tests/Node/Expression/MacroReferenceTest.php b/tests/Node/Expression/MacroReferenceTest.php +index 817542c..5301774 100644 +--- a/tests/Node/Expression/MacroReferenceTest.php ++++ b/tests/Node/Expression/MacroReferenceTest.php +@@ -18,9 +18,7 @@ use Twig\Node\Expression\Variable\TemplateVariable; + + class MacroReferenceTest extends TestCase + { +- /** +- * @dataProvider provideInvalidMacroNames +- */ ++ #[DataProvider('provideInvalidMacroNames')] + public function testConstructorRejectsNonIdentifierName(string $name) + { + $this->expectException(\LogicException::class); diff --git a/tests/Node/NodeTest.php b/tests/Node/NodeTest.php -index 13ad334..a4d3f4a 100644 +index a71d936..b3b7aef 100644 --- a/tests/Node/NodeTest.php +++ b/tests/Node/NodeTest.php -@@ -11,6 +11,7 @@ namespace Twig\Tests\Node; +@@ -20,6 +20,7 @@ namespace Twig\Tests\Node; * file that was distributed with this source code. */ @@ -1062,7 +1318,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Twig\Node\NameDeprecation; -@@ -75,9 +76,7 @@ EOF +@@ -81,9 +82,7 @@ EOF, (string) $node); $this->assertFalse($node->getAttribute('foo', false)); } @@ -1073,7 +1329,7 @@ public function testAttributeDeprecationWithoutAlternative() { $node = new NodeForTest([], ['foo' => false]); -@@ -87,9 +86,7 @@ EOF +@@ -93,9 +92,7 @@ EOF, (string) $node); $this->assertFalse($node->getAttribute('foo')); } @@ -1084,7 +1340,7 @@ public function testAttributeDeprecationWithAlternative() { $node = new NodeForTest([], ['foo' => false]); -@@ -107,9 +104,7 @@ EOF +@@ -113,9 +110,7 @@ EOF, (string) $node); $this->assertSame($foo, $node->getNode('foo', false)); } @@ -1095,7 +1351,7 @@ public function testNodeDeprecationWithoutAlternative() { $node = new NodeForTest(['foo' => $foo = new NodeForTest()]); -@@ -119,9 +114,7 @@ EOF +@@ -125,9 +120,7 @@ EOF, (string) $node); $this->assertSame($foo, $node->getNode('foo')); } @@ -1107,10 +1363,10 @@ { $node = new NodeForTest(['foo' => $foo = new NodeForTest()]); diff --git a/tests/NodeVisitor/OptimizerTest.php b/tests/NodeVisitor/OptimizerTest.php -index 5964b7b..a7ebfd7 100644 +index b333f56..c62efdf 100644 --- a/tests/NodeVisitor/OptimizerTest.php +++ b/tests/NodeVisitor/OptimizerTest.php -@@ -11,6 +11,7 @@ namespace Twig\Tests\NodeVisitor; +@@ -20,6 +20,7 @@ namespace Twig\Tests\NodeVisitor; * file that was distributed with this source code. */ @@ -1118,7 +1374,7 @@ use PHPUnit\Framework\TestCase; use Twig\Environment; use Twig\Loader\ArrayLoader; -@@ -78,9 +79,7 @@ class OptimizerTest extends TestCase +@@ -87,9 +88,7 @@ class OptimizerTest extends TestCase } } @@ -1130,10 +1386,10 @@ { $env = new Environment(new ArrayLoader(), ['cache' => false]); diff --git a/tests/ParserTest.php b/tests/ParserTest.php -index 1b222d0..5483791 100644 +index 37c468e..d96a587 100644 --- a/tests/ParserTest.php +++ b/tests/ParserTest.php -@@ -11,6 +11,7 @@ namespace Twig\Tests; +@@ -20,6 +20,7 @@ namespace Twig\Tests; * file that was distributed with this source code. */ @@ -1141,7 +1397,7 @@ use PHPUnit\Framework\TestCase; use Twig\Environment; use Twig\Error\SyntaxError; -@@ -61,9 +62,7 @@ class ParserTest extends TestCase +@@ -71,9 +72,7 @@ class ParserTest extends TestCase $parser->parse($stream); } @@ -1152,7 +1408,7 @@ public function testFilterBodyNodes($input, $expected) { $parser = $this->getParser(); -@@ -91,9 +90,7 @@ class ParserTest extends TestCase +@@ -100,9 +99,7 @@ class ParserTest extends TestCase ]; } @@ -1163,7 +1419,7 @@ public function testFilterBodyNodesThrowsException($input) { $parser = $this->getParser(); -@@ -113,9 +110,7 @@ class ParserTest extends TestCase +@@ -121,9 +118,7 @@ class ParserTest extends TestCase ]; } @@ -1175,10 +1431,10 @@ { $parser = $this->getParser(); diff --git a/tests/Runtime/EscaperRuntimeTest.php b/tests/Runtime/EscaperRuntimeTest.php -index 706c007..86f2e2a 100644 +index 7d02249..5553825 100644 --- a/tests/Runtime/EscaperRuntimeTest.php +++ b/tests/Runtime/EscaperRuntimeTest.php -@@ -11,6 +11,7 @@ namespace Twig\Tests\Runtime; +@@ -20,6 +20,7 @@ namespace Twig\Tests\Runtime; * file that was distributed with this source code. */ @@ -1186,7 +1442,7 @@ use PHPUnit\Framework\TestCase; use Twig\Error\RuntimeError; use Twig\Runtime\EscaperRuntime; -@@ -347,9 +348,7 @@ class EscaperRuntimeTest extends TestCase +@@ -383,9 +384,7 @@ class EscaperRuntimeTest extends TestCase (new EscaperRuntime())->escape('foo', 'bar'); } @@ -1197,7 +1453,7 @@ public function testCustomEscaper($expected, $string, $strategy, $charset) { $escaper = new EscaperRuntime(); -@@ -366,9 +365,7 @@ class EscaperRuntimeTest extends TestCase +@@ -402,9 +401,7 @@ class EscaperRuntimeTest extends TestCase ]; } @@ -1209,10 +1465,10 @@ { $obj = new Extension_TestClass(); diff --git a/tests/TemplateTest.php b/tests/TemplateTest.php -index eb0e2db..afe2882 100644 +index 7102db9..612d70b 100644 --- a/tests/TemplateTest.php +++ b/tests/TemplateTest.php -@@ -11,6 +11,7 @@ namespace Twig\Tests; +@@ -20,6 +20,7 @@ namespace Twig\Tests; * file that was distributed with this source code. */ @@ -1220,7 +1476,7 @@ use PHPUnit\Framework\TestCase; use Twig\Environment; use Twig\Error\RuntimeError; -@@ -34,9 +35,7 @@ class TemplateTest extends TestCase +@@ -43,9 +44,7 @@ class TemplateTest extends TestCase $template->displayBlock('foo', [], ['foo' => [new \stdClass(), 'foo']]); } @@ -1231,7 +1487,7 @@ public function testGetAttributeExceptions($template, $message) { $templates = ['index' => $template]; -@@ -84,9 +83,7 @@ class TemplateTest extends TestCase +@@ -93,9 +92,7 @@ class TemplateTest extends TestCase ]; } @@ -1242,7 +1498,7 @@ public function testGetAttributeWithSandbox($object, $item, $allowed) { $twig = new Environment(new ArrayLoader()); -@@ -123,9 +120,7 @@ class TemplateTest extends TestCase +@@ -132,9 +129,7 @@ class TemplateTest extends TestCase ]; } @@ -1253,7 +1509,18 @@ public function testRenderTemplateWithoutOutput(string $template) { $twig = new Environment(new ArrayLoader(['index' => $template])); -@@ -206,9 +201,7 @@ class TemplateTest extends TestCase +@@ -150,9 +145,7 @@ class TemplateTest extends TestCase + ]; + } + +- /** +- * @dataProvider getNullCoalesceWithImportedMacroData +- */ ++ #[DataProvider('getNullCoalesceWithImportedMacroData')] + public function testNullCoalesceWithImportedMacro(array $templates, string $expected) + { + $twig = new Environment(new ArrayLoader($templates)); +@@ -252,9 +245,7 @@ class TemplateTest extends TestCase $this->assertSame('EmptyString', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, null), 'null is treated as "" when accessing a sequence/mapping (equals PHP behavior)'); } @@ -1264,7 +1531,7 @@ public function testGetAttribute($defined, $value, $object, $item, $arguments, $type) { $twig = new Environment(new ArrayLoader()); -@@ -217,9 +210,7 @@ class TemplateTest extends TestCase +@@ -263,9 +254,7 @@ class TemplateTest extends TestCase $this->assertEquals($value, CoreExtension::getAttribute($twig, $template->getSourceContext(), $object, $item, $arguments, $type)); } @@ -1275,7 +1542,7 @@ public function testGetAttributeStrict($defined, $value, $object, $item, $arguments, $type, $exceptionMessage = null) { $twig = new Environment(new ArrayLoader(), ['strict_variables' => true]); -@@ -236,9 +227,7 @@ class TemplateTest extends TestCase +@@ -282,9 +271,7 @@ class TemplateTest extends TestCase } } @@ -1286,7 +1553,7 @@ public function testGetAttributeDefined($defined, $value, $object, $item, $arguments, $type) { $twig = new Environment(new ArrayLoader()); -@@ -247,9 +236,7 @@ class TemplateTest extends TestCase +@@ -293,9 +280,7 @@ class TemplateTest extends TestCase $this->assertEquals($defined, CoreExtension::getAttribute($twig, $template->getSourceContext(), $object, $item, $arguments, $type, true)); } @@ -1298,10 +1565,10 @@ { $twig = new Environment(new ArrayLoader(), ['strict_variables' => true]); diff --git a/tests/TokenParser/TypesTokenParserTest.php b/tests/TokenParser/TypesTokenParserTest.php -index 0acbb4d..428b059 100644 +index 49e0ac0..c285368 100644 --- a/tests/TokenParser/TypesTokenParserTest.php +++ b/tests/TokenParser/TypesTokenParserTest.php -@@ -2,6 +2,7 @@ +@@ -11,6 +11,7 @@ namespace Twig\Tests\TokenParser; @@ -1309,7 +1576,7 @@ use PHPUnit\Framework\TestCase; use Twig\Environment; use Twig\Loader\ArrayLoader; -@@ -10,7 +11,7 @@ use Twig\Source; +@@ -19,7 +20,7 @@ use Twig\Source; class TypesTokenParserTest extends TestCase { @@ -1318,3 +1585,18 @@ public function testMappingParsing(string $template, array $expected): void { $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); +diff --git a/tests/Util/CallableArgumentsExtractorTest.php b/tests/Util/CallableArgumentsExtractorTest.php +index f97bf57..aba5546 100644 +--- a/tests/Util/CallableArgumentsExtractorTest.php ++++ b/tests/Util/CallableArgumentsExtractorTest.php +@@ -95,9 +95,7 @@ class CallableArgumentsExtractorTest extends TestCase + $this->assertEquals(['arg1'], $this->getArguments('custom_static_function', __CLASS__.'::customStaticFunction', ['arg1' => 'arg1'])); + } + +- /** +- * @dataProvider getGetArgumentsConversionData +- */ ++ #[DataProvider('getGetArgumentsConversionData')] + public function testGetArgumentsConversion($arg1, $arg2) + { + $this->assertEquals([null], $this->getArguments('custom', eval("return fn (\$$arg1) => '';"), [$arg1 => null])); diff -Nru php-twig-3.20.0/debian/patches/0007-Twig-4-Add-PHPUnit-as-dev-dependency.patch php-twig-3.26.0/debian/patches/0007-Twig-4-Add-PHPUnit-as-dev-dependency.patch --- php-twig-3.20.0/debian/patches/0007-Twig-4-Add-PHPUnit-as-dev-dependency.patch 2025-02-19 05:49:38.000000000 +0000 +++ php-twig-3.26.0/debian/patches/0007-Twig-4-Add-PHPUnit-as-dev-dependency.patch 2026-05-26 07:00:53.000000000 +0000 @@ -4,39 +4,17 @@ Origin: backport, https://github.com/twigphp/twig/commit/c352a8d283bdf39e1b6bcc31e1a9be0bcfc82cb1 --- - composer.json | 4 ++-- - tests/DeprecatedCallableInfoTest.php | 5 ++--- - tests/LexerTest.php | 12 +++--------- - tests/Util/CallableArgumentsExtractorTest.php | 5 ++--- - 4 files changed, 9 insertions(+), 17 deletions(-) + tests/DeprecatedCallableInfoTest.php | 1 + + tests/Extension/AttributeExtensionTest.php | 1 + + tests/Node/Expression/MacroReferenceTest.php | 1 + + tests/Util/CallableArgumentsExtractorTest.php | 1 + + 4 files changed, 4 insertions(+) -diff --git a/composer.json b/composer.json -index 3662366..1962434 100644 ---- a/composer.json -+++ b/composer.json -@@ -5,7 +5,6 @@ - "keywords": ["templating"], - "homepage": "https://twig.symfony.com", - "license": "BSD-3-Clause", -- "minimum-stability": "dev", - "authors": [ - { - "name": "Fabien Potencier", -@@ -32,7 +31,8 @@ - "require-dev": { - "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0", - "psr/container": "^1.0|^2.0", -- "phpstan/phpstan": "^2.0" -+ "phpstan/phpstan": "^2.0", -+ "phpunit/phpunit": "^11.4" - }, - "autoload": { - "files": [ diff --git a/tests/DeprecatedCallableInfoTest.php b/tests/DeprecatedCallableInfoTest.php -index 454d826..f30cc2e 100644 +index 4a24827..4aa50f1 100644 --- a/tests/DeprecatedCallableInfoTest.php +++ b/tests/DeprecatedCallableInfoTest.php -@@ -11,14 +11,13 @@ namespace Twig\Tests; +@@ -20,6 +20,7 @@ namespace Twig\Tests; * file that was distributed with this source code. */ @@ -44,57 +22,35 @@ use PHPUnit\Framework\TestCase; use Twig\DeprecatedCallableInfo; - class DeprecatedCallableInfoTest extends TestCase - { -- /** -- * @dataProvider provideTestsForTriggerDeprecation -- */ -+ #[DataProvider('provideTestsForTriggerDeprecation')] - public function testTriggerDeprecation($expected, DeprecatedCallableInfo $info) - { - $info->setType('function'); -diff --git a/tests/LexerTest.php b/tests/LexerTest.php -index 7ae9985..4a8cb53 100644 ---- a/tests/LexerTest.php -+++ b/tests/LexerTest.php -@@ -540,9 +540,7 @@ bar - $this->assertTrue($stream->isEOF()); - } - -- /** -- * @dataProvider getTemplateForInlineCommentsForVariable -- */ -+ #[DataProvider('getTemplateForInlineCommentsForVariable')] - public function testInlineCommentForVariable(string $template) - { - $lexer = new Lexer(new Environment(new ArrayLoader())); -@@ -573,9 +571,7 @@ bar - }}']; - } - -- /** -- * @dataProvider getTemplateForInlineCommentsForBlock -- */ -+ #[DataProvider('getTemplateForInlineCommentsForBlock')] - public function testInlineCommentForBlock(string $template) - { - $lexer = new Lexer(new Environment(new ArrayLoader())); -@@ -611,9 +607,7 @@ bar - %}me{% endif %}']; - } - -- /** -- * @dataProvider getTemplateForInlineCommentsForComment -- */ -+ #[DataProvider('getTemplateForInlineCommentsForComment')] - public function testInlineCommentForComment(string $template) - { - $lexer = new Lexer(new Environment(new ArrayLoader())); +diff --git a/tests/Extension/AttributeExtensionTest.php b/tests/Extension/AttributeExtensionTest.php +index 6e24f56..0e1a90c 100644 +--- a/tests/Extension/AttributeExtensionTest.php ++++ b/tests/Extension/AttributeExtensionTest.php +@@ -11,6 +11,7 @@ + + namespace Twig\Tests\Extension; + ++use PHPUnit\Framework\Attributes\DataProvider; + use PHPUnit\Framework\TestCase; + use Twig\DeprecatedCallableInfo; + use Twig\Error\RuntimeError; +diff --git a/tests/Node/Expression/MacroReferenceTest.php b/tests/Node/Expression/MacroReferenceTest.php +index 5301774..c007a16 100644 +--- a/tests/Node/Expression/MacroReferenceTest.php ++++ b/tests/Node/Expression/MacroReferenceTest.php +@@ -11,6 +11,7 @@ + + namespace Twig\Tests\Node\Expression; + ++use PHPUnit\Framework\Attributes\DataProvider; + use PHPUnit\Framework\TestCase; + use Twig\Node\Expression\ArrayExpression; + use Twig\Node\Expression\MacroReferenceExpression; diff --git a/tests/Util/CallableArgumentsExtractorTest.php b/tests/Util/CallableArgumentsExtractorTest.php -index a06e19a..25e91c6 100644 +index aba5546..e65bba9 100644 --- a/tests/Util/CallableArgumentsExtractorTest.php +++ b/tests/Util/CallableArgumentsExtractorTest.php -@@ -11,6 +11,7 @@ namespace Twig\Tests\Util; +@@ -20,6 +20,7 @@ namespace Twig\Tests\Util; * file that was distributed with this source code. */ @@ -102,14 +58,3 @@ use PHPUnit\Framework\TestCase; use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Twig\Error\SyntaxError; -@@ -86,9 +87,7 @@ class CallableArgumentsExtractorTest extends TestCase - $this->assertEquals(['arg1'], $this->getArguments('custom_static_function', __CLASS__.'::customStaticFunction', ['arg1' => 'arg1'])); - } - -- /** -- * @dataProvider getGetArgumentsConversionData -- */ -+ #[DataProvider('getGetArgumentsConversionData')] - public function testGetArgumentsConversion($arg1, $arg2) - { - $this->assertEquals([null], $this->getArguments('custom', eval("return fn (\$$arg1) => '';"), [$arg1 => null])); diff -Nru php-twig-3.20.0/debian/patches/0008-Compatibility-with-recent-PHPUnit-12.patch php-twig-3.26.0/debian/patches/0008-Compatibility-with-recent-PHPUnit-12.patch --- php-twig-3.20.0/debian/patches/0008-Compatibility-with-recent-PHPUnit-12.patch 2025-02-19 05:49:38.000000000 +0000 +++ php-twig-3.26.0/debian/patches/0008-Compatibility-with-recent-PHPUnit-12.patch 2026-05-26 07:00:53.000000000 +0000 @@ -3,16 +3,187 @@ Subject: Compatibility with recent PHPUnit (12) --- - tests/Extension/EscaperTest.php | 3 +++ - tests/Extension/SandboxTest.php | 1 + - tests/LexerTest.php | 3 +++ - 3 files changed, 7 insertions(+) + extra/cssinliner-extra/Tests/LegacyFunctionsTest.php | 2 ++ + extra/html-extra/Tests/LegacyFunctionsTest.php | 2 ++ + extra/inky-extra/Tests/LegacyFunctionsTest.php | 2 ++ + extra/markdown-extra/Tests/FunctionalTest.php | 4 +++- + extra/markdown-extra/Tests/LegacyFunctionsTest.php | 2 ++ + tests/EnvironmentTest.php | 7 ++----- + tests/ExpressionParserTest.php | 15 ++++++--------- + tests/Extension/EscaperTest.php | 1 + + tests/Extension/SandboxTest.php | 6 +++--- + tests/Fixtures/tags/sandbox/array.legacy.test | 18 ------------------ + tests/LexerTest.php | 6 ++++++ + tests/NodeVisitor/OptimizerTest.php | 1 + + tests/TemplateTest.php | 6 +++--- + 13 files changed, 33 insertions(+), 39 deletions(-) + delete mode 100644 tests/Fixtures/tags/sandbox/array.legacy.test +diff --git a/extra/cssinliner-extra/Tests/LegacyFunctionsTest.php b/extra/cssinliner-extra/Tests/LegacyFunctionsTest.php +index d62e267..0b8bd7b 100644 +--- a/extra/cssinliner-extra/Tests/LegacyFunctionsTest.php ++++ b/extra/cssinliner-extra/Tests/LegacyFunctionsTest.php +@@ -11,6 +11,7 @@ + + namespace Twig\Extra\CssInliner\Tests; + ++use PHPUnit\Framework\Attributes\IgnoreDeprecations; + use PHPUnit\Framework\TestCase; + use Twig\Extra\CssInliner\CssInlinerExtension; + +@@ -21,6 +22,7 @@ use function Twig\Extra\CssInliner\twig_inline_css; + */ + class LegacyFunctionsTest extends TestCase + { ++ #[IgnoreDeprecations] + public function testInlineCss() + { + $this->assertSame(CssInlinerExtension::inlineCss('

body

', 'p { color: red }'), twig_inline_css('

body

', 'p { color: red }')); +diff --git a/extra/html-extra/Tests/LegacyFunctionsTest.php b/extra/html-extra/Tests/LegacyFunctionsTest.php +index accf8af..757553c 100644 +--- a/extra/html-extra/Tests/LegacyFunctionsTest.php ++++ b/extra/html-extra/Tests/LegacyFunctionsTest.php +@@ -11,6 +11,7 @@ + + namespace Twig\Extra\Html\Tests; + ++use PHPUnit\Framework\Attributes\IgnoreDeprecations; + use PHPUnit\Framework\TestCase; + use Twig\Extra\Html\HtmlExtension; + +@@ -19,6 +20,7 @@ use Twig\Extra\Html\HtmlExtension; + */ + class LegacyFunctionsTest extends TestCase + { ++ #[IgnoreDeprecations] + public function testHtmlToMarkdown() + { + $this->assertSame(HtmlExtension::htmlClasses(['charset' => 'utf-8']), twig_html_classes(['charset' => 'utf-8'])); +diff --git a/extra/inky-extra/Tests/LegacyFunctionsTest.php b/extra/inky-extra/Tests/LegacyFunctionsTest.php +index 4810235..e83848c 100644 +--- a/extra/inky-extra/Tests/LegacyFunctionsTest.php ++++ b/extra/inky-extra/Tests/LegacyFunctionsTest.php +@@ -11,6 +11,7 @@ + + namespace Twig\Extra\Inky\Tests; + ++use PHPUnit\Framework\Attributes\IgnoreDeprecations; + use PHPUnit\Framework\TestCase; + use Twig\Extra\Inky\InkyExtension; + +@@ -21,6 +22,7 @@ use function Twig\Extra\Inky\twig_inky; + */ + class LegacyFunctionsTest extends TestCase + { ++ #[IgnoreDeprecations] + public function testInlineCss() + { + $this->assertSame(InkyExtension::inky('

Foo

'), twig_inky('

Foo

')); +diff --git a/extra/markdown-extra/Tests/FunctionalTest.php b/extra/markdown-extra/Tests/FunctionalTest.php +index ff7ea8b..438150d 100644 +--- a/extra/markdown-extra/Tests/FunctionalTest.php ++++ b/extra/markdown-extra/Tests/FunctionalTest.php +@@ -12,6 +12,7 @@ + namespace Twig\Extra\Markdown\Tests; + + use PHPUnit\Framework\Attributes\DataProvider; ++use PHPUnit\Framework\Attributes\IgnoreDeprecations; + use PHPUnit\Framework\TestCase; + use Twig\Environment; + use Twig\Extra\Markdown\DefaultMarkdown; +@@ -26,7 +27,8 @@ use Twig\RuntimeLoader\RuntimeLoaderInterface; + class FunctionalTest extends TestCase + { + #[DataProvider('getMarkdownTests')] +- public function testMarkdown(string $template, string $expected) ++ #[IgnoreDeprecations] ++ public function testMarkdown(string $template, string $expected): void + { + foreach ([LeagueMarkdown::class, ErusevMarkdown::class, /* MichelfMarkdown::class, */ DefaultMarkdown::class] as $class) { + $twig = new Environment(new ArrayLoader([ +diff --git a/extra/markdown-extra/Tests/LegacyFunctionsTest.php b/extra/markdown-extra/Tests/LegacyFunctionsTest.php +index 19a861d..c3a8708 100644 +--- a/extra/markdown-extra/Tests/LegacyFunctionsTest.php ++++ b/extra/markdown-extra/Tests/LegacyFunctionsTest.php +@@ -11,6 +11,7 @@ + + namespace Twig\Extra\Markdown\Tests; + ++use PHPUnit\Framework\Attributes\IgnoreDeprecations; + use PHPUnit\Framework\TestCase; + + use function Twig\Extra\Markdown\html_to_markdown; +@@ -22,6 +23,7 @@ use Twig\Extra\Markdown\MarkdownExtension; + */ + class LegacyFunctionsTest extends TestCase + { ++ #[IgnoreDeprecations] + public function testHtmlToMarkdown() + { + $this->assertSame(MarkdownExtension::htmlToMarkdown('

foo

'), html_to_markdown('

foo

')); +diff --git a/tests/EnvironmentTest.php b/tests/EnvironmentTest.php +index 3c8243d..2a670f2 100644 +--- a/tests/EnvironmentTest.php ++++ b/tests/EnvironmentTest.php +@@ -460,11 +460,8 @@ class EnvironmentTest extends TestCase + $this->assertSame('dynamic', $parser->getTag()); + } + +- /** +- * @group legacy +- * +- * @requires PHP 8 +- */ ++ #[\PHPUnit\Framework\Attributes\RequiresPhp('8')] ++ #[\PHPUnit\Framework\Attributes\Group('legacy')] + public function testLegacyEchoingNode() + { + $loader = new ArrayLoader(['echo_bar' => 'A{% set v %}B{% test %}C{% endset %}D{% test %}E{{ v }}F{% set w %}{% test %}{% endset %}G{{ w }}H']); +diff --git a/tests/ExpressionParserTest.php b/tests/ExpressionParserTest.php +index 56a6a81..48c5ed4 100644 +--- a/tests/ExpressionParserTest.php ++++ b/tests/ExpressionParserTest.php +@@ -591,9 +591,8 @@ class ExpressionParserTest extends TestCase + $this->expectNotToPerformAssertions(); + } + +- /** +- * @group legacy +- */ ++ #[\PHPUnit\Framework\Attributes\Group('legacy')] ++ #[\PHPUnit\Framework\Attributes\DoesNotPerformAssertions] + public function testNotReadyFunction() + { + $this->expectDeprecation('Since twig/twig 3.12: Twig node "Twig\Tests\NotReadyFunctionExpression" is not marked as ready for passing a "TwigFunction" in the constructor instead of its name; please update your code and then add #[FirstClassTwigCallableReady] attribute to the constructor.'); +@@ -606,9 +605,8 @@ class ExpressionParserTest extends TestCase + $parser->parse($env->tokenize(new Source('{{ foo() }}', 'index'))); + } + +- /** +- * @group legacy +- */ ++ #[\PHPUnit\Framework\Attributes\Group('legacy')] ++ #[\PHPUnit\Framework\Attributes\DoesNotPerformAssertions] + public function testNotReadyFilter() + { + $this->expectDeprecation('Since twig/twig 3.12: Twig node "Twig\Tests\NotReadyFilterExpression" is not marked as ready for passing a "TwigFilter" in the constructor instead of its name; please update your code and then add #[FirstClassTwigCallableReady] attribute to the constructor.'); +@@ -621,9 +619,8 @@ class ExpressionParserTest extends TestCase + $parser->parse($env->tokenize(new Source('{{ 1|foo }}', 'index'))); + } + +- /** +- * @group legacy +- */ ++ #[\PHPUnit\Framework\Attributes\Group('legacy')] ++ #[\PHPUnit\Framework\Attributes\DoesNotPerformAssertions] + public function testNotReadyTest() + { + $this->expectDeprecation('Since twig/twig 3.12: Twig node "Twig\Tests\NotReadyTestExpression" is not marked as ready for passing a "TwigTest" in the constructor instead of its name; please update your code and then add #[FirstClassTwigCallableReady] attribute to the constructor.'); diff --git a/tests/Extension/EscaperTest.php b/tests/Extension/EscaperTest.php -index 28df82d..7b59644 100644 +index 8f0a835..13da0e9 100644 --- a/tests/Extension/EscaperTest.php +++ b/tests/Extension/EscaperTest.php -@@ -11,6 +11,7 @@ namespace Twig\Tests; +@@ -20,6 +20,7 @@ namespace Twig\Tests; * file that was distributed with this source code. */ @@ -20,59 +191,118 @@ use PHPUnit\Framework\TestCase; use Twig\Environment; use Twig\Extension\EscaperExtension; -@@ -24,6 +25,7 @@ class EscaperTest extends TestCase - * - * @group legacy - */ -+ #[DataProvider('provideCustomEscaperCases')] - public function testCustomEscaper($expected, $string, $strategy) - { - $twig = new Environment(new ArrayLoader()); -@@ -46,6 +48,7 @@ class EscaperTest extends TestCase - * - * @group legacy - */ -+ #[DataProvider('provideCustomEscaperCases')] - public function testCustomEscaperWithoutCallingSetEscaperRuntime($expected, $string, $strategy) - { - $twig = new Environment(new ArrayLoader()); diff --git a/tests/Extension/SandboxTest.php b/tests/Extension/SandboxTest.php -index fd655f7..3dc75de 100644 +index 6cd278c..2f0e00d 100644 --- a/tests/Extension/SandboxTest.php +++ b/tests/Extension/SandboxTest.php -@@ -116,6 +116,7 @@ class SandboxTest extends TestCase - * +@@ -125,6 +125,8 @@ class SandboxTest extends TestCase * @group legacy */ -+ #[DataProvider('getSandboxedForExtendsAndUseTagsTests')] + #[DataProvider('getSandboxedForExtendsAndUseTagsTests')] ++ #[\PHPUnit\Framework\Attributes\Group('legacy')] ++ #[\PHPUnit\Framework\Attributes\DoesNotPerformAssertions] public function testSandboxForExtendsAndUseTags(string $tag, string $template) { $this->expectDeprecation(\sprintf('Since twig/twig 3.12: The "%s" tag is always allowed in sandboxes, but won\'t be in 4.0, please enable it explicitly in your sandbox policy if needed.', $tag)); +@@ -183,9 +185,7 @@ class SandboxTest extends TestCase + } + } + +- /** +- * @group legacy +- */ ++ #[\PHPUnit\Framework\Attributes\Group('legacy')] + public function testIfSandBoxIsDisabledAfterSyntaxErrorLegacy() + { + $twig = $this->getEnvironment(false, [], self::$templates); +diff --git a/tests/Fixtures/tags/sandbox/array.legacy.test b/tests/Fixtures/tags/sandbox/array.legacy.test +deleted file mode 100644 +index df9d1e4..0000000 +--- a/tests/Fixtures/tags/sandbox/array.legacy.test ++++ /dev/null +@@ -1,18 +0,0 @@ +---TEST-- +-sandbox tag +---DEPRECATION-- +-Since twig/twig 3.15: The "sandbox" tag is deprecated in "index.twig" at line 2. +---TEMPLATE-- +-{%- sandbox %} +- {%- include "foo.twig" %} +-{%- endsandbox %} +---TEMPLATE(foo.twig)-- +-{{ [a][0] }} +-{{ dump([a][0]) }} +---DATA-- +-return ['a' => 'b'] +---CONFIG-- +-return ['autoescape' => false, 'debug' => true] +---EXPECT-- +-b +-string(1) "b" diff --git a/tests/LexerTest.php b/tests/LexerTest.php -index 4a8cb53..2f1a5a9 100644 +index a6962a4..8d4bef3 100644 --- a/tests/LexerTest.php +++ b/tests/LexerTest.php -@@ -259,6 +259,7 @@ class LexerTest extends TestCase - * - * @dataProvider getStringWithEscapedDelimiterProducingDeprecation - */ -+ #[DataProvider('getStringWithEscapedDelimiterProducingDeprecation')] - public function testStringWithEscapedDelimiterProducingDeprecation(string $template, string $expected, string $expectedDeprecation) - { - $this->expectDeprecation($expectedDeprecation); -@@ -625,6 +626,7 @@ bar - /** - * @dataProvider getTemplateForUnclosedBracketInExpression - */ -+ #[DataProvider('getTemplateForUnclosedBracketInExpression')] +@@ -670,6 +670,9 @@ bar + #}']; + } + ++ /** ++ * @dataProvider getTemplateForUnclosedBracketInExpression ++ */ + #[DataProvider('getTemplateForUnclosedBracketInExpression')] public function testUnclosedBracketInExpression(string $template, string $bracket) { - $lexer = new Lexer(new Environment(new ArrayLoader())); -@@ -646,6 +648,7 @@ bar - /** - * @dataProvider getTemplateForUnexpectedBracketInExpression - */ -+ #[DataProvider('getTemplateForUnexpectedBracketInExpression')] +@@ -689,6 +692,9 @@ bar + yield ['{{ (([1]) + 3 }}', '(']; + } + ++ /** ++ * @dataProvider getTemplateForUnexpectedBracketInExpression ++ */ + #[DataProvider('getTemplateForUnexpectedBracketInExpression')] public function testUnexpectedBracketInExpression(string $template, string $bracket) { - $lexer = new Lexer(new Environment(new ArrayLoader())); +diff --git a/tests/NodeVisitor/OptimizerTest.php b/tests/NodeVisitor/OptimizerTest.php +index c62efdf..cb22bb0 100644 +--- a/tests/NodeVisitor/OptimizerTest.php ++++ b/tests/NodeVisitor/OptimizerTest.php +@@ -65,6 +65,7 @@ class OptimizerTest extends TestCase + $this->assertTrue($node->getAttribute('output')); + } + ++ #[\PHPUnit\Framework\Attributes\DoesNotPerformAssertions] + public function testForVarOptimizer() + { + $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); +diff --git a/tests/TemplateTest.php b/tests/TemplateTest.php +index 612d70b..c54ac6e 100644 +--- a/tests/TemplateTest.php ++++ b/tests/TemplateTest.php +@@ -246,7 +246,7 @@ class TemplateTest extends TestCase + } + + #[DataProvider('getGetAttributeTests')] +- public function testGetAttribute($defined, $value, $object, $item, $arguments, $type) ++ public function testGetAttribute($defined, $value, $object, $item, $arguments, $type, $exceptionMessage = null) + { + $twig = new Environment(new ArrayLoader()); + $template = new TemplateForTest($twig); +@@ -272,7 +272,7 @@ class TemplateTest extends TestCase + } + + #[DataProvider('getGetAttributeTests')] +- public function testGetAttributeDefined($defined, $value, $object, $item, $arguments, $type) ++ public function testGetAttributeDefined($defined, $value, $object, $item, $arguments, $type, $exceptionMessage = null) + { + $twig = new Environment(new ArrayLoader()); + $template = new TemplateForTest($twig); +@@ -281,7 +281,7 @@ class TemplateTest extends TestCase + } + + #[DataProvider('getGetAttributeTests')] +- public function testGetAttributeDefinedStrict($defined, $value, $object, $item, $arguments, $type) ++ public function testGetAttributeDefinedStrict($defined, $value, $object, $item, $arguments, $type, $exceptionMessage = null) + { + $twig = new Environment(new ArrayLoader(), ['strict_variables' => true]); + $template = new TemplateForTest($twig); diff -Nru php-twig-3.20.0/debian/patches/0009-Drop-redundant-require_once-in-tests.patch php-twig-3.26.0/debian/patches/0009-Drop-redundant-require_once-in-tests.patch --- php-twig-3.20.0/debian/patches/0009-Drop-redundant-require_once-in-tests.patch 1970-01-01 00:00:00.000000000 +0000 +++ php-twig-3.26.0/debian/patches/0009-Drop-redundant-require_once-in-tests.patch 2026-05-26 07:00:53.000000000 +0000 @@ -0,0 +1,21 @@ +From: =?utf-8?q?David_Pr=C3=A9vot?= +Date: Thu, 21 May 2026 11:33:40 +0200 +Subject: Drop redundant require_once in tests + +--- + tests/Resources/LegacyCoreTest.php | 2 -- + 1 file changed, 2 deletions(-) + +diff --git a/tests/Resources/LegacyCoreTest.php b/tests/Resources/LegacyCoreTest.php +index dc58ae0..bc11b8f 100644 +--- a/tests/Resources/LegacyCoreTest.php ++++ b/tests/Resources/LegacyCoreTest.php +@@ -23,8 +23,6 @@ use Twig\Source; + use Twig\Template; + use Twig\TemplateWrapper; + +-require_once __DIR__.'/../../src/Resources/core.php'; +- + /** + * Tests that the deprecated wrappers in src/Resources/core.php still enforce + * sandbox checks. diff -Nru php-twig-3.20.0/debian/patches/series php-twig-3.26.0/debian/patches/series --- php-twig-3.20.0/debian/patches/series 2025-02-19 05:49:38.000000000 +0000 +++ php-twig-3.26.0/debian/patches/series 2026-05-26 07:00:53.000000000 +0000 @@ -6,3 +6,4 @@ 0006-Mark-getTests-fonction-as-static.patch 0007-Twig-4-Add-PHPUnit-as-dev-dependency.patch 0008-Compatibility-with-recent-PHPUnit-12.patch +0009-Drop-redundant-require_once-in-tests.patch diff -Nru php-twig-3.20.0/debian/rules php-twig-3.26.0/debian/rules --- php-twig-3.20.0/debian/rules 2025-02-19 05:49:38.000000000 +0000 +++ php-twig-3.26.0/debian/rules 2026-05-26 11:43:06.000000000 +0000 @@ -21,41 +21,44 @@ doc debian/build-tmp/php-twig-doc # php-twig phpabtpl --basedir src composer.json > debian/autoload.php.tpl + sed -i "s#'/usr/share/php/Symfony#__DIR__.'/../Symfony#" debian/autoload.php.tpl phpab \ --output src/autoload.php \ --template debian/autoload.php.tpl \ src # php-twig testsuite - mkdir --parents vendor Twig/Extra + mkdir --parents vendor phpabtpl \ --require psr/container \ --require symfony/phpunit-bridge \ --require erusev/parsedown \ --require league/commonmark \ --require league/html-to-markdown \ - --require twig/twig \ - --require-file ../extra/twig-extra-bundle/autoload.php \ - --require-file ../extra/cache-extra/autoload.php \ - --require-file ../extra/cssinliner-extra/autoload.php \ - --require-file ../extra/html-extra/autoload.php \ - --require-file ../extra/inky-extra/autoload.php \ - --require-file ../extra/intl-extra/autoload.php \ - --require-file ../extra/markdown-extra/autoload.php \ - --require-file ../extra/string-extra/autoload.php \ + --require-file ../Twig/autoload.php \ + --require-file ../Twig/Extra/TwigExtraBundle/autoload.php \ + --require-file ../Twig/Extra/Cache/autoload.php \ + --require-file ../Twig/Extra/CssInliner/autoload.php \ + --require-file ../Twig/Extra/Html/autoload.php \ + --require-file ../Twig/Extra/Inky/autoload.php \ + --require-file ../Twig/Extra/Intl/autoload.php \ + --require-file ../Twig/Extra/Markdown/autoload.php \ + --require-file ../Twig/Extra/String/autoload.php \ > debian/autoload.tests.php.tpl phpab --output vendor/autoload.php \ --template debian/autoload.tests.php.tpl \ tests extra/*/Tests # Mimic install path ln -s src Twig - ln -s ../../extra/cache-extra Twig/Extra/Cache - ln -s ../../extra/cssinliner-extra Twig/Extra/CssInliner - ln -s ../../extra/html-extra Twig/Extra/Html - ln -s ../../extra/inky-extra Twig/Extra/Inky - ln -s ../../extra/intl-extra Twig/Extra/Intl - ln -s ../../extra/markdown-extra Twig/Extra/Markdown - ln -s ../../extra/string-extra Twig/Extra/String - ln -s ../../extra/twig-extra-bundle Twig/Extra/TwigExtraBundle + mkdir --parents Twig/Extra + cp -a extra/cache-extra Twig/Extra/Cache + cp -a extra/cssinliner-extra Twig/Extra/CssInliner + cp -a extra/html-extra Twig/Extra/Html + cp -a extra/inky-extra Twig/Extra/Inky + cp -a extra/intl-extra Twig/Extra/Intl + cp -a extra/markdown-extra Twig/Extra/Markdown + cp -a extra/string-extra Twig/Extra/String + cp -a extra/twig-extra-bundle Twig/Extra/TwigExtraBundle + cp -a /usr/share/php/Symfony . # php-twig extra # Parts from the symfony package’s debian/rules. mkdir --parents debian/packages_to_build debian/autoloaders @@ -73,7 +76,7 @@ deb_pkg_name=php-$$(cat $$src_path/composer.json | jq -r '.name | tostring' | sed -r 's|/|-|'); \ if [ $$(grep -c -E -e "^Package: $$deb_pkg_name\$$" debian/control) -ne 1 ]; then \ echo "W: No Debian package '$$deb_pkg_name' defined in debian/control," 1>&2; \ - echo " therefor not considering Symfony $$src_path," 1>&2; \ + echo " therefor not considering Twig $$src_path," 1>&2; \ continue; \ fi; \ echo "# This file contains some variables sourced" > debian/packages_to_build/$$deb_pkg_name; \ @@ -82,8 +85,11 @@ echo "src_path='$$src_path'" >> debian/packages_to_build/$$deb_pkg_name; \ echo "twig $${deb_pkg_name#php-twig-} $$src_path/autoload.php" > debian/autoloaders/$$deb_pkg_name; \ phpabtpl --basedir $$src_path $$src_path/composer.json > debian/$$deb_pkg_name.autoload.php.tpl; \ + sed -i "s#'/usr/share/php/Twig#__DIR__.'/../..#" debian/$$deb_pkg_name.autoload.php.tpl; \ + sed -i "s#'/usr/share/php/Symfony#__DIR__.'/../../../Symfony#" debian/$$deb_pkg_name.autoload.php.tpl; \ phpab \ --blacklist '*\\tests\\*' \ + --tolerant \ --output $$src_path/autoload.php \ --template debian/$$deb_pkg_name.autoload.php.tpl \ $$src_path; \ @@ -91,6 +97,13 @@ done override_dh_auto_test: + # Mimic install path + cp -a /usr/share/php/DeepCopy . + cp -a /usr/share/php/PharIo . + cp -a /usr/share/php/PHPUnit . + cp -a /usr/share/php/Psr . + cp -a /usr/share/php/SebastianBergmann . + cp -a /usr/share/php/staabm . SYMFONY_DEPRECATIONS_HELPER=weak phpunit # testsuite for extra packages set -e; \ @@ -106,6 +119,7 @@ override_dh_install: dh_install --package=php-twig --package=php-twig-doc -X/Extra/ + rm -rf debian/php-twig/usr/share/php/Twig/Extra/ # -X/Extra/ is not enough # In debian/packages_to_build/ a file containing shell variables exists # for each package that should be build. Source one file after another # to make the shell variables available and run dh_install in order to diff -Nru php-twig-3.20.0/doc/_build/build.php php-twig-3.26.0/doc/_build/build.php --- php-twig-3.20.0/doc/_build/build.php 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/doc/_build/build.php 2026-05-20 07:31:59.000000000 +0000 @@ -1,6 +1,15 @@ #!/usr/bin/env php register('build-docs') ->addOption('disable-cache', null, InputOption::VALUE_NONE, 'Use this option to force a full regeneration of all doc contents') - ->setCode(function (InputInterface $input, OutputInterface $output) { + ->setCode(static function (InputInterface $input, OutputInterface $output) { $io = new SymfonyStyle($input, $output); $io->text('Building all Twig docs...'); @@ -40,9 +49,9 @@ file_put_contents($htmlFilePath, str_replace('href="assets/', 'href="/assets/', $htmlContents)); } - $io->success(\sprintf('The Twig docs were successfully built at %s', realpath($outputDir))); + $io->success(sprintf('The Twig docs were successfully built at %s', realpath($outputDir))); } else { - $io->error(\sprintf("There were some errors while building the docs:\n\n%s\n", $result->getErrorTrace())); + $io->error(sprintf("There were some errors while building the docs:\n\n%s\n", $result->getErrorTrace())); $io->newLine(); $io->comment('Tip: you can add the -v, -vv or -vvv flags to this command to get debug information.'); diff -Nru php-twig-3.20.0/doc/advanced.rst php-twig-3.26.0/doc/advanced.rst --- php-twig-3.20.0/doc/advanced.rst 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/doc/advanced.rst 2026-05-20 07:31:59.000000000 +0000 @@ -216,6 +216,34 @@ // ... }, ['needs_context' => true, 'needs_environment' => true]); +Sandbox-aware Filters +~~~~~~~~~~~~~~~~~~~~~ + +.. versionadded:: 3.25 + + The ``needs_is_sandboxed`` option was added in Twig 3.25. + +If you want to know whether the current template is sandboxed in your +filter, set the ``needs_is_sandboxed`` option to ``true``; Twig will pass the +current sandbox state as a boolean to the filter call (as the first +argument, or after the charset, the environment, and the context if they +are also requested):: + + $filter = new \Twig\TwigFilter('rot13', function (bool $isSandboxed, $string) { + if ($isSandboxed) { + // adjust behavior when running in a sandboxed template + } + + return str_rot13($string); + }, ['needs_is_sandboxed' => true]); + +The sandbox state is resolved against the current template source, which +means it takes both the global sandbox state and any ``SourcePolicy`` into +account. + +The same ``needs_is_sandboxed`` option is also available on functions and +tests. + Automatic Escaping ~~~~~~~~~~~~~~~~~~ @@ -504,6 +532,7 @@ public function parse(\Twig\Token $token) { $parser = $this->parser; + $lineno = $token->getLine(); $stream = $parser->getStream(); $name = $stream->expect(\Twig\Token::NAME_TYPE)->getValue(); @@ -511,7 +540,7 @@ $value = $parser->getExpressionParser()->parseExpression(); $stream->expect(\Twig\Token::BLOCK_END_TYPE); - return new CustomSetNode($name, $value, $token->getLine()); + return new CustomSetNode($name, $value, $lineno); } public function getTag() @@ -546,6 +575,18 @@ Parsing expressions is done by calling the ``parseExpression()`` like we did for the ``set`` tag. +When encountering a syntax error during parsing, throw an exception:: + + throw new SyntaxError('Some error message.', $stream->getCurrent()->getLine(), $stream->getSourceContext()); + +For better error reporting to the user, follow these recommendations: + + * Use ``\Twig\Error\SyntaxError``; + + * **Always** pass the line number of the node and the source context; + + * End the exception message with a dot. + .. tip:: Reading the existing ``TokenParser`` classes is the best way to learn all @@ -590,7 +631,8 @@ ``\Twig\Node\ForNode`` for a usage example). * ``addDebugInfo()``: Adds the line of the original template file related to - the current node as a comment. + the current node as a comment. It's highly recommended to call this method + when implementing custom nodes. * ``indent()``: Indents the generated code (see ``\Twig\Node\BlockNode`` for a usage example). @@ -598,6 +640,10 @@ * ``outdent()``: Outdents the generated code (see ``\Twig\Node\BlockNode`` for a usage example). +For structural nodes, always call ``addDebugInfo()`` early on in the +compilation process to improve error reporting to the user in case the code +would throw an exception. + .. _creating_extensions: Creating an Extension @@ -656,11 +702,11 @@ public function getFunctions(); /** - * Returns a list of operators to add to the existing list. + * Returns a list of expression parsers to add to the existing list. * - * @return array First array of unary operators, second array of binary operators + * @return \Twig\ExpressionParser\ExpressionParserInterface[] */ - public function getOperators(); + public function getExpressionParsers(); } To keep your extension class clean and lean, inherit from the built-in @@ -775,26 +821,20 @@ Operators ~~~~~~~~~ -The ``getOperators()`` methods lets you add new operators. Here is how to add -the ``!``, ``||``, and ``&&`` operators:: +.. versionadded:: 3.21 - class CustomTwigExtension extends \Twig\Extension\AbstractExtension - { - public function getOperators() - { - return [ - [ - '!' => ['precedence' => 50, 'class' => \Twig\Node\Expression\Unary\NotUnary::class], - ], - [ - '||' => ['precedence' => 10, 'class' => \Twig\Node\Expression\Binary\OrBinary::class, 'associativity' => \Twig\ExpressionParser::OPERATOR_LEFT], - '&&' => ['precedence' => 15, 'class' => \Twig\Node\Expression\Binary\AndBinary::class, 'associativity' => \Twig\ExpressionParser::OPERATOR_LEFT], - ], - ]; - } + The ``getExpressionParsers()`` method was added in Twig 3.21. - // ... - } +.. deprecated:: 3.21 + + The ``getExpressionParsers()`` method replaces the now deprecated + ``getOperators()`` method. See the :doc:`deprecated ` page for + details on how to upgrade from ``getOperators()`` to + ``getExpressionParsers()``. + +The ``getExpressionParsers()`` method lets you add new operators. To implement +a new one, have a look at the default operators provided by +``Twig\Extension\CoreExtension``. Tests ~~~~~ @@ -813,6 +853,107 @@ // ... } +Using PHP Attributes to define Extensions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. versionadded:: 3.21 + + The attribute classes were added in Twig 3.21. + +You can add the ``#[AsTwigFilter]``, ``#[AsTwigFunction]``, and ``#[AsTwigTest]`` +attributes to public methods of any class to define filters, functions, and tests. + +Create a class using these attributes:: + + use Twig\Attribute\AsTwigFilter; + use Twig\Attribute\AsTwigFunction; + use Twig\Attribute\AsTwigTest; + + class ProjectExtension + { + #[AsTwigFilter('rot13')] + public static function rot13(string $string): string + { + // ... + } + + #[AsTwigFunction('lipsum')] + public static function lipsum(int $count): string + { + // ... + } + + #[AsTwigTest('even')] + public static function isEven(int $number): bool + { + // ... + } + } + +Then register the ``Twig\Extension\AttributeExtension`` with the class name:: + + $twig = new \Twig\Environment($loader); + $twig->addExtension(new \Twig\Extension\AttributeExtension(ProjectExtension::class)); + +If all the methods are static, you are done. The ``ProjectExtension`` class will +never be instantiated and the class attributes will be scanned only when a template +is compiled. + +Otherwise, if some methods are not static, you need to register the class as +a runtime extension using one of the runtime loaders:: + + use Twig\Attribute\AsTwigFunction; + + class ProjectExtension + { + // Inject hypothetical dependencies + public function __construct(private LipsumProvider $lipsumProvider) {} + + #[AsTwigFunction('lipsum')] + public function lipsum(int $count): string + { + return $this->lipsumProvider->lipsum($count); + } + } + + $twig = new \Twig\Environment($loader); + $twig->addExtension(new \Twig\Extension\AttributeExtension(ProjectExtension::class); + $twig->addRuntimeLoader(new \Twig\RuntimeLoader\FactoryLoader([ + ProjectExtension::class => function () use ($lipsumProvider) { + return new ProjectExtension($lipsumProvider); + }, + ])); + +If you want to access the current environment instance in your filter or function, +add the ``Twig\Environment`` type to the first argument of the method:: + + class ProjectExtension + { + #[AsTwigFunction('lipsum')] + public function lipsum(\Twig\Environment $env, int $count): string + { + // ... + } + } + +``#[AsTwigFilter]`` and ``#[AsTwigFunction]`` support variadic arguments +automatically when applied to variadic methods:: + + class ProjectExtension + { + #[AsTwigFilter('thumbnail')] + public function thumbnail(string $file, mixed ...$options): string + { + // ... + } + } + +The attributes support other options used to configure the Twig Callables: + + * ``AsTwigFilter``: ``needsCharset``, ``needsEnvironment``, ``needsContext``, ``isSafe``, ``isSafeCallback``, ``preEscape``, ``preservesSafety``, ``deprecationInfo`` + * ``AsTwigFunction``: ``needsCharset``, ``needsEnvironment``, ``needsContext``, ``isSafe``, ``isSafeCallback``, ``deprecationInfo`` + * ``AsTwigTest``: ``needsCharset``, ``needsEnvironment``, ``needsContext``, ``deprecationInfo`` + Definition vs Runtime ~~~~~~~~~~~~~~~~~~~~~ diff -Nru php-twig-3.20.0/doc/api.rst php-twig-3.26.0/doc/api.rst --- php-twig-3.20.0/doc/api.rst 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/doc/api.rst 2026-05-20 07:31:59.000000000 +0000 @@ -35,7 +35,7 @@ Notice that the second argument of the environment is an array of options. The ``cache`` option is a compilation cache directory, where Twig caches - the compiled templates to avoid the parsing phase for sub-sequent + the compiled templates to avoid the parsing phase for subsequent requests. It is very different from the cache you might want to add for the evaluated templates. For such a need, you can use any available PHP cache library. @@ -130,8 +130,8 @@ * ``autoescape`` *string* - Sets the default auto-escaping strategy (``name``, ``html``, ``js``, ``css``, - ``url``, ``html_attr``, or a PHP callback that takes the template "filename" + Sets the default auto-escaping strategy (``name``, ``html``, ``js``, ``css``, ``url``, + ``html_attr``, ``html_attr_relaxed``, or a PHP callback that takes the template "filename" and returns the escaping strategy to use -- the callback cannot be a function name to avoid collision with built-in escaping strategies); set it to ``false`` to disable auto-escaping. The ``name`` escaping strategy determines @@ -145,6 +145,17 @@ (default to ``-1`` -- all optimizations are enabled; set it to ``0`` to disable). +* ``use_yield`` *boolean* + + ``true``: forces templates to exclusively use ``yield`` instead of ``echo`` + (all extensions must be yield ready) + + ``false`` (default): allows templates to use a mix of ``yield`` and ``echo`` + calls to allow for a progressive migration. + + Switch to ``true`` when possible as this will be the only supported mode in + Twig 4.0. + Loaders ------- @@ -185,7 +196,7 @@ $loader->addPath($templateDir3); $loader->prependPath($templateDir4); -The filesystem loader also supports namespaced templates. This allows to group +The filesystem loader also supports namespaced templates. This allows you to group your templates under different namespaces which have their own template paths. When using the ``setPaths()``, ``addPath()``, and ``prependPath()`` methods, @@ -327,23 +338,23 @@ Twig comes bundled with the following extensions: -* *Twig\Extension\CoreExtension*: Defines all the core features of Twig. +* ``\Twig\Extension\CoreExtension``: Defines all the core features of Twig. -* *Twig\Extension\DebugExtension*: Defines the ``dump`` function to help debug +* ``\Twig\Extension\DebugExtension``: Defines the ``dump`` function to help debug template variables. -* *Twig\Extension\EscaperExtension*: Adds automatic output-escaping and the +* ``\Twig\Extension\EscaperExtension``: Adds automatic output-escaping and the possibility to escape/unescape blocks of code. -* *Twig\Extension\SandboxExtension*: Adds a sandbox mode to the default Twig +* ``\Twig\Extension\SandboxExtension``: Adds a sandbox mode to the default Twig environment, making it safe to evaluate untrusted code. -* *Twig\Extension\ProfilerExtension*: Enables the built-in Twig profiler. +* ``\Twig\Extension\ProfilerExtension``: Enables the built-in Twig profiler. -* *Twig\Extension\OptimizerExtension*: Optimizes the node tree before +* ``\Twig\Extension\OptimizerExtension``: Optimizes the node tree before compilation. -* *Twig\Extension\StringLoaderExtension*: Defines the ``template_from_string`` +* ``\Twig\Extension\StringLoaderExtension``: Defines the ``template_from_string`` function to allow loading templates from string in a template. The Core, Escaper, and Optimizer extensions are registered by default. diff -Nru php-twig-3.20.0/doc/coding_standards.rst php-twig-3.26.0/doc/coding_standards.rst --- php-twig-3.20.0/doc/coding_standards.rst 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/doc/coding_standards.rst 2026-05-20 07:31:59.000000000 +0000 @@ -11,12 +11,13 @@ standards: * Put exactly one space after the start of a delimiter (``{{``, ``{%``, - and ``{#``) and before the end of a delimiter (``}}``, ``%}``, and ``#}``): + and ``{#``) and before the end of a delimiter (``}}``, ``%}``, and ``#}``) + if the content is non empty: .. code-block:: twig {{ user }} - {# comment #} + {# comment #} {##} {% if user %}{% endif %} When using the whitespace control character, do not put any spaces between @@ -25,7 +26,7 @@ .. code-block:: twig {{- user -}} - {#- comment -#} + {#- comment -#} {#--#} {%- if user -%}{%- endif -%} * Put exactly one space before and after the following operators: diff -Nru php-twig-3.20.0/doc/deprecated.rst php-twig-3.26.0/doc/deprecated.rst --- php-twig-3.20.0/doc/deprecated.rst 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/doc/deprecated.rst 2026-05-20 07:31:59.000000000 +0000 @@ -72,7 +72,7 @@ deprecated. * The ``Twig\Node\Expression\NameExpression::isSimple()`` and - ``Twig\Node\Expression\NameExpression::isSpecial()`` methods are deprecated as + ``Twig\Node\Expression\NameExpression::isSpecial()`` methods are deprecated as of Twig 3.11 and will be removed in Twig 4.0. * The ``filter`` node of ``Twig\Node\Expression\FilterExpression`` is @@ -196,6 +196,27 @@ * The ``Twig\Node\Expression\ConditionalExpression`` class is deprecated as of Twig 3.17, use ``Twig\Node\Expression\Ternary\ConditionalTernary`` instead. + * The ``is_defined_test`` attribute is deprecated as of Twig 3.21, use + ``Twig\Node\Expression\SupportDefinedTestInterface`` instead. + +* Instantiating ``Twig\Node\Node`` directly is deprecated as of Twig 3.15. Use + ``EmptyNode`` or ``Nodes`` instead depending on the use case. The + ``Twig\Node\Node`` class will be abstract in Twig 4.0. + +* Not passing ``AbstractExpression`` arguments to the following ``Node`` class + constructors is deprecated as of Twig 3.15: + + * ``AbstractBinary`` + * ``AbstractUnary`` + * ``BlockReferenceExpression`` + * ``TestExpression`` + * ``DefinedTest`` + * ``FilterExpression`` + * ``RawFilter`` + * ``DefaultFilter`` + * ``InlinePrint`` + * ``NullCoalesceExpression`` + Node Visitors ------------- @@ -210,27 +231,44 @@ Parser ------ -* Passing a second argument to ``ExpressionParser::parseFilterExpressionRaw()`` - is deprecated as of Twig 3.12. - * The following methods from ``Twig\Parser`` are deprecated as of Twig 3.12: ``getBlockStack()``, ``hasBlock()``, ``getBlock()``, ``hasMacro()``, ``hasTraits()``, ``getParent()``. -* The ``Twig\ExpressionParser::parseHashExpression()`` method is deprecated, use - ``Twig\ExpressionParser::parseMappingExpression()`` instead. - -* The ``Twig\ExpressionParser::parseArrayExpression()`` method is deprecated, use - ``Twig\ExpressionParser::parseSequenceExpression()`` instead. - * Passing ``null`` to ``Twig\Parser::setParent()`` is deprecated as of Twig 3.12. -* The ``Twig\ExpressionParser::parseOnlyArguments()`` and - ``Twig\ExpressionParser::parseArguments()`` methods are deprecated, use - ``Twig\ExpressionParser::parseNamedArguments()`` instead. +* Passing a non-``AbstractExpression`` node to ``Twig\Parser::setParent()`` is + deprecated as of Twig 3.24; the method will require an ``AbstractExpression`` + instance in Twig 4.0. + +* Passing non-``AbstractExpression`` nodes to + ``Twig\Node\Expression\Binary\MatchesBinary`` constructor is deprecated as of + Twig 3.24; the constructor will require an ``AbstractExpression`` instance in Twig + 4.0. + +* The ``Twig\Parser::getExpressionParser()`` method is deprecated as of Twig + 3.21, use ``Twig\Parser::parseExpression()`` instead. + +* The ``Twig\ExpressionParser`` class is deprecated as of Twig 3.21: + + * ``parseExpression()``, use ``Parser::parseExpression()`` + * ``parsePrimaryExpression()``, use ``Parser::parseExpression()`` + * ``parseStringExpression()``, use ``Parser::parseExpression()`` + * ``parseHashExpression()``, use ``Parser::parseExpression()`` + * ``parseMappingExpression()``, use ``Parser::parseExpression()`` + * ``parseArrayExpression()``, use ``Parser::parseExpression()`` + * ``parseSequenceExpression()``, use ``Parser::parseExpression()`` + * ``parsePostfixExpression`` + * ``parseSubscriptExpression`` + * ``parseFilterExpression`` + * ``parseFilterExpressionRaw`` + * ``parseArguments()``, use ``Twig\ExpressionParser\Infix\ArgumentsTrait::parseNamedArguments()`` + * ``parseAssignmentExpression``, use ``AbstractTokenParser::parseAssignmentExpression`` + * ``parseMultitargetExpression`` + * ``parseOnlyArguments()``, use ``Twig\ExpressionParser\Infix\ArgumentsTrait::parseNamedArguments()`` -Lexer +Token ----- * Not passing a ``Source`` instance to ``Twig\TokenStream`` constructor is @@ -239,13 +277,19 @@ * The ``Token::getType()`` method is deprecated as of Twig 3.19, use ``Token::test()`` instead. +* The ``Token::ARROW_TYPE`` constant is deprecated as of Twig 3.21, the arrow + ``=>`` is now an operator (``Token::OPERATOR_TYPE``). + +* The ``Token::PUNCTUATION_TYPE`` with values ``(``, ``[``, ``|``, ``.``, + ``?``, or ``?:`` are now of the ``Token::OPERATOR_TYPE`` type. + Templates --------- +* The method ``Template::loadTemplate()`` is deprecated. * Passing ``Twig\Template`` instances to Twig public API is deprecated (like - in ``Environment::resolveTemplate()``, ``Environment::load()``, and - ``Template::loadTemplate()``); pass instances of ``Twig\TemplateWrapper`` - instead. + in ``Environment::resolveTemplate()`` and ``Environment::load()``); pass + instances of ``Twig\TemplateWrapper`` instead. Filters ------- @@ -340,30 +384,11 @@ ``TwigFunction::getSafe()`` is deprecated as of Twig 3.16; return ``[]`` instead. -Node ----- - -* Instantiating ``Twig\Node\Node`` directly is deprecated as of Twig 3.15. Use - ``EmptyNode`` or ``Nodes`` instead depending on the use case. The - ``Twig\Node\Node`` class will be abstract in Twig 4.0. - -* Not passing ``AbstractExpression`` arguments to the following ``Node`` class - constructors is deprecated as of Twig 3.15: - - * ``AbstractBinary`` - * ``AbstractUnary`` - * ``BlockReferenceExpression`` - * ``TestExpression`` - * ``DefinedTest`` - * ``FilterExpression`` - * ``RawFilter`` - * ``DefaultFilter`` - * ``InlinePrint`` - * ``NullCoalesceExpression`` - Operators --------- +* An operator precedence must be part of the [0, 512] range as of Twig 3.21. + * The ``.`` operator allows accessing class constants as of Twig 3.15. This can be a BC break if you don't use UPPERCASE constant names. @@ -418,3 +443,55 @@ {# or #} {{ (not 1) * 2 }} {# this is equivalent to what Twig 4.x will do without the parentheses #} + +* Using the ``|`` operator in an expression with ``+`` or ``-`` without explicit + parentheses to clarify precedence triggers a deprecation as of Twig 3.21 (in + Twig 4.0, ``|`` will have a higher precedence than ``+`` and ``-``). + + For example, the following expression will trigger a deprecation in Twig 3.21:: + + {{ -1|abs }} + + To avoid the deprecation, add parentheses to clarify the precedence:: + + {{ -(1|abs) }} {# this is equivalent to what Twig 3.x does without the parentheses #} + + {# or #} + + {{ (-1)|abs }} {# this is equivalent to what Twig 4.x will do without the parentheses #} + +* The ``Twig\Extension\ExtensionInterface::getOperators()`` method is deprecated + as of Twig 3.21, use ``Twig\Extension\ExtensionInterface::getExpressionParsers()`` + instead: + + Before:: + + public function getOperators(): array { + return [ + 'not' => [ + 'precedence' => 10, + 'class' => NotUnary::class, + ], + ]; + } + + After:: + + public function getExpressionParsers(): array { + return [ + new UnaryOperatorExpressionParser(NotUnary::class, 'not', 10), + ]; + } + +* The ``Twig\OperatorPrecedenceChange`` class is deprecated as of Twig 3.21, + use ``Twig\ExpressionParser\PrecedenceChange`` instead. + +* Not implementing the ``getOperatorTokens()`` method in + ``Twig\ExpressionParser\ExpressionParserInterface`` implementations is + deprecated as of Twig 3.24. This method will be added to the interface in + Twig 4.0. It returns the operator token strings that the expression parser + handles (used by the Lexer and the parser registry). If your custom + expression parser extends ``Twig\ExpressionParser\AbstractExpressionParser``, + the default implementation returns ``[$this->getName(), ...$this->getAliases()]``. + Override it if your parser doesn't handle operator tokens (return ``[]``) or if + the operator tokens differ from the parser name. diff -Nru php-twig-3.20.0/doc/filters/escape.rst php-twig-3.26.0/doc/filters/escape.rst --- php-twig-3.20.0/doc/filters/escape.rst 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/doc/filters/escape.rst 2026-05-20 07:31:59.000000000 +0000 @@ -39,11 +39,11 @@ The ``escape`` filter supports the following escaping strategies for HTML documents: -* ``html``: escapes a string for the **HTML body** context, +* ``html``: escapes a string for the **HTML body** context, or for HTML attributes values **inside quotes**. -* ``js``: escapes a string for the **JavaScript** context. This is intended for - use in JavaScript or JSON strings, and encodes values using backslash escape +* ``js``: escapes a string for the **JavaScript** context. This is intended for + use in JavaScript or JSON strings, and encodes values using backslash escape sequences. * ``css``: escapes a string for the **CSS** context. CSS escaping can be @@ -53,8 +53,19 @@ * ``url``: escapes a string for the **URI or parameter** contexts. This should not be used to escape an entire URI; only a subcomponent being inserted. -* ``html_attr``: escapes a string for the **HTML attribute** context, - **without quotes** around HTML attribute values. +* ``html_attr``: escapes a string when used as an **HTML attribute** name, and + also when used as the value of an HTML attribute **without quotes** + (e.g. ``data-attribute={{ some_value }}``). + +* ``html_attr_relaxed``: like ``html_attr``, but **does not** escape the ``@``, ``:``, + ``[`` and ``]`` characters. You may want to use this in combination with front-end + frameworks that use attribute names like ``v-bind:href`` or ``@click``. But, be + aware that in some processing contexts like XML, characters like the colon ``:`` + may have meaning like for XML namespace separation. + +.. versionadded:: 3.24 + + The ``html_attr_relaxed`` strategy has been added in 3.23. Note that doing contextual escaping in HTML documents is hard and choosing the right escaping strategy depends on a lot of factors. Please, read related @@ -96,23 +107,22 @@ .. tip:: - The ``html_attr`` escaping strategy can be useful when you need to - escape a **dynamic HTML attribute name**: + The ``html_attr`` escaping strategy can be useful when you need to escape a + **dynamic HTML attribute name**: .. code-block:: html+twig

- It can also be used for escaping a **dynamic HTML attribute value** - if it is not quoted, but this is **less performant**. - Instead, it is recommended to quote the HTML attribute value and use - the ``html`` escaping strategy: + It can also be used for escaping a **dynamic HTML attribute value** if it is + not quoted, but this is **less performant**. Instead, it is recommended to + quote the HTML attribute value and use the ``html`` escaping strategy: .. code-block:: html+twig

- {# is equivalent to, but is less performant #} + {# this is equivalent, but less performant #}

Custom Escapers diff -Nru php-twig-3.20.0/doc/filters/filter.rst php-twig-3.26.0/doc/filters/filter.rst --- php-twig-3.20.0/doc/filters/filter.rst 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/doc/filters/filter.rst 2026-05-20 07:31:59.000000000 +0000 @@ -11,7 +11,7 @@ {{ sizes|filter(v => v > 38)|join(', ') }} {# output 40, 42 #} -Combined with the ``for`` tag, it allows to filter the items to iterate over: +Combined with the ``for`` tag, it allows you to filter the items to iterate over: .. code-block:: twig diff -Nru php-twig-3.20.0/doc/filters/format_datetime.rst php-twig-3.26.0/doc/filters/format_datetime.rst --- php-twig-3.20.0/doc/filters/format_datetime.rst 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/doc/filters/format_datetime.rst 2026-05-20 07:31:59.000000000 +0000 @@ -8,6 +8,29 @@ {# Aug 7, 2019, 11:39:12 PM #} {{ '2019-08-07 23:39:12'|format_datetime() }} +.. note:: + + The ``format_datetime`` filter is part of the ``IntlExtension`` which is not + installed by default. Install it first: + + .. code-block:: bash + + $ composer require twig/intl-extra + + Then, on Symfony projects, install the ``twig/extra-bundle``: + + .. code-block:: bash + + $ composer require twig/extra-bundle + + Otherwise, add the extension explicitly on the Twig environment:: + + use Twig\Extra\Intl\IntlExtension; + + $twig = new \Twig\Environment(...); + $twig->addExtension(new IntlExtension()); + + Format ------ diff -Nru php-twig-3.20.0/doc/filters/format_number.rst php-twig-3.26.0/doc/filters/format_number.rst --- php-twig-3.20.0/doc/filters/format_number.rst 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/doc/filters/format_number.rst 2026-05-20 07:31:59.000000000 +0000 @@ -22,37 +22,37 @@ * ``grouping_used``: Specifies whether to use grouping separator for thousands:: {# 1,234,567.89 #} - {{ 1234567.89|format_number({grouping_used:true}, locale='en') }} + {{ 1234567.89|format_number({grouping_used:true}, locale: 'en') }} * ``decimal_always_shown``: Specifies whether to always show the decimal part, even if it's zero:: {# 123. #} - {{ 123|format_number({decimal_always_shown:true}, locale='en') }} + {{ 123|format_number({decimal_always_shown:true}, locale: 'en') }} * ``max_integer_digit``: * ``min_integer_digit``: * ``integer_digit``: Define constraints on the integer part:: {# 345.679 #} - {{ 12345.6789|format_number({max_integer_digit:3, min_integer_digit:2}, locale='en') }} + {{ 12345.6789|format_number({max_integer_digit:3, min_integer_digit:2}, locale: 'en') }} * ``max_fraction_digit``: * ``min_fraction_digit``: * ``fraction_digit``: Define constraints on the fraction part:: {# 123.46 #} - {{ 123.456789|format_number({max_fraction_digit:2, min_fraction_digit:1}, locale='en') }} + {{ 123.456789|format_number({max_fraction_digit:2, min_fraction_digit:1}, locale: 'en') }} * ``multiplier``: Multiplies the value before formatting:: {# 123,000 #} - {{ 123|format_number({multiplier:1000}, locale='en') }} + {{ 123|format_number({multiplier:1000}, locale: 'en') }} * ``grouping_size``: * ``secondary_grouping_size``: Set the size of the primary and secondary grouping separators:: {# 1,23,45,678 #} - {{ 12345678|format_number({grouping_size:3, secondary_grouping_size:2}, locale='en') }} + {{ 12345678|format_number({grouping_size:3, secondary_grouping_size:2}, locale: 'en') }} * ``rounding_mode``: * ``rounding_increment``: Control rounding behavior, here is a list of all rounding_mode available: @@ -60,14 +60,14 @@ * ``floor``: Floor rounding * ``down``: Rounding towards zero * ``up``: Rounding away from zero - * ``half_even``: Round halves to the nearest even integer - * ``half_up``: Round halves up - * ``half_down``: Round halves down + * ``halfeven``: Round halves to the nearest even integer + * ``halfup``: Round halves up + * ``halfdown``: Round halves down .. code-block:: twig {# 123.5 #} - {{ 123.456|format_number({rounding_mode:'ceiling', rounding_increment:0.05}, locale='en') }} + {{ 123.456|format_number({rounding_mode:'ceiling', rounding_increment:0.05}, locale: 'en') }} * ``format_width``: * ``padding_position``: Set width and padding for the formatted number, here is a list of all padding_position available: @@ -79,19 +79,19 @@ .. code-block:: twig {# 123 #} - {{ 123|format_number({format_width:10, padding_position:'before_suffix'}, locale='en') }} + {{ 123|format_number({format_width:10, padding_position:'before_suffix'}, locale: 'en') }} * ``significant_digits_used``: * ``min_significant_digits_used``: * ``max_significant_digits_used``: Control significant digits in formatting:: {# 123.4568 #} - {{ 123.456789|format_number({significant_digits_used:true, min_significant_digits_used:4, max_significant_digits_used:7}, locale='en') }} + {{ 123.456789|format_number({significant_digits_used:true, min_significant_digits_used:4, max_significant_digits_used:7}, locale: 'en') }} * ``lenient_parse``: If true, allows lenient parsing of the input:: {# 123 #} - {{ 123|format_number({lenient_parse:true}, locale='en') }} + {{ 123|format_number({lenient_parse:true}, locale: 'en') }} Besides plain numbers, the filter can also format numbers in various styles:: @@ -109,37 +109,37 @@ * ``decimal``:: {# 1,234.568 #} - {{ 1234.56789 | format_number(style='decimal', locale='en') }} + {{ 1234.56789 | format_number(style: 'decimal', locale: 'en') }} * ``currency``:: {# $1,234.56 #} - {{ 1234.56 | format_number(style='currency', locale='en') }} + {{ 1234.56 | format_number(style: 'currency', locale: 'en') }} * ``percent``:: {# 12% #} - {{ 0.1234 | format_number(style='percent', locale='en') }} + {{ 0.1234 | format_number(style: 'percent', locale: 'en') }} * ``scientific``:: {# 1.23456789e+3 #} - {{ 1234.56789 | format_number(style='scientific', locale='en') }} + {{ 1234.56789 | format_number(style: 'scientific', locale: 'en') }} * ``spellout``:: {# one thousand two hundred thirty-four point five six seven eight nine #} - {{ 1234.56789 | format_number(style='spellout', locale='en') }} + {{ 1234.56789 | format_number(style: 'spellout', locale: 'en') }} * ``ordinal``:: {# 1st #} - {{ 1 | format_number(style='ordinal', locale='en') }} + {{ 1 | format_number(style: 'ordinal', locale: 'en') }} * ``duration``:: {# 2:30:00 #} - {{ 9000 | format_number(style='duration', locale='en') }} + {{ 9000 | format_number(style: 'duration', locale: 'en') }} As a shortcut, you can use the ``format_*_number`` filters by replacing ``*`` with a style:: diff -Nru php-twig-3.20.0/doc/filters/html_attr_merge.rst php-twig-3.26.0/doc/filters/html_attr_merge.rst --- php-twig-3.20.0/doc/filters/html_attr_merge.rst 1970-01-01 00:00:00.000000000 +0000 +++ php-twig-3.26.0/doc/filters/html_attr_merge.rst 2026-05-20 07:31:59.000000000 +0000 @@ -0,0 +1,141 @@ +``html_attr_merge`` +=================== + +.. _html_attr_merge: + +.. versionadded:: 3.24 + + The ``html_attr_merge`` filter was added in Twig 3.24. + +The ``html_attr_merge`` filter merges multiple mappings that represent +HTML attribute values. Such mappings contain the names of the HTML attributes +as keys, and the corresponding values represent the attributes' values. + +It is primarily designed for working with arrays that are passed to the +:ref:`html_attr` function. It closely resembles the :doc:`merge <../filters/merge>` +filter, but has different merge behavior for values that are iterables +themselves, as it will merge such values in turn. + +The filter returns a new merged array: + +.. code-block:: twig + + {% set base = {class: ['btn'], type: 'button'} %} + {% set variant = {class: ['btn-primary'], disabled: true} %} + + {% set merged = base|html_attr_merge(variant) %} + + {# merged is now: { + class: ['btn', 'btn-primary'], + type: 'button', + disabled: true + } #} + +The filter accepts multiple arrays as arguments and merges them from left to right: + +.. code-block:: twig + + {% set merged = base|html_attr_merge(variant1, variant2, variant3) %} + +A common use case is to build attribute mappings conditionally by merging multiple +parts based on conditions. To make this conditional merging more convenient, filter +arguments that are ``false``, ``null`` or empty arrays are ignored: + +.. code-block:: twig + + {% set button_attrs = { + type: 'button', + class: ['btn'] + }|html_attr_merge( + variant == 'primary' ? { class: ['btn-primary'] }, + variant == 'secondary' ? { class: ['btn-secondary'] }, + size == 'large' ? { class: ['btn-lg'] }, + size == 'small' ? { class: ['btn-sm'] }, + disabled ? { disabled: true, class: ['btn-disabled'] }, + loading ? { 'aria-busy': 'true', class: ['btn-loading'] }, + ) %} + + {# Example with variant='primary', size='large', disabled=false, loading=true: + + The false values (secondary variant, small size, disabled state) are ignored. + + button_attrs is: + { + type: 'button', + class: ['btn', 'btn-primary', 'btn-lg', 'btn-loading'], + 'aria-busy': 'true' + } + #} + +Merging Rules +------------- + +The filter follows these rules when merging attribute values: + +**Scalar values**: Later values override earlier ones. + +.. code-block:: twig + + {% set result = {id: 'old'}|html_attr_merge({id: 'new'}) %} + {# result: {id: 'new'} #} + +**Array values**: Arrays are merged like in PHP's ``array_merge`` function - numeric keys are +appended, non-numeric keys replace. + +.. code-block:: twig + + {# Numeric keys (appended): #} + {% set result = {class: ['btn']}|html_attr_merge({class: ['btn-primary']}) %} + {# result: {class: ['btn', 'btn-primary']} #} + + {# Non-numeric keys (replaced): #} + {% set result = {class: {base: 'btn', size: 'small'}}|html_attr_merge({class: {variant: 'primary', size: 'large'}}) %} + {# result: {class: {base: 'btn', size: 'large', variant: 'primary'}} #} + +.. note:: + + Remember, attribute mappings passed to or returned from this filter are regular + Twig mappings after all. If you want to completely replace an attribute value + that is an iterable with another value, you can use the :doc:`merge <../filters/merge>` + filter to do that. + +**``MergeableInterface`` implementations**: For advanced use cases, attribute values can be objects +that implement the ``MergeableInterface``. These objects can define their own, custom merge +behavior that takes precedence over the default rules. See the docblocks in that interface +for details. + +.. note:: + + The ``html_attr_merge`` filter is part of the ``HtmlExtension`` which is not + installed by default. Install it first: + + .. code-block:: bash + + $ composer require twig/html-extra + + Then, on Symfony projects, install the ``twig/extra-bundle``: + + .. code-block:: bash + + $ composer require twig/extra-bundle + + Otherwise, add the extension explicitly on the Twig environment:: + + use Twig\Extra\Html\HtmlExtension; + + $twig = new \Twig\Environment(...); + $twig->addExtension(new HtmlExtension()); + +Arguments +--------- + +The filter accepts a variadic list of arguments to merge. Each argument can be: + +* A map of attributes +* ``false`` or ``null`` (ignored, useful for conditional merging) +* An empty string ``''`` (ignored, to support implicit else in ternary operators) + +.. seealso:: + + :ref:`html_attr`, + :doc:`html_attr_type` diff -Nru php-twig-3.20.0/doc/filters/html_attr_type.rst php-twig-3.26.0/doc/filters/html_attr_type.rst --- php-twig-3.20.0/doc/filters/html_attr_type.rst 1970-01-01 00:00:00.000000000 +0000 +++ php-twig-3.26.0/doc/filters/html_attr_type.rst 2026-05-20 07:31:59.000000000 +0000 @@ -0,0 +1,123 @@ +``html_attr_type`` +================== + +.. _html_attr_type: + +.. versionadded:: 3.24 + + The ``html_attr_type`` filter was added in Twig 3.24. + +The ``html_attr_type`` filter converts arrays into specialized attribute value +objects that implement custom rendering logic. It is designed for use +with the :ref:`html_attr` function for attributes where +the attribute value follows special formatting rules. + +.. code-block:: html+twig + + + + {# Output: #} + +Available Types +--------------- + +Space-Separated Token List (``sst``) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Used for attributes that expect space-separated values, like ``class`` or +``aria-labelledby``: + +.. code-block:: html+twig + + {% set classes = ['btn', 'btn-primary']|html_attr_type('sst') %} + + + + {# Output: #} + +This is the default type used when the :ref:`html_attr` function encounters an +array value (except for ``style`` attributes). + +Comma-Separated Token List (``cst``) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Used for attributes that expect comma-separated values, like ``srcset`` or +``sizes``: + +.. code-block:: html+twig + + + + {# Output: #} + +Inline Style (``style``) +~~~~~~~~~~~~~~~~~~~~~~~~ + +Used for style attributes. Handles both maps (property - value pairs) and sequences (CSS declarations): + +.. code-block:: html+twig + + {# Associative array #} + {% set styles = {color: 'red', 'font-size': '14px'}|html_attr_type('style') %} + +

+ Styled content +
+ + {# Output:
Styled content
#} + + {# Numeric array #} + {% set styles = ['color: red', 'font-size: 14px']|html_attr_type('style') %} + +
+ Styled content +
+ + {# Output:
Styled content
#} + +The ``style`` type is automatically applied by the :ref:`html_attr` function when +it encounters an array value for the ``style`` attribute. + +.. note:: + + The ``html_attr_type`` filter is part of the ``HtmlExtension`` which is not + installed by default. Install it first: + + .. code-block:: bash + + $ composer require twig/html-extra + + Then, on Symfony projects, install the ``twig/extra-bundle``: + + .. code-block:: bash + + $ composer require twig/extra-bundle + + Otherwise, add the extension explicitly on the Twig environment:: + + use Twig\Extra\Html\HtmlExtension; + + $twig = new \Twig\Environment(...); + $twig->addExtension(new HtmlExtension()); + +Arguments +--------- + +* ``value``: The sequence of attributes to convert +* ``type``: The attribute type. One of: + + * ``sst`` (default): Space-separated token list + * ``cst``: Comma-separated token list + * ``style``: Inline CSS styles + +.. seealso:: + + :ref:`html_attr`, + :ref:`html_attr_merge` diff -Nru php-twig-3.20.0/doc/filters/index.rst php-twig-3.26.0/doc/filters/index.rst --- php-twig-3.20.0/doc/filters/index.rst 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/doc/filters/index.rst 2026-05-20 07:31:59.000000000 +0000 @@ -26,9 +26,12 @@ format_datetime format_number format_time + html_attr_merge + html_attr_type html_to_markdown inline_css inky_to_html + invoke join json_encode keys diff -Nru php-twig-3.20.0/doc/filters/inky_to_html.rst php-twig-3.26.0/doc/filters/inky_to_html.rst --- php-twig-3.20.0/doc/filters/inky_to_html.rst 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/doc/filters/inky_to_html.rst 2026-05-20 07:31:59.000000000 +0000 @@ -2,7 +2,7 @@ ================ The ``inky_to_html`` filter processes an `inky email template -`_: +`_: .. code-block:: html+twig diff -Nru php-twig-3.20.0/doc/filters/number_format.rst php-twig-3.26.0/doc/filters/number_format.rst --- php-twig-3.20.0/doc/filters/number_format.rst 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/doc/filters/number_format.rst 2026-05-20 07:31:59.000000000 +0000 @@ -15,15 +15,21 @@ {{ 9800.333|number_format(2, '.', ',') }} -To format negative numbers or math calculation, wrap the previous statement -with parentheses (needed because of Twig's :ref:`precedence of operators -`): +To format negative numbers, wrap the previous statement with parentheses (note +that as of Twig 3.21, not using parentheses is deprecated as the filter +operator will change precedence in Twig 4.0): .. code-block:: twig {{ -9800.333|number_format(2, '.', ',') }} {# outputs : -9 #} {{ (-9800.333)|number_format(2, '.', ',') }} {# outputs : -9,800.33 #} - {{ 1 + 0.2|number_format(2) }} {# outputs : 1.2 #} + +To format math calculation, wrap the previous statement with parentheses +(needed because of Twig's :ref:`precedence of operators -`): + +.. code-block:: twig + + {{ 1 + 0.2|number_format(2) }} {# outputs : 1.2 #} {{ (1 + 0.2)|number_format(2) }} {# outputs : 1.20 #} If no formatting options are provided then Twig will use the default formatting diff -Nru php-twig-3.20.0/doc/filters/reduce.rst php-twig-3.26.0/doc/filters/reduce.rst --- php-twig-3.20.0/doc/filters/reduce.rst 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/doc/filters/reduce.rst 2026-05-20 07:31:59.000000000 +0000 @@ -10,14 +10,14 @@ {% set numbers = [1, 2, 3] %} - {{ numbers|reduce((carry, v, k) => carry + v * k) }} + {{ numbers|reduce((carry, value, key) => carry + value * key) }} {# output 8 #} The ``reduce`` filter takes an ``initial`` value as a second argument: .. code-block:: twig - {{ numbers|reduce((carry, v, k) => carry + v * k, 10) }} + {{ numbers|reduce((carry, value, key) => carry + value * key, 10) }} {# output 18 #} Note that the arrow function has access to the current context. @@ -27,9 +27,3 @@ * ``arrow``: The arrow function * ``initial``: The initial value - -.. note:: - - Internally, Twig uses the PHP `array_reduce`_ function. - -.. _`array_reduce`: https://www.php.net/array_reduce diff -Nru php-twig-3.20.0/doc/filters/replace.rst php-twig-3.26.0/doc/filters/replace.rst --- php-twig-3.20.0/doc/filters/replace.rst 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/doc/filters/replace.rst 2026-05-20 07:31:59.000000000 +0000 @@ -6,6 +6,8 @@ .. code-block:: twig + {% set fruit = 'apples' %} + {{ "I like %this% and %that%."|replace({'%this%': fruit, '%that%': "oranges"}) }} {# if the "fruit" variable is set to "apples", #} {# it outputs "I like apples and oranges" #} diff -Nru php-twig-3.20.0/doc/filters/singular.rst php-twig-3.26.0/doc/filters/singular.rst --- php-twig-3.20.0/doc/filters/singular.rst 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/doc/filters/singular.rst 2026-05-20 07:31:59.000000000 +0000 @@ -43,11 +43,11 @@ --------- * ``locale``: The locale of the original string (limited to languages supported by the from Symfony `inflector`_, part of the String component) -* ``all``: Whether to return all possible plurals as an array, default is ``false`` +* ``all``: Whether to return all possible singulars as an array, default is ``false`` .. note:: Internally, Twig uses the `singularize`_ method from the Symfony String component. -.. _`inflector`: -.. _`singularize`: +.. _`inflector`: https://symfony.com/doc/current/components/string.html#inflector +.. _`singularize`: https://symfony.com/doc/current/components/string.html#inflector diff -Nru php-twig-3.20.0/doc/filters/slug.rst php-twig-3.26.0/doc/filters/slug.rst --- php-twig-3.20.0/doc/filters/slug.rst 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/doc/filters/slug.rst 2026-05-20 07:31:59.000000000 +0000 @@ -2,7 +2,7 @@ ======== The ``slug`` filter transforms a given string into another string that -only includes safe ASCII characters. +only includes safe ASCII characters. Here is an example: @@ -11,8 +11,8 @@ {{ 'Wôrķšƥáçè ~~sèťtïñğš~~'|slug }} Workspace-settings -The default separator between words is a dash (``-``), but you can -define a selector of your choice by passing it as an argument: +The default separator between words is a dash (``-``), but you can +define a separator of your choice by passing it as an argument: .. code-block:: twig @@ -27,8 +27,8 @@ {{ '...'|slug('-', 'ko') }} -The ``slug`` filter uses the method by the same name in Symfony's -`AsciiSlugger `_. +The ``slug`` filter uses the method by the same name in Symfony's +`AsciiSlugger `_. .. note:: diff -Nru php-twig-3.20.0/doc/filters/spaceless.rst php-twig-3.26.0/doc/filters/spaceless.rst --- php-twig-3.20.0/doc/filters/spaceless.rst 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/doc/filters/spaceless.rst 2026-05-20 07:31:59.000000000 +0000 @@ -6,6 +6,16 @@ The ``spaceless`` filter is deprecated as of Twig 3.12. While not a full replacement, you can check the :ref:`whitespace control features `. +.. caution:: + + The ``spaceless`` filter is declared safe for the ``html`` context, so its + output is not auto-escaped. Its input is therefore pre-escaped by the + auto-escaper when needed. If you re-implement this filter in your own + code, declare it with both ``'pre_escape' => 'html'`` and + ``'is_safe' => ['html']``: declaring ``is_safe`` without ``pre_escape`` + is equivalent to piping user input through ``|raw`` and opens an XSS + vector. + Use the ``spaceless`` filter to remove whitespace *between HTML tags*, not whitespace within HTML tags or whitespace in plain text: diff -Nru php-twig-3.20.0/doc/filters/u.rst php-twig-3.26.0/doc/filters/u.rst --- php-twig-3.20.0/doc/filters/u.rst 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/doc/filters/u.rst 2026-05-20 07:31:59.000000000 +0000 @@ -31,7 +31,7 @@ {{ 'Lorem ipsum'|u.truncate(8, '...') }} Lorem... -The ``truncate`` method also accepts a third argument to preserve whole words: +By default, ``truncate`` cuts text at the exact length. Pass ``false`` as the third argument to preserve whole words: .. code-block:: twig diff -Nru php-twig-3.20.0/doc/functions/country_timezones.rst php-twig-3.26.0/doc/functions/country_timezones.rst --- php-twig-3.20.0/doc/functions/country_timezones.rst 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/doc/functions/country_timezones.rst 2026-05-20 07:31:59.000000000 +0000 @@ -2,14 +2,14 @@ ===================== The ``country_timezones`` function returns the names of the timezones associated -with a given country its ISO-3166 code: +with a given country code (ISO-3166): .. code-block:: twig {# Europe/Paris #} {{ country_timezones('FR')|join(', ') }} -If the specified country were to be unknown, it will return an empty array +If the specified country is unknown, it will return an empty array. .. note:: diff -Nru php-twig-3.20.0/doc/functions/date.rst php-twig-3.26.0/doc/functions/date.rst --- php-twig-3.20.0/doc/functions/date.rst 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/doc/functions/date.rst 2026-05-20 07:31:59.000000000 +0000 @@ -9,7 +9,7 @@ {# do something #} {% endif %} -The argument must be in one of PHP’s supported `date and time formats`_. +The argument must be in one of PHP's supported `date and time formats`_. You can pass a timezone as the second argument: diff -Nru php-twig-3.20.0/doc/functions/enum.rst php-twig-3.26.0/doc/functions/enum.rst --- php-twig-3.20.0/doc/functions/enum.rst 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/doc/functions/enum.rst 2026-05-20 07:31:59.000000000 +0000 @@ -10,15 +10,25 @@ .. code-block:: twig {# display one specific case of a backed enum #} - {{ enum('App\\MyEnum').SomeCase.value }} + {{ enum('App\\CardSuite').Clubs.value }} {# "clubs" #} + + {# display one specific case of a backed enum, with a dynamic name #} + {% set case_name = 'Spades' %} + {{ enum('App\\CardSuite').(case_name).name }} {# "Spades" #} {# get all cases of an enum #} - {% for case in enum('App\\MyEnum').cases %} + {% for case in enum('App\\CardSuite').cases %} {{ case.value }} {% endfor %} + {# "clubs", "spades", "hearts", "diamonds" #} + + {# get a specific case of an enum by value #} + {% set card_suite = enum('App\\CardSuite').from('hearts') %} + {{ card_suite.name }} {# "Hearts" #} + {{ card_suite.value }} {# "hearts" #} {# call any methods of the enum class #} - {{ enum('App\\MyEnum').someMethod() }} + {{ enum('App\\CardSuite').someMethod() }} When using a string literal for the ``enum`` argument, it will be validated during compile time to be a valid enum name. diff -Nru php-twig-3.20.0/doc/functions/enum_cases.rst php-twig-3.26.0/doc/functions/enum_cases.rst --- php-twig-3.20.0/doc/functions/enum_cases.rst 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/doc/functions/enum_cases.rst 2026-05-20 07:31:59.000000000 +0000 @@ -9,9 +9,10 @@ .. code-block:: twig - {% for case in enum_cases('App\\MyEnum') %} + {% for case in enum_cases('App\\CardSuite') %} {{ case.value }} {% endfor %} + {# "clubs", "spades", "hearts", "diamonds" #} When using a string literal for the ``enum`` argument, it will be validated during compile time to be a valid enum name. diff -Nru php-twig-3.20.0/doc/functions/html_attr.rst php-twig-3.26.0/doc/functions/html_attr.rst --- php-twig-3.20.0/doc/functions/html_attr.rst 1970-01-01 00:00:00.000000000 +0000 +++ php-twig-3.26.0/doc/functions/html_attr.rst 2026-05-20 07:31:59.000000000 +0000 @@ -0,0 +1,202 @@ +``html_attr`` +============= + +.. _html_attr: + +.. versionadded:: 3.24 + + The ``html_attr`` function was added in Twig 3.24. + +The ``html_attr`` function renders HTML attributes from one or more mappings, +taking care of proper escaping. The mappings contain the names of HTML +attributes as keys, and the corresponding values represent the attributes' +values. + +.. note:: + + Attribute names are escaped using the ``html_attr_relaxed`` strategy. + +.. code-block:: html+twig + +
+ Content +
+ + {# Output:
Content
#} + +The function accepts multiple attribute maps. Internally, it uses +:ref:`html_attr_merge` to combine the arguments: + +.. code-block:: html+twig + + {% set base_attrs = {class: ['btn']} %} + {% set variant_attrs = {class: ['btn-primary'], disabled: true} %} + + + + {# Output: #} + +.. note:: + + To make best use of the special merge behavior of ``html_attr_merge`` and + to avoid confusion, you should consistently use iterables (mappings or sequences) + for attributes that can take multiple values, like ``class``, ``srcset`` or ``aria-describedby``. + + Use non-iterable values for attributes that contain a single value only, like + ``id`` or ``href``. + +Shorthand notation for mappings can be particularly helpful: + +.. code-block:: html+twig + + {% set id = 'user-123' %} + {% set href = '/profile' %} + + Profile + + {# Output: Profile #} + +``null`` and Boolean Attribute Values +------------------------------------- + +``null`` values always omit printing an attribute altogether. + +The boolean ``false`` value also omits the attribute altogether, with an +exception for ``aria-*`` attribute names, see below. + +.. code-block:: html+twig + + {# null omits the attribute entirely, and so does false for non-"aria-*" #} + + {# Output: #} + +``true`` will print the attribute with the empty value ``""``. This is XHTML compatible, +and in HTML 5 equivalent to using the short attribute notation without a value. An exception +is made for ``data-*`` and ``aria-*`` attributes, see below. + +.. code-block:: html+twig + + {# true becomes an empty string value #} + + {# Output: , which is equivalent to #} + +Array Values +------------ + +Attribute values that are iterables are automatically converted to space-separated +token lists of the values. Exceptions apply for ``data-*`` and ``style`` attributes, +described further below. + +.. code-block:: html+twig + +
+ Button +
+ + {# Output:
Button
#} + +.. note:: + + This is not bound to the ``class`` attribute name, but works for any attribute. + +You can use the :ref:`html_attr_type` filter to specify a different strategy for +concatenating values (e.g., comma-separated for ``srcset`` attributes). This would +also override the special behavior for ``data-*`` and ``style``. + +WAI-ARIA Attributes +------------------- + +To make it more convenient to work with the `WAI-ARIA type mapping for HTML +_`, boolean values for ``aria-*`` +attributes are converted to strings ``"true"`` and ``"false"``. + +.. code-block:: html+twig + + + + {# Output: #} + +Data Attributes +--------------- + +For ``data-*`` attributes, boolean ``true`` values will be converted to ``"true"``. +Values that are not scalars are automatically JSON-encoded. + +.. code-block:: html+twig + +
+ Content +
+ + {# Output:
Content
#} + +Style Attribute +---------------- + +The ``style`` attribute name has special handling when its value is iterable: + +.. code-block:: html+twig + + {# Non-numeric keys will be used as CSS properties and printed #} +
+ Styled text +
+ + {# Output:
Styled text
#} + + {# Numeric keys will be assumed to have values that are individual CSS declarations #} +
+ Styled text +
+ + {# Output:
Styled text
#} + + {# Merging style attributes #} +
+ Styled text +
+ + {# Output:
Styled text
#} + +.. warning:: + + No additional escaping specific to CSS is applied to key or values from this array. + Do not use it to pass untrusted, user-provided data, neither as key nor as value. + +``AttributeValueInterface`` Implementations +------------------------------------------- + +For advanced use cases, attribute values can be objects that implement the ``AttributeValueInterface``. +These objects can define their own conversion logic for the ``html_attr`` function that will take +precedence over all rules described here. See the docblocks in that interface for details. + +.. note:: + + The ``html_attr`` function is part of the ``HtmlExtension`` which is not + installed by default. Install it first: + + .. code-block:: bash + + $ composer require twig/html-extra + + Then, on Symfony projects, install the ``twig/extra-bundle``: + + .. code-block:: bash + + $ composer require twig/extra-bundle + + Otherwise, add the extension explicitly on the Twig environment:: + + use Twig\Extra\Html\HtmlExtension; + + $twig = new \Twig\Environment(...); + $twig->addExtension(new HtmlExtension()); + +.. seealso:: + + :ref:`html_attr_merge`, + :ref:`html_attr_type` diff -Nru php-twig-3.20.0/doc/functions/html_cva.rst php-twig-3.26.0/doc/functions/html_cva.rst --- php-twig-3.20.0/doc/functions/html_cva.rst 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/doc/functions/html_cva.rst 2026-05-20 07:31:59.000000000 +0000 @@ -5,7 +5,6 @@ The ``html_cva`` function was added in Twig 3.12. - `CVA (Class Variant Authority)`_ is a concept from the JavaScript world and used by the well-known `shadcn/ui`_ library. The CVA concept is used to render multiple variations of components, applying @@ -45,13 +44,13 @@ {# index.html.twig #} {{ include('alert.html.twig', {'color': 'blue', 'size': 'md'}) }} - // class="alert bg-blue text-md" + {# class="alert bg-blue text-md" #} {{ include('alert.html.twig', {'color': 'green', 'size': 'sm'}) }} - // class="alert bg-green text-sm" + {# class="alert bg-green text-sm" #} {{ include('alert.html.twig', {'color': 'red', 'class': 'flex items-center justify-center'}) }} - // class="alert bg-red flex items-center justify-center" + {# class="alert bg-red flex items-center justify-center" #} CVA and Tailwind CSS -------------------- @@ -61,18 +60,18 @@ ``tailwind_merge()`` filter from `tales-from-a-dev/twig-tailwind-extra`_ with the ``html_cva()`` function: -.. code-block:: terminal +.. code-block:: bash $ composer require tales-from-a-dev/twig-tailwind-extra .. code-block:: html+twig {% set alert = html_cva( - // ... + ... ) %}
- ... + ...
Compound Variants @@ -97,28 +96,28 @@ lg: 'text-lg', } }, - compoundVariants: [{ - // if color = red AND size = (md or lg), add the `font-bold` class + compound_variants: [{ + # if color = red AND size = (md or lg), add the `font-bold` class color: ['red'], size: ['md', 'lg'], - class: 'font-bold' + class: 'font-bold', }] ) %}
- ... + ...
{# index.html.twig #} {{ include('alert.html.twig', {color: 'red', size: 'lg'}) }} - // class="alert bg-red text-lg font-bold" + {# class="alert bg-red text-lg font-bold" #} {{ include('alert.html.twig', {color: 'green', size: 'sm'}) }} - // class="alert bg-green text-sm" + {# class="alert bg-green text-sm" #} {{ include('alert.html.twig', {color: 'red', size: 'md'}) }} - // class="alert bg-green text-md font-bold" + {# class="alert bg-green text-md font-bold" #} Default Variants ---------------- @@ -146,7 +145,7 @@ lg: 'rounded-lg', } }, - defaultVariant: { + default_variant: { rounded: 'md', } ) %} @@ -158,7 +157,7 @@ {# index.html.twig #} {{ include('alert.html.twig', {color: 'red', size: 'lg'}) }} - // class="alert bg-red text-lg rounded-md" + {# class="alert bg-red text-lg rounded-md" #} .. note:: diff -Nru php-twig-3.20.0/doc/functions/include.rst php-twig-3.26.0/doc/functions/include.rst --- php-twig-3.20.0/doc/functions/include.rst 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/doc/functions/include.rst 2026-05-20 07:31:59.000000000 +0000 @@ -27,12 +27,12 @@ .. code-block:: twig {# only the name variable will be accessible #} - {{ include('template.html.twig', {name: 'Fabien'}, with_context = false) }} + {{ include('template.html.twig', {name: 'Fabien'}, with_context: false) }} .. code-block:: twig {# no variables will be accessible #} - {{ include('template.html.twig', with_context = false) }} + {{ include('template.html.twig', with_context: false) }} And if the expression evaluates to a ``\Twig\Template`` or a ``\Twig\TemplateWrapper`` instance, Twig will use it directly:: @@ -48,7 +48,7 @@ .. code-block:: twig - {{ include('sidebar.html.twig', ignore_missing = true) }} + {{ include('sidebar.html.twig', ignore_missing: true) }} You can also provide a list of templates that are checked for existence before inclusion. The first template that exists will be rendered: diff -Nru php-twig-3.20.0/doc/functions/index.rst php-twig-3.26.0/doc/functions/index.rst --- php-twig-3.20.0/doc/functions/index.rst 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/doc/functions/index.rst 2026-05-20 07:31:59.000000000 +0000 @@ -12,6 +12,7 @@ dump enum enum_cases + html_attr html_classes html_cva include diff -Nru php-twig-3.20.0/doc/functions/random.rst php-twig-3.26.0/doc/functions/random.rst --- php-twig-3.20.0/doc/functions/random.rst 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/doc/functions/random.rst 2026-05-20 07:31:59.000000000 +0000 @@ -10,6 +10,11 @@ * a random integer between the integer parameter (when negative) and 0 (inclusive). * a random integer between the first integer and the second integer parameter (inclusive). +.. caution:: + + The ``random`` function does not produce cryptographically secure random numbers. + Do not use them for purposes that require returned values to be unguessable. + .. code-block:: twig {{ random(['apple', 'orange', 'citrus']) }} {# example output: orange #} diff -Nru php-twig-3.20.0/doc/functions/template_from_string.rst php-twig-3.26.0/doc/functions/template_from_string.rst --- php-twig-3.20.0/doc/functions/template_from_string.rst 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/doc/functions/template_from_string.rst 2026-05-20 07:31:59.000000000 +0000 @@ -43,6 +43,16 @@ with the ``include`` function, you can use it with any tag or function that takes a template as an argument (like the ``embed`` or ``extends`` tags). +.. danger:: + + Never allow ``template_from_string`` in a sandboxed template. There is no + legitimate use case for letting untrusted template code build and render + arbitrary new templates on the fly, and doing so makes any sandbox policy + impossible to reason about. In particular, when sandboxing is driven by a + ``SourcePolicyInterface``, the template created by ``template_from_string`` + gets a synthesized name that is not possible to match, so the inner + template ends up running with no sandbox at all. + Arguments --------- diff -Nru php-twig-3.20.0/doc/index.rst php-twig-3.26.0/doc/index.rst --- php-twig-3.20.0/doc/index.rst 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/doc/index.rst 2026-05-20 07:31:59.000000000 +0000 @@ -14,6 +14,7 @@ deprecated recipes coding_standards + operators_precedence tags/index filters/index functions/index diff -Nru php-twig-3.20.0/doc/operators_precedence.rst php-twig-3.26.0/doc/operators_precedence.rst --- php-twig-3.20.0/doc/operators_precedence.rst 1970-01-01 00:00:00.000000000 +0000 +++ php-twig-3.26.0/doc/operators_precedence.rst 2026-05-20 07:31:59.000000000 +0000 @@ -0,0 +1,204 @@ + ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| Precedence | Operator | Type | Associativity | Description | ++============+==================+=========+===============+===================================================================+ +| 512 | ``...`` | prefix | n/a | Spread operator | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| => 300 | ``|`` | infix | Left | Twig filter call | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``(`` | | | Twig function call | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``.``, ``?.`` | | | Get an attribute on a variable | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``[`` | | | Array access | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 500 | ``-`` | prefix | n/a | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``+`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 300 => 5 | ``??`` | infix | Right | Null coalescing operator (a ?? b) | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 250 | ``=>`` | infix | Left | Arrow function (x => expr) | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 200 | ``**`` | infix | Right | Exponentiation operator | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 100 | ``is`` | infix | Left | Twig tests | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``is not`` | | | Twig tests | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 60 | ``*`` | infix | Left | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``/`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``//`` | | | Floor division | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``%`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 50 => 70 | ``not`` | prefix | n/a | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 40 => 27 | ``~`` | infix | Left | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 30 | ``+`` | infix | Left | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``-`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 25 | ``..`` | infix | Left | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 20 | ``==`` | infix | Left | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``!=`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``<=>`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``<`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``>`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``>=`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``<=`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``not in`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``in`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``matches`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``starts with`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``ends with`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``has some`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``has every`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``===`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``!==`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 18 | ``b-and`` | infix | Left | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 17 | ``b-xor`` | infix | Left | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 16 | ``b-or`` | infix | Left | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 15 | ``and`` | infix | Left | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 12 | ``xor`` | infix | Left | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 10 | ``or`` | infix | Left | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 5 | ``?:``, ``? :`` | infix | Right | Elvis operator (a ?: b) | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 0 | ``(`` | prefix | n/a | Explicit group expression (a) | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``?`` | infix | Left | Conditional operator (a ? b : c) | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``=`` | | Right | Assignment operator | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``literal`` | prefix | n/a | A literal value (boolean, string, number, sequence, mapping, ...) | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ + +When a precedence will change in 4.0, the new precedence is indicated by the arrow ``=>``. + +Here is the same table for Twig 4.0 with adjusted precedences: + ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| Precedence | Operator | Type | Associativity | Description | ++============+==================+=========+===============+===================================================================+ +| 512 | ``...`` | prefix | n/a | Spread operator | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``(`` | infix | Left | Twig function call | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``.``, ``?.`` | | | Get an attribute on a variable | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``[`` | | | Array access | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 500 | ``-`` | prefix | n/a | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``+`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 300 | ``|`` | infix | Left | Twig filter call | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 250 | ``=>`` | infix | Left | Arrow function (x => expr) | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 200 | ``**`` | infix | Right | Exponentiation operator | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 100 | ``is`` | infix | Left | Twig tests | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``is not`` | | | Twig tests | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 70 | ``not`` | prefix | n/a | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 60 | ``*`` | infix | Left | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``/`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``//`` | | | Floor division | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``%`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 30 | ``+`` | infix | Left | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``-`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 27 | ``~`` | infix | Left | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 25 | ``..`` | infix | Left | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 20 | ``==`` | infix | Left | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``!=`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``<=>`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``<`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``>`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``>=`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``<=`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``not in`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``in`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``matches`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``starts with`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``ends with`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``has some`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``has every`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``===`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``!==`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 18 | ``b-and`` | infix | Left | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 17 | ``b-xor`` | infix | Left | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 16 | ``b-or`` | infix | Left | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 15 | ``and`` | infix | Left | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 12 | ``xor`` | infix | Left | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 10 | ``or`` | infix | Left | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 5 | ``??`` | infix | Right | Null coalescing operator (a ?? b) | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``?:``, ``? :`` | | | Elvis operator (a ?: b) | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 0 | ``(`` | prefix | n/a | Explicit group expression (a) | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``?`` | infix | Left | Conditional operator (a ? b : c) | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``=`` | | Right | Assignment operator | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``literal`` | prefix | n/a | A literal value (boolean, string, number, sequence, mapping, ...) | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ diff -Nru php-twig-3.20.0/doc/recipes.rst php-twig-3.26.0/doc/recipes.rst --- php-twig-3.20.0/doc/recipes.rst 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/doc/recipes.rst 2026-05-20 07:31:59.000000000 +0000 @@ -31,7 +31,7 @@ template names as keys and template contents as values (as done by ``\Twig\Util\TemplateDirIterator``). -However, this code won't find all deprecations (like using deprecated some Twig +However, this code won't find all deprecations (like using some deprecated Twig classes). To catch all notices, register a custom error handler like the one below:: @@ -271,13 +271,19 @@ The ``registerUndefinedTokenParserCallback()`` method was added in Twig 3.2. -When a function/filter/tag is not defined, Twig defaults to throw a +.. versionadded:: 3.22 + + The ``registerUndefinedTestCallback()`` method was added in Twig + 3.22. + +When a function/filter/test/tag is not defined, Twig defaults to throw a ``\Twig\Error\SyntaxError`` exception. However, it can also call a `callback`_ -(any valid PHP callable) which should return a function/filter/tag. +(any valid PHP callable) which should return a function/filter/test/tag. For tags, register callbacks with ``registerUndefinedTokenParserCallback()``. For filters, register callbacks with ``registerUndefinedFilterCallback()``. -For functions, use ``registerUndefinedFunctionCallback()``:: +For functions, use ``registerUndefinedFunctionCallback()``. +For tests, use ``registerUndefinedTestCallback()``:: // auto-register all native PHP functions as Twig functions // NEVER do this in a project as it's NOT secure @@ -289,7 +295,7 @@ return false; }); -If the callable is not able to return a valid function/filter/tag, it must +If the callable is not able to return a valid function/filter/test/tag, it must return ``false``. If you register more than one callback, Twig will call them in turn until one @@ -297,7 +303,7 @@ .. tip:: - As the resolution of functions/filters/tags is done during compilation, + As the resolution of functions/filters/tests/tags is done during compilation, there is no overhead when registering these callbacks. .. warning:: diff -Nru php-twig-3.20.0/doc/sandbox.rst php-twig-3.26.0/doc/sandbox.rst --- php-twig-3.20.0/doc/sandbox.rst 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/doc/sandbox.rst 2026-05-20 07:31:59.000000000 +0000 @@ -17,7 +17,7 @@ the ``SandboxExtension`` constructor. By default, Twig comes with one policy class: ``\Twig\Sandbox\SecurityPolicy``. -This class allows you to allow-list some tags, filters, functions, but also +This class allows you to allow-list some tags, filters, functions, and properties and methods on objects:: $tags = ['if']; @@ -31,11 +31,11 @@ $functions = ['range']; $policy = new \Twig\Sandbox\SecurityPolicy($tags, $filters, $methods, $properties, $functions); -With the previous configuration, the security policy will only allow usage of -the ``if`` tag, and the ``upper`` filter. Moreover, the templates will only be -able to call the ``getTitle()`` and ``getBody()`` methods on ``Article`` -objects, and the ``title`` and ``body`` public properties. Everything else -won't be allowed and will generate a ``\Twig\Sandbox\SecurityError`` exception. +With the above configuration, the security policy will only allow usage of the +``if`` tag, and the ``upper`` filter. Moreover, the templates will only be able +to call the ``getTitle()`` and ``getBody()`` methods on ``Article`` objects, +and the ``title`` and ``body`` public properties. Everything else won't be +allowed and will generate a ``\Twig\Sandbox\SecurityError`` exception. .. note:: @@ -66,7 +66,22 @@ You can sandbox all templates by passing ``true`` as the second argument of the extension constructor:: - $sandbox = new \Twig\Extension\SandboxExtension($policy, true); + $twig->addExtension(new \Twig\Extension\SandboxExtension($policy, true)); + +Limiting Resource Usage +----------------------- + +The sandbox prevents untrusted templates from reaching code, data, methods, or +properties they shouldn't. It does **not** prevent a template from consuming +CPU, memory, or wall-clock time, even under the strictest allow-list. + +This is by design: any limit baked into Twig itself would be both arbitrary +and trivial to work around, since there are many ways a template can burn +resources (large ranges, nested loops, large string operations, recursive +macros, expensive filters, deeply nested includes, and so on). + +If you render untrusted templates, you should contain them at the process level +rather than at the template engine level. Accepting Callables Arguments ----------------------------- diff -Nru php-twig-3.20.0/doc/tags/block.rst php-twig-3.26.0/doc/tags/block.rst --- php-twig-3.20.0/doc/tags/block.rst 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/doc/tags/block.rst 2026-05-20 07:31:59.000000000 +0000 @@ -5,7 +5,7 @@ the same time. They are documented in detail in the documentation for the :doc:`extends<../tags/extends>` tag. -Block names must consist of alphanumeric characters, and underscores. The first char can't be a digit and dashes are not permitted. +Block names must consist of alphanumeric characters, and underscores. The first character can't be a digit and dashes are not permitted. .. seealso:: diff -Nru php-twig-3.20.0/doc/tags/cache.rst php-twig-3.26.0/doc/tags/cache.rst --- php-twig-3.20.0/doc/tags/cache.rst 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/doc/tags/cache.rst 2026-05-20 07:31:59.000000000 +0000 @@ -42,7 +42,7 @@ and ``post.updated_at`` returns a timestamp that represents the time where the blog post was last modified. -Using such a strategy for naming cache keys allows to avoid using a ``ttl``. +Using such a strategy for naming cache keys allows you to avoid using a ``ttl``. It's like using a "validation" strategy instead of an "expiration" strategy as we do for HTTP caches. diff -Nru php-twig-3.20.0/doc/tags/extends.rst php-twig-3.26.0/doc/tags/extends.rst --- php-twig-3.20.0/doc/tags/extends.rst 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/doc/tags/extends.rst 2026-05-20 07:31:59.000000000 +0000 @@ -23,12 +23,12 @@ {% endblock %} -
{% block content %}{% endblock %}
- + @@ -71,8 +71,8 @@ You can't define multiple ``block`` tags with the same name in the same template. This limitation exists because a block tag works in "both" -directions. That is, a block tag doesn't just provide a hole to fill - it also -defines the content that fills the hole in the *parent*. If there were two +directions. That is, a block tag doesn't just provide a slot to fill - it also +defines the content that fills the slot in the *parent*. If there were two similarly-named ``block`` tags in a template, that template's parent wouldn't know which one of the blocks' content to use. diff -Nru php-twig-3.20.0/doc/tags/index.rst php-twig-3.26.0/doc/tags/index.rst --- php-twig-3.20.0/doc/tags/index.rst 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/doc/tags/index.rst 2026-05-20 07:31:59.000000000 +0000 @@ -12,10 +12,10 @@ do embed extends - guard flush for from + guard if import include diff -Nru php-twig-3.20.0/doc/tags/set.rst php-twig-3.26.0/doc/tags/set.rst --- php-twig-3.20.0/doc/tags/set.rst 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/doc/tags/set.rst 2026-05-20 07:31:59.000000000 +0000 @@ -27,6 +27,16 @@ {% set user = {'name': 'Fabien'} %} {% set name = 'Fabien' ~ ' ' ~ 'Potencier' %} +.. tip:: + + To assign a value within an expression, use the :ref:`= operator + `: + + .. code-block:: twig + + {# use assignment within a larger expression #} + {{ (result = fetch_data()) ? result : 'default' }} + Several variables can be assigned in one block: .. code-block:: twig diff -Nru php-twig-3.20.0/doc/templates.rst php-twig-3.26.0/doc/templates.rst --- php-twig-3.20.0/doc/templates.rst 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/doc/templates.rst 2026-05-20 07:31:59.000000000 +0000 @@ -68,11 +68,10 @@ You might also be interested in: +* `Twig CS Fixer`_: a tool to check/fix your templates code style * `Twig Language Server`_: provides some language features like syntax highlighting, diagnostics, auto complete, ... - * `TwigQI`_: an extension which analyzes your templates for common bugs during compilation - * `TwigStan`_: a static analyzer for Twig templates powered by PHPStan Variables @@ -132,14 +131,33 @@ Setting Variables ~~~~~~~~~~~~~~~~~ -You can assign values to variables inside code blocks. Assignments use the -:doc:`set` tag: +You can assign values to variables inside code blocks using either the +:doc:`set` tag or the :ref:`= operator `: .. code-block:: twig {% set name = 'Fabien' %} {% set numbers = [1, 2] %} {% set map = {'city': 'Paris'} %} + {% set first, last = 'Fabien', 'Potencier' %} + + {# or #} + + {% do name = 'Fabien' %} + {% do numbers = [1, 2] %} + {% do map = {'city': 'Paris'} %} + {% do [first, last] = ['Fabien', 'Potencier'] %} + +The ``set`` tag can also be used to capture template content into +a variable: + + .. code-block:: html+twig + + {% set content %} + + {% endset %} + +See the :doc:`set` tag documentation for more details. Filters ------- @@ -186,28 +204,6 @@ {{ ('HELLO' ~ 'FABIEN')|lower }} - A common mistake is to forget using parentheses for filters on negative - numbers as a negative number in Twig is represented by the ``-`` operator - followed by a positive number. As the ``-`` operator has a lower precedence - than the filter operator, it can lead to confusion: - - .. code-block:: twig - - {{ -1|abs }} {# returns -1 #} - {{ -1**0 }} {# returns -1 #} - - {# as it is equivalent to #} - - {{ -(1|abs) }} - {{ -(1**0) }} - - For such cases, use parentheses to force the precedence: - - .. code-block:: twig - - {{ (-1)|abs }} {# returns 1 as expected #} - {{ (-1)**0 }} {# returns 1 as expected #} - Functions --------- @@ -365,7 +361,7 @@ }} As inline comments continue until the end of the current line, the following -code does not work as ``}}``would be part of the comment: +code does not work as ``}}`` would be part of the comment: .. code-block:: twig @@ -614,7 +610,7 @@ * ``42`` / ``42.23``: Integers and floating point numbers are created by writing the number down. If a dot is present the number is a float, - otherwise an integer. Underscores can be used as digits separator to + otherwise an integer. Underscores can be used as digits separator to improve readability (``-3_141.592_65`` is equivalent to ``-3141.59265``). * ``["first_name", "last_name"]``: Sequences are defined by a sequence of expressions @@ -703,14 +699,16 @@ ``4``. * ``//``: Divides two numbers and returns the floored integer result. ``{{ 20 - // 7 }}`` is ``2``, ``{{ -20 // 7 }}`` is ``-3`` (this is just syntactic + // 7 }}`` is ``2``, ``{{ -20 // 7 }}`` is ``-3`` (this is just syntactic sugar for the :doc:`round` filter). * ``*``: Multiplies the left operand with the right one. ``{{ 2 * 2 }}`` would return ``4``. * ``**``: Raises the left operand to the power of the right operand. ``{{ 2 ** - 3 }}`` would return ``8``. + 3 }}`` would return ``8``. Be careful as the ``**`` operator is right + associative, which means that ``{{ -1**0 }}`` is equivalent to ``{{ -(1**0) + }}`` and not ``{{ (-1)**0 }}``. .. _template_logic: @@ -743,6 +741,9 @@ The following mathematical comparison operators are supported in any expression: ``==``, ``!=``, ``<``, ``>``, ``>=``, and ``<=``. +In addition, the ``===`` and ``!==`` strict comparison operators are supported +(they are equivalent to the ``same as`` and ``not same as`` tests). + Spaceship Operator ~~~~~~~~~~~~~~~~~~ @@ -793,6 +794,21 @@ You can use this operator to perform a containment test on strings, sequences, mappings, or objects implementing the ``Traversable`` interface. +.. note:: + + For sequences, mappings, and ``Traversable`` objects, ``in`` uses a loose + comparison (similar to ``==``); use :doc:`same as ` for a + strict comparison. Like PHP's ``in_array()``, this can yield unexpected + results when the left operand is a boolean: + + .. code-block:: twig + + {# returns true because true == 'foo' under PHP loose comparison #} + {{ true in ['foo', 'bar'] }} + + Containment on strings only accepts string, integer, and float operands on + the left; other types always return ``false``. + To perform a negative test, use the ``not in`` operator: .. code-block:: twig @@ -883,7 +899,7 @@ .. _dot_operator: -* ``.``, ``[]``: Gets an attribute of a variable. +* ``.``, ``?.``, ``[]``: Gets an attribute of a variable. The (``.``) operator abstracts getting an attribute of a variable (methods, properties or constants of a PHP object, or items of a PHP array): @@ -892,11 +908,27 @@ {{ user.name }} - Twig supports a specific syntax via the ``[]`` operator for accessing items - on sequences and mappings, like in ``user['name']``: + The null-safe operator (``?.``) works like the dot operator but returns + ``null`` instead of throwing an exception when the left operand is ``null``. + If the operand is part of a chain, the rest of the chain is skipped: + + .. code-block:: twig + + {{ user?.name }} + {# returns null if user is null, otherwise returns user.name #} - After the ``.``, you can use any expression by wrapping it with parenthesis - ``()``. + {{ user?.address?.city }} + {# can be chained for safe navigation through potentially null values #} + + {{ user?.address.city }} + {# returns null if user is null, the rest of the chain is skipped (address.city is not evaluated) #} + + .. versionadded:: 3.23 + + The null-safe operator was added in Twig 3.23. + + After the ``.`` or ``?.``, you can use any expression by wrapping it with + parenthesis ``()``. One use case is when the attribute contains special characters (like ``-`` that would be interpreted as the minus operator): @@ -905,6 +937,7 @@ {# equivalent to the non-working user.first-name #} {{ user.('first-name') }} + {{ user?.('first-name') }} Another use case is when the attribute is "dynamic" (defined via a variable): @@ -912,6 +945,7 @@ {{ user.(name) }} {{ user.('get' ~ name) }} + {{ user?.(name) }} Before Twig 3.15, use the :doc:`attribute ` function instead for the two previous use cases. @@ -937,7 +971,7 @@ To resolve ``user.name`` to a PHP call, Twig uses the following algorithm at runtime: - * check if ``user`` is a PHP array or a ArrayObject/ArrayAccess object and + * check if ``user`` is a PHP array or an ArrayObject/ArrayAccess object and ``name`` a valid element; * if not, and if ``user`` is a PHP object, check that ``name`` is a valid property; * if not, and if ``user`` is a PHP object, check that ``name`` is a class constant; @@ -947,6 +981,12 @@ * if not, and if ``strict_variables`` is ``false``, return ``null``; * if not, throw an exception. + To resolve ``user?.name`` to a PHP call, Twig checks if ``user`` is + ``null`` first: + + * if ``user`` is ``null``, return ``null``; + * otherwise, use the same algorithm as for ``user.name``. + To resolve ``user['name']`` to a PHP call, Twig uses the following algorithm at runtime: @@ -957,7 +997,7 @@ Twig supports a specific syntax via the ``()`` operator for calling methods on objects, like in ``user.name()``: - * check if ``user`` is a object and has the ``name()``, ``getName()``, + * check if ``user`` is an object and has the ``name()``, ``getName()``, ``isName()``, or ``hasName()`` method; * if not, and if ``strict_variables`` is ``false``, return ``null``; * if not, throw an exception. @@ -992,6 +1032,32 @@ Support for expanding the arguments of a function call was introduced in Twig 3.15. +.. _templates-assignment-operator: + +* ``=``: The assignment operator assigns a value to a variable within an + expression: + + .. code-block:: twig + + {# assign #} + {% do b = 1 + 3 %} + + {# assign and output the result #} + {{ b = 1 + 3 }} + + {# assignments can be chained #} + {% do a = b = 'foo' %} + + {# assignment can be used inside other expressions #} + {% do a = (b = 4) + 5 %} + + The assignment operator also supports :ref:`destructuring + `. + + .. versionadded:: 3.23 + + The ``=`` assignment operator was added in Twig 3.23. + * ``=>``: The arrow operator allows the creation of functions. A function is made of arguments (use parentheses for multiple arguments) and an arrow (``=>``) followed by an expression to execute. The expression has access to @@ -1032,36 +1098,10 @@ Understanding the precedence of these operators is crucial for writing correct and efficient Twig templates. -The operator precedence rules are as follows, with the lowest-precedence -operators listed first: +The operator precedence rules are as follows, with the highest-precedence +operators listed first. -============================= =================================== ===================================================== -Operator Score of precedence Description -============================= =================================== ===================================================== -``?:`` 0 Ternary operator, conditional statement -``or`` 10 Logical OR operation between two boolean expressions -``xor`` 12 Logical XOR operation between two boolean expressions -``and`` 15 Logical AND operation between two boolean expressions -``b-or`` 16 Bitwise OR operation on integers -``b-xor`` 17 Bitwise XOR operation on integers -``b-and`` 18 Bitwise AND operation on integers -``==``, ``!=``, ``<=>``, 20 Comparison operators -``<``, ``>``, ``>=``, -``<=``, ``not in``, ``in``, -``matches``, ``starts with``, -``ends with``, ``has some``, -``has every`` -``..`` 25 Range of values -``+``, ``-`` 30 Addition and subtraction on numbers -``~`` 40 String concatenation -``not`` 50 Negates a statement -``*``, ``/``, ``//``, ``%`` 60 Arithmetic operations on numbers -``is``, ``is not`` 100 Tests -``**`` 200 Raises a number to the power of another -``??`` 300 Default value when a variable is null -``+``, ``-`` 500 Unary operations on numbers -``|``,``[]``,``.`` - Filters, sequence, mapping, and attribute access -============================= =================================== ===================================================== +.. include:: operators_precedence.rst Without using any parentheses, the operator precedence rules are used to determine how to convert the code to PHP: @@ -1085,6 +1125,98 @@ {# use parenthesis to change precedence #} {{ (greeting ~ name)|lower }} {# hello fabien #} +.. _templates-destructuring: + +Destructuring +------------- + +.. versionadded:: 3.23 + + Destructuring was added in Twig 3.23. + +Destructuring allows you to extract values from sequences and assign them to +variables in a single operation using the ``=`` :ref:`assignment operator +`. + +Like in PHP, destructuring expressions return the right-hand side value, not +the extracted values: + +.. code-block:: twig + + {# returns the full user object, allowing chained access #} + {{ ({name} = user).email }} + +Sequence Destructuring +~~~~~~~~~~~~~~~~~~~~~~ + +Use square brackets on the left side of an assignment to destructure a +sequence: + +.. code-block:: twig + + {% do [first, last] = ['Fabien', 'Potencier'] %} + + {{ first }} {# Fabien #} + {{ last }} {# Potencier #} + +If there are more variables than values, the extra variables are set to +``null``: + +.. code-block:: twig + + {# extra will be null #} + {% do [first, last, extra] = ['Fabien', 'Potencier'] %} + +You can skip values by leaving a slot empty: + +.. code-block:: twig + + {# only assign the second value #} + {% do [, last] = ['Fabien', 'Potencier'] %} + +Object Destructuring +~~~~~~~~~~~~~~~~~~~~ + +Use curly braces on the left side of an assignment to destructure an object +or mapping by extracting values based on property/key names: + +.. code-block:: twig + + {% do {name, email} = user %} + + {{ name }} {# user.name #} + {{ email }} {# user.email #} + +You can rename variables during destructuring by using the ``key: variable`` +syntax, where the key is the property to extract and the variable is the name +to assign to: + +.. code-block:: twig + + {% do {name: userName, email: userEmail} = user %} + + {{ userName }} {# user.name #} + {{ userEmail }} {# user.email #} + +This is especially useful when you need to destructure multiple objects that +share the same property names: + +.. code-block:: twig + + {% do {data: product, error: productError} = loadProduct() %} + {% do {data: stock, error: stockError} = loadStock() %} + + {{ product }} {# loadProduct().data #} + {{ productError }} {# loadProduct().error #} + {{ stock }} {# loadStock().data #} + {{ stockError }} {# loadStock().error #} + +.. note:: + + Object destructuring uses the :ref:`dot operator ` to access + values, so ``{name} = user`` is equivalent to ``name = user.name`` or + ``name = user["name"]`` depending on the type of the variable. + .. _templates-whitespace-control: Whitespace Control @@ -1104,7 +1236,7 @@ * *Line whitespace trimming* via the ``~`` modifier: Removes all whitespace (excluding newlines). Using this modifier on the right disables the default - removal of the first newline inherited from PHP. + removal of the first newline mentioned above. The modifiers can be used on either side of the tags like in ``{%-`` or ``-%}`` and they consume all whitespace for that side of the tag. It is possible to use @@ -1151,6 +1283,7 @@ .. _`TwigStan`: https://github.com/twigstan/twigstan .. _`Twig pack`: https://marketplace.visualstudio.com/items?itemName=bajdzis.vscode-twig-pack .. _`Modern Twig`: https://marketplace.visualstudio.com/items?itemName=Stanislav.vscode-twig +.. _`Twig CS Fixer`: https://github.com/VincentLanglet/Twig-CS-Fixer .. _`Twig Language Server`: https://github.com/kaermorchen/twig-language-server/tree/master/packages/language-server .. _`Twiggy`: https://marketplace.visualstudio.com/items?itemName=moetelo.twiggy .. _`PHP spaceship operator documentation`: https://www.php.net/manual/en/language.operators.comparison.php diff -Nru php-twig-3.20.0/doc/tests/index.rst php-twig-3.26.0/doc/tests/index.rst --- php-twig-3.20.0/doc/tests/index.rst 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/doc/tests/index.rst 2026-05-20 07:31:59.000000000 +0000 @@ -10,6 +10,8 @@ empty even iterable + mapping null odd sameas + sequence diff -Nru php-twig-3.20.0/extra/cache-extra/.gitattributes php-twig-3.26.0/extra/cache-extra/.gitattributes --- php-twig-3.20.0/extra/cache-extra/.gitattributes 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/extra/cache-extra/.gitattributes 2026-05-20 07:31:59.000000000 +0000 @@ -1,2 +1,3 @@ /Tests export-ignore +/.git* export-ignore /phpunit.xml.dist export-ignore diff -Nru php-twig-3.20.0/extra/cache-extra/Tests/FunctionalTest.php php-twig-3.26.0/extra/cache-extra/Tests/FunctionalTest.php --- php-twig-3.20.0/extra/cache-extra/Tests/FunctionalTest.php 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/extra/cache-extra/Tests/FunctionalTest.php 2026-05-20 07:31:59.000000000 +0000 @@ -29,7 +29,7 @@ $twig = $this->createEnvironment(['index' => '{% cache "city;v1" %}{{- city -}}{% endcache %}'], $cache); $this->assertSame('Paris', $twig->render('index', ['city' => 'Paris'])); - $value = $cache->get('city;v1', function () { throw new \RuntimeException('Key should be in the cache'); }); + $value = $cache->get('city;v1', static function () { throw new \RuntimeException('Key should be in the cache'); }); $this->assertSame('Paris', $value); } diff -Nru php-twig-3.20.0/extra/cache-extra/TokenParser/CacheTokenParser.php php-twig-3.26.0/extra/cache-extra/TokenParser/CacheTokenParser.php --- php-twig-3.20.0/extra/cache-extra/TokenParser/CacheTokenParser.php 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/extra/cache-extra/TokenParser/CacheTokenParser.php 2026-05-20 07:31:59.000000000 +0000 @@ -24,24 +24,23 @@ public function parse(Token $token): Node { $stream = $this->parser->getStream(); - $expressionParser = $this->parser->getExpressionParser(); - $key = $expressionParser->parseExpression(); + $key = $this->parser->parseExpression(); $ttl = null; $tags = null; while ($stream->test(Token::NAME_TYPE)) { $k = $stream->getCurrent()->getValue(); - if (!in_array($k, ['ttl', 'tags'])) { + if (!\in_array($k, ['ttl', 'tags'], true)) { throw new SyntaxError(\sprintf('Unknown "%s" configuration.', $k), $stream->getCurrent()->getLine(), $stream->getSourceContext()); } $stream->next(); - $stream->expect(Token::PUNCTUATION_TYPE, '('); + $stream->expect(Token::OPERATOR_TYPE, '('); $line = $stream->getCurrent()->getLine(); if ($stream->test(Token::PUNCTUATION_TYPE, ')')) { throw new SyntaxError(\sprintf('The "%s" modifier takes exactly one argument (0 given).', $k), $line, $stream->getSourceContext()); } - $arg = $expressionParser->parseExpression(); + $arg = $this->parser->parseExpression(); if ($stream->test(Token::PUNCTUATION_TYPE, ',')) { throw new SyntaxError(\sprintf('The "%s" modifier takes exactly one argument (2 given).', $k), $line, $stream->getSourceContext()); } diff -Nru php-twig-3.20.0/extra/cache-extra/composer.json php-twig-3.26.0/extra/cache-extra/composer.json --- php-twig-3.20.0/extra/cache-extra/composer.json 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/extra/cache-extra/composer.json 2026-05-20 07:31:59.000000000 +0000 @@ -16,8 +16,8 @@ ], "require": { "php": ">=8.1.0", - "symfony/cache": "^5.4|^6.4|^7.0", - "twig/twig": "^3.19|^4.0" + "symfony/cache": "^5.4|^6.4|^7.0|^8.0", + "twig/twig": "^3.21|^4.0" }, "require-dev": { "symfony/phpunit-bridge": "^6.4|^7.0" diff -Nru php-twig-3.20.0/extra/cssinliner-extra/.gitattributes php-twig-3.26.0/extra/cssinliner-extra/.gitattributes --- php-twig-3.20.0/extra/cssinliner-extra/.gitattributes 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/extra/cssinliner-extra/.gitattributes 2026-05-20 07:31:59.000000000 +0000 @@ -1,2 +1,3 @@ /Tests export-ignore +/.git* export-ignore /phpunit.xml.dist export-ignore diff -Nru php-twig-3.20.0/extra/cssinliner-extra/CssInlinerExtension.php php-twig-3.26.0/extra/cssinliner-extra/CssInlinerExtension.php --- php-twig-3.20.0/extra/cssinliner-extra/CssInlinerExtension.php 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/extra/cssinliner-extra/CssInlinerExtension.php 2026-05-20 07:31:59.000000000 +0000 @@ -1,9 +1,9 @@ + * (c) Fabien Potencier * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -20,7 +20,7 @@ public function getFilters(): array { return [ - new TwigFilter('inline_css', [self::class, 'inlineCss'], ['is_safe' => ['all']]), + new TwigFilter('inline_css', [self::class, 'inlineCss'], ['is_safe' => ['html'], 'pre_escape' => 'html']), ]; } diff -Nru php-twig-3.20.0/extra/cssinliner-extra/Resources/functions.php php-twig-3.26.0/extra/cssinliner-extra/Resources/functions.php --- php-twig-3.20.0/extra/cssinliner-extra/Resources/functions.php 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/extra/cssinliner-extra/Resources/functions.php 2026-05-20 07:31:59.000000000 +0000 @@ -1,9 +1,9 @@ + * (c) Fabien Potencier * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff -Nru php-twig-3.20.0/extra/cssinliner-extra/Tests/FunctionalTest.php php-twig-3.26.0/extra/cssinliner-extra/Tests/FunctionalTest.php --- php-twig-3.20.0/extra/cssinliner-extra/Tests/FunctionalTest.php 1970-01-01 00:00:00.000000000 +0000 +++ php-twig-3.26.0/extra/cssinliner-extra/Tests/FunctionalTest.php 2026-05-20 07:31:59.000000000 +0000 @@ -0,0 +1,47 @@ + "{% autoescape 'js' %}{% apply inline_css %}

x

{% endapply %}{% endautoescape %}", + ])); + $twig->addExtension(new CssInlinerExtension()); + + $output = $twig->render('index'); + + $this->assertStringNotContainsString('

', $output); + $this->assertStringNotContainsString('

', $output); + $this->assertMatchesRegularExpression('{\\\\u003[Cc]p\\\\u003[Ee]x\\\\u003[Cc]\\\\/p\\\\u003[Ee]}', $output); + } + + public function testInlineCssPreEscapesUnsafeInput() + { + $twig = new Environment(new ArrayLoader([ + 'index' => '{{ payload|inline_css }}', + ])); + $twig->addExtension(new CssInlinerExtension()); + + $output = $twig->render('index', ['payload' => '']); + + $this->assertStringNotContainsString(''], + ], + ]; + + // Variadic merging scenarios + yield 'scalar value overrides from left to right' => [ + 'id="final"', + [ + ['id' => 'first'], + ['id' => 'second'], + ['id' => 'final'], + ], + ]; + + yield 'variadic with mixed false and null values' => [ + 'id="test"', + [ + ['id' => 'test'], + null, + false, + null, + ], + ]; + + yield 'variadic with empty arrays' => [ + 'id="test"', + [ + [], + ['id' => 'test'], + [], + ], + ]; + + yield 'variadic with empty string values' => [ + 'id="test"', + [ + '', + ['id' => 'test'], + '', + ], + ]; + + // AttributeValueInterface + yield 'AttributeValueInterface with string value' => [ + 'custom="custom-value"', + [ + ['custom' => new AttributeValueStub('custom-value')], + ], + ]; + + yield 'AttributeValueInterface with null value omits attribute' => [ + '', + [ + ['custom' => new AttributeValueStub(null)], + ], + ]; + + yield 'AttributeValueInterface wins over special case handling for style and data-*' => [ + 'style="some style" data-custom="not JSON"', + [ + ['style' => new AttributeValueStub('some style'), 'data-custom' => new AttributeValueStub('not JSON')], + ], + ]; + + // Edge cases + yield 'numeric attribute value' => [ + 'tabindex="0"', + [ + ['tabindex' => 0], + ], + ]; + + yield 'zero is not treated as falsy' => [ + 'data-count="0"', + [ + ['data-count' => 0], + ], + ]; + + // Scalar and object merging in rendering + yield 'string replaces object in rendering' => [ + 'value="new-string"', + [ + ['value' => new \stdClass()], + ['value' => 'new-string'], + ], + ]; + + yield 'object replaces string in rendering uses __toString if available' => [ + 'value="stringable-object"', + [ + ['value' => 'old-string'], + ['value' => new StringableStub('stringable-object')], + ], + ]; + } + + public function testIterableObjectCastedToArray() + { + /* + This test case demonstrates how objects could e. g. implement helper logic + to construct more complex attribute combinations and sets, and be passed as + one argument to html_attr as well. + */ + $object = new class implements \IteratorAggregate { + public function getIterator(): \Traversable + { + return new \ArrayIterator([ + 'data-controller' => new SeparatedTokenList(['dropdown', 'tooltip']), + 'data-action' => new SeparatedTokenList(['click->dropdown#toggle', 'mouseover->tooltip#show']), + ]); + } + }; + + $result = HtmlExtension::htmlAttr(new Environment(new ArrayLoader()), $object); + + self::assertSame('data-controller="dropdown tooltip" data-action="click->dropdown#toggle mouseover->tooltip#show"', $result); + } + + public function testDataAttributeWithNonJsonEncodableValueThrowsRuntimeError() + { + $this->expectException(RuntimeError::class); + $this->expectExceptionMessage('The "data-bad" attribute value cannot be JSON encoded.'); + + HtmlExtension::htmlAttr( + new Environment(new ArrayLoader()), + ['data-bad' => [\INF]] // INF cannot be JSON-encoded + ); + } + + public function testNonStringableObjectAsAttributeValueThrowsRuntimeError() + { + $this->expectException(RuntimeError::class); + $this->expectExceptionMessage('The "title" attribute value should be a scalar, an iterable, or an object implementing "Stringable"'); + + HtmlExtension::htmlAttr( + new Environment(new ArrayLoader()), + ['title' => new \stdClass()] + ); + } +} + +class StringableStub implements \Stringable +{ + public function __construct(private readonly string $value) + { + } + + public function __toString(): string + { + return $this->value; + } +} + +class AttributeValueStub implements AttributeValueInterface +{ + public function __construct(private readonly ?string $value) + { + } + + public function getValue(): ?string + { + return $this->value; + } +} diff -Nru php-twig-3.20.0/extra/html-extra/composer.json php-twig-3.26.0/extra/html-extra/composer.json --- php-twig-3.20.0/extra/html-extra/composer.json 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/extra/html-extra/composer.json 2026-05-20 07:31:59.000000000 +0000 @@ -17,7 +17,7 @@ "require": { "php": ">=8.1.0", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/mime": "^5.4|^6.4|^7.0", + "symfony/mime": "^5.4|^6.4|^7.0|^8.0", "twig/twig": "^3.13|^4.0" }, "require-dev": { diff -Nru php-twig-3.20.0/extra/inky-extra/.gitattributes php-twig-3.26.0/extra/inky-extra/.gitattributes --- php-twig-3.20.0/extra/inky-extra/.gitattributes 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/extra/inky-extra/.gitattributes 2026-05-20 07:31:59.000000000 +0000 @@ -1,2 +1,3 @@ /Tests export-ignore +/.git* export-ignore /phpunit.xml.dist export-ignore diff -Nru php-twig-3.20.0/extra/inky-extra/InkyExtension.php php-twig-3.26.0/extra/inky-extra/InkyExtension.php --- php-twig-3.20.0/extra/inky-extra/InkyExtension.php 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/extra/inky-extra/InkyExtension.php 2026-05-20 07:31:59.000000000 +0000 @@ -1,9 +1,9 @@ + * (c) Fabien Potencier * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -20,7 +20,7 @@ public function getFilters(): array { return [ - new TwigFilter('inky_to_html', [self::class, 'inky'], ['is_safe' => ['html']]), + new TwigFilter('inky_to_html', [self::class, 'inky'], ['is_safe' => ['html'], 'pre_escape' => 'html']), ]; } diff -Nru php-twig-3.20.0/extra/inky-extra/Resources/functions.php php-twig-3.26.0/extra/inky-extra/Resources/functions.php --- php-twig-3.20.0/extra/inky-extra/Resources/functions.php 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/extra/inky-extra/Resources/functions.php 2026-05-20 07:31:59.000000000 +0000 @@ -1,9 +1,9 @@ + * (c) Fabien Potencier * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff -Nru php-twig-3.20.0/extra/inky-extra/Tests/FunctionalTest.php php-twig-3.26.0/extra/inky-extra/Tests/FunctionalTest.php --- php-twig-3.20.0/extra/inky-extra/Tests/FunctionalTest.php 1970-01-01 00:00:00.000000000 +0000 +++ php-twig-3.26.0/extra/inky-extra/Tests/FunctionalTest.php 2026-05-20 07:31:59.000000000 +0000 @@ -0,0 +1,33 @@ + '{{ payload|inky_to_html }}', + ])); + $twig->addExtension(new InkyExtension()); + + $output = $twig->render('index', ['payload' => '']); + + $this->assertStringNotContainsString(''] +--EXPECT-- +1. <script> alert(1) </script> + +2. <script> alert(1) </script> + +3. diff -Nru php-twig-3.20.0/tests/Fixtures/operators/contat_vs_add_sub.legacy.test php-twig-3.26.0/tests/Fixtures/operators/contat_vs_add_sub.legacy.test --- php-twig-3.20.0/tests/Fixtures/operators/contat_vs_add_sub.legacy.test 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/tests/Fixtures/operators/contat_vs_add_sub.legacy.test 2026-05-20 07:31:59.000000000 +0000 @@ -1,8 +1,8 @@ --TEST-- +/- will have a higher precedence over ~ in Twig 4.0 --DEPRECATION-- -Since twig/twig 3.15: Add explicit parentheses around the "~" binary operator to avoid behavior change in the next major version as its precedence will change in "index.twig" at line 2. -Since twig/twig 3.15: Add explicit parentheses around the "~" binary operator to avoid behavior change in the next major version as its precedence will change in "index.twig" at line 3. +Since twig/twig 3.15: As the "~" infix operator will change its precedence in the next major version, add explicit parentheses to avoid behavior change in "index.twig" at line 2. +Since twig/twig 3.15: As the "~" infix operator will change its precedence in the next major version, add explicit parentheses to avoid behavior change in "index.twig" at line 3. --TEMPLATE-- {{ '42' ~ 1 + 41 }} {{ '42' ~ 43 - 1 }} diff -Nru php-twig-3.20.0/tests/Fixtures/operators/minus_vs_pipe.legacy.test php-twig-3.26.0/tests/Fixtures/operators/minus_vs_pipe.legacy.test --- php-twig-3.20.0/tests/Fixtures/operators/minus_vs_pipe.legacy.test 1970-01-01 00:00:00.000000000 +0000 +++ php-twig-3.26.0/tests/Fixtures/operators/minus_vs_pipe.legacy.test 2026-05-20 07:31:59.000000000 +0000 @@ -0,0 +1,10 @@ +--TEST-- +| will have a higher precedence over + and - in Twig 4.0 +--DEPRECATION-- +Since twig/twig 3.21: As the "|" infix operator will change its precedence in the next major version, add explicit parentheses to avoid behavior change in "index.twig" at line 2. +--TEMPLATE-- +{{ -1|abs }} +--DATA-- +return [] +--EXPECT-- +-1 diff -Nru php-twig-3.20.0/tests/Fixtures/operators/not_precedence.legacy.test php-twig-3.26.0/tests/Fixtures/operators/not_precedence.legacy.test --- php-twig-3.20.0/tests/Fixtures/operators/not_precedence.legacy.test 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/tests/Fixtures/operators/not_precedence.legacy.test 2026-05-20 07:31:59.000000000 +0000 @@ -1,7 +1,7 @@ --TEST-- *, /, //, and % will have a higher precedence over not in Twig 4.0 --DEPRECATION-- -Since twig/twig 3.15: Add explicit parentheses around the "not" unary operator to avoid behavior change in the next major version as its precedence will change in "index.twig" at line 2. +Since twig/twig 3.15: As the "not" prefix operator will change its precedence in the next major version, add explicit parentheses to avoid behavior change in "index.twig" at line 2. --TEMPLATE-- {{ not 1 * 2 }} --DATA-- diff -Nru php-twig-3.20.0/tests/Fixtures/operators/not_precedence.test php-twig-3.26.0/tests/Fixtures/operators/not_precedence.test --- php-twig-3.20.0/tests/Fixtures/operators/not_precedence.test 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/tests/Fixtures/operators/not_precedence.test 2026-05-20 07:31:59.000000000 +0000 @@ -2,7 +2,7 @@ *, /, //, and % will have a higher precedence over not in Twig 4.0 --TEMPLATE-- {{ (not 1) * 2 }} -{{ (not 1 * 2) }} +{{ not (1 * 2) }} --DATA-- return [] --EXPECT-- diff -Nru php-twig-3.20.0/tests/Fixtures/regression/4701-block-inheritance-issue.test php-twig-3.26.0/tests/Fixtures/regression/4701-block-inheritance-issue.test --- php-twig-3.20.0/tests/Fixtures/regression/4701-block-inheritance-issue.test 1970-01-01 00:00:00.000000000 +0000 +++ php-twig-3.26.0/tests/Fixtures/regression/4701-block-inheritance-issue.test 2026-05-20 07:31:59.000000000 +0000 @@ -0,0 +1,21 @@ +--TEST-- +#4701 Accessing arrays with stringable objects as key +--TEMPLATE-- +{% set hash = { + 'foo': 'FOO', + 'bar': 'BAR', +} %} + +{{ hash[key] }} +--DATA-- +class MyObj { + public function __toString() { + return 'foo'; + } +} + +return [ + 'key' => new MyObj(), +]; +--EXPECT-- +FOO diff -Nru php-twig-3.20.0/tests/Fixtures/regression/markup_test.test php-twig-3.26.0/tests/Fixtures/regression/markup_test.test --- php-twig-3.20.0/tests/Fixtures/regression/markup_test.test 1970-01-01 00:00:00.000000000 +0000 +++ php-twig-3.26.0/tests/Fixtures/regression/markup_test.test 2026-05-20 07:31:59.000000000 +0000 @@ -0,0 +1,18 @@ +--TEST-- +Twig outputs 0 nodes correctly +--TEMPLATE-- +{{ empty|trim ? 'KO' : 'ok' }} +{{ spaces|trim ? 'KO' : 'ok' }} +{% if empty %}KO{% else %}ok{% endif %} + +{% if spaces|trim %}KO{% else %}ok{% endif %} + +{% set bar %} {% endset %}{{ bar|trim ? 'KO' : 'ok' }} +--DATA-- +return ['spaces' => new Twig\Markup(' ', 'UTF-8'), 'empty' => new Twig\Markup('', 'UTF-8')] +--EXPECT-- +ok +ok +ok +ok +ok diff -Nru php-twig-3.20.0/tests/Fixtures/tags/guard/basic.test php-twig-3.26.0/tests/Fixtures/tags/guard/basic.test --- php-twig-3.20.0/tests/Fixtures/tags/guard/basic.test 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/tests/Fixtures/tags/guard/basic.test 2026-05-20 07:31:59.000000000 +0000 @@ -13,9 +13,26 @@ {% else %} NEVER {% endguard %} + +{% guard test foobar %} + NEVER + {{ 'a'|foobar }} +{% else -%} + The foobar test doesn't exist +{% endguard %} + +{% guard test divisible by -%} + The divisible by function does exist +{% else %} + NEVER +{% endguard %} --DATA-- return [] --EXPECT-- The foobar filter doesn't exist The constant function does exist + +The foobar test doesn't exist + +The divisible by function does exist diff -Nru php-twig-3.20.0/tests/Fixtures/tags/guard/throwing_handler.test php-twig-3.26.0/tests/Fixtures/tags/guard/throwing_handler.test --- php-twig-3.20.0/tests/Fixtures/tags/guard/throwing_handler.test 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/tests/Fixtures/tags/guard/throwing_handler.test 2026-05-20 07:31:59.000000000 +0000 @@ -14,9 +14,27 @@ {% else -%} The throwing_undefined_function function doesn't exist {% endguard %} + +{% guard test throwing_undefined_test -%} + NEVER + {% if 'a' is throwing_undefined_test('b') %}{% endif %} +{% else -%} + The throwing_undefined_test test doesn't exist +{% endguard %} + +{% guard test throwing_undefined_two words_test -%} + NEVER + {% if 'a' is throwing_undefined_test words_test('b') %}{% endif %} +{% else -%} + The throwing_undefined_two words_test test doesn't exist +{% endguard %} --DATA-- return [] --EXPECT-- The throwing_undefined_filter filter doesn't exist The throwing_undefined_function function doesn't exist + +The throwing_undefined_test test doesn't exist + +The throwing_undefined_two words_test test doesn't exist diff -Nru php-twig-3.20.0/tests/Fixtures/tests/null_coalesce.legacy.test php-twig-3.26.0/tests/Fixtures/tests/null_coalesce.legacy.test --- php-twig-3.20.0/tests/Fixtures/tests/null_coalesce.legacy.test 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/tests/Fixtures/tests/null_coalesce.legacy.test 2026-05-20 07:31:59.000000000 +0000 @@ -1,16 +1,16 @@ --TEST-- Twig supports the ?? operator --DEPRECATION-- -Since twig/twig 3.15: Add explicit parentheses around the "??" binary operator to avoid behavior change in the next major version as its precedence will change in "index.twig" at line 4. -Since twig/twig 3.15: Add explicit parentheses around the "??" binary operator to avoid behavior change in the next major version as its precedence will change in "index.twig" at line 5. -Since twig/twig 3.15: Add explicit parentheses around the "??" binary operator to avoid behavior change in the next major version as its precedence will change in "index.twig" at line 6. -Since twig/twig 3.15: Add explicit parentheses around the "??" binary operator to avoid behavior change in the next major version as its precedence will change in "index.twig" at line 7. -Since twig/twig 3.15: Add explicit parentheses around the "??" binary operator to avoid behavior change in the next major version as its precedence will change in "index.twig" at line 10. -Since twig/twig 3.15: Add explicit parentheses around the "~" binary operator to avoid behavior change in the next major version as its precedence will change in "index.twig" at line 9. -Since twig/twig 3.15: Add explicit parentheses around the "~" binary operator to avoid behavior change in the next major version as its precedence will change in "index.twig" at line 11. -Since twig/twig 3.15: Add explicit parentheses around the "??" binary operator to avoid behavior change in the next major version as its precedence will change in "index.twig" at line 16. -Since twig/twig 3.15: Add explicit parentheses around the "~" binary operator to avoid behavior change in the next major version as its precedence will change in "index.twig" at line 15. -Since twig/twig 3.15: Add explicit parentheses around the "~" binary operator to avoid behavior change in the next major version as its precedence will change in "index.twig" at line 17. +Since twig/twig 3.15: As the "??" infix operator will change its precedence in the next major version, add explicit parentheses to avoid behavior change in "index.twig" at line 4. +Since twig/twig 3.15: As the "??" infix operator will change its precedence in the next major version, add explicit parentheses to avoid behavior change in "index.twig" at line 5. +Since twig/twig 3.15: As the "??" infix operator will change its precedence in the next major version, add explicit parentheses to avoid behavior change in "index.twig" at line 6. +Since twig/twig 3.15: As the "??" infix operator will change its precedence in the next major version, add explicit parentheses to avoid behavior change in "index.twig" at line 7. +Since twig/twig 3.15: As the "??" infix operator will change its precedence in the next major version, add explicit parentheses to avoid behavior change in "index.twig" at line 10. +Since twig/twig 3.15: As the "~" infix operator will change its precedence in the next major version, add explicit parentheses to avoid behavior change in "index.twig" at line 9. +Since twig/twig 3.15: As the "~" infix operator will change its precedence in the next major version, add explicit parentheses to avoid behavior change in "index.twig" at line 11. +Since twig/twig 3.15: As the "??" infix operator will change its precedence in the next major version, add explicit parentheses to avoid behavior change in "index.twig" at line 16. +Since twig/twig 3.15: As the "~" infix operator will change its precedence in the next major version, add explicit parentheses to avoid behavior change in "index.twig" at line 15. +Since twig/twig 3.15: As the "~" infix operator will change its precedence in the next major version, add explicit parentheses to avoid behavior change in "index.twig" at line 17. --TEMPLATE-- {{ nope ?? nada ?? 'OK' -}} {# no deprecation as the operators have the same precedence #} diff -Nru php-twig-3.20.0/tests/IntegrationTest.php php-twig-3.26.0/tests/IntegrationTest.php --- php-twig-3.20.0/tests/IntegrationTest.php 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/tests/IntegrationTest.php 2026-05-20 07:31:59.000000000 +0000 @@ -1,5 +1,14 @@ ''); + } + + return false; + }, + ]; + } + + protected function getUndefinedFilterCallbacks(): array { return [ static function (string $name) { @@ -210,7 +240,7 @@ new TwigFilter('*_path', [$this, 'dynamic_path']), new TwigFilter('*_foo_*_bar', [$this, 'dynamic_foo']), new TwigFilter('not', [$this, 'notFilter']), - new TwigFilter('anon_foo', function ($name) { return '*'.$name.'*'; }), + new TwigFilter('anon_foo', static function ($name) { return '*'.$name.'*'; }), ]; } @@ -224,8 +254,8 @@ new TwigFunction('static_call_array', ['Twig\Tests\TwigTestExtension', 'staticCall']), new TwigFunction('*_path', [$this, 'dynamic_path']), new TwigFunction('*_foo_*_bar', [$this, 'dynamic_foo']), - new TwigFunction('anon_foo', function ($name) { return '*'.$name.'*'; }), - new TwigFunction('deprecated_function', function () { return 'foo'; }, ['deprecation_info' => new DeprecatedCallableInfo('foo/bar', '1.1', 'not_deprecated_function')]), + new TwigFunction('anon_foo', static function ($name) { return '*'.$name.'*'; }), + new TwigFunction('deprecated_function', static function () { return 'foo'; }, ['deprecation_info' => new DeprecatedCallableInfo('foo/bar', '1.1', 'not_deprecated_function')]), ]; } @@ -307,7 +337,7 @@ public function is_multi_word($value) { - return false !== strpos($value, ' '); + return str_contains($value, ' '); } public function __call($method, $arguments) diff -Nru php-twig-3.20.0/tests/LexerTest.php php-twig-3.26.0/tests/LexerTest.php --- php-twig-3.20.0/tests/LexerTest.php 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/tests/LexerTest.php 2026-05-20 07:31:59.000000000 +0000 @@ -1,5 +1,14 @@ assertEquals(2, $this->countToken($template, Token::PUNCTUATION_TYPE, '}')); } - public function testSpreadOperator() - { - $template = '{{ { a: "a", ...{ b: "b" } } }}'; - - $this->assertEquals(1, $this->countToken($template, Token::SPREAD_TYPE, '...')); - // sanity check on lexing after spread - $this->assertEquals(2, $this->countToken($template, Token::PUNCTUATION_TYPE, '{')); - $this->assertEquals(2, $this->countToken($template, Token::PUNCTUATION_TYPE, '}')); - } - protected function countToken($template, $type, $value = null) { $lexer = new Lexer(new Environment(new ArrayLoader())); @@ -421,6 +420,56 @@ $this->addToAssertionCount(1); } + public function testFilterAndAttributeNamedAfterOperator() + { + // Ensure that filters/attributes aren't mistaken for operators when their names conflict + // (see https://github.com/twigphp/Twig/issues/4767) + $template = '{{ \'foo\'|and }}' + .'{{ \'bar\' | and }}' + .'{{ foo.and }}' + .'{{ bar . and }}' + .'{{ foo and bar }}'; + + $lexer = new Lexer(new Environment(new ArrayLoader())); + $stream = $lexer->tokenize(new Source($template, 'index')); + foreach (['foo', 'bar'] as $value) { + $stream->expect(Token::VAR_START_TYPE); + $stream->expect(Token::STRING_TYPE, $value); + $stream->expect(Token::OPERATOR_TYPE, '|'); + $stream->expect(Token::NAME_TYPE, 'and'); + $stream->expect(Token::VAR_END_TYPE); + } + foreach (['foo', 'bar'] as $value) { + $stream->expect(Token::VAR_START_TYPE); + $stream->expect(Token::NAME_TYPE, $value); + $stream->expect(Token::OPERATOR_TYPE, '.'); + $stream->expect(Token::NAME_TYPE, 'and'); + $stream->expect(Token::VAR_END_TYPE); + } + $stream->expect(Token::VAR_START_TYPE); + $stream->expect(Token::NAME_TYPE, 'foo'); + $stream->expect(Token::OPERATOR_TYPE, 'and'); + $stream->expect(Token::NAME_TYPE, 'bar'); + $stream->expect(Token::VAR_END_TYPE); + + // add a dummy assertion here to satisfy PHPUnit, the only thing we want to test is that the code above + // can be executed without throwing any exceptions + } + + public function testLiteralIsNotAnOperator() + { + // "literal" is the name of the LiteralExpressionParser but should not be treated as an operator token + $template = '{{ literal }}'; + + $lexer = new Lexer(new Environment(new ArrayLoader())); + $stream = $lexer->tokenize(new Source($template, 'index')); + $stream->expect(Token::VAR_START_TYPE); + $stream->expect(Token::NAME_TYPE, 'literal'); + $stream->expect(Token::VAR_END_TYPE); + + $this->addToAssertionCount(1); + } + public function testUnterminatedVariable() { $template = ' diff -Nru php-twig-3.20.0/tests/Loader/ArrayTest.php php-twig-3.26.0/tests/Loader/ArrayTest.php --- php-twig-3.20.0/tests/Loader/ArrayTest.php 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/tests/Loader/ArrayTest.php 2026-05-20 07:31:59.000000000 +0000 @@ -1,5 +1,14 @@ assertFalse($node->hasNode('variables')); + $this->assertEquals('foo.twig', $node->getAttribute('name')); + $this->assertEquals(0, $node->getAttribute('index')); + $this->assertFalse($node->getAttribute('only')); + $this->assertFalse($node->getAttribute('ignore_missing')); + + $vars = new ArrayExpression([new ConstantExpression('foo', 1), new ConstantExpression(true, 1)], 1); + $node = new EmbedNode('bar.twig', 1, $vars, true, false, 1); + $this->assertEquals($vars, $node->getNode('variables')); + $this->assertTrue($node->getAttribute('only')); + $this->assertEquals('bar.twig', $node->getAttribute('name')); + $this->assertEquals(1, $node->getAttribute('index')); + } + + public static function provideTests(): iterable + { + $tests = []; + + $node = new EmbedNode('foo.twig', 0, null, false, false, 1); + $tests[] = [$node, <<<'EOF' +// line 1 +yield from $this->load("foo.twig", 1, 0)->unwrap()->yield($context); +EOF + ]; + + $node = new EmbedNode('foo.twig', 1, null, false, false, 1); + $tests[] = [$node, <<<'EOF' +// line 1 +yield from $this->load("foo.twig", 1, 1)->unwrap()->yield($context); +EOF + ]; + + $vars = new ArrayExpression([new ConstantExpression('foo', 1), new ConstantExpression(true, 1)], 1); + $node = new EmbedNode('foo.twig', 0, $vars, false, false, 1); + $tests[] = [$node, <<<'EOF' +// line 1 +yield from $this->load("foo.twig", 1, 0)->unwrap()->yield(CoreExtension::merge($context, ["foo" => true])); +EOF + ]; + + $node = new EmbedNode('foo.twig', 0, $vars, true, false, 1); + $tests[] = [$node, <<<'EOF' +// line 1 +yield from $this->load("foo.twig", 1, 0)->unwrap()->yield(CoreExtension::toArray(["foo" => true])); +EOF + ]; + + $node = new EmbedNode('foo.twig', 2, $vars, true, true, 1); + $tests[] = [$node, <<load("foo.twig", 1, 2); + \$_v0->getParent(\$context); +; +} catch (LoaderError \$e) { + // ignore missing template + \$_v0 = null; +} +if (\$_v0) { + yield from \$_v0->unwrap()->yield(CoreExtension::toArray(["foo" => true])); +} +EOF + ]; + + return $tests; + } +} diff -Nru php-twig-3.20.0/tests/Node/Expression/ArrayTest.php php-twig-3.26.0/tests/Node/Expression/ArrayTest.php --- php-twig-3.20.0/tests/Node/Expression/ArrayTest.php 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/tests/Node/Expression/ArrayTest.php 2026-05-20 07:31:59.000000000 +0000 @@ -1,5 +1,14 @@ setAccessible(true); return $m->invokeArgs($call, $args); } diff -Nru php-twig-3.20.0/tests/Node/Expression/ConditionalTest.php php-twig-3.26.0/tests/Node/Expression/ConditionalTest.php --- php-twig-3.20.0/tests/Node/Expression/ConditionalTest.php 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/tests/Node/Expression/ConditionalTest.php 2026-05-20 07:31:59.000000000 +0000 @@ -1,5 +1,14 @@ env->getFilter(\'anonymous\')->getCallable()("foo")']; + // needs sandbox + $node = self::createFilter($environment, $string, 'bar_sandbox'); + $tests[] = [$node, 'Twig\Tests\Node\Expression\twig_tests_filter_sandbox($this->env->hasExtension(\Twig\Extension\SandboxExtension::class) && $this->env->getExtension(\Twig\Extension\SandboxExtension::class)->isSandboxed($this->source), "abc")', $environment]; + + // needs charset, environment, context, and sandbox + $node = self::createFilter($environment, $string, 'bar_all'); + $tests[] = [$node, 'Twig\Tests\Node\Expression\twig_tests_filter_all($this->env->getCharset(), $this->env, $context, $this->env->hasExtension(\Twig\Extension\SandboxExtension::class) && $this->env->getExtension(\Twig\Extension\SandboxExtension::class)->isSandboxed($this->source), "abc")', $environment]; + // needs environment $node = self::createFilter($environment, $string, 'bar'); $tests[] = [$node, 'Twig\Tests\Node\Expression\twig_tests_filter_dummy($this->env, "abc")', $environment]; @@ -165,10 +182,12 @@ protected static function createEnvironment(): Environment { $env = new Environment(new ArrayLoader()); - $env->addFilter(new TwigFilter('anonymous', function () {})); + $env->addFilter(new TwigFilter('anonymous', static function () {})); $env->addFilter(new TwigFilter('bar', 'Twig\Tests\Node\Expression\twig_tests_filter_dummy', ['needs_environment' => true])); $env->addFilter(new TwigFilter('bar_closure', \Closure::fromCallable(twig_tests_filter_dummy::class), ['needs_environment' => true])); $env->addFilter(new TwigFilter('barbar', 'Twig\Tests\Node\Expression\twig_tests_filter_barbar', ['needs_context' => true, 'is_variadic' => true])); + $env->addFilter(new TwigFilter('bar_sandbox', 'Twig\Tests\Node\Expression\twig_tests_filter_sandbox', ['needs_is_sandboxed' => true])); + $env->addFilter(new TwigFilter('bar_all', 'Twig\Tests\Node\Expression\twig_tests_filter_all', ['needs_charset' => true, 'needs_environment' => true, 'needs_context' => true, 'needs_is_sandboxed' => true])); $env->addFilter(new TwigFilter('magic_static', __NAMESPACE__.'\ChildMagicCallStub::magicStaticCall')); if (\PHP_VERSION_ID >= 80111) { $env->addExtension(new FilterTestExtension()); @@ -208,6 +227,14 @@ { } +function twig_tests_filter_sandbox(bool $isSandboxed, $string) +{ +} + +function twig_tests_filter_all(string $charset, Environment $env, array $context, bool $isSandboxed, $string) +{ +} + class ChildMagicCallStub extends ParentMagicCallStub { public static function identifier() diff -Nru php-twig-3.20.0/tests/Node/Expression/FilterTestExtension.php php-twig-3.26.0/tests/Node/Expression/FilterTestExtension.php --- php-twig-3.20.0/tests/Node/Expression/FilterTestExtension.php 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/tests/Node/Expression/FilterTestExtension.php 2026-05-20 07:31:59.000000000 +0000 @@ -1,5 +1,14 @@ addFunction(new TwigFunction('anonymous', function () {})); + $env->addFunction(new TwigFunction('anonymous', static function () {})); $env->addFunction(new TwigFunction('foo', 'Twig\Tests\Node\Expression\twig_tests_function_dummy', [])); $env->addFunction(new TwigFunction('foo_closure', \Closure::fromCallable(twig_tests_function_dummy::class), [])); $env->addFunction(new TwigFunction('bar', 'Twig\Tests\Node\Expression\twig_tests_function_dummy', ['needs_environment' => true])); diff -Nru php-twig-3.20.0/tests/Node/Expression/GetAttrTest.php php-twig-3.26.0/tests/Node/Expression/GetAttrTest.php --- php-twig-3.20.0/tests/Node/Expression/GetAttrTest.php 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/tests/Node/Expression/GetAttrTest.php 2026-05-20 07:31:59.000000000 +0000 @@ -1,5 +1,14 @@ assertEquals($attr, $node->getNode('attribute')); $this->assertEquals($args, $node->getNode('arguments')); $this->assertEquals(Template::ARRAY_CALL, $node->getAttribute('type')); + $this->assertFalse($node->getAttribute('null_safe')); } public static function provideTests(): iterable @@ -41,10 +51,27 @@ $expr = new ContextVariable('foo', 1); $attr = new ConstantExpression('bar', 1); + $attr2 = new ConstantExpression('baz', 1); + $attr3 = new ConstantExpression('qux', 1); + $attr4 = new ConstantExpression('corge', 1); $args = new ArrayExpression([], 1); + $node = new GetAttrExpression($expr, $attr, $args, Template::ANY_CALL, 1); $tests[] = [$node, \sprintf('%s%s, "bar", [], "any", false, false, false, 1)', self::createAttributeGetter(), self::createVariableGetter('foo', 1))]; + $node = new GetAttrExpression($expr, $attr, $args, Template::ANY_CALL, 1, true); + $tests[] = [$node, '((null === ($_v%s = // line 1'."\n".'($context["foo"] ?? null))) ? null : '.self::createAttributeGetter().'$_v%s, "bar", [], "any", false, false, false, 1))', null, true]; + + $node = new GetAttrExpression($expr, $attr, $args, Template::ANY_CALL, 1, true); + $node = new GetAttrExpression($node, $attr2, $args, Template::METHOD_CALL, 1); + $tests[] = [$node, '((null === ($_v%s = // line 1'."\n".'($context["foo"] ?? null))) ? null : '.self::createAttributeGetter().self::createAttributeGetter().'$_v%s, "bar", [], "any", false, false, false, 1), "baz", [], "method", false, false, false, 1))', null, true]; + + $node = new GetAttrExpression($expr, $attr, $args, Template::ANY_CALL, 1, true); + $node = new GetAttrExpression($node, $attr2, $args, Template::ANY_CALL, 1); + $node = new GetAttrExpression($node, $attr3, $args, Template::METHOD_CALL, 1, true); + $node = new GetAttrExpression($node, $attr4, $args, Template::ANY_CALL, 1); + $tests[] = [$node, '((null === ($_v0 = ((null === ($_v1 = // line 1'."\n".'($context["foo"] ?? null))) ? null : '.self::createAttributeGetter().self::createAttributeGetter().'$_v1, "bar", [], "any", false, false, false, 1), "baz", [], "any", false, false, false, 1)))) ? null : '.self::createAttributeGetter().self::createAttributeGetter().'$_v0, "qux", [], "method", false, false, false, 1), "corge", [], "any", false, false, false, 1))', null]; + $node = new GetAttrExpression($expr, $attr, $args, Template::ARRAY_CALL, 1); $tests[] = [$node, '(($_v%s = // line 1'."\n". '($context["foo"] ?? null)) && is_array($_v%s) || $_v%s instanceof ArrayAccess ? ($_v%s["bar"] ?? null) : null)', null, true, ]; diff -Nru php-twig-3.20.0/tests/Node/Expression/MacroReferenceTest.php php-twig-3.26.0/tests/Node/Expression/MacroReferenceTest.php --- php-twig-3.20.0/tests/Node/Expression/MacroReferenceTest.php 1970-01-01 00:00:00.000000000 +0000 +++ php-twig-3.26.0/tests/Node/Expression/MacroReferenceTest.php 2026-05-20 07:31:59.000000000 +0000 @@ -0,0 +1,41 @@ +expectException(\LogicException::class); + $this->expectExceptionMessage(\sprintf('Macro name "%s" is not a valid PHP identifier.', $name)); + + new MacroReferenceExpression(new TemplateVariable('foo', 1), $name, new ArrayExpression([], 1), 1); + } + + public static function provideInvalidMacroNames(): iterable + { + yield 'empty' => ['']; + yield 'starts with digit' => ['1foo']; + yield 'contains space' => ['foo bar']; + yield 'contains semicolon' => ['foo;bar']; + yield 'PHP injection payload' => ['macro_foo + 1; trigger_error("BAD") //']; + yield 'contains NUL byte' => ["foo\x00bar"]; + } +} diff -Nru php-twig-3.20.0/tests/Node/Expression/NullCoalesceTest.php php-twig-3.26.0/tests/Node/Expression/NullCoalesceTest.php --- php-twig-3.20.0/tests/Node/Expression/NullCoalesceTest.php 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/tests/Node/Expression/NullCoalesceTest.php 2026-05-20 07:31:59.000000000 +0000 @@ -1,5 +1,14 @@ addTest(new TwigTest('anonymous', function () {})); + $env->addTest(new TwigTest('anonymous', static function () {})); $env->addTest(new TwigTest('barbar', 'Twig\Tests\Node\Expression\twig_tests_test_barbar', ['is_variadic' => true, 'need_context' => true])); return $env; diff -Nru php-twig-3.20.0/tests/Node/Expression/Unary/NegTest.php php-twig-3.26.0/tests/Node/Expression/Unary/NegTest.php --- php-twig-3.20.0/tests/Node/Expression/Unary/NegTest.php 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/tests/Node/Expression/Unary/NegTest.php 2026-05-20 07:31:59.000000000 +0000 @@ -1,5 +1,14 @@ [$node, "// line 1\n$compiled"]; $node = new ContextVariable($special, 1); - $node->setAttribute('is_defined_test', true); + $node->enableDefinedTest(); yield $special.'_defined_test' => [$node, "// line 1\ntrue"]; } @@ -59,13 +68,13 @@ // is defined test $node = new ContextVariable('foo', 1); - $node->setAttribute('is_defined_test', true); + $node->enableDefinedTest(); yield 'is_defined_test_strict' => [$node, "// line 1\narray_key_exists(\"foo\", \$context)", $envStrict]; yield 'is_defined_test_non_strict' => [$node, "// line 1\narray_key_exists(\"foo\", \$context)", $env]; // is defined test // always defined $node = new ContextVariable('foo', 1); - $node->setAttribute('is_defined_test', true); + $node->enableDefinedTest(); $node->setAttribute('always_defined', true); yield 'is_defined_test_always_defined_strict' => [$node, "// line 1\ntrue", $envStrict]; yield 'is_defined_test_always_defined_non_strict' => [$node, "// line 1\ntrue", $env]; diff -Nru php-twig-3.20.0/tests/Node/ForTest.php php-twig-3.26.0/tests/Node/ForTest.php --- php-twig-3.20.0/tests/Node/ForTest.php 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/tests/Node/ForTest.php 2026-05-20 07:31:59.000000000 +0000 @@ -1,5 +1,14 @@ macros["macro"] = \$this->loadTemplate("foo.twig", null, 1)->unwrap(); +\$macros["macro"] = \$this->macros["macro"] = \$this->load("foo.twig", 1)->unwrap(); EOF ]; diff -Nru php-twig-3.20.0/tests/Node/IncludeTest.php php-twig-3.26.0/tests/Node/IncludeTest.php --- php-twig-3.20.0/tests/Node/IncludeTest.php 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/tests/Node/IncludeTest.php 2026-05-20 07:31:59.000000000 +0000 @@ -1,5 +1,14 @@ loadTemplate("foo.twig", null, 1)->unwrap()->yield($context); +yield from $this->load("foo.twig", 1)->unwrap()->yield($context); EOF ]; @@ -55,7 +64,7 @@ $node = new IncludeNode($expr, null, false, false, 1); $tests[] = [$node, <<<'EOF' // line 1 -yield from $this->loadTemplate(((true) ? ("foo") : ("foo")), null, 1)->unwrap()->yield($context); +yield from $this->load(((true) ? ("foo") : ("foo")), 1)->unwrap()->yield($context); EOF ]; @@ -64,14 +73,14 @@ $node = new IncludeNode($expr, $vars, false, false, 1); $tests[] = [$node, <<<'EOF' // line 1 -yield from $this->loadTemplate("foo.twig", null, 1)->unwrap()->yield(CoreExtension::merge($context, ["foo" => true])); +yield from $this->load("foo.twig", 1)->unwrap()->yield(CoreExtension::merge($context, ["foo" => true])); EOF ]; $node = new IncludeNode($expr, $vars, true, false, 1); $tests[] = [$node, <<<'EOF' // line 1 -yield from $this->loadTemplate("foo.twig", null, 1)->unwrap()->yield(CoreExtension::toArray(["foo" => true])); +yield from $this->load("foo.twig", 1)->unwrap()->yield(CoreExtension::toArray(["foo" => true])); EOF ]; @@ -79,7 +88,7 @@ $tests[] = [$node, <<loadTemplate("foo.twig", null, 1); + \$_v%s = \$this->load("foo.twig", 1); } catch (LoaderError \$e) { // ignore missing template \$_v%s = null; @@ -87,8 +96,7 @@ if (\$_v%s) { yield from \$_v%s->unwrap()->yield(CoreExtension::toArray(["foo" => true])); } -EOF - , null, true]; +EOF, null, true]; return $tests; } diff -Nru php-twig-3.20.0/tests/Node/MacroTest.php php-twig-3.26.0/tests/Node/MacroTest.php --- php-twig-3.20.0/tests/Node/MacroTest.php 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/tests/Node/MacroTest.php 2026-05-20 07:31:59.000000000 +0000 @@ -1,5 +1,14 @@ env->getCharset()); } -EOF - , new Environment(new ArrayLoader(), ['use_yield' => true]), +EOF, new Environment(new ArrayLoader(), ['use_yield' => true]), ]; yield 'with use_yield = false' => [$node, <<env->getCharset()); } -EOF - , new Environment(new ArrayLoader(), ['use_yield' => false]), +EOF, new Environment(new ArrayLoader(), ['use_yield' => false]), ]; } } diff -Nru php-twig-3.20.0/tests/Node/ModuleTest.php php-twig-3.26.0/tests/Node/ModuleTest.php --- php-twig-3.20.0/tests/Node/ModuleTest.php 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/tests/Node/ModuleTest.php 2026-05-20 07:31:59.000000000 +0000 @@ -1,5 +1,14 @@ assertEquals($source->getName(), $node->getTemplateName()); } + public function testUseTagTemplateNameDoesNotInjectPhpInCompiledOutput() + { + $evilName = "evil' . print('BAD-EOL') . '.twig"; + $loader = new ArrayLoader([ + $evilName => '{% block existing %}ok{% endblock %}', + 'main.twig' => "{% use \"$evilName\" with absent_block as alias %}", + ]); + $twig = new Environment($loader); + + ob_start(); + $message = null; + try { + $twig->load('main.twig'); + } catch (RuntimeError $e) { + $message = $e->getMessage(); + } + $stdout = ob_get_clean(); + + $this->assertSame('', $stdout, 'No code from the template name must execute when the trait is loaded.'); + $this->assertNotNull($message, 'A RuntimeError must be raised for the missing block.'); + $this->assertStringContainsString($evilName, $message, 'The error message must contain the literal template name.'); + } + public static function provideTests(): iterable { $twig = new Environment(new ArrayLoader(['foo.twig' => '{{ foo }}'])); @@ -128,8 +161,7 @@ return new Source("", "foo.twig", ""); } } -EOF - , $twig, true]; +EOF, $twig, true]; $import = new ImportNode(new ConstantExpression('foo.twig', 1), new AssignTemplateVariable(new TemplateVariable('macro', 2), true), 2); @@ -183,9 +215,9 @@ { \$macros = \$this->macros; // line 2 - \$macros["macro"] = \$this->macros["macro"] = \$this->loadTemplate("foo.twig", "foo.twig", 2)->unwrap(); + \$macros["macro"] = \$this->macros["macro"] = \$this->load("foo.twig", 2)->unwrap(); // line 1 - \$this->parent = \$this->loadTemplate("layout.twig", "foo.twig", 1); + \$this->parent = \$this->load("layout.twig", 1); yield from \$this->parent->unwrap()->yield(\$context, array_merge(\$this->blocks, \$blocks)); } @@ -218,8 +250,7 @@ return new Source("", "foo.twig", ""); } } -EOF - , $twig, true]; +EOF, $twig, true]; $set = new SetNode(false, new Nodes([new AssignContextVariable('foo', 4)]), new Nodes([new ConstantExpression('foo', 4)]), 4); $body = new BodyNode([$set]); @@ -271,7 +302,7 @@ protected function doGetParent(array \$context): bool|string|Template|TemplateWrapper { // line 2 - return \$this->loadTemplate(((true) ? ("foo") : ("foo")), "foo.twig", 2); + return \$this->load(((true) ? ("foo") : ("foo")), 2); } protected function doDisplay(array \$context, array \$blocks = []): iterable @@ -312,8 +343,7 @@ return new Source("{{ foo }}", "foo.twig", ""); } } -EOF - , $twig, true]; +EOF, $twig, true]; return $tests; } diff -Nru php-twig-3.20.0/tests/Node/NodeTest.php php-twig-3.26.0/tests/Node/NodeTest.php --- php-twig-3.20.0/tests/Node/NodeTest.php 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/tests/Node/NodeTest.php 2026-05-20 07:31:59.000000000 +0000 @@ -1,5 +1,14 @@ function () { return '1'; }], 1); + $node = new NodeForTest([], ['value' => static function () { return '1'; }], 1); $this->assertEquals(<<assertEquals(<<env->getCharset()); -EOF - , new Environment(new ArrayLoader(), ['use_yield' => true]), +EOF, new Environment(new ArrayLoader(), ['use_yield' => true]), ]; $tests[] = [$node, <<<'EOF' @@ -68,8 +76,7 @@ yield "foo"; yield from []; })())) ? '' : new Markup($tmp, $this->env->getCharset()); -EOF - , new Environment(new ArrayLoader(), ['use_yield' => false]), +EOF, new Environment(new ArrayLoader(), ['use_yield' => false]), ]; $names = new Nodes([new AssignContextVariable('foo', 1)], 1); diff -Nru php-twig-3.20.0/tests/Node/TextTest.php php-twig-3.26.0/tests/Node/TextTest.php --- php-twig-3.20.0/tests/Node/TextTest.php 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/tests/Node/TextTest.php 2026-05-20 07:31:59.000000000 +0000 @@ -1,5 +1,14 @@ getAttribute('name')) { + if (NameExpression::class === $n::class && $target === $n->getAttribute('name')) { $this->assertTrue($n->getAttribute('always_defined')); } else { $this->checkForVarConfiguration($n, $target); diff -Nru php-twig-3.20.0/tests/NodeVisitor/SandboxTest.php php-twig-3.26.0/tests/NodeVisitor/SandboxTest.php --- php-twig-3.20.0/tests/NodeVisitor/SandboxTest.php 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/tests/NodeVisitor/SandboxTest.php 2026-05-20 07:31:59.000000000 +0000 @@ -1,5 +1,14 @@ assertNotInstanceOf(CheckToStringNode::class, $node->getNode('body')->getNode(0)->getNode('expr')); $this->assertSame("// line 1\nyield from (\$context[\"foo\"] ?? null);\n", $env->compile($node->getNode('body'))); } + + public function testCustomNodeImplementingCoercesChildrenToStringInterfaceIsWrapped() + { + $env = new Environment(new ArrayLoader()); + $custom = new CustomCoercingExpression(new ContextVariable('foo', 1), new ContextVariable('bar', 1), 1); + // wrap inside a PrintNode so it lives in a module; the wrapping must happen on the + // custom node itself regardless of the print context + $node = new ModuleNode(new BodyNode([new PrintNode($custom, 1)]), null, new EmptyNode(), new EmptyNode(), new EmptyNode(), new EmptyNode(), new Source('foo', 'foo')); + $traverser = new NodeTraverser($env, [new SandboxNodeVisitor($env)]); + $node = $traverser->traverse($node); + + $custom = $node->getNode('body')->getNode(0)->getNode('expr'); + $this->assertInstanceOf(CheckToStringNode::class, $custom->getNode('left')); + $this->assertInstanceOf(CheckToStringNode::class, $custom->getNode('right')); + } + + public function testCustomNonExpressionNodeImplementingCoercesChildrenToStringInterfaceIsWrapped() + { + $env = new Environment(new ArrayLoader()); + $custom = new CustomCoercingNode(['expr' => new ContextVariable('foo', 1)], [], 1); + $node = new ModuleNode(new BodyNode([$custom]), null, new EmptyNode(), new EmptyNode(), new EmptyNode(), new EmptyNode(), new Source('foo', 'foo')); + $traverser = new NodeTraverser($env, [new SandboxNodeVisitor($env)]); + $node = $traverser->traverse($node); + + $custom = $node->getNode('body')->getNode(0); + $this->assertInstanceOf(CheckToStringNode::class, $custom->getNode('expr')); + } + + public function testSelfIsNeverWrapped() + { + $env = new Environment(new ArrayLoader()); + $self = new ContextVariable('_self', 1); + $custom = new CustomCoercingNode(['expr' => $self], [], 1); + $node = new ModuleNode(new BodyNode([$custom]), null, new EmptyNode(), new EmptyNode(), new EmptyNode(), new EmptyNode(), new Source('foo', 'foo')); + $traverser = new NodeTraverser($env, [new SandboxNodeVisitor($env)]); + $node = $traverser->traverse($node); + + $this->assertNotInstanceOf(CheckToStringNode::class, $node->getNode('body')->getNode(0)->getNode('expr')); + } +} + +class CustomCoercingExpression extends AbstractExpression implements CoercesChildrenToStringInterface +{ + public function __construct(AbstractExpression $left, AbstractExpression $right, int $lineno) + { + parent::__construct(['left' => $left, 'right' => $right], [], $lineno); + } + + public function getStringCoercedChildNames(): array + { + return ['left', 'right']; + } +} + +class CustomCoercingNode extends Node implements CoercesChildrenToStringInterface +{ + public function getStringCoercedChildNames(): array + { + return ['expr']; + } } diff -Nru php-twig-3.20.0/tests/ParserTest.php php-twig-3.26.0/tests/ParserTest.php --- php-twig-3.20.0/tests/ParserTest.php 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/tests/ParserTest.php 2026-05-20 07:31:59.000000000 +0000 @@ -1,5 +1,14 @@ getParser(); $m = new \ReflectionMethod($parser, 'filterBodyNodes'); - $m->setAccessible(true); $this->assertEquals($expected, $m->invoke($parser, $input)); } @@ -99,7 +108,6 @@ $parser = $this->getParser(); $m = new \ReflectionMethod($parser, 'filterBodyNodes'); - $m->setAccessible(true); $this->expectException(SyntaxError::class); $m->invoke($parser, $input); @@ -121,7 +129,6 @@ $parser = $this->getParser(); $m = new \ReflectionMethod($parser, 'filterBodyNodes'); - $m->setAccessible(true); $this->assertNull($m->invoke($parser, new TextNode(\chr(0xEF).\chr(0xBB).\chr(0xBF).$emptyNode, 1))); } @@ -156,7 +163,6 @@ ], new Source('', ''))); $p = new \ReflectionProperty($parser, 'parent'); - $p->setAccessible(true); $this->assertNull($p->getValue($parser)); } @@ -173,8 +179,7 @@ {% macro foo() %} {{ foo }} {% endmacro %} -EOF - , 'index'))); +EOF, 'index'))); // The getVarName() must not depend on the template loaders, // If this test does not throw any exception, that's good. @@ -201,13 +206,26 @@ $this->assertTrue($argumentNodes->getNode(3)->getAttribute('value')); } + public function testEmbeddedTemplatesHaveSequentialIndices(): void + { + $template = new Source('{% embed "first" %}{% endembed %}{% embed "second" %}{% endembed %}', 'index'); + $lexer = new Lexer(new Environment(new ArrayLoader())); + $stream = $lexer->tokenize($template); + + $embeds = $this->getParser() + ->parse($stream) + ->getAttribute('embedded_templates'); + + $this->assertSame(1, $embeds->getNode(0)->getAttribute('index')); + $this->assertSame(2, $embeds->getNode(1)->getAttribute('index')); + } + protected function getParser() { $parser = new Parser(new Environment(new ArrayLoader())); - $parser->setParent(new EmptyNode()); + $parser->setParent(new ConstantExpression('base.html', 1)); $p = new \ReflectionProperty($parser, 'stream'); - $p->setAccessible(true); $p->setValue($parser, new TokenStream([], new Source('', ''))); return $parser; diff -Nru php-twig-3.20.0/tests/Profiler/Dumper/BlackfireTest.php php-twig-3.26.0/tests/Profiler/Dumper/BlackfireTest.php --- php-twig-3.20.0/tests/Profiler/Dumper/BlackfireTest.php 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/tests/Profiler/Dumper/BlackfireTest.php 2026-05-20 07:31:59.000000000 +0000 @@ -1,5 +1,14 @@ embedded.twig//2 %d %d %d embedded.twig==>included.twig//2 %d %d %d index.twig==>index.twig::macro(foo)//1 %d %d %d -EOF - , $dumper->dump($this->getProfile())); +EOF, $dumper->dump($this->getProfile())); } } diff -Nru php-twig-3.20.0/tests/Profiler/Dumper/HtmlTest.php php-twig-3.26.0/tests/Profiler/Dumper/HtmlTest.php --- php-twig-3.20.0/tests/Profiler/Dumper/HtmlTest.php 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/tests/Profiler/Dumper/HtmlTest.php 2026-05-20 07:31:59.000000000 +0000 @@ -1,5 +1,14 @@ embedded.twig └ included.twig -EOF - , $dumper->dump($this->getProfile())); +EOF, $dumper->dump($this->getProfile())); + } + + public function testDumpEscapesTemplateAndProfileNames() + { + $root = new Profile('main'); + $child = new Profile('', Profile::TEMPLATE); + $grandchild = new Profile('', Profile::MACRO, ''); + + (new \ReflectionProperty($child, 'profiles'))->setValue($child, [$grandchild]); + (new \ReflectionProperty($root, 'profiles'))->setValue($root, [$child]); + + $output = (new HtmlDumper())->dump($root); + + $this->assertStringNotContainsString('', $output); + $this->assertStringNotContainsString('', $output); + $this->assertStringNotContainsString('', $output); + $this->assertStringContainsString('<img src=x onerror=alert(1)>', $output); + $this->assertStringContainsString('<img src=x onerror=alert(2)>', $output); + $this->assertStringContainsString('<img src=x onerror=alert(3)>', $output); } } diff -Nru php-twig-3.20.0/tests/Profiler/Dumper/ProfilerTestCase.php php-twig-3.26.0/tests/Profiler/Dumper/ProfilerTestCase.php --- php-twig-3.20.0/tests/Profiler/Dumper/ProfilerTestCase.php 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/tests/Profiler/Dumper/ProfilerTestCase.php 2026-05-20 07:31:59.000000000 +0000 @@ -1,5 +1,14 @@ setAccessible(true); $p->setValue($profile, $subProfiles); return $profile; @@ -83,18 +91,15 @@ $profile = new Profile($templateName, $type, $name); $p = new \ReflectionProperty($profile, 'profiles'); - $p->setAccessible(true); $p->setValue($profile, $subProfiles); $starts = new \ReflectionProperty($profile, 'starts'); - $starts->setAccessible(true); $starts->setValue($profile, [ 'wt' => 0, 'mu' => 0, 'pmu' => 0, ]); $ends = new \ReflectionProperty($profile, 'ends'); - $ends->setAccessible(true); $ends->setValue($profile, [ 'wt' => $duration, 'mu' => 0, diff -Nru php-twig-3.20.0/tests/Profiler/Dumper/TextTest.php php-twig-3.26.0/tests/Profiler/Dumper/TextTest.php --- php-twig-3.20.0/tests/Profiler/Dumper/TextTest.php 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/tests/Profiler/Dumper/TextTest.php 2026-05-20 07:31:59.000000000 +0000 @@ -1,5 +1,14 @@ dump($this->getProfile())); +EOF, $dumper->dump($this->getProfile())); } } diff -Nru php-twig-3.20.0/tests/Profiler/ProfileTest.php php-twig-3.26.0/tests/Profiler/ProfileTest.php --- php-twig-3.20.0/tests/Profiler/ProfileTest.php 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/tests/Profiler/ProfileTest.php 2026-05-20 07:31:59.000000000 +0000 @@ -1,5 +1,14 @@ createSandboxedEnvironment(true); + $template = new LegacyCoreTestTemplate($env, 'index.twig'); + + $this->expectException(RuntimeError::class); + $this->expectExceptionMessageMatches('/must be a Closure in sandbox mode/'); + + $template->callLegacySort(['b', 'a'], 'strnatcasecmp'); + } + + public function testTwigSortFilterRecoversSourceForSourcePolicy() + { + $env = $this->createSandboxedEnvironment(false, new class implements SourcePolicyInterface { + public function enableSandbox(Source $source): bool + { + return 'sandboxed.twig' === $source->getName(); + } + }); + + $sandboxedTemplate = new LegacyCoreTestTemplate($env, 'sandboxed.twig'); + $trustedTemplate = new LegacyCoreTestTemplate($env, 'trusted.twig'); + + // Trusted template: non-Closure callable is allowed (only a deprecation is triggered). + $this->assertSame(['a', 'b'], array_values($trustedTemplate->callLegacySort(['b', 'a'], 'strnatcasecmp'))); + + // Sandboxed template: non-Closure callable is rejected thanks to Source recovery. + $this->expectException(RuntimeError::class); + $this->expectExceptionMessageMatches('/must be a Closure in sandbox mode/'); + $sandboxedTemplate->callLegacySort(['b', 'a'], 'strnatcasecmp'); + } + + public function testTwigArrayFilterRecoversSourceForSourcePolicy() + { + $env = $this->createSandboxedEnvironment(false, new class implements SourcePolicyInterface { + public function enableSandbox(Source $source): bool + { + return 'sandboxed.twig' === $source->getName(); + } + }); + + $template = new LegacyCoreTestTemplate($env, 'sandboxed.twig'); + + $this->expectException(RuntimeError::class); + $this->expectExceptionMessageMatches('/must be a Closure in sandbox mode/'); + iterator_to_array($template->callLegacyArrayFilter(['a', 'b'], 'is_string')); + } + + public function testTwigArrayMapRecoversSourceForSourcePolicy() + { + $env = $this->createSandboxedEnvironment(false, new class implements SourcePolicyInterface { + public function enableSandbox(Source $source): bool + { + return 'sandboxed.twig' === $source->getName(); + } + }); + + $template = new LegacyCoreTestTemplate($env, 'sandboxed.twig'); + + $this->expectException(RuntimeError::class); + $this->expectExceptionMessageMatches('/must be a Closure in sandbox mode/'); + $template->callLegacyArrayMap(['a', 'b'], 'strtoupper'); + } + + public function testTwigArrayColumnEnforcesSandbox() + { + $env = $this->createSandboxedEnvironment(true); + $template = new LegacyCoreTestTemplate($env, 'index.twig'); + + $this->expectException(SecurityNotAllowedPropertyError::class); + $template->callLegacyArrayColumn([new LegacyColumnObject()], 'bar'); + } + + public function testTwigArrayReduceRecoversSourceForSourcePolicy() + { + $env = $this->createSandboxedEnvironment(false, new class implements SourcePolicyInterface { + public function enableSandbox(Source $source): bool + { + return 'sandboxed.twig' === $source->getName(); + } + }); + + $template = new LegacyCoreTestTemplate($env, 'sandboxed.twig'); + + $this->expectException(RuntimeError::class); + $this->expectExceptionMessageMatches('/must be a Closure in sandbox mode/'); + $template->callLegacyArrayReduce([1, 2], 'intval'); + } + + private function createSandboxedEnvironment(bool $globallySandboxed, ?SourcePolicyInterface $sourcePolicy = null): Environment + { + $env = new Environment(new ArrayLoader([]), ['cache' => false, 'autoescape' => false]); + $env->addExtension(new SandboxExtension(new SecurityPolicy(), $globallySandboxed, $sourcePolicy)); + + return $env; + } +} + +class LegacyCoreTestTemplate extends Template +{ + public function __construct(Environment $env, private string $name) + { + parent::__construct($env); + } + + public function callLegacySort($array, $arrow) + { + return twig_sort_filter($this->env, $array, $arrow); + } + + public function callLegacyArrayFilter($array, $arrow) + { + return twig_array_filter($this->env, $array, $arrow); + } + + public function callLegacyArrayMap($array, $arrow) + { + return twig_array_map($this->env, $array, $arrow); + } + + public function callLegacyArrayReduce($array, $arrow, $initial = null) + { + return twig_array_reduce($this->env, $array, $arrow, $initial); + } + + public function callLegacyArrayColumn($array, $name, $index = null) + { + return twig_array_column($this->env, $array, $name, $index); + } + + public function getTemplateName(): string + { + return $this->name; + } + + public function getDebugInfo(): array + { + return []; + } + + public function getSourceContext(): Source + { + return new Source('', $this->name); + } + + protected function doGetParent(array $context): bool|string|Template|TemplateWrapper + { + return false; + } + + protected function doDisplay(array $context, array $blocks = []): iterable + { + return []; + } +} + +class LegacyColumnObject +{ + public $bar = 'bar'; +} diff -Nru php-twig-3.20.0/tests/Runtime/EscaperRuntimeTest.php php-twig-3.26.0/tests/Runtime/EscaperRuntimeTest.php --- php-twig-3.20.0/tests/Runtime/EscaperRuntimeTest.php 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/tests/Runtime/EscaperRuntimeTest.php 2026-05-20 07:31:59.000000000 +0000 @@ -1,5 +1,14 @@ htmlAttrSpecialChars as $key => $value) { + $this->assertEquals($value, (new EscaperRuntime())->escape($key, 'html_attr_relaxed'), 'Failed to escape: '.$key); + } + } + public function testJavascriptEscapingConvertsSpecialChars() { foreach ($this->jsSpecialChars as $key => $value) { @@ -319,6 +335,26 @@ } } } + } + + public function testHtmlAttributeRelaxedEscapingEscapesOwaspRecommendedRanges() + { + $immune = [',', '.', '-', '_', ':', '@', '[', ']']; // Exceptions to escaping ranges + for ($chr = 0; $chr < 0xFF; ++$chr) { + if ($chr >= 0x30 && $chr <= 0x39 + || $chr >= 0x41 && $chr <= 0x5A + || $chr >= 0x61 && $chr <= 0x7A) { + $literal = $this->codepointToUtf8($chr); + $this->assertEquals($literal, (new EscaperRuntime())->escape($literal, 'html_attr_relaxed')); + } else { + $literal = $this->codepointToUtf8($chr); + if (\in_array($literal, $immune)) { + $this->assertEquals($literal, (new EscaperRuntime())->escape($literal, 'html_attr_relaxed')); + } else { + $this->assertNotEquals($literal, (new EscaperRuntime())->escape($literal, 'html_attr_relaxed'), "$literal should be escaped!"); + } + } + } } public function testCssEscapingEscapesOwaspRecommendedRanges() diff -Nru php-twig-3.20.0/tests/TemplateTest.php php-twig-3.26.0/tests/TemplateTest.php --- php-twig-3.20.0/tests/TemplateTest.php 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/tests/TemplateTest.php 2026-05-20 07:31:59.000000000 +0000 @@ -1,5 +1,14 @@ assertSame($expected, trim($twig->render('index.twig'))); + } + + public static function getNullCoalesceWithImportedMacroData(): array + { + return [ + 'from import' => [ + [ + 'index.twig' => '{% from "helper.twig" import foo %}{{ foo("bar") ?? "" }}', + 'helper.twig' => '{% macro foo(param) %}{{ param }}{% endmacro %}', + ], + 'bar', + ], + 'from import with undefined macro falls back' => [ + [ + 'index.twig' => '{% from "helper.twig" import foo, nonexistent %}{{ nonexistent("bar") ?? "fallback" }}', + 'helper.twig' => '{% macro foo(param) %}{{ param }}{% endmacro %}', + ], + 'fallback', + ], + 'from import used multiple times' => [ + [ + 'index.twig' => '{% from "helper.twig" import foo %}{{ foo("a") ?? "" }}-{{ foo("b") ?? "" }}', + 'helper.twig' => '{% macro foo(param) %}{{ param }}{% endmacro %}', + ], + 'a-b', + ], + ]; + } + public function testRenderBlockWithUndefinedBlock() { $twig = new Environment(new ArrayLoader()); @@ -194,7 +240,7 @@ } $this->assertSame('FloatButString', $array['1.5']); $this->assertSame('IntegerButStringWithLeadingZeros', $array['01']); - $this->assertSame('EmptyString', $array[null]); + $this->assertSame('EmptyString', $array['']); $this->assertSame('Zero', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, false), 'false is treated as 0 when accessing a sequence/mapping (equals PHP behavior)'); $this->assertSame('One', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, true), 'true is treated as 1 when accessing a sequence/mapping (equals PHP behavior)'); @@ -395,8 +441,8 @@ ]); // test for Closure::__invoke() - $tests[] = [true, 'closure called', fn (): string => 'closure called', '__invoke', [], $anyType]; - $tests[] = [true, 'closure called', fn (): string => 'closure called', '__invoke', [], $methodType]; + $tests[] = [true, 'closure called', static fn (): string => 'closure called', '__invoke', [], $anyType]; + $tests[] = [true, 'closure called', static fn (): string => 'closure called', '__invoke', [], $methodType]; // tests when input is not an array or object $tests = array_merge($tests, [ @@ -558,7 +604,7 @@ { public $defined = 'defined'; public $zero = 0; - public $null = null; + public $null; public $bar = true; public $foo = true; public $baz = 'baz'; diff -Nru php-twig-3.20.0/tests/TemplateWrapperTest.php php-twig-3.26.0/tests/TemplateWrapperTest.php --- php-twig-3.20.0/tests/TemplateWrapperTest.php 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/tests/TemplateWrapperTest.php 2026-05-20 07:31:59.000000000 +0000 @@ -1,5 +1,14 @@ expectNotToPerformAssertions(); $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); - $env->registerUndefinedFunctionCallback(fn ($name) => throw new SyntaxError('boom.')); + $env->registerUndefinedFunctionCallback(static fn ($name) => throw new SyntaxError('boom.')); (new Parser($env))->parse($env->tokenize(new Source('{% guard function boom %}{% endguard %}', ''))); } } diff -Nru php-twig-3.20.0/tests/TokenParser/TypesTokenParserTest.php php-twig-3.26.0/tests/TokenParser/TypesTokenParserTest.php --- php-twig-3.20.0/tests/TokenParser/TypesTokenParserTest.php 2025-02-13 08:34:43.000000000 +0000 +++ php-twig-3.26.0/tests/TokenParser/TypesTokenParserTest.php 2026-05-20 07:31:59.000000000 +0000 @@ -1,5 +1,14 @@