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