1: <?php
2:
3: declare(strict_types=1);
4:
5: namespace PHPStan\BetterReflection\Reflection;
6:
7: use Closure;
8: use OutOfBoundsException;
9: use PhpParser\Node;
10: use PhpParser\Node\Stmt\ClassMethod as MethodNode;
11: use ReflectionException;
12: use ReflectionMethod as CoreReflectionMethod;
13: use PHPStan\BetterReflection\Reflection\Adapter\ReflectionMethod as ReflectionMethodAdapter;
14: use PHPStan\BetterReflection\Reflection\Exception\ClassDoesNotExist;
15: use PHPStan\BetterReflection\Reflection\Exception\NoObjectProvided;
16: use PHPStan\BetterReflection\Reflection\Exception\ObjectNotInstanceOfClass;
17: use PHPStan\BetterReflection\Reflection\StringCast\ReflectionMethodStringCast;
18: use PHPStan\BetterReflection\Reflector\Exception\IdentifierNotFound;
19: use PHPStan\BetterReflection\Reflector\Reflector;
20: use PHPStan\BetterReflection\SourceLocator\Located\LocatedSource;
21: use PHPStan\BetterReflection\Util\ClassExistenceChecker;
22:
23: use function array_map;
24: use function assert;
25: use function sprintf;
26: use function strtolower;
27:
28: /** @psalm-immutable */
29: class ReflectionMethod
30: {
31: use ReflectionFunctionAbstract;
32:
33: /** @var int-mask-of<ReflectionMethodAdapter::IS_*> */
34: private $modifiers;
35: /**
36: * @var \PHPStan\BetterReflection\Reflector\Reflector
37: */
38: private $reflector;
39: /**
40: * @var \PHPStan\BetterReflection\SourceLocator\Located\LocatedSource
41: */
42: private $locatedSource;
43: /**
44: * @var string|null
45: */
46: private $namespace;
47: /**
48: * @var \PHPStan\BetterReflection\Reflection\ReflectionClass
49: */
50: private $declaringClass;
51: /**
52: * @var \PHPStan\BetterReflection\Reflection\ReflectionClass
53: */
54: private $implementingClass;
55: /**
56: * @var \PHPStan\BetterReflection\Reflection\ReflectionClass
57: */
58: private $currentClass;
59: /**
60: * @var non-empty-string|null
61: */
62: private $aliasName;
63: /** @param non-empty-string|null $aliasName
64: * @param MethodNode|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure|\PhpParser\Node\Expr\ArrowFunction $node */
65: private function __construct(Reflector $reflector, $node, LocatedSource $locatedSource, ?string $namespace, ReflectionClass $declaringClass, ReflectionClass $implementingClass, ReflectionClass $currentClass, ?string $aliasName)
66: {
67: $this->reflector = $reflector;
68: $this->locatedSource = $locatedSource;
69: $this->namespace = $namespace;
70: $this->declaringClass = $declaringClass;
71: $this->implementingClass = $implementingClass;
72: $this->currentClass = $currentClass;
73: $this->aliasName = $aliasName;
74: assert($node instanceof MethodNode);
75: $name = $node->name->name;
76: assert($name !== '');
77: $this->name = $name;
78: $this->modifiers = $this->computeModifiers($node);
79: $this->fillFromNode($node);
80: }
81: /**
82: * @internal
83: *
84: * @param non-empty-string|null $aliasName
85: */
86: public static function createFromNode(Reflector $reflector, MethodNode $node, LocatedSource $locatedSource, ?string $namespace, ReflectionClass $declaringClass, ReflectionClass $implementingClass, ReflectionClass $currentClass, ?string $aliasName = null): self
87: {
88: return new self($reflector, $node, $locatedSource, $namespace, $declaringClass, $implementingClass, $currentClass, $aliasName);
89: }
90:
91: /**
92: * Create a reflection of a method by it's name using a named class
93: *
94: * @param non-empty-string $methodName
95: *
96: * @throws IdentifierNotFound
97: * @throws OutOfBoundsException
98: */
99: public static function createFromName(string $className, string $methodName): self
100: {
101: $method = ReflectionClass::createFromName($className)->getMethod($methodName);
102:
103: if ($method === null) {
104: throw new OutOfBoundsException(sprintf('Could not find method: %s', $methodName));
105: }
106:
107: return $method;
108: }
109:
110: /**
111: * Create a reflection of a method by it's name using an instance
112: *
113: * @param non-empty-string $methodName
114: *
115: * @throws ReflectionException
116: * @throws IdentifierNotFound
117: * @throws OutOfBoundsException
118: */
119: public static function createFromInstance(object $instance, string $methodName): self
120: {
121: $method = ReflectionClass::createFromInstance($instance)->getMethod($methodName);
122:
123: if ($method === null) {
124: throw new OutOfBoundsException(sprintf('Could not find method: %s', $methodName));
125: }
126:
127: return $method;
128: }
129:
130: /**
131: * @internal
132: *
133: * @param non-empty-string|null $aliasName
134: * @param int-mask-of<ReflectionMethodAdapter::IS_*> $modifiers
135: */
136: public function withImplementingClass(ReflectionClass $implementingClass, ?string $aliasName, int $modifiers): self
137: {
138: $clone = clone $this;
139:
140: $clone->aliasName = $aliasName;
141: $clone->modifiers = $modifiers;
142: $clone->implementingClass = $implementingClass;
143: $clone->currentClass = $implementingClass;
144:
145: if ($clone->returnType !== null) {
146: $clone->returnType = $clone->returnType->withOwner($clone);
147: }
148:
149: $clone->parameters = array_map(static function (ReflectionParameter $parameter) use ($clone) : ReflectionParameter {
150: return $parameter->withFunction($clone);
151: }, $this->parameters);
152:
153: $clone->attributes = array_map(static function (ReflectionAttribute $attribute) use ($clone) : ReflectionAttribute {
154: return $attribute->withOwner($clone);
155: }, $this->attributes);
156:
157: return $clone;
158: }
159:
160: /** @internal */
161: public function withCurrentClass(ReflectionClass $currentClass): self
162: {
163: $clone = clone $this;
164: $clone->currentClass = $currentClass;
165:
166: if ($clone->returnType !== null) {
167: $clone->returnType = $clone->returnType->withOwner($clone);
168: }
169:
170: // We don't need to clone parameters and attributes
171:
172: return $clone;
173: }
174:
175: /** @return non-empty-string */
176: public function getShortName(): string
177: {
178: if ($this->aliasName !== null) {
179: return $this->aliasName;
180: }
181:
182: return $this->name;
183: }
184:
185: /** @return non-empty-string|null */
186: public function getAliasName(): ?string
187: {
188: return $this->aliasName;
189: }
190:
191: /**
192: * Find the prototype for this method, if it exists. If it does not exist
193: * it will throw a MethodPrototypeNotFound exception.
194: *
195: * @throws Exception\MethodPrototypeNotFound
196: */
197: public function getPrototype(): self
198: {
199: $currentClass = $this->getImplementingClass();
200:
201: foreach ($currentClass->getImmediateInterfaces() as $interface) {
202: $interfaceMethod = $interface->getMethod($this->getName());
203:
204: if ($interfaceMethod !== null) {
205: return $interfaceMethod;
206: }
207: }
208:
209: $currentClass = $currentClass->getParentClass();
210:
211: if ($currentClass !== null) {
212: $prototype = ($getMethod = $currentClass->getMethod($this->getName())) ? $getMethod->findPrototype() : null;
213:
214: if (
215: $prototype !== null
216: && (
217: ! $this->isConstructor()
218: || $prototype->isAbstract()
219: )
220: ) {
221: return $prototype;
222: }
223: }
224:
225: throw new Exception\MethodPrototypeNotFound(sprintf('Method %s::%s does not have a prototype', $this->getDeclaringClass()->getName(), $this->getName()));
226: }
227:
228: private function findPrototype(): ?self
229: {
230: if ($this->isAbstract()) {
231: return $this;
232: }
233:
234: if ($this->isPrivate()) {
235: return null;
236: }
237:
238: try {
239: return $this->getPrototype();
240: } catch (Exception\MethodPrototypeNotFound $exception) {
241: return $this;
242: }
243: }
244:
245: /**
246: * Get the core-reflection-compatible modifier values.
247: *
248: * @return int-mask-of<ReflectionMethodAdapter::IS_*>
249: */
250: public function getModifiers(): int
251: {
252: return $this->modifiers;
253: }
254:
255: /** @return int-mask-of<ReflectionMethodAdapter::IS_*> */
256: private function computeModifiers(MethodNode $node): int
257: {
258: $modifiers = $node->isStatic() ? CoreReflectionMethod::IS_STATIC : 0;
259: $modifiers += $node->isPublic() ? CoreReflectionMethod::IS_PUBLIC : 0;
260: $modifiers += $node->isProtected() ? CoreReflectionMethod::IS_PROTECTED : 0;
261: $modifiers += $node->isPrivate() ? CoreReflectionMethod::IS_PRIVATE : 0;
262: $modifiers += $node->isAbstract() ? CoreReflectionMethod::IS_ABSTRACT : 0;
263: $modifiers += $node->isFinal() ? CoreReflectionMethod::IS_FINAL : 0;
264:
265: return $modifiers;
266: }
267:
268: /** @return non-empty-string */
269: public function __toString(): string
270: {
271: return ReflectionMethodStringCast::toString($this);
272: }
273:
274: public function inNamespace(): bool
275: {
276: return false;
277: }
278:
279: public function getNamespaceName(): ?string
280: {
281: return null;
282: }
283:
284: public function isClosure(): bool
285: {
286: return false;
287: }
288:
289: /**
290: * Is the method abstract.
291: */
292: public function isAbstract(): bool
293: {
294: return ($this->modifiers & CoreReflectionMethod::IS_ABSTRACT) === CoreReflectionMethod::IS_ABSTRACT
295: || $this->declaringClass->isInterface();
296: }
297:
298: /**
299: * Is the method final.
300: */
301: public function isFinal(): bool
302: {
303: return ($this->modifiers & CoreReflectionMethod::IS_FINAL) === CoreReflectionMethod::IS_FINAL;
304: }
305:
306: /**
307: * Is the method private visibility.
308: */
309: public function isPrivate(): bool
310: {
311: return ($this->modifiers & CoreReflectionMethod::IS_PRIVATE) === CoreReflectionMethod::IS_PRIVATE;
312: }
313:
314: /**
315: * Is the method protected visibility.
316: */
317: public function isProtected(): bool
318: {
319: return ($this->modifiers & CoreReflectionMethod::IS_PROTECTED) === CoreReflectionMethod::IS_PROTECTED;
320: }
321:
322: /**
323: * Is the method public visibility.
324: */
325: public function isPublic(): bool
326: {
327: return ($this->modifiers & CoreReflectionMethod::IS_PUBLIC) === CoreReflectionMethod::IS_PUBLIC;
328: }
329:
330: /**
331: * Is the method static.
332: */
333: public function isStatic(): bool
334: {
335: return ($this->modifiers & CoreReflectionMethod::IS_STATIC) === CoreReflectionMethod::IS_STATIC;
336: }
337:
338: /**
339: * Is the method a constructor.
340: */
341: public function isConstructor(): bool
342: {
343: if (strtolower($this->getName()) === '__construct') {
344: return true;
345: }
346:
347: $declaringClass = $this->getDeclaringClass();
348: if ($declaringClass->inNamespace()) {
349: return false;
350: }
351:
352: return strtolower($this->getName()) === strtolower($declaringClass->getShortName());
353: }
354:
355: /**
356: * Is the method a destructor.
357: */
358: public function isDestructor(): bool
359: {
360: return strtolower($this->getName()) === '__destruct';
361: }
362:
363: /**
364: * Get the class that declares this method.
365: */
366: public function getDeclaringClass(): ReflectionClass
367: {
368: return $this->declaringClass;
369: }
370:
371: /**
372: * Get the class that implemented the method based on trait use.
373: */
374: public function getImplementingClass(): ReflectionClass
375: {
376: return $this->implementingClass;
377: }
378:
379: /**
380: * Get the current reflected class.
381: *
382: * @internal
383: */
384: public function getCurrentClass(): ReflectionClass
385: {
386: return $this->currentClass;
387: }
388:
389: /**
390: * @throws ClassDoesNotExist
391: * @throws NoObjectProvided
392: * @throws ObjectNotInstanceOfClass
393: */
394: public function getClosure($object = null): Closure
395: {
396: $declaringClassName = $this->getDeclaringClass()->getName();
397:
398: if ($this->isStatic()) {
399: $this->assertClassExist($declaringClassName);
400:
401: return function (...$args) {
402: return $this->callStaticMethod($args);
403: };
404: }
405:
406: $instance = $this->assertObject($object);
407:
408: return function (...$args) use ($instance) {
409: return $this->callObjectMethod($instance, $args);
410: };
411: }
412:
413: /**
414: * @throws ClassDoesNotExist
415: * @throws NoObjectProvided
416: * @throws ObjectNotInstanceOfClass
417: * @param mixed ...$args
418: * @return mixed
419: */
420: public function invoke($object = null, ...$args)
421: {
422: return $this->invokeArgs($object, $args);
423: }
424:
425: /**
426: * @param array<mixed> $args
427: *
428: * @throws ClassDoesNotExist
429: * @throws NoObjectProvided
430: * @throws ObjectNotInstanceOfClass
431: * @return mixed
432: */
433: public function invokeArgs($object = null, array $args = [])
434: {
435: $implementingClassName = $this->getImplementingClass()->getName();
436:
437: if ($this->isStatic()) {
438: $this->assertClassExist($implementingClassName);
439:
440: return $this->callStaticMethod($args);
441: }
442:
443: return $this->callObjectMethod($this->assertObject($object), $args);
444: }
445:
446: /** @param array<mixed> $args
447: * @return mixed */
448: private function callStaticMethod(array $args)
449: {
450: $implementingClassName = $this->getImplementingClass()->getName();
451:
452: /** @psalm-suppress InvalidStringClass */
453: $closure = Closure::bind(function (string $implementingClassName, string $_methodName, array $methodArgs) {
454: return $implementingClassName::{$_methodName}(...$methodArgs);
455: }, null, $implementingClassName);
456:
457: assert($closure instanceof Closure);
458:
459: return $closure->__invoke($implementingClassName, $this->getName(), $args);
460: }
461:
462: /** @param array<mixed> $args
463: * @return mixed */
464: private function callObjectMethod(object $object, array $args)
465: {
466: /** @psalm-suppress MixedMethodCall */
467: $closure = Closure::bind(function (object $object, string $methodName, array $methodArgs) {
468: return $object->{$methodName}(...$methodArgs);
469: }, $object, $this->getImplementingClass()->getName());
470:
471: assert($closure instanceof Closure);
472:
473: return $closure->__invoke($object, $this->getName(), $args);
474: }
475:
476: /** @throws ClassDoesNotExist */
477: private function assertClassExist(string $className): void
478: {
479: if (! ClassExistenceChecker::classExists($className, true) && ! ClassExistenceChecker::traitExists($className, true)) {
480: throw new ClassDoesNotExist(sprintf('Method of class %s cannot be used as the class does not exist', $className));
481: }
482: }
483:
484: /**
485: * @throws NoObjectProvided
486: * @throws ObjectNotInstanceOfClass
487: */
488: private function assertObject($object): object
489: {
490: if ($object === null) {
491: throw NoObjectProvided::create();
492: }
493:
494: $implementingClassName = $this->getImplementingClass()->getName();
495:
496: if (get_class($object) !== $implementingClassName) {
497: throw ObjectNotInstanceOfClass::fromClassName($implementingClassName);
498: }
499:
500: return $object;
501: }
502: }
503: