expectException(\LogicException::class); $template->renderBlock('foo', [], ['foo' => [new \stdClass(), 'foo']]); } #[DataProvider('getAttributeExceptions')] public function testGetAttributeExceptions($template, $message): void { $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')] public function testGetAttributeWithSandbox($object, $item, $allowed): void { $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')] public function testRenderTemplateWithoutOutput(string $template): void { $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')] public function testNullCoalesceWithImportedMacro(array $templates, string $expected): void { $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(): void { $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 testRenderBlockWithUndefinedParentBlock(): void { $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->renderBlock('foo', [], ['foo' => [new TemplateForTest($twig, 'index.twig'), 'block_foo']], false); } public function testGetAttributeOnArrayWithConfusableKey(): void { $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]); $this->assertSame('One', $array['1']); $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')] public function testArrayWithStringableKeyIsConsistentAcrossStrictModes(bool $strict): void { $twig = new Environment(new ArrayLoader(['index' => '{{ array[object] }}']), [ 'strict_variables' => $strict, 'autoescape' => false, ]); $key = new TemplateStringableKey(); $this->assertSame('value', $twig->render('index', ['array' => ['string' => 'value'], 'object' => $key])); } /** * @dataProvider getStringableKeyArrayAccessContainers */ #[DataProvider('getStringableKeyArrayAccessContainers')] public function testStringableKeyIsCoercedForInternalArrayAccess(bool $strict, bool $sandboxed, \ArrayAccess $data): void { $twig = new Environment(new ArrayLoader(['index' => '{{ data[key] }}']), [ 'strict_variables' => $strict, 'autoescape' => false, ]); $key = new TemplateStringableKey(); if ($sandboxed) { $twig->addExtension(new SandboxExtension(new SecurityPolicy([], [], [$key::class => ['__toString']], [], []), true)); } $this->assertSame('value', $twig->render('index', ['data' => $data, 'key' => $key])); $this->assertSame(1, $key->toStringCalls); } public static function getStringableKeyArrayAccessContainers(): iterable { foreach (['lax' => false, 'strict' => true] as $mode => $strict) { foreach (['unsandboxed' => false, 'sandboxed' => true] as $sandboxMode => $sandboxed) { yield $mode.' '.$sandboxMode.' ArrayObject' => [$strict, $sandboxed, new \ArrayObject(['string' => 'value'])]; yield $mode.' '.$sandboxMode.' ArrayIterator' => [$strict, $sandboxed, new \ArrayIterator(['string' => 'value'])]; yield $mode.' '.$sandboxMode.' RecursiveArrayIterator' => [$strict, $sandboxed, new \RecursiveArrayIterator(['string' => 'value'])]; } } } /** * @dataProvider getStrictVariablesModes */ #[DataProvider('getStrictVariablesModes')] public function testArrayAccessWithObjectKeyKeepsTheObjectKey(bool $strict): void { $twig = new Environment(new ArrayLoader(['index' => '{{ data[key] }}']), [ 'strict_variables' => $strict, 'autoescape' => false, ]); $key = new TemplateStringableKey(); $data = new \SplObjectStorage(); $data[$key] = 'value'; $this->assertSame('value', $twig->render('index', ['data' => $data, 'key' => $key])); $this->assertSame(0, $key->toStringCalls); } /** * @dataProvider getStrictVariablesModes */ #[DataProvider('getStrictVariablesModes')] public function testArrayAccessLookupDoesNotRepeatOffsetChecks(bool $strict): void { $twig = new Environment(new ArrayLoader(['index' => '{{ data[key] }}']), [ 'strict_variables' => $strict, 'autoescape' => false, ]); $key = new TemplateStringableKey(); $data = new TemplateTrackingArrayAccess($key, false); $this->assertSame('value', $twig->render('index', ['data' => $data, 'key' => $key])); $this->assertSame([$key], $data->offsetExistsCalls); $this->assertSame([$key], $data->offsetGetCalls); $this->assertSame(0, $key->toStringCalls); } public function testArrayAccessDefinedTestDoesNotReadTheOffset(): void { $twig = new Environment(new ArrayLoader(['index' => '{{ data[key] is defined ? "yes" : "no" }}'])); $key = new TemplateStringableKey(); $data = new TemplateTrackingArrayAccess($key, false); $this->assertSame('yes', $twig->render('index', ['data' => $data, 'key' => $key])); $this->assertSame([$key], $data->offsetExistsCalls); $this->assertSame([], $data->offsetGetCalls); } /** * @dataProvider getStrictVariablesModes */ #[DataProvider('getStrictVariablesModes')] public function testRejectedStringableArrayAccessKeyRethrowsOriginalTypeError(bool $strict): void { $twig = new Environment(new ArrayLoader(['index' => '{{ data[key] }}']), [ 'strict_variables' => $strict, 'autoescape' => false, ]); $key = new TemplateStringableKey(); $data = new TemplateTrackingArrayAccess($key, true); try { $twig->render('index', ['data' => $data, 'key' => $key]); $this->fail('The original TypeError must be rethrown.'); } catch (RuntimeError $e) { $this->assertSame($data->offsetExistsError, $e->getPrevious()); } $this->assertSame([$key], $data->offsetExistsCalls); $this->assertSame([], $data->offsetGetCalls); $this->assertSame(0, $key->toStringCalls); } /** * @dataProvider getStrictVariablesModes */ #[DataProvider('getStrictVariablesModes')] public function testSandboxDoesNotAuthorizeStringPropertyForArrayAccessObjectKey(bool $strict): void { $twig = new Environment(new ArrayLoader(['index' => '{{ data[key] }}']), [ 'strict_variables' => $strict, 'autoescape' => false, ]); $key = new TemplateStringableKey(); $data = new TemplateTrackingArrayAccess($key, false); $twig->addExtension(new SandboxExtension(new SecurityPolicy([], [], [$key::class => ['__toString']], [$data::class => ['string']], []), true)); $this->expectException(SecurityNotAllowedPropertyError::class); try { $twig->render('index', ['data' => $data, 'key' => $key]); } finally { $this->assertSame([], $data->offsetExistsCalls); $this->assertSame([], $data->offsetGetCalls); } } /** * @dataProvider getStrictVariablesModes */ #[DataProvider('getStrictVariablesModes')] public function testArrayWithStringableKeyIsCheckedBySandbox(bool $strict): void { $key = new TemplateStringableKey(); $context = ['array' => ['string' => 'value'], 'key' => $key]; $twig = new Environment(new ArrayLoader(['index' => '{{ array[key] }}']), ['strict_variables' => $strict, 'autoescape' => false]); $twig->addExtension(new SandboxExtension(new SecurityPolicy([], [], [], [], []), true)); try { $twig->render('index', $context); $this->fail('The sandbox must reject the __toString() coercion of the array key.'); } catch (SecurityError $e) { $this->assertStringContainsStringIgnoringCase('__toString', $e->getMessage()); } $this->assertSame(0, $key->toStringCalls); $twig = new Environment(new ArrayLoader(['index' => '{{ array[key] }}']), ['strict_variables' => $strict, 'autoescape' => false]); $twig->addExtension(new SandboxExtension(new SecurityPolicy([], [], [$key::class => ['__toString']], [], []), true)); $this->assertSame('value', $twig->render('index', $context)); $this->assertSame(1, $key->toStringCalls); } /** * @dataProvider getStrictVariablesModes */ #[DataProvider('getStrictVariablesModes')] public function testInternalArrayAccessWithStringableKeyIsCheckedBySandbox(bool $strict): void { $key = new TemplateStringableKey(); $twig = new Environment(new ArrayLoader(['index' => '{{ data[key] }}']), ['strict_variables' => $strict, 'autoescape' => false]); $twig->addExtension(new SandboxExtension(new SecurityPolicy([], [], [], [], []), true)); try { $twig->render('index', ['data' => new \ArrayObject(['string' => 'value']), 'key' => $key]); $this->fail('The sandbox must reject the __toString() coercion of the ArrayAccess key.'); } catch (SecurityError $e) { $this->assertStringContainsStringIgnoringCase('__toString', $e->getMessage()); } $this->assertSame(0, $key->toStringCalls); } public function testSandboxedArrayAccessWithObjectKeyKeepsTheObjectKey(): void { $key = new TemplateStringableKey(); $data = new \SplObjectStorage(); $data[$key] = 'value'; $twig = new Environment(new ArrayLoader(['index' => '{{ data[key] }}']), ['autoescape' => false]); $twig->addExtension(new SandboxExtension(new SecurityPolicy([], [], [$key::class => ['__toString']], [], []), true)); $this->assertSame('value', $twig->render('index', ['data' => $data, 'key' => $key])); $this->assertSame(0, $key->toStringCalls); } public static function getStrictVariablesModes(): iterable { yield 'lax' => [false]; yield 'strict' => [true]; } #[DataProvider('getGetAttributeTests')] public function testGetAttribute($defined, $value, $object, $item, $arguments, $type): void { $twig = new Environment(new ArrayLoader()); $template = new TemplateForTest($twig); $this->assertEquals($value, CoreExtension::getAttribute($twig, $template->getSourceContext(), $object, $item, $arguments, $type)); } #[DataProvider('getGetAttributeTests')] public function testGetAttributeStrict($defined, $value, $object, $item, $arguments, $type, $exceptionMessage = null): void { $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')] public function testGetAttributeDefined($defined, $value, $object, $item, $arguments, $type): void { $twig = new Environment(new ArrayLoader()); $template = new TemplateForTest($twig); $this->assertEquals($defined, CoreExtension::getAttribute($twig, $template->getSourceContext(), $object, $item, $arguments, $type, true)); } #[DataProvider('getGetAttributeTests')] public function testGetAttributeDefinedStrict($defined, $value, $object, $item, $arguments, $type): void { $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(): void { $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): void { $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(): void { $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 = []): void { } } final class TemplateStringableKey implements \Stringable { public int $toStringCalls = 0; public function __toString(): string { ++$this->toStringCalls; return 'string'; } } final class TemplateTrackingArrayAccess implements \ArrayAccess { public array $offsetExistsCalls = []; public ?\TypeError $offsetExistsError = null; public array $offsetGetCalls = []; public function __construct( private object $key, private bool $rejectObjectOffset, ) { } public function offsetExists(mixed $offset): bool { $this->offsetExistsCalls[] = $offset; if ($this->rejectObjectOffset && \is_object($offset)) { throw $this->offsetExistsError = new \TypeError('Object offsets are not supported.'); } return $this->key === $offset; } public function offsetGet(mixed $offset): mixed { $this->offsetGetCalls[] = $offset; return 'value'; } public function offsetSet(mixed $offset, mixed $value): void { } public function offsetUnset(mixed $offset): void { } } 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); } public function offsetGet($name): mixed { 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); } public function offsetGet($offset): mixed { 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(): void { } 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(): void { } public function is(): void { } } 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); } public function offsetGet($offset): mixed { 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): void { throw new \BadMethodCallException(\sprintf('Unknown method "%s".', $method)); } }