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