diff --git a/src/Node/Expression/CallExpression.php b/src/Node/Expression/CallExpression.php index 549e8c43a..19b7d1bad 100644 --- a/src/Node/Expression/CallExpression.php +++ b/src/Node/Expression/CallExpression.php @@ -15,10 +15,11 @@ use Twig\Compiler; use Twig\Error\SyntaxError; use Twig\Extension\ExtensionInterface; use Twig\Node\Node; +use Twig\Util\ReflectionCallable; abstract class CallExpression extends AbstractExpression { - private $reflector; + private ?ReflectionCallable $reflector = null; protected function compileCallable(Compiler $compiler) { @@ -27,7 +28,9 @@ abstract class CallExpression extends AbstractExpression if (\is_string($callable) && !str_contains($callable, '::')) { $compiler->raw($callable); } else { - [$r, $callable] = $this->reflectCallable($callable); + $rc = $this->reflectCallable($callable); + $r = $rc->getReflector(); + $callable = $rc->getCallable(); if (\is_string($callable)) { $compiler->raw($callable); @@ -251,7 +254,9 @@ abstract class CallExpression extends AbstractExpression private function getCallableParameters($callable, bool $isVariadic): array { - [$r, , $callableName] = $this->reflectCallable($callable); + $rc = $this->reflectCallable($callable); + $r = $rc->getReflector(); + $callableName = $rc->getName(); $parameters = $r->getParameters(); if ($this->hasNode('node')) { @@ -288,49 +293,8 @@ abstract class CallExpression extends AbstractExpression return [$parameters, $isPhpVariadic]; } - private function reflectCallable($callable) + private function reflectCallable($callable): ReflectionCallable { - if (null !== $this->reflector) { - return $this->reflector; - } - - if (\is_string($callable) && false !== $pos = strpos($callable, '::')) { - $callable = [substr($callable, 0, $pos), substr($callable, 2 + $pos)]; - } - - if (\is_array($callable) && method_exists($callable[0], $callable[1])) { - $r = new \ReflectionMethod($callable[0], $callable[1]); - - return $this->reflector = [$r, $callable, $r->class.'::'.$r->name]; - } - - $checkVisibility = $callable instanceof \Closure; - try { - $closure = \Closure::fromCallable($callable); - } catch (\TypeError $e) { - throw new \LogicException(\sprintf('Callback for %s "%s" is not callable in the current scope.', $this->getAttribute('type'), $this->getAttribute('name')), 0, $e); - } - $r = new \ReflectionFunction($closure); - - if (str_contains($r->name, '{closure')) { - return $this->reflector = [$r, $callable, 'Closure']; - } - - if ($object = $r->getClosureThis()) { - $callable = [$object, $r->name]; - $callableName = get_debug_type($object).'::'.$r->name; - } elseif (\PHP_VERSION_ID >= 80111 && $class = $r->getClosureCalledClass()) { - $callableName = $class->name.'::'.$r->name; - } elseif (\PHP_VERSION_ID < 80111 && $class = $r->getClosureScopeClass()) { - $callableName = (\is_array($callable) ? $callable[0] : $class->name).'::'.$r->name; - } else { - $callable = $callableName = $r->name; - } - - if ($checkVisibility && \is_array($callable) && method_exists(...$callable) && !(new \ReflectionMethod(...$callable))->isPublic()) { - $callable = $r->getClosure(); - } - - return $this->reflector = [$r, $callable, $callableName]; + return $this->reflector ??= new ReflectionCallable($callable, $this->getAttribute('type'), $this->getAttribute('name')); } } diff --git a/src/Util/ReflectionCallable.php b/src/Util/ReflectionCallable.php new file mode 100644 index 000000000..5b593e58d --- /dev/null +++ b/src/Util/ReflectionCallable.php @@ -0,0 +1,86 @@ + + * + * @internal + */ +final class ReflectionCallable +{ + private \ReflectionFunctionAbstract $reflector; + private \Closure|string|array|null $callable = null; + private string $name; + + public function __construct($callable, string $debugType = 'unknown', string $debugName = 'unknown') + { + if (\is_string($callable) && false !== $pos = strpos($callable, '::')) { + $callable = [substr($callable, 0, $pos), substr($callable, 2 + $pos)]; + } + + if (\is_array($callable) && method_exists($callable[0], $callable[1])) { + $this->reflector = $r = new \ReflectionMethod($callable[0], $callable[1]); + $this->callable = $callable; + $this->name = $r->class.'::'.$r->name; + + return; + } + + $checkVisibility = $callable instanceof \Closure; + try { + $closure = \Closure::fromCallable($callable); + } catch (\TypeError $e) { + throw new \LogicException(\sprintf('Callback for %s "%s" is not callable in the current scope.', $debugType, $debugName), 0, $e); + } + $this->reflector = $r = new \ReflectionFunction($closure); + + if (str_contains($r->name, '{closure')) { + $this->callable = $callable; + $this->name = 'Closure'; + + return; + } + + if ($object = $r->getClosureThis()) { + $callable = [$object, $r->name]; + $this->name = get_debug_type($object).'::'.$r->name; + } elseif (\PHP_VERSION_ID >= 80111 && $class = $r->getClosureCalledClass()) { + $this->name = $class->name.'::'.$r->name; + } elseif (\PHP_VERSION_ID < 80111 && $class = $r->getClosureScopeClass()) { + $this->name = (\is_array($callable) ? $callable[0] : $class->name).'::'.$r->name; + } else { + $callable = $this->name = $r->name; + } + + if ($checkVisibility && \is_array($callable) && method_exists(...$callable) && !(new \ReflectionMethod(...$callable))->isPublic()) { + $callable = $r->getClosure(); + } + + $this->callable = $callable; + } + + public function getReflector(): \ReflectionFunctionAbstract + { + return $this->reflector; + } + + public function getCallable(): \Closure|string|array|null + { + return $this->callable; + } + + public function getName(): string + { + return $this->name; + } +}