mirror of
https://github.com/twigphp/Twig.git
synced 2026-08-30 20:16:45 +00:00
feature #4235 Add types tag (drjayvee)
This PR was squashed before being merged into the 3.x branch.
Discussion
----------
Add types tag
The is a draft implementation for #4165.
Commits
-------
67dabd81 Add types tag
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# 3.13.0 (2024-XX-XX)
|
||||
|
||||
* n/a
|
||||
* Add the `types` tag (experimental)
|
||||
|
||||
# 3.12.0 (2024-08-29)
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ Tags
|
||||
macro
|
||||
sandbox
|
||||
set
|
||||
types
|
||||
use
|
||||
verbatim
|
||||
with
|
||||
|
||||
@@ -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 <twig-expressions>` 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 <creating_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.
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Twig\Node;
|
||||
|
||||
use Twig\Attribute\YieldReady;
|
||||
use Twig\Compiler;
|
||||
|
||||
/**
|
||||
* Represents a types node.
|
||||
*
|
||||
* @author Jeroen Versteeg <jeroen@alisqi.com>
|
||||
*/
|
||||
#[YieldReady]
|
||||
class TypesNode extends Node implements NodeCaptureInterface
|
||||
{
|
||||
/**
|
||||
* @param array<string, array{type: string, optional: bool}> $types
|
||||
*/
|
||||
public function __construct(array $types, int $lineno)
|
||||
{
|
||||
parent::__construct([], ['mapping' => $types], $lineno);
|
||||
}
|
||||
|
||||
public function compile(Compiler $compiler)
|
||||
{
|
||||
// Don't compile anything.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Twig.
|
||||
*
|
||||
* (c) Fabien Potencier
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Twig\TokenParser;
|
||||
|
||||
use Twig\Error\SyntaxError;
|
||||
use Twig\Node\Node;
|
||||
use Twig\Node\TypesNode;
|
||||
use Twig\Token;
|
||||
use Twig\TokenStream;
|
||||
|
||||
/**
|
||||
* Declare variable types.
|
||||
*
|
||||
* {% types {foo: 'int', bar?: 'string'} %}
|
||||
*
|
||||
* @author Jeroen Versteeg <jeroen@alisqi.com>
|
||||
* @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<string, array{type: string, optional: bool}>
|
||||
*
|
||||
* @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';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace Twig\Tests\Node;
|
||||
|
||||
use Twig\Node\TypesNode;
|
||||
use Twig\Test\NodeTestCase;
|
||||
|
||||
class TypesTest extends NodeTestCase
|
||||
{
|
||||
private function getValidMapping(): array
|
||||
{
|
||||
// {foo: 'string', bar?: 'int'}
|
||||
return [
|
||||
'foo' => [
|
||||
'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),
|
||||
''
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace Twig\Tests\TokenParser;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Twig\Environment;
|
||||
use Twig\Loader\ArrayLoader;
|
||||
use Twig\Parser;
|
||||
use Twig\Source;
|
||||
|
||||
class TypesTokenParserTest extends TestCase
|
||||
{
|
||||
/** @dataProvider getMappingTests */
|
||||
public function testMappingParsing(string $template, array $expected): void
|
||||
{
|
||||
$env = new Environment(new ArrayLoader(), ['cache' => 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]
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user