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