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