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