1: <?php
2:
3: declare(strict_types=1);
4:
5: namespace PHPStan\BetterReflection\Reflection;
6:
7: use Closure;
8: use PhpParser\Node;
9: use ReflectionClass as CoreReflectionClass;
10: use PHPStan\BetterReflection\BetterReflection;
11: use PHPStan\BetterReflection\Reflection\Adapter\Exception\NotImplemented;
12: use PHPStan\BetterReflection\Reflection\Exception\FunctionDoesNotExist;
13: use PHPStan\BetterReflection\Reflection\StringCast\ReflectionFunctionStringCast;
14: use PHPStan\BetterReflection\Reflector\DefaultReflector;
15: use PHPStan\BetterReflection\Reflector\Exception\IdentifierNotFound;
16: use PHPStan\BetterReflection\Reflector\Reflector;
17: use PHPStan\BetterReflection\SourceLocator\Located\LocatedSource;
18: use PHPStan\BetterReflection\SourceLocator\Type\AggregateSourceLocator;
19: use PHPStan\BetterReflection\SourceLocator\Type\ClosureSourceLocator;
20:
21: use function array_merge;
22: use function assert;
23: use function function_exists;
24:
25: /** @psalm-immutable */
26: class ReflectionFunction implements Reflection
27: {
28: private Reflector $reflector;
29: private LocatedSource $locatedSource;
30: /**
31: * @var non-empty-string|null
32: */
33: private $namespace = null;
34: use ReflectionFunctionAbstract;
35:
36: public const VARIADIC_FUNCTIONS = [
37: 'func_get_args',
38: 'func_get_arg',
39: 'func_num_args',
40: ];
41:
42: public const CLOSURE_NAME = '{closure}';
43:
44: private bool $isStatic;
45:
46: /** @param non-empty-string|null $namespace
47: * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure|\PhpParser\Node\Expr\ArrowFunction $node */
48: private function __construct(
49: Reflector $reflector,
50: $node,
51: LocatedSource $locatedSource,
52: ?string $namespace = null
53: ) {
54: $this->reflector = $reflector;
55: $this->locatedSource = $locatedSource;
56: $this->namespace = $namespace;
57: assert($node instanceof Node\Stmt\Function_ || $node instanceof Node\Expr\Closure || $node instanceof Node\Expr\ArrowFunction);
58:
59: $this->name = $node instanceof Node\Expr\Closure || $node instanceof Node\Expr\ArrowFunction
60: ? self::CLOSURE_NAME
61: : $node->name->name;
62:
63: $this->fillFromNode($node);
64:
65: $isClosure = $node instanceof Node\Expr\Closure || $node instanceof Node\Expr\ArrowFunction;
66:
67: $this->isStatic = $isClosure && $node->static;
68: $this->isClosure = $isClosure;
69: }
70:
71: // In ReflectionFunction:
72:
73: /** @return array<string, mixed> */
74: public function exportToCache(): array
75: {
76: return array_merge($this->exportFunctionAbstractToCache(), [
77: 'isStatic' => $this->isStatic,
78: 'namespace' => $this->namespace,
79: 'locatedSource' => $this->locatedSource->exportToCache(),
80: ]);
81: }
82:
83: /** @param array<string, mixed> $data */
84: public static function importFromCache(Reflector $reflector, array $data): self
85: {
86: $reflection = new CoreReflectionClass(self::class);
87: $ref = $reflection->newInstanceWithoutConstructor();
88: assert($ref instanceof self);
89: $ref->reflector = $reflector;
90: $ref->locatedSource = LocatedSource::importFromCache($data['locatedSource']);
91: $ref->namespace = $data['namespace'];
92: $ref->isStatic = $data['isStatic'];
93:
94: self::importFunctionAbstractFromCache($ref, $reflector, $data);
95:
96: return $ref;
97: }
98:
99: /** @throws IdentifierNotFound */
100: public static function createFromClosure(Closure $closure): self
101: {
102: $configuration = new BetterReflection();
103:
104: return (new DefaultReflector(new AggregateSourceLocator([
105: $configuration->sourceLocator(),
106: new ClosureSourceLocator($closure, $configuration->phpParser()),
107: ])))->reflectFunction(self::CLOSURE_NAME);
108: }
109:
110: /** @return non-empty-string */
111: public function __toString(): string
112: {
113: return ReflectionFunctionStringCast::toString($this);
114: }
115:
116: /**
117: * @internal
118: *
119: * @param non-empty-string|null $namespace
120: * @param \PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure|\PhpParser\Node\Expr\ArrowFunction $node
121: */
122: public static function createFromNode(
123: Reflector $reflector,
124: $node,
125: LocatedSource $locatedSource,
126: ?string $namespace = null
127: ): self {
128: return new self($reflector, $node, $locatedSource, $namespace);
129: }
130:
131: /**
132: * Get the "short" name of the function (e.g. for A\B\foo, this will return
133: * "foo").
134: *
135: * @return non-empty-string
136: */
137: public function getShortName(): string
138: {
139: return $this->name;
140: }
141:
142: /**
143: * Check to see if this function has been disabled (by the PHP INI file
144: * directive `disable_functions`).
145: *
146: * Note - we cannot reflect on internal functions (as there is no PHP source
147: * code we can access. This means, at present, we can only EVER return false
148: * from this function, because you cannot disable user-defined functions.
149: *
150: * @see https://php.net/manual/en/ini.core.php#ini.disable-functions
151: *
152: * @todo https://github.com/Roave/BetterReflection/issues/14
153: */
154: public function isDisabled(): bool
155: {
156: return false;
157: }
158:
159: public function isStatic(): bool
160: {
161: return $this->isStatic;
162: }
163:
164: /**
165: * @throws NotImplemented
166: * @throws FunctionDoesNotExist
167: */
168: public function getClosure(): Closure
169: {
170: $this->assertIsNoClosure();
171:
172: $functionName = $this->getName();
173:
174: $this->assertFunctionExist($functionName);
175:
176: return static fn (...$args) => $functionName(...$args);
177: }
178:
179: /**
180: * @throws NotImplemented
181: * @throws FunctionDoesNotExist
182: * @param mixed ...$args
183: * @return mixed
184: */
185: public function invoke(...$args)
186: {
187: return $this->invokeArgs($args);
188: }
189:
190: /**
191: * @param array<mixed> $args
192: *
193: * @throws NotImplemented
194: * @throws FunctionDoesNotExist
195: * @return mixed
196: */
197: public function invokeArgs(array $args = [])
198: {
199: $this->assertIsNoClosure();
200:
201: $functionName = $this->getName();
202:
203: $this->assertFunctionExist($functionName);
204:
205: return $functionName(...$args);
206: }
207:
208: /** @throws NotImplemented */
209: private function assertIsNoClosure(): void
210: {
211: if ($this->isClosure()) {
212: throw new NotImplemented('Not implemented for closures');
213: }
214: }
215:
216: /**
217: * @throws FunctionDoesNotExist
218: *
219: * @phpstan-assert callable $functionName
220: */
221: private function assertFunctionExist(string $functionName): void
222: {
223: if (! function_exists($functionName)) {
224: throw FunctionDoesNotExist::fromName($functionName);
225: }
226: }
227: }
228: