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