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