1<?php
2
3/*
4 * This file is part of Twig.
5 *
6 * (c) Fabien Potencier
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Twig\Util;
13
14use Twig\TwigCallableInterface;
15
16/**
17 * @author Fabien Potencier <fabien@symfony.com>
18 *
19 * @internal
20 */
21final class ReflectionCallable
22{
23    private $reflector;
24    private $callable;
25    private $name;
26
27    public function __construct(
28        TwigCallableInterface $twigCallable,
29    ) {
30        $callable = $twigCallable->getCallable();
31        if (\is_string($callable) && false !== $pos = strpos($callable, '::')) {
32            $callable = [substr($callable, 0, $pos), substr($callable, 2 + $pos)];
33        }
34
35        if (\is_array($callable) && method_exists($callable[0], $callable[1])) {
36            $this->reflector = $r = new \ReflectionMethod($callable[0], $callable[1]);
37            $this->callable = $callable;
38            $this->name = $r->class.'::'.$r->name;
39
40            return;
41        }
42
43        $checkVisibility = $callable instanceof \Closure;
44        try {
45            $closure = \Closure::fromCallable($callable);
46        } catch (\TypeError $e) {
47            throw new \LogicException(\sprintf('Callback for %s "%s" is not callable in the current scope.', $twigCallable->getType(), $twigCallable->getName()), 0, $e);
48        }
49        $this->reflector = $r = new \ReflectionFunction($closure);
50
51        if (str_contains($r->name, '{closure')) {
52            $this->callable = $callable;
53            $this->name = 'Closure';
54
55            return;
56        }
57
58        if ($object = $r->getClosureThis()) {
59            $callable = [$object, $r->name];
60            $this->name = get_debug_type($object).'::'.$r->name;
61        } elseif (\PHP_VERSION_ID >= 80111 && $class = $r->getClosureCalledClass()) {
62            $callable = [$class->name, $r->name];
63            $this->name = $class->name.'::'.$r->name;
64        } elseif (\PHP_VERSION_ID < 80111 && $class = $r->getClosureScopeClass()) {
65            $callable = [\is_array($callable) ? $callable[0] : $class->name, $r->name];
66            $this->name = (\is_array($callable) ? $callable[0] : $class->name).'::'.$r->name;
67        } else {
68            $callable = $this->name = $r->name;
69        }
70
71        if ($checkVisibility && \is_array($callable) && method_exists(...$callable) && !(new \ReflectionMethod(...$callable))->isPublic()) {
72            $callable = $r->getClosure();
73        }
74
75        $this->callable = $callable;
76    }
77
78    public function getReflector(): \ReflectionFunctionAbstract
79    {
80        return $this->reflector;
81    }
82
83    /**
84     * @return callable
85     */
86    public function getCallable()
87    {
88        return $this->callable;
89    }
90
91    public function getName(): string
92    {
93        return $this->name;
94    }
95}
96