diff --git a/doc/tags/for.rst b/doc/tags/for.rst
index 549189b20..fdc41d678 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.
@@ -94,6 +94,21 @@ the :doc:`slice <../filters/slice>` filter:
{% 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 %}
+
+
Adding a Condition
------------------
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/Compiler.php b/src/Compiler.php
index 6aeb1d752..8310e4f6d 100644
--- a/src/Compiler.php
+++ b/src/Compiler.php
@@ -22,17 +22,16 @@ class Compiler
private ?int $lastLine;
private string $source;
private int $indentation;
- private Environment $env;
- private array $debugInfo;
+ private array $debugInfo = [];
private int $sourceOffset;
private int $sourceLine;
- private int $varNameSalt;
+ private int $varNameSalt = 0;
private string|false $didUseEcho = false;
private array $didUseEchoStack = [];
- public function __construct(Environment $env)
- {
- $this->env = $env;
+ public function __construct(
+ private Environment $env,
+ ) {
$this->reset();
}
diff --git a/src/Environment.php b/src/Environment.php
index d6c6c81cb..5441157a5 100644
--- a/src/Environment.php
+++ b/src/Environment.php
@@ -112,7 +112,7 @@ class Environment
* (default to -1 which means that all optimizations are enabled;
* set it to 0 to disable).
*/
- 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 fbedcc547..c116aab72 100644
--- a/src/ExpressionParser.php
+++ b/src/ExpressionParser.php
@@ -47,17 +47,15 @@ class ExpressionParser
public const OPERATOR_LEFT = 1;
public const OPERATOR_RIGHT = 2;
- private Parser $parser;
- private Environment $env;
/** @var array}> */
private array $unaryOperators;
/** @var array, associativity: self::OPERATOR_*}> */
private array $binaryOperators;
- 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/CoreExtension.php b/src/Extension/CoreExtension.php
index 111a37fd4..f9996abc7 100644
--- a/src/Extension/CoreExtension.php
+++ b/src/Extension/CoreExtension.php
@@ -86,6 +86,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;
@@ -186,6 +187,7 @@ final class CoreExtension extends AbstractExtension
new ImportTokenParser(),
new FromTokenParser(),
new SetTokenParser(),
+ new TypesTokenParser(),
new FlushTokenParser(),
new DoTokenParser(),
new EmbedTokenParser(),
@@ -1259,6 +1261,12 @@ final class CoreExtension extends AbstractExtension
}
/**
+ * @template TSequence
+ *
+ * @param TSequence $seq
+ *
+ * @return ($seq is iterable ? TSequence : array{})
+ *
* @internal
*/
public static function toArray($seq, $preserveKeys = true)
diff --git a/src/Extension/OptimizerExtension.php b/src/Extension/OptimizerExtension.php
index bf2cd7bf7..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 int $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/Loader/ArrayLoader.php b/src/Loader/ArrayLoader.php
index a7cb64d1c..b4127e2d0 100644
--- a/src/Loader/ArrayLoader.php
+++ b/src/Loader/ArrayLoader.php
@@ -28,17 +28,12 @@ use Twig\Source;
*/
final class ArrayLoader implements LoaderInterface
{
- /**
- * @var array
- */
- private array $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 1f0a6a393..deca5d50a 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 iterable $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 81f5ae7a5..950c68a29 100644
--- a/src/Markup.php
+++ b/src/Markup.php
@@ -19,7 +19,7 @@ namespace Twig;
class Markup implements \Countable, \JsonSerializable, \Stringable
{
private string $content;
- private string $charset;
+ private ?string $charset;
public function __construct($content, $charset)
{
diff --git a/src/Node/ModuleNode.php b/src/Node/ModuleNode.php
index c8c15da2f..856c2547c 100644
--- a/src/Node/ModuleNode.php
+++ b/src/Node/ModuleNode.php
@@ -108,7 +108,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 ')
@@ -153,7 +153,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
@@ -164,6 +166,9 @@ final class ModuleNode extends Node
->write("{\n")
->indent()
->write("private Source \$source;\n")
+ ->write("/**\n")
+ ->write(" * @var array\n")
+ ->write(" */\n")
->write("private array \$macros = [];\n\n")
;
}
@@ -370,7 +375,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())
@@ -427,7 +432,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()
@@ -441,7 +446,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()
@@ -452,7 +457,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/Node/TypesNode.php b/src/Node/TypesNode.php
new file mode 100644
index 000000000..c4e0f081b
--- /dev/null
+++ b/src/Node/TypesNode.php
@@ -0,0 +1,28 @@
+
+ */
+#[YieldReady]
+class TypesNode extends Node
+{
+ /**
+ * @param array $types
+ */
+ public function __construct(array $types, int $lineno)
+ {
+ parent::__construct([], ['mapping' => $types], $lineno);
+ }
+
+ public function compile(Compiler $compiler): void
+ {
+ // Don't compile anything.
+ }
+}
diff --git a/src/NodeVisitor/OptimizerNodeVisitor.php b/src/NodeVisitor/OptimizerNodeVisitor.php
index 54bbf7cd1..2fb743ce1 100644
--- a/src/NodeVisitor/OptimizerNodeVisitor.php
+++ b/src/NodeVisitor/OptimizerNodeVisitor.php
@@ -51,18 +51,16 @@ final class OptimizerNodeVisitor implements NodeVisitorInterface
* @var string[]
*/
private array $loopsTargets = [];
- private int $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)) {
throw new \InvalidArgumentException(\sprintf('Optimizer mode "%s" is not valid.', $optimizers));
}
-
- $this->optimizers = $optimizers;
}
public function enterNode(Node $node, Environment $env): Node
diff --git a/src/Parser.php b/src/Parser.php
index 4ce372afd..5834d834c 100644
--- a/src/Parser.php
+++ b/src/Parser.php
@@ -53,7 +53,6 @@ class Parser
* @var MacroNode[]
*/
private array $macros;
- private Environment $env;
private array $importedSymbols;
/**
* @var Node[]
@@ -65,9 +64,9 @@ class Parser
private array $embeddedTemplates = [];
private int $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 1b9d6f8e0..0a2a87f81 100644
--- a/src/Profiler/NodeVisitor/ProfilerNodeVisitor.php
+++ b/src/Profiler/NodeVisitor/ProfilerNodeVisitor.php
@@ -27,13 +27,12 @@ use Twig\Profiler\Profile;
*/
final class ProfilerNodeVisitor implements NodeVisitorInterface
{
- private string $extensionName;
private string $varName;
- public function __construct(string $extensionName)
- {
- $this->extensionName = $extensionName;
- $this->varName = \sprintf('__internal_%s', hash('xxh128', $extensionName));
+ public function __construct(
+ private string $extensionName,
+ ) {
+ $this->varName = \sprintf('__internal_%s', hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', $extensionName));
}
public function enterNode(Node $node, Environment $env): Node
diff --git a/src/Profiler/Profile.php b/src/Profiler/Profile.php
index 042baaff6..57204f8d5 100644
--- a/src/Profiler/Profile.php
+++ b/src/Profiler/Profile.php
@@ -21,9 +21,6 @@ final class Profile implements \IteratorAggregate, \Serializable
public const TEMPLATE = 'template';
public const MACRO = 'macro';
- private string $template;
- private string $name;
- private string $type;
private array $starts = [];
private array $ends = [];
/**
@@ -31,10 +28,11 @@ final class Profile implements \IteratorAggregate, \Serializable
*/
private array $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/RuntimeLoader/ContainerRuntimeLoader.php b/src/RuntimeLoader/ContainerRuntimeLoader.php
index 6646cfbaa..89862b09f 100644
--- a/src/RuntimeLoader/ContainerRuntimeLoader.php
+++ b/src/RuntimeLoader/ContainerRuntimeLoader.php
@@ -23,11 +23,9 @@ use Psr\Container\ContainerInterface;
*/
class ContainerRuntimeLoader implements RuntimeLoaderInterface
{
- private ContainerInterface $container;
-
- public function __construct(ContainerInterface $container)
- {
- $this->container = $container;
+ public function __construct(
+ private ContainerInterface $container,
+ ) {
}
public function load(string $class): ?object
diff --git a/src/RuntimeLoader/FactoryRuntimeLoader.php b/src/RuntimeLoader/FactoryRuntimeLoader.php
index 5adfa395d..71950cfd2 100644
--- a/src/RuntimeLoader/FactoryRuntimeLoader.php
+++ b/src/RuntimeLoader/FactoryRuntimeLoader.php
@@ -18,17 +18,12 @@ namespace Twig\RuntimeLoader;
*/
class FactoryRuntimeLoader implements RuntimeLoaderInterface
{
- /**
- * @var array
- */
- private array $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): ?object
diff --git a/src/Source.php b/src/Source.php
index 87427da94..0f626b62d 100644
--- a/src/Source.php
+++ b/src/Source.php
@@ -18,20 +18,16 @@ namespace Twig;
*/
final class Source
{
- private string $code;
- private string $name;
- private string $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 d85c9fcf5..fa7c0065c 100644
--- a/src/Template.php
+++ b/src/Template.php
@@ -34,38 +34,33 @@ abstract class Template
protected $parent;
protected $parents = [];
- protected $env;
protected $blocks = [];
protected $traits = [];
protected $extensions = [];
protected $sandbox;
- public function __construct(Environment $env)
- {
- $this->env = $env;
+ public function __construct(
+ protected Environment $env,
+ ) {
$this->extensions = $env->getExtensions();
}
/**
* 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;
/**
* Auto-generated method to display the template with the given context.
@@ -111,12 +106,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/src/TemplateWrapper.php b/src/TemplateWrapper.php
index 45e1b4b93..c31f50161 100644
--- a/src/TemplateWrapper.php
+++ b/src/TemplateWrapper.php
@@ -18,19 +18,16 @@ namespace Twig;
*/
final class TemplateWrapper
{
- private Environment $env;
- private Template $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 fe1738a99..6bbee7841 100644
--- a/src/Token.php
+++ b/src/Token.php
@@ -17,10 +17,6 @@ namespace Twig;
*/
final class Token
{
- private $value;
- private int $type;
- private int $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/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/src/TokenStream.php b/src/TokenStream.php
index 5dc234f69..b99efd04f 100644
--- a/src/TokenStream.php
+++ b/src/TokenStream.php
@@ -21,16 +21,15 @@ use Twig\Error\SyntaxError;
*/
final class TokenStream
{
- /**
- * @var Token[]
- */
- private array $tokens;
private int $current = 0;
- private Source $source;
- public function __construct(array $tokens, ?Source $source = null)
- {
- $this->tokens = $tokens;
+ /**
+ * @param Token[] $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 af882257d..0ea26ed4b 100644
--- a/src/Util/DeprecationCollector.php
+++ b/src/Util/DeprecationCollector.php
@@ -20,11 +20,9 @@ use Twig\Source;
*/
final class DeprecationCollector
{
- private Environment $twig;
-
- public function __construct(Environment $twig)
- {
- $this->twig = $twig;
+ public function __construct(
+ private Environment $twig,
+ ) {
}
/**
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--
+-諺- -/- -こ-と-わ-ざ.
diff --git a/tests/Node/ModuleTest.php b/tests/Node/ModuleTest.php
index eef62f8f5..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,11 +75,15 @@ 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 \$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,11 +151,15 @@ 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 \$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,11 +246,15 @@ 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 \$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/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/TemplateTest.php b/tests/TemplateTest.php
index 18e196161..66437d2d1 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
{
@@ -425,22 +426,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;
}
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]
+ ],
+ ],
+ ];
+ }
+}