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->aliasName = $aliasName;
252: $clone->modifiers = $modifiers;
253: $clone->implementingClass = $implementingClass;
254: $clone->currentClass = $implementingClass;
255:
256: if ($clone->returnType !== null) {
257: $clone->returnType = $clone->returnType->withOwner($clone);
258: }
259:
260: $clone->parameters = array_map(static fn (ReflectionParameter $parameter): ReflectionParameter => $parameter->withFunction($clone), $this->parameters);
261:
262: $clone->attributes = array_map(static fn (ReflectionAttribute $attribute): ReflectionAttribute => $attribute->withOwner($clone), $this->attributes);
263:
264: return $clone;
265: }
266:
267: /** @internal */
268: public function withCurrentClass(ReflectionClass $currentClass): self
269: {
270: $clone = clone $this;
271: /** @phpstan-ignore property.readOnlyByPhpDocAssignNotInConstructor */
272: $clone->currentClass = $currentClass;
273:
274: if ($clone->returnType !== null) {
275: $clone->returnType = $clone->returnType->withOwner($clone);
276: }
277:
278: // We don't need to clone parameters and attributes
279:
280: return $clone;
281: }
282:
283: /** @return non-empty-string */
284: public function getShortName(): string
285: {
286: if ($this->aliasName !== null) {
287: return $this->aliasName;
288: }
289:
290: return $this->name;
291: }
292:
293: /** @return non-empty-string|null */
294: public function getAliasName(): ?string
295: {
296: return $this->aliasName;
297: }
298:
299: /**
300: * Find the prototype for this method, if it exists. If it does not exist
301: * it will throw a MethodPrototypeNotFound exception.
302: *
303: * @throws Exception\MethodPrototypeNotFound
304: */
305: public function getPrototype(): self
306: {
307: $currentClass = $this->getImplementingClass();
308:
309: foreach ($currentClass->getImmediateInterfaces() as $interface) {
310: $interfaceMethod = $interface->getMethod($this->getName());
311:
312: if ($interfaceMethod !== null) {
313: return $interfaceMethod;
314: }
315: }
316:
317: $currentClass = $currentClass->getParentClass();
318:
319: if ($currentClass !== null) {
320: $prototype = ($nullsafeVariable1 = $currentClass->getMethod($this->getName())) ? $nullsafeVariable1->findPrototype() : null;
321:
322: if (
323: $prototype !== null
324: && (
325: ! $this->isConstructor()
326: || $prototype->isAbstract()
327: )
328: ) {
329: return $prototype;
330: }
331: }
332:
333: throw new Exception\MethodPrototypeNotFound(sprintf(
334: 'Method %s::%s does not have a prototype',
335: $this->getDeclaringClass()->getName(),
336: $this->getName(),
337: ));
338: }
339:
340: private function findPrototype(): ?self
341: {
342: if ($this->isAbstract()) {
343: return $this;
344: }
345:
346: if ($this->isPrivate()) {
347: return null;
348: }
349:
350: try {
351: return $this->getPrototype();
352: } catch (Exception\MethodPrototypeNotFound $exception) {
353: return $this;
354: }
355: }
356:
357: /**
358: * Get the core-reflection-compatible modifier values.
359: *
360: * @return int-mask-of<ReflectionMethodAdapter::IS_*>
361: */
362: public function getModifiers(): int
363: {
364: return $this->modifiers;
365: }
366:
367: /** @return int-mask-of<ReflectionMethodAdapter::IS_*>
368: * @param MethodNode|\PhpParser\Node\PropertyHook $node */
369: private function computeModifiers($node): int
370: {
371: $modifiers = 0;
372:
373: if ($node instanceof MethodNode) {
374: $modifiers += $node->isStatic() ? CoreReflectionMethod::IS_STATIC : 0;
375: $modifiers += $node->isPublic() ? CoreReflectionMethod::IS_PUBLIC : 0;
376: $modifiers += $node->isProtected() ? CoreReflectionMethod::IS_PROTECTED : 0;
377: $modifiers += $node->isPrivate() ? CoreReflectionMethod::IS_PRIVATE : 0;
378: $modifiers += $node->isAbstract() ? CoreReflectionMethod::IS_ABSTRACT : 0;
379: }
380:
381: $modifiers += $node->isFinal() ? CoreReflectionMethod::IS_FINAL : 0;
382:
383: return $modifiers;
384: }
385:
386: /** @return non-empty-string */
387: public function __toString(): string
388: {
389: return ReflectionMethodStringCast::toString($this);
390: }
391:
392: public function inNamespace(): bool
393: {
394: return false;
395: }
396:
397: public function getNamespaceName(): ?string
398: {
399: return null;
400: }
401:
402: public function isClosure(): bool
403: {
404: return false;
405: }
406:
407: /**
408: * Is the method abstract.
409: */
410: public function isAbstract(): bool
411: {
412: return (bool) ($this->modifiers & CoreReflectionMethod::IS_ABSTRACT)
413: || $this->getDeclaringClass()->isInterface();
414: }
415:
416: /**
417: * Is the method final.
418: */
419: public function isFinal(): bool
420: {
421: return (bool) ($this->modifiers & CoreReflectionMethod::IS_FINAL);
422: }
423:
424: /**
425: * Is the method private visibility.
426: */
427: public function isPrivate(): bool
428: {
429: return (bool) ($this->modifiers & CoreReflectionMethod::IS_PRIVATE);
430: }
431:
432: /**
433: * Is the method protected visibility.
434: */
435: public function isProtected(): bool
436: {
437: return (bool) ($this->modifiers & CoreReflectionMethod::IS_PROTECTED);
438: }
439:
440: /**
441: * Is the method public visibility.
442: */
443: public function isPublic(): bool
444: {
445: return (bool) ($this->modifiers & CoreReflectionMethod::IS_PUBLIC);
446: }
447:
448: /**
449: * Is the method static.
450: */
451: public function isStatic(): bool
452: {
453: return (bool) ($this->modifiers & CoreReflectionMethod::IS_STATIC);
454: }
455:
456: /**
457: * Is the method a constructor.
458: */
459: public function isConstructor(): bool
460: {
461: if (strtolower($this->getName()) === '__construct') {
462: return true;
463: }
464:
465: $declaringClass = $this->getDeclaringClass();
466: if ($declaringClass->inNamespace()) {
467: return false;
468: }
469:
470: return strtolower($this->getName()) === strtolower($declaringClass->getShortName());
471: }
472:
473: /**
474: * Is the method a destructor.
475: */
476: public function isDestructor(): bool
477: {
478: return strtolower($this->getName()) === '__destruct';
479: }
480:
481: /**
482: * Get the class that declares this method.
483: */
484: public function getDeclaringClass(): ReflectionClass
485: {
486: return $this->declaringClass ??= $this->reflector->reflectClass($this->declaringClassName);
487: }
488:
489: /**
490: * Get the class that implemented the method based on trait use.
491: */
492: public function getImplementingClass(): ReflectionClass
493: {
494: return $this->implementingClass ??= $this->reflector->reflectClass($this->implementingClassName);
495: }
496:
497: /**
498: * Get the current reflected class.
499: *
500: * @internal
501: */
502: public function getCurrentClass(): ReflectionClass
503: {
504: return $this->currentClass ??= $this->reflector->reflectClass($this->currentClassName);
505: }
506:
507: /**
508: * @throws ClassDoesNotExist
509: * @throws NoObjectProvided
510: * @throws ObjectNotInstanceOfClass
511: */
512: public function getClosure(?object $object = null): Closure
513: {
514: $declaringClassName = $this->getDeclaringClass()->getName();
515:
516: if ($this->isStatic()) {
517: $this->assertClassExist($declaringClassName);
518:
519: return fn (...$args) => $this->callStaticMethod($args);
520: }
521:
522: $instance = $this->assertObject($object);
523:
524: return fn (...$args) => $this->callObjectMethod($instance, $args);
525: }
526:
527: /** @psalm-assert-if-true !null $this->getHookProperty() */
528: public function isHook(): bool
529: {
530: return $this->hookProperty !== null;
531: }
532:
533: public function getHookProperty(): ?\PHPStan\BetterReflection\Reflection\ReflectionProperty
534: {
535: return $this->hookProperty;
536: }
537:
538: /**
539: * @throws ClassDoesNotExist
540: * @throws NoObjectProvided
541: * @throws ObjectNotInstanceOfClass
542: * @param mixed ...$args
543: * @return mixed
544: */
545: public function invoke(?object $object = null, ...$args)
546: {
547: return $this->invokeArgs($object, $args);
548: }
549:
550: /**
551: * @param array<mixed> $args
552: *
553: * @throws ClassDoesNotExist
554: * @throws NoObjectProvided
555: * @throws ObjectNotInstanceOfClass
556: * @return mixed
557: */
558: public function invokeArgs(?object $object = null, array $args = [])
559: {
560: $implementingClassName = $this->getImplementingClass()->getName();
561:
562: if ($this->isStatic()) {
563: $this->assertClassExist($implementingClassName);
564:
565: return $this->callStaticMethod($args);
566: }
567:
568: return $this->callObjectMethod($this->assertObject($object), $args);
569: }
570:
571: /** @param array<mixed> $args
572: * @return mixed */
573: private function callStaticMethod(array $args)
574: {
575: $implementingClassName = $this->getImplementingClass()->getName();
576:
577: /** @psalm-suppress InvalidStringClass */
578: $closure = Closure::bind(fn (string $implementingClassName, string $_methodName, array $methodArgs) => $implementingClassName::{$_methodName}(...$methodArgs), null, $implementingClassName);
579:
580: /** @phpstan-ignore function.alreadyNarrowedType, instanceof.alwaysTrue */
581: assert($closure instanceof Closure);
582:
583: return $closure->__invoke($implementingClassName, $this->getName(), $args);
584: }
585:
586: /** @param array<mixed> $args
587: * @return mixed */
588: private function callObjectMethod(object $object, array $args)
589: {
590: /** @psalm-suppress MixedMethodCall */
591: $closure = Closure::bind(fn (object $object, string $methodName, array $methodArgs) => $object->{$methodName}(...$methodArgs), $object, $this->getImplementingClass()->getName());
592:
593: /** @phpstan-ignore function.alreadyNarrowedType, instanceof.alwaysTrue */
594: assert($closure instanceof Closure);
595:
596: return $closure->__invoke($object, $this->getName(), $args);
597: }
598:
599: /** @throws ClassDoesNotExist */
600: private function assertClassExist(string $className): void
601: {
602: if (! ClassExistenceChecker::classExists($className, true) && ! ClassExistenceChecker::traitExists($className, true)) {
603: throw new ClassDoesNotExist(sprintf('Method of class %s cannot be used as the class does not exist', $className));
604: }
605: }
606:
607: /**
608: * @throws NoObjectProvided
609: * @throws ObjectNotInstanceOfClass
610: */
611: private function assertObject(?object $object): object
612: {
613: if ($object === null) {
614: throw NoObjectProvided::create();
615: }
616:
617: $implementingClassName = $this->getImplementingClass()->getName();
618:
619: if (get_class($object) !== $implementingClassName) {
620: throw ObjectNotInstanceOfClass::fromClassName($implementingClassName);
621: }
622:
623: return $object;
624: }
625: }
626: