diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index f8f80ac2f0..aa3923ee34 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -1624,11 +1624,13 @@ public static function getAttribute(Environment $env, Source $source, $object, $ { // array if (Template::METHOD_CALL !== $type) { - $arrayItem = \is_bool($item) || \is_float($item) ? (int) $item : $item; + $arrayItem = \is_bool($item) || \is_float($item) ? (int) $item : $item = (string) $item; - if (((\is_array($object) || $object instanceof \ArrayObject) && (isset($object[$arrayItem]) || \array_key_exists($arrayItem, (array) $object))) - || ($object instanceof \ArrayAccess && isset($object[$arrayItem])) - ) { + if (match (true) { + \is_array($object) => \array_key_exists($arrayItem, $object), + $object instanceof \ArrayAccess => $object->offsetExists($arrayItem), + default => false, + }) { if ($isDefinedTest) { return true; } @@ -1697,7 +1699,23 @@ public static function getAttribute(Environment $env, Source $source, $object, $ // object property if (Template::METHOD_CALL !== $type) { - if (isset($object->$item) || \array_key_exists((string) $item, (array) $object)) { + static $propertyCheckers = []; + + if (isset($object->$item) + || ($propertyCheckers[$object::class][$item] ??= self::getPropertyChecker($object::class, $item))($object, $item) + ) { + if ($isDefinedTest) { + return true; + } + + if ($sandboxed) { + $env->getExtension(SandboxExtension::class)->checkPropertyAllowed($object, $item, $lineno, $source); + } + + return $object->$item; + } + + if ($object instanceof \DateTimeInterface && \in_array($item, ['date', 'timezone', 'timezone_type'], true)) { if ($isDefinedTest) { return true; } @@ -1706,7 +1724,7 @@ public static function getAttribute(Environment $env, Source $source, $object, $ $env->getExtension(SandboxExtension::class)->checkPropertyAllowed($object, $item, $lineno, $source); } - return isset($object->$item) ? $object->$item : ((array) $object)[(string) $item]; + return ((array) $object)[$item]; } if (\defined($object::class.'::'.$item)) { @@ -1720,6 +1738,8 @@ public static function getAttribute(Environment $env, Source $source, $object, $ return \constant($object::class.'::'.$item); } + } else { + $item = (string) $item; } static $cache = []; @@ -2055,4 +2075,27 @@ public static function parseAttributeFunction(Parser $parser, Node $fakeNode, $a return new GetAttrExpression($args[0], $args[1], $args[2] ?? null, Template::ANY_CALL, $line); } + + private static function getPropertyChecker(string $class, string $property): \Closure + { + static $classReflectors = []; + + $class = $classReflectors[$class] ??= new \ReflectionClass($class); + + if (!$class->hasProperty($property)) { + static $propertyExists; + + return $propertyExists ??= \Closure::fromCallable('property_exists'); + } + + $property = $class->getProperty($property); + + if (!$property->isPublic()) { + static $false; + + return $false ??= static fn () => false; + } + + return static fn ($object) => $property->isInitialized($object); + } }