mirror of
https://github.com/twigphp/Twig.git
synced 2026-08-17 08:50:50 +00:00
913 lines
30 KiB
PHP
913 lines
30 KiB
PHP
<?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\Tests;
|
|
|
|
/*
|
|
* 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.
|
|
*/
|
|
|
|
use PHPUnit\Framework\Attributes\DataProvider;
|
|
use PHPUnit\Framework\TestCase;
|
|
use Twig\Environment;
|
|
use Twig\Error\RuntimeError;
|
|
use Twig\Extension\CoreExtension;
|
|
use Twig\Extension\SandboxExtension;
|
|
use Twig\Loader\ArrayLoader;
|
|
use Twig\Sandbox\SecurityError;
|
|
use Twig\Sandbox\SecurityPolicy;
|
|
use Twig\Source;
|
|
use Twig\Template;
|
|
use Twig\TemplateWrapper;
|
|
|
|
class TemplateTest extends TestCase
|
|
{
|
|
public function testDisplayBlocksAcceptTemplateOnlyAsBlocks()
|
|
{
|
|
$twig = new Environment(new ArrayLoader());
|
|
$template = new TemplateForTest($twig);
|
|
|
|
$this->expectException(\LogicException::class);
|
|
$template->displayBlock('foo', [], ['foo' => [new \stdClass(), 'foo']]);
|
|
}
|
|
|
|
/**
|
|
* @dataProvider getAttributeExceptions
|
|
*/
|
|
#[DataProvider('getAttributeExceptions')]
|
|
public function testGetAttributeExceptions($template, $message)
|
|
{
|
|
$templates = ['index' => $template];
|
|
$env = new Environment(new ArrayLoader($templates), ['strict_variables' => true]);
|
|
$template = $env->load('index');
|
|
|
|
$context = [
|
|
'string' => 'foo',
|
|
'null' => null,
|
|
'empty_array' => [],
|
|
'array' => ['foo' => 'foo'],
|
|
'array_access' => new TemplateArrayAccessObject(),
|
|
'magic_exception' => new TemplateMagicPropertyObjectWithException(),
|
|
'object' => new \stdClass(),
|
|
];
|
|
|
|
try {
|
|
$template->render($context);
|
|
$this->fail('Accessing an invalid attribute should throw an exception.');
|
|
} catch (RuntimeError $e) {
|
|
$this->assertSame(\sprintf($message, 'index'), $e->getMessage());
|
|
}
|
|
}
|
|
|
|
public static function getAttributeExceptions()
|
|
{
|
|
return [
|
|
['{{ string["a"] }}', 'Impossible to access a key ("a") on a string variable ("foo") in "%s" at line 1.'],
|
|
['{{ null["a"] }}', 'Impossible to access a key ("a") on a null variable in "%s" at line 1.'],
|
|
['{{ 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_access["a"] }}', 'Key "a" does not exist in ArrayAccess-able object of class "Twig\Tests\TemplateArrayAccessObject" in "%s" at line 1.'],
|
|
['{{ string.a }}', 'Impossible to access an attribute ("a") on a string variable ("foo") in "%s" at line 1.'],
|
|
['{{ string.a() }}', 'Impossible to invoke a method ("a") on a string variable ("foo") in "%s" at line 1.'],
|
|
['{{ null.a }}', 'Impossible to access an attribute ("a") on a null variable in "%s" at line 1.'],
|
|
['{{ null.a() }}', 'Impossible to invoke a method ("a") on a null variable in "%s" at line 1.'],
|
|
['{{ array.a() }}', 'Impossible to invoke a method ("a") on a sequence/mapping in "%s" at line 1.'],
|
|
['{{ 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.'],
|
|
['{{ 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.'],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @dataProvider getGetAttributeWithSandbox
|
|
*/
|
|
#[DataProvider('getGetAttributeWithSandbox')]
|
|
public function testGetAttributeWithSandbox($object, $item, $allowed)
|
|
{
|
|
$twig = new Environment(new ArrayLoader());
|
|
$policy = new SecurityPolicy([], [], [/* method */], [/* prop */], []);
|
|
$twig->addExtension(new SandboxExtension($policy, !$allowed));
|
|
$template = new TemplateForTest($twig);
|
|
|
|
try {
|
|
CoreExtension::getAttribute($twig, $template->getSourceContext(), $object, $item, [], 'any', false, false, true);
|
|
|
|
if (!$allowed) {
|
|
$this->fail();
|
|
} else {
|
|
$this->addToAssertionCount(1);
|
|
}
|
|
} catch (SecurityError $e) {
|
|
if ($allowed) {
|
|
$this->fail();
|
|
} else {
|
|
$this->addToAssertionCount(1);
|
|
}
|
|
|
|
$this->assertStringContainsString('is not allowed', $e->getMessage());
|
|
}
|
|
}
|
|
|
|
public static function getGetAttributeWithSandbox()
|
|
{
|
|
return [
|
|
[new TemplatePropertyObject(), 'defined', false],
|
|
[new TemplatePropertyObject(), 'defined', true],
|
|
[new TemplateMethodObject(), 'defined', false],
|
|
[new TemplateMethodObject(), 'defined', true],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @dataProvider getRenderTemplateWithoutOutputData
|
|
*/
|
|
#[DataProvider('getRenderTemplateWithoutOutputData')]
|
|
public function testRenderTemplateWithoutOutput(string $template)
|
|
{
|
|
$twig = new Environment(new ArrayLoader(['index' => $template]));
|
|
$this->assertSame('', $twig->render('index'));
|
|
}
|
|
|
|
public static function getRenderTemplateWithoutOutputData()
|
|
{
|
|
return [
|
|
[''],
|
|
['{% for var in [] %}{% endfor %}'],
|
|
['{% if false %}{% endif %}'],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @dataProvider getNullCoalesceWithImportedMacroData
|
|
*/
|
|
#[DataProvider('getNullCoalesceWithImportedMacroData')]
|
|
public function testNullCoalesceWithImportedMacro(array $templates, string $expected)
|
|
{
|
|
$twig = new Environment(new ArrayLoader($templates));
|
|
|
|
$this->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());
|
|
$template = new TemplateForTest($twig, 'index.twig');
|
|
|
|
$this->expectException(RuntimeError::class);
|
|
$this->expectExceptionMessage('Block "unknown" on template "index.twig" does not exist in "index.twig".');
|
|
|
|
$template->renderBlock('unknown', []);
|
|
}
|
|
|
|
public function testDisplayBlockWithUndefinedBlock()
|
|
{
|
|
$twig = new Environment(new ArrayLoader());
|
|
$template = new TemplateForTest($twig, 'index.twig');
|
|
|
|
$this->expectException(RuntimeError::class);
|
|
$this->expectExceptionMessage('Block "unknown" on template "index.twig" does not exist in "index.twig".');
|
|
|
|
$template->displayBlock('unknown', []);
|
|
}
|
|
|
|
public function testDisplayBlockWithUndefinedParentBlock()
|
|
{
|
|
$twig = new Environment(new ArrayLoader());
|
|
$template = new TemplateForTest($twig, 'parent.twig');
|
|
|
|
$this->expectException(RuntimeError::class);
|
|
$this->expectExceptionMessage('Block "foo" should not call parent() in "index.twig" as the block does not exist in the parent template "parent.twig"');
|
|
|
|
$template->displayBlock('foo', [], ['foo' => [new TemplateForTest($twig, 'index.twig'), 'block_foo']], false);
|
|
}
|
|
|
|
public function testGetAttributeOnArrayWithConfusableKey()
|
|
{
|
|
$twig = new Environment(new ArrayLoader());
|
|
$template = new TemplateForTest($twig);
|
|
|
|
$array = ['Zero', 'One', -1 => 'MinusOne', '' => 'EmptyString', '1.5' => 'FloatButString', '01' => 'IntegerButStringWithLeadingZeros'];
|
|
|
|
$this->assertSame('Zero', $array[false]);
|
|
$this->assertSame('One', $array[true]);
|
|
if (\PHP_VERSION_ID < 80100) {
|
|
// This line will trigger a deprecation warning on PHP 8.1.
|
|
$this->assertSame('One', $array[1.5]);
|
|
}
|
|
$this->assertSame('One', $array['1']);
|
|
if (\PHP_VERSION_ID < 80100) {
|
|
// This line will trigger a deprecation warning on PHP 8.1.
|
|
$this->assertSame('MinusOne', $array[-1.5]);
|
|
}
|
|
$this->assertSame('FloatButString', $array['1.5']);
|
|
$this->assertSame('IntegerButStringWithLeadingZeros', $array['01']);
|
|
$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)');
|
|
$this->assertSame('One', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, 1.5), 'float is casted to int when accessing a sequence/mapping (equals PHP behavior)');
|
|
$this->assertSame('One', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, '1'), '"1" is treated as integer 1 when accessing a sequence/mapping (equals PHP behavior)');
|
|
$this->assertSame('MinusOne', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, -1.5), 'negative float is casted to int when accessing a sequence/mapping (equals PHP behavior)');
|
|
$this->assertSame('FloatButString', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, '1.5'), '"1.5" is treated as-is when accessing a sequence/mapping (equals PHP behavior)');
|
|
$this->assertSame('IntegerButStringWithLeadingZeros', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, '01'), '"01" is treated as-is when accessing a sequence/mapping (equals PHP behavior)');
|
|
$this->assertSame('EmptyString', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, null), 'null is treated as "" when accessing a sequence/mapping (equals PHP behavior)');
|
|
}
|
|
|
|
/**
|
|
* @dataProvider getStrictVariablesModes
|
|
*/
|
|
#[DataProvider('getStrictVariablesModes')]
|
|
public function testArrayAccessWithStringableKeyIsConsistentAcrossStrictModes(bool $strict)
|
|
{
|
|
$twig = new Environment(new ArrayLoader(['index' => '{{ array[object] }}']), [
|
|
'strict_variables' => $strict,
|
|
'autoescape' => false,
|
|
]);
|
|
|
|
$object = new class implements \Stringable {
|
|
public function __toString(): string
|
|
{
|
|
return 'string';
|
|
}
|
|
};
|
|
|
|
$this->assertSame('value', $twig->render('index', ['array' => ['string' => 'value'], 'object' => $object]));
|
|
}
|
|
|
|
public static function getStrictVariablesModes(): iterable
|
|
{
|
|
yield 'lax' => [false];
|
|
yield 'strict' => [true];
|
|
}
|
|
|
|
public function testArrayAccessWithStringableKeyIsCheckedBySandbox()
|
|
{
|
|
$object = new class implements \Stringable {
|
|
public function __toString(): string
|
|
{
|
|
return 'string';
|
|
}
|
|
};
|
|
$data = ['array' => ['string' => 'value'], 'object' => $object];
|
|
|
|
$twig = new Environment(new ArrayLoader(['index' => '{{ array[object] }}']), ['autoescape' => false]);
|
|
$twig->addExtension(new SandboxExtension(new SecurityPolicy([], [], [], [], []), true));
|
|
|
|
try {
|
|
$twig->render('index', $data);
|
|
$this->fail('The sandbox must reject the __toString() coercion of the array key.');
|
|
} catch (SecurityError $e) {
|
|
$this->assertStringContainsStringIgnoringCase('__toString', $e->getMessage());
|
|
}
|
|
|
|
$twig = new Environment(new ArrayLoader(['index' => '{{ array[object] }}']), ['autoescape' => false]);
|
|
$twig->addExtension(new SandboxExtension(new SecurityPolicy([], [], [$object::class => ['__toString']], [], []), true));
|
|
|
|
$this->assertSame('value', $twig->render('index', $data));
|
|
}
|
|
|
|
/**
|
|
* @dataProvider getGetAttributeTests
|
|
*/
|
|
#[DataProvider('getGetAttributeTests')]
|
|
public function testGetAttribute($defined, $value, $object, $item, $arguments, $type)
|
|
{
|
|
$twig = new Environment(new ArrayLoader());
|
|
$template = new TemplateForTest($twig);
|
|
|
|
$this->assertEquals($value, CoreExtension::getAttribute($twig, $template->getSourceContext(), $object, $item, $arguments, $type));
|
|
}
|
|
|
|
/**
|
|
* @dataProvider getGetAttributeTests
|
|
*/
|
|
#[DataProvider('getGetAttributeTests')]
|
|
public function testGetAttributeStrict($defined, $value, $object, $item, $arguments, $type, $exceptionMessage = null)
|
|
{
|
|
$twig = new Environment(new ArrayLoader(), ['strict_variables' => true]);
|
|
$template = new TemplateForTest($twig);
|
|
|
|
if ($defined) {
|
|
$this->assertEquals($value, CoreExtension::getAttribute($twig, $template->getSourceContext(), $object, $item, $arguments, $type));
|
|
} else {
|
|
$this->expectException(RuntimeError::class);
|
|
if (null !== $exceptionMessage) {
|
|
$this->expectExceptionMessage($exceptionMessage);
|
|
}
|
|
$this->assertEquals($value, CoreExtension::getAttribute($twig, $template->getSourceContext(), $object, $item, $arguments, $type));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @dataProvider getGetAttributeTests
|
|
*/
|
|
#[DataProvider('getGetAttributeTests')]
|
|
public function testGetAttributeDefined($defined, $value, $object, $item, $arguments, $type)
|
|
{
|
|
$twig = new Environment(new ArrayLoader());
|
|
$template = new TemplateForTest($twig);
|
|
|
|
$this->assertEquals($defined, CoreExtension::getAttribute($twig, $template->getSourceContext(), $object, $item, $arguments, $type, true));
|
|
}
|
|
|
|
/**
|
|
* @dataProvider getGetAttributeTests
|
|
*/
|
|
#[DataProvider('getGetAttributeTests')]
|
|
public function testGetAttributeDefinedStrict($defined, $value, $object, $item, $arguments, $type)
|
|
{
|
|
$twig = new Environment(new ArrayLoader(), ['strict_variables' => true]);
|
|
$template = new TemplateForTest($twig);
|
|
|
|
$this->assertEquals($defined, CoreExtension::getAttribute($twig, $template->getSourceContext(), $object, $item, $arguments, $type, true));
|
|
}
|
|
|
|
public function testGetAttributeCallExceptions()
|
|
{
|
|
$twig = new Environment(new ArrayLoader());
|
|
$template = new TemplateForTest($twig);
|
|
|
|
$object = new TemplateMagicMethodExceptionObject();
|
|
|
|
$this->assertNull(CoreExtension::getAttribute($twig, $template->getSourceContext(), $object, 'foo'));
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideNonStringPrintValues
|
|
*/
|
|
public function testPrintingANonStringReportsTheErrorAtThePrintLocation($value, string $expectedMessage)
|
|
{
|
|
$twig = new Environment(new ArrayLoader(['index' => "foo\n{{ value }}\nbar"]));
|
|
|
|
set_error_handler(static function (int $type, string $msg, string $file, int $line): bool {
|
|
throw new \ErrorException($msg, 0, $type, $file, $line);
|
|
}, \E_WARNING);
|
|
|
|
try {
|
|
$twig->render('index', ['value' => $value]);
|
|
$this->fail('Printing a non-string value should fail.');
|
|
} catch (RuntimeError $e) {
|
|
$this->assertSame('index', $e->getSourceContext()->getName());
|
|
$this->assertSame(2, $e->getTemplateLine());
|
|
$this->assertSame($expectedMessage, $e->getPrevious()->getMessage());
|
|
} finally {
|
|
restore_error_handler();
|
|
}
|
|
}
|
|
|
|
public static function provideNonStringPrintValues(): iterable
|
|
{
|
|
yield 'array' => [['a', 'b'], 'Array to string conversion'];
|
|
yield 'non-Stringable object' => [new \stdClass(), 'Object of class stdClass could not be converted to string'];
|
|
}
|
|
|
|
public static function getGetAttributeTests()
|
|
{
|
|
$array = [
|
|
'defined' => 'defined',
|
|
'zero' => 0,
|
|
'null' => null,
|
|
'1' => 1,
|
|
'bar' => true,
|
|
'foo' => true,
|
|
'baz' => 'baz',
|
|
'baf' => 'baf',
|
|
'09' => '09',
|
|
'+4' => '+4',
|
|
];
|
|
|
|
$objectArray = new TemplateArrayAccessObject();
|
|
$arrayObject = new \ArrayObject($array);
|
|
$stdObject = (object) $array;
|
|
$magicPropertyObject = new TemplateMagicPropertyObject();
|
|
$propertyObject = new TemplatePropertyObject();
|
|
$propertyObject1 = new TemplatePropertyObjectAndIterator();
|
|
$propertyObject2 = new TemplatePropertyObjectAndArrayAccess();
|
|
$propertyObject3 = new TemplatePropertyObjectDefinedWithUndefinedValue();
|
|
$methodObject = new TemplateMethodObject();
|
|
$magicMethodObject = new TemplateMagicMethodObject();
|
|
|
|
$anyType = Template::ANY_CALL;
|
|
$methodType = Template::METHOD_CALL;
|
|
$arrayType = Template::ARRAY_CALL;
|
|
|
|
$basicTests = [
|
|
// array(defined, value, property to fetch)
|
|
[true, 'defined', 'defined'],
|
|
[false, null, 'undefined'],
|
|
[false, null, 'protected'],
|
|
[true, 0, 'zero'],
|
|
[true, 1, 1],
|
|
[true, 1, 1.0],
|
|
[true, null, 'null'],
|
|
[true, true, 'bar'],
|
|
[true, true, 'foo'],
|
|
[true, 'baz', 'baz'],
|
|
[true, 'baf', 'baf'],
|
|
[true, '09', '09'],
|
|
[true, '+4', '+4'],
|
|
];
|
|
$testObjects = [
|
|
// array(object, type of fetch)
|
|
[$array, $arrayType],
|
|
[$objectArray, $arrayType],
|
|
[$arrayObject, $anyType],
|
|
[$stdObject, $anyType],
|
|
[$magicPropertyObject, $anyType],
|
|
[$methodObject, $methodType],
|
|
[$methodObject, $anyType],
|
|
[$propertyObject, $anyType],
|
|
[$propertyObject1, $anyType],
|
|
[$propertyObject2, $anyType],
|
|
];
|
|
|
|
$tests = [];
|
|
foreach ($testObjects as $testObject) {
|
|
foreach ($basicTests as $test) {
|
|
// properties cannot be numbers
|
|
if (($testObject[0] instanceof \stdClass || $testObject[0] instanceof TemplatePropertyObject) && is_numeric($test[2])) {
|
|
continue;
|
|
}
|
|
|
|
if ('+4' === $test[2] && $methodObject === $testObject[0]) {
|
|
continue;
|
|
}
|
|
|
|
$tests[] = [$test[0], $test[1], $testObject[0], $test[2], [], $testObject[1]];
|
|
}
|
|
}
|
|
|
|
// additional properties tests
|
|
$tests = array_merge($tests, [
|
|
[true, null, $propertyObject3, 'foo', [], $anyType],
|
|
]);
|
|
|
|
// additional method tests
|
|
$tests = array_merge($tests, [
|
|
[true, 'defined', $methodObject, 'defined', [], $methodType],
|
|
[true, 'defined', $methodObject, 'DEFINED', [], $methodType],
|
|
[true, 'defined', $methodObject, 'getDefined', [], $methodType],
|
|
[true, 'defined', $methodObject, 'GETDEFINED', [], $methodType],
|
|
[true, 'static', $methodObject, 'static', [], $methodType],
|
|
[true, 'static', $methodObject, 'getStatic', [], $methodType],
|
|
|
|
[true, '__call_undefined', $magicMethodObject, 'undefined', [], $methodType],
|
|
[true, '__call_UNDEFINED', $magicMethodObject, 'UNDEFINED', [], $methodType],
|
|
]);
|
|
|
|
// add the same tests for the any type
|
|
foreach ($tests as $test) {
|
|
if ($anyType !== $test[5]) {
|
|
$test[5] = $anyType;
|
|
$tests[] = $test;
|
|
}
|
|
}
|
|
|
|
$methodAndPropObject = new TemplateMethodAndPropObject();
|
|
|
|
// additional method tests
|
|
$tests = array_merge($tests, [
|
|
[true, 'a', $methodAndPropObject, 'a', [], $anyType],
|
|
[true, 'a', $methodAndPropObject, 'a', [], $methodType],
|
|
[false, null, $methodAndPropObject, 'a', [], $arrayType],
|
|
|
|
[true, 'b_prop', $methodAndPropObject, 'b', [], $anyType],
|
|
[true, 'b', $methodAndPropObject, 'B', [], $anyType],
|
|
[true, 'b', $methodAndPropObject, 'b', [], $methodType],
|
|
[true, 'b', $methodAndPropObject, 'B', [], $methodType],
|
|
[false, null, $methodAndPropObject, 'b', [], $arrayType],
|
|
|
|
[false, null, $methodAndPropObject, 'c', [], $anyType],
|
|
[false, null, $methodAndPropObject, 'c', [], $methodType],
|
|
[false, null, $methodAndPropObject, 'c', [], $arrayType],
|
|
]);
|
|
|
|
$arrayAccess = new TemplateArrayAccess();
|
|
$tests = array_merge($tests, [
|
|
[true, ['foo' => 'bar'], $arrayAccess, 'vars', [], $anyType],
|
|
]);
|
|
|
|
// test for Closure::__invoke()
|
|
$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, [
|
|
[false, null, 42, 'a', [], $anyType, 'Impossible to access an attribute ("a") on a int variable ("42") in "index.twig".'],
|
|
[false, null, 'string', 'a', [], $anyType, 'Impossible to access an attribute ("a") on a string variable ("string") in "index.twig".'],
|
|
[false, null, [], 'a', [], $anyType, 'Key "a" does not exist as the sequence/mapping is empty in "index.twig".'],
|
|
]);
|
|
|
|
return $tests;
|
|
}
|
|
|
|
public function testGetIsMethods()
|
|
{
|
|
$twig = new Environment(new ArrayLoader());
|
|
|
|
$getIsObject = new TemplateGetIsMethods();
|
|
$template = new TemplateForTest($twig, 'index.twig');
|
|
// first time should not create a cache for "get"
|
|
$this->assertNull(CoreExtension::getAttribute($twig, $template->getSourceContext(), $getIsObject, 'get'));
|
|
// 0 should be in the method cache now, so this should fail
|
|
$this->assertNull(CoreExtension::getAttribute($twig, $template->getSourceContext(), $getIsObject, 0));
|
|
}
|
|
}
|
|
|
|
class TemplateForTest extends Template
|
|
{
|
|
private $name;
|
|
|
|
public function __construct(Environment $env, $name = 'index.twig')
|
|
{
|
|
parent::__construct($env);
|
|
$this->name = $name;
|
|
}
|
|
|
|
public function getZero()
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
public function getEmpty()
|
|
{
|
|
return '';
|
|
}
|
|
|
|
public function getString()
|
|
{
|
|
return 'some_string';
|
|
}
|
|
|
|
public function getTrue()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
public function getTemplateName(): string
|
|
{
|
|
return $this->name;
|
|
}
|
|
|
|
public function getDebugInfo(): array
|
|
{
|
|
return [];
|
|
}
|
|
|
|
public function getSourceContext(): Source
|
|
{
|
|
return new Source('', $this->getTemplateName());
|
|
}
|
|
|
|
protected function doGetParent(array $context): bool|string|Template|TemplateWrapper
|
|
{
|
|
return false;
|
|
}
|
|
|
|
protected function doDisplay(array $context, array $blocks = []): iterable
|
|
{
|
|
}
|
|
|
|
public function block_name($context, array $blocks = [])
|
|
{
|
|
}
|
|
}
|
|
|
|
class TemplateArrayAccessObject implements \ArrayAccess
|
|
{
|
|
protected $protected = 'protected';
|
|
|
|
public $attributes = [
|
|
'defined' => 'defined',
|
|
'zero' => 0,
|
|
'null' => null,
|
|
'1' => 1,
|
|
'bar' => true,
|
|
'foo' => true,
|
|
'baz' => 'baz',
|
|
'baf' => 'baf',
|
|
'09' => '09',
|
|
'+4' => '+4',
|
|
];
|
|
|
|
public function offsetExists($name): bool
|
|
{
|
|
return \array_key_exists($name, $this->attributes);
|
|
}
|
|
|
|
#[\ReturnTypeWillChange]
|
|
public function offsetGet($name)
|
|
{
|
|
return \array_key_exists($name, $this->attributes) ? $this->attributes[$name] : null;
|
|
}
|
|
|
|
public function offsetSet($name, $value): void
|
|
{
|
|
}
|
|
|
|
public function offsetUnset($name): void
|
|
{
|
|
}
|
|
}
|
|
|
|
class TemplateMagicPropertyObject
|
|
{
|
|
public $defined = 'defined';
|
|
|
|
public $attributes = [
|
|
'zero' => 0,
|
|
'null' => null,
|
|
'1' => 1,
|
|
'bar' => true,
|
|
'foo' => true,
|
|
'baz' => 'baz',
|
|
'baf' => 'baf',
|
|
'09' => '09',
|
|
'+4' => '+4',
|
|
];
|
|
|
|
protected $protected = 'protected';
|
|
|
|
public function __isset($name): bool
|
|
{
|
|
return \array_key_exists($name, $this->attributes);
|
|
}
|
|
|
|
public function __get($name)
|
|
{
|
|
return \array_key_exists($name, $this->attributes) ? $this->attributes[$name] : null;
|
|
}
|
|
}
|
|
|
|
class TemplateMagicPropertyObjectWithException
|
|
{
|
|
public function __isset($key): bool
|
|
{
|
|
throw new \Exception('Hey! Don\'t try to isset me!');
|
|
}
|
|
}
|
|
|
|
class TemplatePropertyObject
|
|
{
|
|
public $defined = 'defined';
|
|
public $zero = 0;
|
|
public $null;
|
|
public $bar = true;
|
|
public $foo = true;
|
|
public $baz = 'baz';
|
|
public $baf = 'baf';
|
|
|
|
protected $protected = 'protected';
|
|
}
|
|
|
|
class TemplatePropertyObjectAndIterator extends TemplatePropertyObject implements \IteratorAggregate
|
|
{
|
|
public function getIterator(): \Traversable
|
|
{
|
|
return new \ArrayIterator(['foo', 'bar']);
|
|
}
|
|
}
|
|
|
|
class TemplatePropertyObjectAndArrayAccess extends TemplatePropertyObject implements \ArrayAccess
|
|
{
|
|
private $data = [
|
|
'defined' => 'defined',
|
|
'zero' => 0,
|
|
'null' => null,
|
|
'bar' => true,
|
|
'foo' => true,
|
|
'baz' => 'baz',
|
|
'baf' => 'baf',
|
|
];
|
|
|
|
public function offsetExists($offset): bool
|
|
{
|
|
return \array_key_exists($offset, $this->data);
|
|
}
|
|
|
|
#[\ReturnTypeWillChange]
|
|
public function offsetGet($offset)
|
|
{
|
|
return $this->offsetExists($offset) ? $this->data[$offset] : 'n/a';
|
|
}
|
|
|
|
public function offsetSet($offset, $value): void
|
|
{
|
|
}
|
|
|
|
public function offsetUnset($offset): void
|
|
{
|
|
}
|
|
}
|
|
|
|
class TemplatePropertyObjectDefinedWithUndefinedValue
|
|
{
|
|
public $foo;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->foo = @$notExist;
|
|
}
|
|
}
|
|
|
|
class TemplateMethodObject
|
|
{
|
|
public function getDefined()
|
|
{
|
|
return 'defined';
|
|
}
|
|
|
|
public function get1()
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
public function get09()
|
|
{
|
|
return '09';
|
|
}
|
|
|
|
public function getZero()
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
public function getNull()
|
|
{
|
|
}
|
|
|
|
public function isBar()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
public function hasFoo()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
public function hasBaz()
|
|
{
|
|
return 'should never be returned (has)';
|
|
}
|
|
|
|
public function isBaz()
|
|
{
|
|
return 'should never be returned (is)';
|
|
}
|
|
|
|
public function getBaz()
|
|
{
|
|
return 'Baz';
|
|
}
|
|
|
|
public function baz()
|
|
{
|
|
return 'baz';
|
|
}
|
|
|
|
public function hasBaf()
|
|
{
|
|
return 'should never be returned (has)';
|
|
}
|
|
|
|
public function isBaf()
|
|
{
|
|
return 'baf';
|
|
}
|
|
|
|
protected function getProtected()
|
|
{
|
|
return 'protected';
|
|
}
|
|
|
|
public static function getStatic()
|
|
{
|
|
return 'static';
|
|
}
|
|
}
|
|
|
|
class TemplateGetIsMethods
|
|
{
|
|
public function get()
|
|
{
|
|
}
|
|
|
|
public function is()
|
|
{
|
|
}
|
|
}
|
|
|
|
class TemplateMethodAndPropObject
|
|
{
|
|
private $a = 'a_prop';
|
|
|
|
public function getA()
|
|
{
|
|
return 'a';
|
|
}
|
|
|
|
public $b = 'b_prop';
|
|
|
|
public function getB()
|
|
{
|
|
return 'b';
|
|
}
|
|
|
|
private $c = 'c_prop';
|
|
|
|
private function getC()
|
|
{
|
|
return 'c';
|
|
}
|
|
}
|
|
|
|
class TemplateArrayAccess implements \ArrayAccess
|
|
{
|
|
public $vars = [
|
|
'foo' => 'bar',
|
|
];
|
|
private $children = [];
|
|
|
|
public function offsetExists($offset): bool
|
|
{
|
|
return \array_key_exists($offset, $this->children);
|
|
}
|
|
|
|
#[\ReturnTypeWillChange]
|
|
public function offsetGet($offset)
|
|
{
|
|
return $this->children[$offset];
|
|
}
|
|
|
|
public function offsetSet($offset, $value): void
|
|
{
|
|
$this->children[$offset] = $value;
|
|
}
|
|
|
|
public function offsetUnset($offset): void
|
|
{
|
|
unset($this->children[$offset]);
|
|
}
|
|
}
|
|
|
|
class TemplateMagicMethodObject
|
|
{
|
|
public function __call($method, $arguments)
|
|
{
|
|
return '__call_'.$method;
|
|
}
|
|
}
|
|
|
|
class TemplateMagicMethodExceptionObject
|
|
{
|
|
public function __call($method, $arguments)
|
|
{
|
|
throw new \BadMethodCallException(\sprintf('Unknown method "%s".', $method));
|
|
}
|
|
}
|