From bb8b9c197ce2bb89bd65f29664eb27c915562eeb Mon Sep 17 00:00:00 2001 From: Ruud Kamphuis Date: Wed, 28 Aug 2024 19:23:03 +0200 Subject: [PATCH 01/11] Add return type `isTraitable` --- src/Node/ModuleNode.php | 21 ++++++++------ src/Template.php | 16 ++++------- tests/Node/ModuleTest.php | 58 ++++++++++++++++++++++++--------------- tests/TemplateTest.php | 9 +++--- 4 files changed, 60 insertions(+), 44 deletions(-) diff --git a/src/Node/ModuleNode.php b/src/Node/ModuleNode.php index dc6191e91..deb05a16f 100644 --- a/src/Node/ModuleNode.php +++ b/src/Node/ModuleNode.php @@ -115,7 +115,7 @@ final class ModuleNode extends Node $parent = $this->getNode('parent'); $compiler - ->write("protected function doGetParent(array \$context)\n", "{\n") + ->write("protected function doGetParent(array \$context): bool|string|Template|TemplateWrapper\n", "{\n") ->indent() ->addDebugInfo($parent) ->write('return ') @@ -160,7 +160,9 @@ final class ModuleNode extends Node ->write("use Twig\Sandbox\SecurityNotAllowedFilterError;\n") ->write("use Twig\Sandbox\SecurityNotAllowedFunctionError;\n") ->write("use Twig\Source;\n") - ->write("use Twig\Template;\n\n") + ->write("use Twig\Template;\n") + ->write("use Twig\TemplateWrapper;\n") + ->write("\n") ; } $compiler @@ -170,8 +172,11 @@ final class ModuleNode extends Node ->raw(" extends Template\n") ->write("{\n") ->indent() - ->write("private \$source;\n") - ->write("private \$macros = [];\n\n") + ->write("private Source \$source;\n") + ->write("/**\n") + ->write(" * @var array\n") + ->write(" */\n") + ->write("private array \$macros = [];\n\n") ; } @@ -377,7 +382,7 @@ final class ModuleNode extends Node ->write("/**\n") ->write(" * @codeCoverageIgnore\n") ->write(" */\n") - ->write("public function getTemplateName()\n", "{\n") + ->write("public function getTemplateName(): string\n", "{\n") ->indent() ->write('return ') ->repr($this->getSourceContext()->getName()) @@ -434,7 +439,7 @@ final class ModuleNode extends Node ->write("/**\n") ->write(" * @codeCoverageIgnore\n") ->write(" */\n") - ->write("public function isTraitable()\n", "{\n") + ->write("public function isTraitable(): bool\n", "{\n") ->indent() ->write("return false;\n") ->outdent() @@ -448,7 +453,7 @@ final class ModuleNode extends Node ->write("/**\n") ->write(" * @codeCoverageIgnore\n") ->write(" */\n") - ->write("public function getDebugInfo()\n", "{\n") + ->write("public function getDebugInfo(): array\n", "{\n") ->indent() ->write(\sprintf("return %s;\n", str_replace("\n", '', var_export(array_reverse($compiler->getDebugInfo(), true), true)))) ->outdent() @@ -459,7 +464,7 @@ final class ModuleNode extends Node protected function compileGetSourceContext(Compiler $compiler) { $compiler - ->write("public function getSourceContext()\n", "{\n") + ->write("public function getSourceContext(): Source\n", "{\n") ->indent() ->write('return new Source(') ->string($compiler->getEnvironment()->isDebug() ? $this->getSourceContext()->getCode() : '') diff --git a/src/Template.php b/src/Template.php index d3c0c229d..e0cbb94e2 100644 --- a/src/Template.php +++ b/src/Template.php @@ -52,24 +52,20 @@ abstract class Template /** * Returns the template name. - * - * @return string The template name */ - abstract public function getTemplateName(); + abstract public function getTemplateName(): string; /** * Returns debug information about the template. * - * @return array Debug information + * @return array Debug information */ - abstract public function getDebugInfo(); + abstract public function getDebugInfo(): array; /** * Returns information about the original template source code. - * - * @return Source */ - abstract public function getSourceContext(); + abstract public function getSourceContext(): Source; /** * Returns the parent template. @@ -107,12 +103,12 @@ abstract class Template return $this->parents[$parent]; } - protected function doGetParent(array $context) + protected function doGetParent(array $context): bool|string|self|TemplateWrapper { return false; } - public function isTraitable() + public function isTraitable(): bool { return true; } diff --git a/tests/Node/ModuleTest.php b/tests/Node/ModuleTest.php index 674e81132..f3081dff9 100644 --- a/tests/Node/ModuleTest.php +++ b/tests/Node/ModuleTest.php @@ -23,6 +23,8 @@ use Twig\Node\Node; use Twig\Node\SetNode; use Twig\Node\TextNode; use Twig\Source; +use Twig\Template; +use Twig\TemplateWrapper; use Twig\Test\NodeTestCase; class ModuleTest extends NodeTestCase @@ -73,12 +75,16 @@ use Twig\Sandbox\SecurityNotAllowedFilterError; use Twig\Sandbox\SecurityNotAllowedFunctionError; use Twig\Source; use Twig\Template; +use Twig\TemplateWrapper; /* foo.twig */ class __TwigTemplate_%x extends Template { - private \$source; - private \$macros = []; + private Source \$source; + /** + * @var array + */ + private array \$macros = []; public function __construct(Environment \$env) { @@ -103,7 +109,7 @@ class __TwigTemplate_%x extends Template /** * @codeCoverageIgnore */ - public function getTemplateName() + public function getTemplateName(): string { return "foo.twig"; } @@ -111,12 +117,12 @@ class __TwigTemplate_%x extends Template /** * @codeCoverageIgnore */ - public function getDebugInfo() + public function getDebugInfo(): array { - return array ( 38 => 1,); + return array ( 42 => 1,); } - public function getSourceContext() + public function getSourceContext(): Source { return new Source("", "foo.twig", ""); } @@ -145,12 +151,16 @@ use Twig\Sandbox\SecurityNotAllowedFilterError; use Twig\Sandbox\SecurityNotAllowedFunctionError; use Twig\Source; use Twig\Template; +use Twig\TemplateWrapper; /* foo.twig */ class __TwigTemplate_%x extends Template { - private \$source; - private \$macros = []; + private Source \$source; + /** + * @var array + */ + private array \$macros = []; public function __construct(Environment \$env) { @@ -162,7 +172,7 @@ class __TwigTemplate_%x extends Template ]; } - protected function doGetParent(array \$context) + protected function doGetParent(array \$context): bool|string|Template|TemplateWrapper { // line 1 return "layout.twig"; @@ -181,7 +191,7 @@ class __TwigTemplate_%x extends Template /** * @codeCoverageIgnore */ - public function getTemplateName() + public function getTemplateName(): string { return "foo.twig"; } @@ -189,7 +199,7 @@ class __TwigTemplate_%x extends Template /** * @codeCoverageIgnore */ - public function isTraitable() + public function isTraitable(): bool { return false; } @@ -197,12 +207,12 @@ class __TwigTemplate_%x extends Template /** * @codeCoverageIgnore */ - public function getDebugInfo() + public function getDebugInfo(): array { - return array ( 44 => 1, 42 => 2, 35 => 1,); + return array ( 48 => 1, 46 => 2, 39 => 1,); } - public function getSourceContext() + public function getSourceContext(): Source { return new Source("", "foo.twig", ""); } @@ -236,12 +246,16 @@ use Twig\Sandbox\SecurityNotAllowedFilterError; use Twig\Sandbox\SecurityNotAllowedFunctionError; use Twig\Source; use Twig\Template; +use Twig\TemplateWrapper; /* foo.twig */ class __TwigTemplate_%x extends Template { - private \$source; - private \$macros = []; + private Source \$source; + /** + * @var array + */ + private array \$macros = []; public function __construct(Environment \$env) { @@ -253,7 +267,7 @@ class __TwigTemplate_%x extends Template ]; } - protected function doGetParent(array \$context) + protected function doGetParent(array \$context): bool|string|Template|TemplateWrapper { // line 2 return \$this->loadTemplate(((true) ? ("foo") : ("foo")), "foo.twig", 2); @@ -271,7 +285,7 @@ class __TwigTemplate_%x extends Template /** * @codeCoverageIgnore */ - public function getTemplateName() + public function getTemplateName(): string { return "foo.twig"; } @@ -279,7 +293,7 @@ class __TwigTemplate_%x extends Template /** * @codeCoverageIgnore */ - public function isTraitable() + public function isTraitable(): bool { return false; } @@ -287,12 +301,12 @@ class __TwigTemplate_%x extends Template /** * @codeCoverageIgnore */ - public function getDebugInfo() + public function getDebugInfo(): array { - return array ( 44 => 2, 42 => 4, 35 => 2,); + return array ( 48 => 2, 46 => 4, 39 => 2,); } - public function getSourceContext() + public function getSourceContext(): Source { return new Source("{{ foo }}", "foo.twig", ""); } diff --git a/tests/TemplateTest.php b/tests/TemplateTest.php index 37d6fe62a..884226e0a 100644 --- a/tests/TemplateTest.php +++ b/tests/TemplateTest.php @@ -21,6 +21,7 @@ use Twig\Sandbox\SecurityError; use Twig\Sandbox\SecurityPolicy; use Twig\Source; use Twig\Template; +use Twig\TemplateWrapper; class TemplateTest extends TestCase { @@ -443,22 +444,22 @@ class TemplateForTest extends Template return true; } - public function getTemplateName() + public function getTemplateName(): string { return $this->name; } - public function getDebugInfo() + public function getDebugInfo() : array { return []; } - public function getSourceContext() + public function getSourceContext() : Source { return new Source('', $this->getTemplateName()); } - protected function doGetParent(array $context) + protected function doGetParent(array $context): bool|string|Template|TemplateWrapper { return false; } From 956d09bf08c312d66ea52990dfec938c5456dde7 Mon Sep 17 00:00:00 2001 From: Ruud Kamphuis Date: Wed, 28 Aug 2024 14:51:36 +0200 Subject: [PATCH 02/11] Add conditional return types to `ensureTraversable` --- src/Extension/CoreExtension.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index a58e1e2c9..f2b0c1794 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -1280,6 +1280,9 @@ final class CoreExtension extends AbstractExtension /** * @internal + * @template TSequence + * @param TSequence $seq + * @return ($seq is iterable ? TSequence : array{}) */ public static function ensureTraversable($seq) { From 33c4879dd8de769eb3ce4f904efb7bae7e18e146 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 29 Aug 2024 10:31:04 +0200 Subject: [PATCH 03/11] Fix CS --- src/Extension/CoreExtension.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index f2b0c1794..fdb11dc7a 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -1279,10 +1279,13 @@ final class CoreExtension extends AbstractExtension } /** - * @internal * @template TSequence + * * @param TSequence $seq + * * @return ($seq is iterable ? TSequence : array{}) + * + * @internal */ public static function ensureTraversable($seq) { From 4d19472d4ac1838e0b1f0e029ce1fa4040eb34ea Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 29 Aug 2024 11:51:12 +0200 Subject: [PATCH 04/11] Prepare the 3.12.0 release --- CHANGELOG | 2 +- src/Environment.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 4d54148ac..ff6951e6d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,4 @@ -# 3.12.0 (2024-XX-XX) +# 3.12.0 (2024-08-29) * Deprecate the fact that the `extends` and `use` tags are always allowed in a sandboxed template. This behavior will change in 4.0 where these tags will need to be explicitly allowed like any other tag. diff --git a/src/Environment.php b/src/Environment.php index 237d59803..097d1ca04 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -43,12 +43,12 @@ use Twig\TokenParser\TokenParserInterface; */ class Environment { - public const VERSION = '3.12.0-DEV'; + public const VERSION = '3.12.0'; public const VERSION_ID = 301200; public const MAJOR_VERSION = 3; public const MINOR_VERSION = 12; public const RELEASE_VERSION = 0; - public const EXTRA_VERSION = 'DEV'; + public const EXTRA_VERSION = ''; private $charset; private $loader; From edcea8bce0d14f871d58e9bb2ad93ff72c28b348 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 29 Aug 2024 11:53:10 +0200 Subject: [PATCH 05/11] Bump version --- CHANGELOG | 4 ++++ src/Environment.php | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index ff6951e6d..6b9fdea19 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +# 3.12.1 (2024-XX-XX) + + * n/a + # 3.12.0 (2024-08-29) * Deprecate the fact that the `extends` and `use` tags are always allowed in a sandboxed template. diff --git a/src/Environment.php b/src/Environment.php index 097d1ca04..f8dbe2352 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -43,12 +43,12 @@ use Twig\TokenParser\TokenParserInterface; */ class Environment { - public const VERSION = '3.12.0'; - public const VERSION_ID = 301200; + public const VERSION = '3.12.1-DEV'; + public const VERSION_ID = 301201; public const MAJOR_VERSION = 3; public const MINOR_VERSION = 12; - public const RELEASE_VERSION = 0; - public const EXTRA_VERSION = ''; + public const RELEASE_VERSION = 1; + public const EXTRA_VERSION = 'DEV'; private $charset; private $loader; From 9120e9f3d1f08ced708a9d43ec58b31c300b5ea0 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 29 Aug 2024 16:16:01 +0200 Subject: [PATCH 06/11] Bump version --- CHANGELOG | 2 +- src/Environment.php | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 6b9fdea19..64ea20ce8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,4 @@ -# 3.12.1 (2024-XX-XX) +# 3.13.0 (2024-XX-XX) * n/a diff --git a/src/Environment.php b/src/Environment.php index f8dbe2352..d612ee5e5 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -43,11 +43,11 @@ use Twig\TokenParser\TokenParserInterface; */ class Environment { - public const VERSION = '3.12.1-DEV'; - public const VERSION_ID = 301201; + public const VERSION = '3.13.0-DEV'; + public const VERSION_ID = 301300; public const MAJOR_VERSION = 3; - public const MINOR_VERSION = 12; - public const RELEASE_VERSION = 1; + public const MINOR_VERSION = 13; + public const RELEASE_VERSION = 0; public const EXTRA_VERSION = 'DEV'; private $charset; From 67dabd8182bccd557070cffce843b3f4226e231b Mon Sep 17 00:00:00 2001 From: Jeroen Versteeg Date: Fri, 23 Aug 2024 16:15:55 +0200 Subject: [PATCH 07/11] Add types tag --- CHANGELOG | 2 +- doc/tags/index.rst | 1 + doc/tags/types.rst | 42 +++++++++++ src/Extension/CoreExtension.php | 2 + src/Node/TypesNode.php | 28 +++++++ src/TokenParser/TypesTokenParser.php | 85 ++++++++++++++++++++++ tests/Node/TypesTest.php | 43 +++++++++++ tests/TokenParser/TypesTokenParserTest.php | 69 ++++++++++++++++++ 8 files changed, 271 insertions(+), 1 deletion(-) create mode 100644 doc/tags/types.rst create mode 100644 src/Node/TypesNode.php create mode 100644 src/TokenParser/TypesTokenParser.php create mode 100644 tests/Node/TypesTest.php create mode 100644 tests/TokenParser/TypesTokenParserTest.php diff --git a/CHANGELOG b/CHANGELOG index 64ea20ce8..86f696fc4 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,6 @@ # 3.13.0 (2024-XX-XX) - * n/a + * Add the `types` tag (experimental) # 3.12.0 (2024-08-29) diff --git a/doc/tags/index.rst b/doc/tags/index.rst index b3c104080..692ca6094 100644 --- a/doc/tags/index.rst +++ b/doc/tags/index.rst @@ -21,6 +21,7 @@ Tags macro sandbox set + types use verbatim with diff --git a/doc/tags/types.rst b/doc/tags/types.rst new file mode 100644 index 000000000..389afc1fc --- /dev/null +++ b/doc/tags/types.rst @@ -0,0 +1,42 @@ +``types`` +========= + +.. versionadded:: 3.13 + + The ``types`` tag was added in Twig 3.13. This tag is **experimental** and can change based on usage and feedback. + +The ``types`` tag declares the types of template variables. + +To do this, specify a :ref:`mapping ` of names to their types as strings. + +Here is how to declare that ``foo`` is a boolean, while ``bar`` is an integer (see note below): + +.. code-block:: twig + + {% types { + foo: 'bool', + bar: 'int', + } %} + +You can declare variables as optional by adding the ``?`` suffix: + +.. code-block:: twig + + {% types { + foo: 'bool', + bar?: 'int', + } %} + +By default, this tag does not affect the template compilation or runtime behavior. + +Its purpose is to enable designers and developers to document and specify the context's available +and/or required variables. While Twig itself does not validate variables or their types, this tag enables extensions +to do this. + +Additionally, :ref:`Twig extensions ` can analyze these tags to perform compile-time and +runtime analysis of templates. + +.. note:: + + The syntax for and contents of type strings are intentionally left out of scope. + diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index fdb11dc7a..92c5fd5a8 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -82,6 +82,7 @@ use Twig\TokenParser\ImportTokenParser; use Twig\TokenParser\IncludeTokenParser; use Twig\TokenParser\MacroTokenParser; use Twig\TokenParser\SetTokenParser; +use Twig\TokenParser\TypesTokenParser; use Twig\TokenParser\UseTokenParser; use Twig\TokenParser\WithTokenParser; use Twig\TwigFilter; @@ -182,6 +183,7 @@ final class CoreExtension extends AbstractExtension new ImportTokenParser(), new FromTokenParser(), new SetTokenParser(), + new TypesTokenParser(), new FlushTokenParser(), new DoTokenParser(), new EmbedTokenParser(), diff --git a/src/Node/TypesNode.php b/src/Node/TypesNode.php new file mode 100644 index 000000000..5cdece665 --- /dev/null +++ b/src/Node/TypesNode.php @@ -0,0 +1,28 @@ + + */ +#[YieldReady] +class TypesNode extends Node implements NodeCaptureInterface +{ + /** + * @param array $types + */ + public function __construct(array $types, int $lineno) + { + parent::__construct([], ['mapping' => $types], $lineno); + } + + public function compile(Compiler $compiler) + { + // Don't compile anything. + } +} diff --git a/src/TokenParser/TypesTokenParser.php b/src/TokenParser/TypesTokenParser.php new file mode 100644 index 000000000..b579fd0c0 --- /dev/null +++ b/src/TokenParser/TypesTokenParser.php @@ -0,0 +1,85 @@ + + * @internal + */ +final class TypesTokenParser extends AbstractTokenParser +{ + public function parse(Token $token): Node + { + $stream = $this->parser->getStream(); + + $types = $this->parseSimpleMappingExpression($stream); + + $stream->expect(Token::BLOCK_END_TYPE); + + return new TypesNode($types, $token->getLine()); + } + + /** + * @return array + * + * @throws SyntaxError + */ + private function parseSimpleMappingExpression(TokenStream $stream): array + { + $stream->expect(Token::PUNCTUATION_TYPE, '{', 'A mapping element was expected'); + + $types = []; + + $first = true; + while (!$stream->test(Token::PUNCTUATION_TYPE, '}')) { + if (!$first) { + $stream->expect(Token::PUNCTUATION_TYPE, ',', 'A type string must be followed by a comma'); + + // trailing ,? + if ($stream->test(Token::PUNCTUATION_TYPE, '}')) { + break; + } + } + $first = false; + + $nameToken = $stream->expect(Token::NAME_TYPE); + $isOptional = $stream->nextIf(Token::PUNCTUATION_TYPE, '?') !== null; + + $stream->expect(Token::PUNCTUATION_TYPE, ':', 'A type name must be followed by a colon (:)'); + + $valueToken = $stream->expect(Token::STRING_TYPE); + + $types[$nameToken->getValue()] = [ + 'type' => $valueToken->getValue(), + 'optional' => $isOptional, + ]; + } + $stream->expect(Token::PUNCTUATION_TYPE, '}', 'An opened mapping is not properly closed'); + + return $types; + } + + public function getTag(): string + { + return 'types'; + } +} diff --git a/tests/Node/TypesTest.php b/tests/Node/TypesTest.php new file mode 100644 index 000000000..ff6c2dfac --- /dev/null +++ b/tests/Node/TypesTest.php @@ -0,0 +1,43 @@ + [ + 'type' => 'string', + 'optional' => false, + ], + 'bar' => [ + 'type' => 'int', + 'optional' => true, + ] + ]; + } + + public function testConstructor() + { + $types = $this->getValidMapping(); + $node = new TypesNode($types, 1); + + $this->assertEquals($types, $node->getAttribute('mapping')); + } + + public function getTests() + { + return [ + // 1st test: Node shouldn't compile at all + [ + new TypesNode($this->getValidMapping(), 1), + '' + ] + ]; + } +} diff --git a/tests/TokenParser/TypesTokenParserTest.php b/tests/TokenParser/TypesTokenParserTest.php new file mode 100644 index 000000000..1d27d9d15 --- /dev/null +++ b/tests/TokenParser/TypesTokenParserTest.php @@ -0,0 +1,69 @@ + false, 'autoescape' => false]); + $stream = $env->tokenize($source = new Source($template, '')); + $parser = new Parser($env); + + $typesNode = $parser->parse($stream)->getNode('body')->getNode('0'); + + self::assertEquals($expected, $typesNode->getAttribute('mapping')); + } + + public function getMappingTests(): array + { + return [ + // empty mapping + [ + '{% types {} %}', + [], + ], + + // simple + [ + '{% types {foo: "bar"} %}', + [ + 'foo' => ['type' => 'bar', 'optional' => false] + ], + ], + + // trailing comma + [ + '{% types {foo: "bar",} %}', + [ + 'foo' => ['type' => 'bar', 'optional' => false] + ], + ], + + // optional name + [ + '{% types {foo?: "bar"} %}', + [ + 'foo' => ['type' => 'bar', 'optional' => true] + ], + ], + + // multiple pairs, duplicate values + [ + '{% types {foo: "foo", bar?: "foo", baz: "baz"} %}', + [ + 'foo' => ['type' => 'foo', 'optional' => false], + 'bar' => ['type' => 'foo', 'optional' => true], + 'baz' => ['type' => 'baz', 'optional' => false] + ], + ], + ]; + } +} From 28923f4269bafe82c7cd6f9d3707dd11cf0aba79 Mon Sep 17 00:00:00 2001 From: "andreybolonin1989@gmail.com" Date: Sat, 24 Aug 2024 11:31:27 +0300 Subject: [PATCH 08/11] Use CPP in full code base --- src/Cache/ChainCache.php | 8 +++----- src/Compiler.php | 7 +++---- src/Environment.php | 2 +- src/ExpressionParser.php | 10 ++++------ src/Extension/OptimizerExtension.php | 8 +++----- src/Extension/YieldNotReadyExtension.php | 8 +++----- src/Loader/ArrayLoader.php | 8 +++----- src/Loader/ChainLoader.php | 11 +++-------- src/Markup.php | 2 +- src/NodeVisitor/OptimizerNodeVisitor.php | 8 +++----- src/NodeVisitor/YieldNotReadyNodeVisitor.php | 7 +++---- src/Parser.php | 7 +++---- src/Profiler/NodeVisitor/ProfilerNodeVisitor.php | 7 +++---- src/Profiler/Profile.php | 13 +++++-------- src/Runtime/EscaperRuntime.php | 8 +++----- src/RuntimeLoader/ContainerRuntimeLoader.php | 8 +++----- src/RuntimeLoader/FactoryRuntimeLoader.php | 8 +++----- src/Source.php | 14 +++++--------- src/Template.php | 7 +++---- src/TemplateWrapper.php | 11 ++++------- src/Token.php | 14 +++++--------- src/TokenStream.php | 9 ++++----- src/Util/DeprecationCollector.php | 8 +++----- 23 files changed, 74 insertions(+), 119 deletions(-) diff --git a/src/Cache/ChainCache.php b/src/Cache/ChainCache.php index 18c66f35f..c94afdb43 100644 --- a/src/Cache/ChainCache.php +++ b/src/Cache/ChainCache.php @@ -21,14 +21,12 @@ namespace Twig\Cache; */ final class ChainCache implements CacheInterface { - private $caches; - /** * @param iterable $caches The ordered list of caches used to store and fetch cached items */ - public function __construct(iterable $caches) - { - $this->caches = $caches; + public function __construct( + private iterable $caches, + ) { } public function generateKey(string $name, string $className): string diff --git a/src/Compiler.php b/src/Compiler.php index 1e7ed04c6..1a43aa7f6 100644 --- a/src/Compiler.php +++ b/src/Compiler.php @@ -22,7 +22,6 @@ class Compiler private $lastLine; private $source; private $indentation; - private $env; private $debugInfo = []; private $sourceOffset; private $sourceLine; @@ -30,9 +29,9 @@ class Compiler private $didUseEcho = false; private $didUseEchoStack = []; - public function __construct(Environment $env) - { - $this->env = $env; + public function __construct( + private Environment $env, + ) { } public function getEnvironment(): Environment diff --git a/src/Environment.php b/src/Environment.php index 237d59803..bb4bc09ae 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -107,7 +107,7 @@ class Environment * 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 */ - public function __construct(LoaderInterface $loader, $options = []) + public function __construct(LoaderInterface $loader, array $options = []) { $this->setLoader($loader); diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index 28b556ccc..bfc1a1d04 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -47,18 +47,16 @@ class ExpressionParser public const OPERATOR_LEFT = 1; public const OPERATOR_RIGHT = 2; - private $parser; - private $env; /** @var array}> */ private $unaryOperators; /** @var array, associativity: self::OPERATOR_*}> */ private $binaryOperators; private $readyNodes = []; - public function __construct(Parser $parser, Environment $env) - { - $this->parser = $parser; - $this->env = $env; + public function __construct( + private Parser $parser, + private Environment $env, + ) { $this->unaryOperators = $env->getUnaryOperators(); $this->binaryOperators = $env->getBinaryOperators(); } diff --git a/src/Extension/OptimizerExtension.php b/src/Extension/OptimizerExtension.php index 965bfdb04..d3fe46a67 100644 --- a/src/Extension/OptimizerExtension.php +++ b/src/Extension/OptimizerExtension.php @@ -15,11 +15,9 @@ use Twig\NodeVisitor\OptimizerNodeVisitor; final class OptimizerExtension extends AbstractExtension { - private $optimizers; - - public function __construct(int $optimizers = -1) - { - $this->optimizers = $optimizers; + public function __construct( + private int $optimizers = -1, + ) { } public function getNodeVisitors(): array diff --git a/src/Extension/YieldNotReadyExtension.php b/src/Extension/YieldNotReadyExtension.php index 2503c8d81..49dfb8085 100644 --- a/src/Extension/YieldNotReadyExtension.php +++ b/src/Extension/YieldNotReadyExtension.php @@ -18,11 +18,9 @@ use Twig\NodeVisitor\YieldNotReadyNodeVisitor; */ final class YieldNotReadyExtension extends AbstractExtension { - private $useYield; - - public function __construct(bool $useYield) - { - $this->useYield = $useYield; + public function __construct( + private bool $useYield, + ) { } public function getNodeVisitors(): array diff --git a/src/Loader/ArrayLoader.php b/src/Loader/ArrayLoader.php index ce613c9cc..2bb54b7a8 100644 --- a/src/Loader/ArrayLoader.php +++ b/src/Loader/ArrayLoader.php @@ -28,14 +28,12 @@ use Twig\Source; */ final class ArrayLoader implements LoaderInterface { - private $templates = []; - /** * @param array $templates An array of templates (keys are the names, and values are the source code) */ - public function __construct(array $templates = []) - { - $this->templates = $templates; + public function __construct( + private array $templates = [], + ) { } public function setTemplate(string $name, string $template): void diff --git a/src/Loader/ChainLoader.php b/src/Loader/ChainLoader.php index 90f798db3..6e4f9511c 100644 --- a/src/Loader/ChainLoader.php +++ b/src/Loader/ChainLoader.php @@ -21,11 +21,6 @@ use Twig\Source; */ final class ChainLoader implements LoaderInterface { - /** - * @var \Traversable|LoaderInterface[] - */ - private $loaders; - /** * @var array */ @@ -34,9 +29,9 @@ final class ChainLoader implements LoaderInterface /** * @param iterable $loaders */ - public function __construct(iterable $loaders = []) - { - $this->loaders = $loaders; + public function __construct( + private iterable $loaders = [], + ) { } public function addLoader(LoaderInterface $loader): void diff --git a/src/Markup.php b/src/Markup.php index 1788acc4f..3020c60c4 100644 --- a/src/Markup.php +++ b/src/Markup.php @@ -19,7 +19,7 @@ namespace Twig; class Markup implements \Countable, \JsonSerializable { private $content; - private $charset; + private ?string $charset; public function __construct($content, $charset) { diff --git a/src/NodeVisitor/OptimizerNodeVisitor.php b/src/NodeVisitor/OptimizerNodeVisitor.php index 0d2dc02d5..a943f45c3 100644 --- a/src/NodeVisitor/OptimizerNodeVisitor.php +++ b/src/NodeVisitor/OptimizerNodeVisitor.php @@ -47,13 +47,13 @@ final class OptimizerNodeVisitor implements NodeVisitorInterface private $loops = []; private $loopsTargets = []; - private $optimizers; /** * @param int $optimizers The optimizer mode */ - public function __construct(int $optimizers = -1) - { + public function __construct( + private int $optimizers = -1, + ) { if ($optimizers > (self::OPTIMIZE_FOR | self::OPTIMIZE_RAW_FILTER | self::OPTIMIZE_TEXT_NODES)) { throw new \InvalidArgumentException(\sprintf('Optimizer mode "%s" is not valid.', $optimizers)); } @@ -65,8 +65,6 @@ final class OptimizerNodeVisitor implements NodeVisitorInterface if (-1 !== $optimizers && self::OPTIMIZE_TEXT_NODES === (self::OPTIMIZE_TEXT_NODES & $optimizers)) { trigger_deprecation('twig/twig', '3.12', 'The "Twig\NodeVisitor\OptimizerNodeVisitor::OPTIMIZE_TEXT_NODES" option is deprecated and does nothing.'); } - - $this->optimizers = $optimizers; } public function enterNode(Node $node, Environment $env): Node diff --git a/src/NodeVisitor/YieldNotReadyNodeVisitor.php b/src/NodeVisitor/YieldNotReadyNodeVisitor.php index 6470bdabc..4b190b414 100644 --- a/src/NodeVisitor/YieldNotReadyNodeVisitor.php +++ b/src/NodeVisitor/YieldNotReadyNodeVisitor.php @@ -21,12 +21,11 @@ use Twig\Node\Node; */ final class YieldNotReadyNodeVisitor implements NodeVisitorInterface { - private $useYield; private $yieldReadyNodes = []; - public function __construct(bool $useYield) - { - $this->useYield = $useYield; + public function __construct( + private bool $useYield, + ) { } public function enterNode(Node $node, Environment $env): Node diff --git a/src/Parser.php b/src/Parser.php index 28cc8a0e1..cd8da2b8e 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -40,15 +40,14 @@ class Parser private $blocks; private $blockStack; private $macros; - private $env; private $importedSymbols; private $traits; private $embeddedTemplates = []; private $varNameSalt = 0; - public function __construct(Environment $env) - { - $this->env = $env; + public function __construct( + private Environment $env, + ) { } public function getVarName(): string diff --git a/src/Profiler/NodeVisitor/ProfilerNodeVisitor.php b/src/Profiler/NodeVisitor/ProfilerNodeVisitor.php index 4d2a58105..1458bc5fc 100644 --- a/src/Profiler/NodeVisitor/ProfilerNodeVisitor.php +++ b/src/Profiler/NodeVisitor/ProfilerNodeVisitor.php @@ -27,12 +27,11 @@ use Twig\Profiler\Profile; */ final class ProfilerNodeVisitor implements NodeVisitorInterface { - private $extensionName; private $varName; - public function __construct(string $extensionName) - { - $this->extensionName = $extensionName; + public function __construct( + private string $extensionName, + ) { $this->varName = \sprintf('__internal_%s', hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', $extensionName)); } diff --git a/src/Profiler/Profile.php b/src/Profiler/Profile.php index 72506b7c8..2928e1646 100644 --- a/src/Profiler/Profile.php +++ b/src/Profiler/Profile.php @@ -20,18 +20,15 @@ final class Profile implements \IteratorAggregate, \Serializable public const BLOCK = 'block'; public const TEMPLATE = 'template'; public const MACRO = 'macro'; - - private $template; - private $name; - private $type; private $starts = []; private $ends = []; private $profiles = []; - public function __construct(string $template = 'main', string $type = self::ROOT, string $name = 'main') - { - $this->template = $template; - $this->type = $type; + public function __construct( + private string $template = 'main', + private string $type = self::ROOT, + private string $name = 'main', + ) { $this->name = str_starts_with($name, '__internal_') ? 'INTERNAL' : $name; $this->enter(); } diff --git a/src/Runtime/EscaperRuntime.php b/src/Runtime/EscaperRuntime.php index b1dac9640..e4aee629a 100644 --- a/src/Runtime/EscaperRuntime.php +++ b/src/Runtime/EscaperRuntime.php @@ -26,11 +26,9 @@ final class EscaperRuntime implements RuntimeExtensionInterface /** @internal */ public $safeLookup = []; - private $charset; - - public function __construct($charset = 'UTF-8') - { - $this->charset = $charset; + public function __construct( + private $charset = 'UTF-8', + ) { } /** diff --git a/src/RuntimeLoader/ContainerRuntimeLoader.php b/src/RuntimeLoader/ContainerRuntimeLoader.php index b360d7bea..05106680c 100644 --- a/src/RuntimeLoader/ContainerRuntimeLoader.php +++ b/src/RuntimeLoader/ContainerRuntimeLoader.php @@ -23,11 +23,9 @@ use Psr\Container\ContainerInterface; */ class ContainerRuntimeLoader implements RuntimeLoaderInterface { - private $container; - - public function __construct(ContainerInterface $container) - { - $this->container = $container; + public function __construct( + private ContainerInterface $container, + ) { } public function load(string $class) diff --git a/src/RuntimeLoader/FactoryRuntimeLoader.php b/src/RuntimeLoader/FactoryRuntimeLoader.php index 130648392..5d4e70b92 100644 --- a/src/RuntimeLoader/FactoryRuntimeLoader.php +++ b/src/RuntimeLoader/FactoryRuntimeLoader.php @@ -18,14 +18,12 @@ namespace Twig\RuntimeLoader; */ class FactoryRuntimeLoader implements RuntimeLoaderInterface { - private $map; - /** * @param array $map An array where keys are class names and values factory callables */ - public function __construct(array $map = []) - { - $this->map = $map; + public function __construct( + private array $map = [], + ) { } public function load(string $class) diff --git a/src/Source.php b/src/Source.php index 3cb02403c..0f626b62d 100644 --- a/src/Source.php +++ b/src/Source.php @@ -18,20 +18,16 @@ namespace Twig; */ final class Source { - private $code; - private $name; - private $path; - /** * @param string $code The template source code * @param string $name The template logical name * @param string $path The filesystem path of the template if any */ - public function __construct(string $code, string $name, string $path = '') - { - $this->code = $code; - $this->name = $name; - $this->path = $path; + public function __construct( + private string $code, + private string $name, + private string $path = '', + ) { } public function getCode(): string diff --git a/src/Template.php b/src/Template.php index d3c0c229d..498c35a6d 100644 --- a/src/Template.php +++ b/src/Template.php @@ -35,7 +35,6 @@ abstract class Template protected $parent; protected $parents = []; - protected $env; protected $blocks = []; protected $traits = []; protected $extensions = []; @@ -43,9 +42,9 @@ abstract class Template private $useYield; - public function __construct(Environment $env) - { - $this->env = $env; + public function __construct( + protected Environment $env, + ) { $this->useYield = $env->useYield(); $this->extensions = $env->getExtensions(); } diff --git a/src/TemplateWrapper.php b/src/TemplateWrapper.php index fcfb070c7..c31f50161 100644 --- a/src/TemplateWrapper.php +++ b/src/TemplateWrapper.php @@ -18,19 +18,16 @@ namespace Twig; */ final class TemplateWrapper { - private $env; - private $template; - /** * This method is for internal use only and should never be called * directly (use Twig\Environment::load() instead). * * @internal */ - public function __construct(Environment $env, Template $template) - { - $this->env = $env; - $this->template = $template; + public function __construct( + private Environment $env, + private Template $template, + ) { } public function render(array $context = []): string diff --git a/src/Token.php b/src/Token.php index 5be39bdc7..237634ad1 100644 --- a/src/Token.php +++ b/src/Token.php @@ -17,10 +17,6 @@ namespace Twig; */ final class Token { - private $value; - private $type; - private $lineno; - public const EOF_TYPE = -1; public const TEXT_TYPE = 0; public const BLOCK_START_TYPE = 1; @@ -37,11 +33,11 @@ final class Token public const ARROW_TYPE = 12; public const SPREAD_TYPE = 13; - public function __construct(int $type, $value, int $lineno) - { - $this->type = $type; - $this->value = $value; - $this->lineno = $lineno; + public function __construct( + private int $type, + private $value, + private int $lineno, + ) { } public function __toString() diff --git a/src/TokenStream.php b/src/TokenStream.php index 32357f931..c91701bfe 100644 --- a/src/TokenStream.php +++ b/src/TokenStream.php @@ -21,13 +21,12 @@ use Twig\Error\SyntaxError; */ final class TokenStream { - private $tokens; private $current = 0; - private $source; - public function __construct(array $tokens, ?Source $source = null) - { - $this->tokens = $tokens; + public function __construct( + private array $tokens, + private ?Source $source = null, + ) { $this->source = $source ?: new Source('', ''); } diff --git a/src/Util/DeprecationCollector.php b/src/Util/DeprecationCollector.php index ad5310617..0ea26ed4b 100644 --- a/src/Util/DeprecationCollector.php +++ b/src/Util/DeprecationCollector.php @@ -20,11 +20,9 @@ use Twig\Source; */ final class DeprecationCollector { - private $twig; - - public function __construct(Environment $twig) - { - $this->twig = $twig; + public function __construct( + private Environment $twig, + ) { } /** From 5e3f9d9f2637fdd4a4417b684d2a18c0bdda3f40 Mon Sep 17 00:00:00 2001 From: Ruud Kamphuis Date: Thu, 29 Aug 2024 19:39:20 +0200 Subject: [PATCH 09/11] Remove NodeCaptureInterface from TypesNode This was accidentally copied from SetNode --- src/Node/TypesNode.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Node/TypesNode.php b/src/Node/TypesNode.php index 5cdece665..ebb304d49 100644 --- a/src/Node/TypesNode.php +++ b/src/Node/TypesNode.php @@ -11,7 +11,7 @@ use Twig\Compiler; * @author Jeroen Versteeg */ #[YieldReady] -class TypesNode extends Node implements NodeCaptureInterface +class TypesNode extends Node { /** * @param array $types From a68804d6149fbfa75e511a5b9e7ab196a062082e Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 30 Aug 2024 11:00:01 +0200 Subject: [PATCH 10/11] Add an example on how to iterate over a string --- doc/tags/for.rst | 15 +++++++++++++++ tests/Fixtures/tags/for/for_on_strings.test | 13 +++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 tests/Fixtures/tags/for/for_on_strings.test diff --git a/doc/tags/for.rst b/doc/tags/for.rst index 656d9c07b..4160d751f 100644 --- a/doc/tags/for.rst +++ b/doc/tags/for.rst @@ -139,3 +139,18 @@ the :doc:`slice <../filters/slice>` filter:
  • {{ user.username|e }}
  • {% endfor %} + +Iterating over a String +----------------------- + +To iterate over the characters of a string, use the +:doc:`split <../filters/split>` filter: + +.. code-block:: html+twig + +

    Characters

    +
      + {% for char in "諺 / ことわざ"|split('') -%} +
    • {{ char }}
    • + {%- endfor %} +
    diff --git a/tests/Fixtures/tags/for/for_on_strings.test b/tests/Fixtures/tags/for/for_on_strings.test new file mode 100644 index 000000000..62d484269 --- /dev/null +++ b/tests/Fixtures/tags/for/for_on_strings.test @@ -0,0 +1,13 @@ +--TEST-- +"for" tag can iterate over a string via the "split" filter +--TEMPLATE-- +{% set jp = "諺 / ことわざ" %} + +{% for letter in jp|split('') -%} + -{{- letter }} + {{- loop.last ? '.' }} +{%- endfor %} +--DATA-- +return [] +--EXPECT-- +-諺- -/- -こ-と-わ-ざ. From 2f8c62189845fa3cdc26d1a92d334e3d727bdbd5 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 30 Aug 2024 11:43:48 +0200 Subject: [PATCH 11/11] Fix doc markup --- doc/tags/for.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/tags/for.rst b/doc/tags/for.rst index 4160d751f..1e78db63c 100644 --- a/doc/tags/for.rst +++ b/doc/tags/for.rst @@ -45,7 +45,7 @@ The ``..`` operator can take any expression at both sides: * {{ letter }} {% endfor %} -.. tip: +.. tip:: If you need a step different from 1, you can use the ``range`` function instead.