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