From 790eee7ae0c7d9d844bd42c2476d2e4e9b69412c Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 18 Oct 2025 12:28:49 +0200 Subject: [PATCH 01/10] Add throw tag to parse methods --- src/ExpressionParser/InfixExpressionParserInterface.php | 4 ++++ src/ExpressionParser/PrefixExpressionParserInterface.php | 4 ++++ src/Parser.php | 6 ++++++ 3 files changed, 14 insertions(+) diff --git a/src/ExpressionParser/InfixExpressionParserInterface.php b/src/ExpressionParser/InfixExpressionParserInterface.php index 8d0ac674c..4641ef815 100644 --- a/src/ExpressionParser/InfixExpressionParserInterface.php +++ b/src/ExpressionParser/InfixExpressionParserInterface.php @@ -11,12 +11,16 @@ namespace Twig\ExpressionParser; +use Twig\Error\SyntaxError; use Twig\Node\Expression\AbstractExpression; use Twig\Parser; use Twig\Token; interface InfixExpressionParserInterface extends ExpressionParserInterface { + /** + * @throws SyntaxError + */ public function parse(Parser $parser, AbstractExpression $left, Token $token): AbstractExpression; public function getAssociativity(): InfixAssociativity; diff --git a/src/ExpressionParser/PrefixExpressionParserInterface.php b/src/ExpressionParser/PrefixExpressionParserInterface.php index 587997c51..8e39dad37 100644 --- a/src/ExpressionParser/PrefixExpressionParserInterface.php +++ b/src/ExpressionParser/PrefixExpressionParserInterface.php @@ -11,11 +11,15 @@ namespace Twig\ExpressionParser; +use Twig\Error\SyntaxError; use Twig\Node\Expression\AbstractExpression; use Twig\Parser; use Twig\Token; interface PrefixExpressionParserInterface extends ExpressionParserInterface { + /** + * @throws SyntaxError + */ public function parse(Parser $parser, Token $token): AbstractExpression; } diff --git a/src/Parser.php b/src/Parser.php index acc1a4dc9..af54f13b2 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -76,6 +76,9 @@ class Parser return \sprintf('__internal_parse_%d', $this->varNameSalt++); } + /** + * @throws SyntaxError + */ public function parse(TokenStream $stream, $test = null, bool $dropNeedle = false): ModuleNode { $vars = get_object_vars($this); @@ -158,6 +161,9 @@ class Parser } } + /** + * @throws SyntaxError + */ public function subparse($test, bool $dropNeedle = false): Node { $lineno = $this->getCurrentToken()->getLine(); From 02c5a4b0b6d039569045a17e7bd012d5e3c7113f Mon Sep 17 00:00:00 2001 From: Andreas Erhard Date: Tue, 4 Nov 2025 11:55:35 +0100 Subject: [PATCH 02/10] Add caution note for random function usage --- doc/functions/random.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/doc/functions/random.rst b/doc/functions/random.rst index aac2986c3..3ebf40cb1 100644 --- a/doc/functions/random.rst +++ b/doc/functions/random.rst @@ -10,6 +10,11 @@ parameter type: * 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 #} From 40248e4ca9c2d10242fef4f086999359d389f22b Mon Sep 17 00:00:00 2001 From: Thomas Cochard <95548437+tcoch@users.noreply.github.com> Date: Mon, 10 Nov 2025 21:22:11 +0100 Subject: [PATCH 03/10] `CoreExtension` : Make error message more readable --- src/Extension/CoreExtension.php | 2 +- tests/TemplateTest.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index fcfbcb116..8cb988d26 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -1884,7 +1884,7 @@ final class CoreExtension extends AbstractExtension return; } - throw new RuntimeError(\sprintf('Neither the property "%1$s" nor one of the methods "%1$s()", "get%1$s()"/"is%1$s()"/"has%1$s()" or "__call()" exist and have public access in class "%2$s".', $item, $class), $lineno, $source); + throw new RuntimeError(\sprintf('Neither the property "%1$s" nor one of the methods "%1$s()", "get%1$s()", "is%1$s()", "has%1$s()" or "__call()" exist and have public access in class "%2$s".', $item, $class), $lineno, $source); } if ($sandboxed) { diff --git a/tests/TemplateTest.php b/tests/TemplateTest.php index 0d851ab9b..8cdb8d065 100644 --- a/tests/TemplateTest.php +++ b/tests/TemplateTest.php @@ -86,8 +86,8 @@ class TemplateTest extends TestCase ['{{ empty_array.a }}', 'Key "a" does not exist as the sequence/mapping is empty in "%s" at line 1.'], ['{{ array.a }}', 'Key "a" for sequence/mapping with keys "foo" does not exist in "%s" at line 1.'], ['{{ array.(-10) }}', 'Key "-10" for sequence/mapping with keys "foo" does not exist in "%s" at line 1.'], - ['{{ array_access.a }}', 'Neither the property "a" nor one of the methods "a()", "geta()"/"isa()"/"hasa()" or "__call()" exist and have public access in class "Twig\Tests\TemplateArrayAccessObject" in "%s" at line 1.'], - ['{% from _self import foo %}{% macro foo(obj) %}{{ obj.missing_method() }}{% endmacro %}{{ foo(array_access) }}', 'Neither the property "missing_method" nor one of the methods "missing_method()", "getmissing_method()"/"ismissing_method()"/"hasmissing_method()" or "__call()" exist and have public access in class "Twig\Tests\TemplateArrayAccessObject" in "%s" at line 1.'], + ['{{ array_access.a }}', 'Neither the property "a" nor one of the methods "a()", "geta()", "isa()", "hasa()" or "__call()" exist and have public access in class "Twig\Tests\TemplateArrayAccessObject" in "%s" at line 1.'], + ['{% from _self import foo %}{% macro foo(obj) %}{{ obj.missing_method() }}{% endmacro %}{{ foo(array_access) }}', 'Neither the property "missing_method" nor one of the methods "missing_method()", "getmissing_method()", "ismissing_method()", "hasmissing_method()" or "__call()" exist and have public access in class "Twig\Tests\TemplateArrayAccessObject" in "%s" at line 1.'], ['{{ magic_exception.test }}', 'An exception has been thrown during the rendering of a template ("Hey! Don\'t try to isset me!") in "%s" at line 1.'], ['{{ object["a"] }}', 'Impossible to access a key "a" on an object of class "stdClass" that does not implement ArrayAccess interface in "%s" at line 1.'], ]; From 6a6543462bd78b09811e81299971f1b581d8acd0 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 15 Nov 2025 09:10:04 +0100 Subject: [PATCH 04/10] Update CHANGELOG --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 7dccdc4bc..e74492451 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,6 @@ # 3.22.1 (2025-XX-XX) - * n/a + * Add support for Symfony 8 # 3.22.0 (2025-10-29) From 1de2ec1fc43ab58a4b7e80b214b96bfc895750f3 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 16 Nov 2025 17:01:12 +0100 Subject: [PATCH 05/10] Prepare the 3.22.1 release --- CHANGELOG | 2 +- src/Environment.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index e74492451..8285c0419 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,4 @@ -# 3.22.1 (2025-XX-XX) +# 3.22.1 (2025-11-16) * Add support for Symfony 8 diff --git a/src/Environment.php b/src/Environment.php index a8c7678eb..82ec3948d 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -43,12 +43,12 @@ use Twig\TokenParser\TokenParserInterface; */ class Environment { - public const VERSION = '3.22.1-DEV'; + public const VERSION = '3.22.1'; public const VERSION_ID = 32201; public const MAJOR_VERSION = 3; public const MINOR_VERSION = 22; public const RELEASE_VERSION = 1; - public const EXTRA_VERSION = 'DEV'; + public const EXTRA_VERSION = ''; private $charset; private $loader; From 34e874e536204b104bc2557acabe75e7d8044ba0 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 16 Nov 2025 17:02:42 +0100 Subject: [PATCH 06/10] Bump version --- CHANGELOG | 4 ++++ src/Environment.php | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 8285c0419..d5d89f7d8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +# 3.22.2 (2025-XX-XX) + + * n/a + # 3.22.1 (2025-11-16) * Add support for Symfony 8 diff --git a/src/Environment.php b/src/Environment.php index 82ec3948d..9d29f35c2 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -43,12 +43,12 @@ use Twig\TokenParser\TokenParserInterface; */ class Environment { - public const VERSION = '3.22.1'; - public const VERSION_ID = 32201; + public const VERSION = '3.22.2-DEV'; + public const VERSION_ID = 32202; public const MAJOR_VERSION = 3; public const MINOR_VERSION = 22; - public const RELEASE_VERSION = 1; - public const EXTRA_VERSION = ''; + public const RELEASE_VERSION = 2; + public const EXTRA_VERSION = 'DEV'; private $charset; private $loader; From 2e0c87477615fa99363de3346137bda6f5cc7a7c Mon Sep 17 00:00:00 2001 From: William Desportes Date: Wed, 19 Nov 2025 14:18:11 +0100 Subject: [PATCH 07/10] Update .gitattributes to exclude phpstan configs --- .gitattributes | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitattributes b/.gitattributes index c07b0dfb5..c9d423680 100644 --- a/.gitattributes +++ b/.gitattributes @@ -8,3 +8,5 @@ /.gitignore export-ignore /.php-cs-fixer.dist.php export-ignore /phpunit.xml.dist export-ignore +/phpstan.neon.dist export-ignore +/phpstan-baseline.neon export-ignore From 473653d19b7e1a2ccf89298a4ccc3626977680e1 Mon Sep 17 00:00:00 2001 From: Younes ENNAJI Date: Sun, 23 Nov 2025 04:08:21 +0100 Subject: [PATCH 08/10] [Core] Fix cycle() with non-countable ArrayAccess+Traversable objects --- src/Extension/CoreExtension.php | 4 +--- tests/Extension/CoreTest.php | 41 +++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 8cb988d26..8e4c31c22 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -417,10 +417,8 @@ final class CoreExtension extends AbstractExtension trigger_deprecation('twig/twig', '3.12', 'Passing a non-countable sequence of values to "%s()" is deprecated.', __METHOD__); - return $values; + $values = self::toArray($values, false); } - - $values = self::toArray($values, false); } if (!$count = \count($values)) { diff --git a/tests/Extension/CoreTest.php b/tests/Extension/CoreTest.php index 10d7e881d..2828d297c 100644 --- a/tests/Extension/CoreTest.php +++ b/tests/Extension/CoreTest.php @@ -21,6 +21,7 @@ namespace Twig\Tests\Extension; */ use PHPUnit\Framework\TestCase; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Twig\Environment; use Twig\Error\RuntimeError; use Twig\Extension\CoreExtension; @@ -31,6 +32,8 @@ use Twig\Sandbox\SecurityPolicy; class CoreTest extends TestCase { + use ExpectDeprecationTrait; + /** * @dataProvider provideCycleCases */ @@ -407,6 +410,44 @@ class CoreTest extends TestCase { $this->assertGreaterThan(1000000000, (new CoreExtension())->getLastModified()); } + + /** + * @group legacy + */ + public function testCycleWithArrayAccessAndTraversableButNotCountable() + { + $this->expectDeprecation('Since twig/twig 3.12: Passing a non-countable sequence of values to "Twig\Extension\CoreExtension::cycle()" is deprecated.'); + + $seq = new class implements \ArrayAccess, \IteratorAggregate { + public function offsetExists($offset): bool + { + return true; + } + + public function offsetGet($offset): mixed + { + return 'val'; + } + + public function offsetSet($offset, $value): void + { + } + + public function offsetUnset($offset): void + { + } + + public function getIterator(): \Traversable + { + yield 'odd'; + yield 'even'; + } + }; + + $result = CoreExtension::cycle($seq, 0); + + $this->assertEquals('odd', $result, 'cycle should return the first item from the traversable sequence, not the sequence itself.'); + } } final class CoreTestIteratorAggregate implements \IteratorAggregate From f7daf71acc0991c82aa5f720ad9c6dfd783055c9 Mon Sep 17 00:00:00 2001 From: David ALLIX <517753+webda2l@users.noreply.github.com> Date: Wed, 26 Nov 2025 20:33:56 +0100 Subject: [PATCH 09/10] Add 'invoke' to filters index --- doc/filters/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/filters/index.rst b/doc/filters/index.rst index 475b35c80..98a5d62dd 100644 --- a/doc/filters/index.rst +++ b/doc/filters/index.rst @@ -29,6 +29,7 @@ Filters html_to_markdown inline_css inky_to_html + invoke join json_encode keys From c4dcd0bb8cbd01f86832d008c4be47c49465b3b4 Mon Sep 17 00:00:00 2001 From: Tac Tacelosky Date: Wed, 3 Dec 2025 08:23:41 -0500 Subject: [PATCH 10/10] use getShareDir as an indicator of Symfony version --- extra/twig-extra-bundle/TwigExtraBundle.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/extra/twig-extra-bundle/TwigExtraBundle.php b/extra/twig-extra-bundle/TwigExtraBundle.php index e2d181145..495785623 100644 --- a/extra/twig-extra-bundle/TwigExtraBundle.php +++ b/extra/twig-extra-bundle/TwigExtraBundle.php @@ -13,9 +13,10 @@ namespace Twig\Extra\TwigExtraBundle; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\Bundle; +use Symfony\Component\HttpKernel\KernelInterface; use Twig\Extra\TwigExtraBundle\DependencyInjection\Compiler\MissingExtensionSuggestorPass; -if (!method_exists(ContainerBuilder::class, 'getAutoconfiguredAttributes')) { +if (method_exists(KernelInterface::class, 'getShareDir')) { class TwigExtraBundle extends Bundle { public function build(ContainerBuilder $container): void