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 = $node instanceof MethodNode ? $this->computeModifiers($node) : 0;
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: private function computeModifiers(MethodNode $node): int
286: {
287: $modifiers = $node->isStatic() ? CoreReflectionMethod::IS_STATIC : 0;
288: $modifiers += $node->isPublic() ? CoreReflectionMethod::IS_PUBLIC : 0;
289: $modifiers += $node->isProtected() ? CoreReflectionMethod::IS_PROTECTED : 0;
290: $modifiers += $node->isPrivate() ? CoreReflectionMethod::IS_PRIVATE : 0;
291: $modifiers += $node->isAbstract() ? CoreReflectionMethod::IS_ABSTRACT : 0;
292: $modifiers += $node->isFinal() ? CoreReflectionMethod::IS_FINAL : 0;
293:
294: return $modifiers;
295: }
296:
297: /** @return non-empty-string */
298: public function __toString(): string
299: {
300: return ReflectionMethodStringCast::toString($this);
301: }
302:
303: public function inNamespace(): bool
304: {
305: return false;
306: }
307:
308: public function getNamespaceName(): ?string
309: {
310: return null;
311: }
312:
313: public function isClosure(): bool
314: {
315: return false;
316: }
317:
318: /**
319: * Is the method abstract.
320: */
321: public function isAbstract(): bool
322: {
323: return (bool) ($this->modifiers & CoreReflectionMethod::IS_ABSTRACT)
324: || $this->declaringClass->isInterface();
325: }
326:
327: /**
328: * Is the method final.
329: */
330: public function isFinal(): bool
331: {
332: return (bool) ($this->modifiers & CoreReflectionMethod::IS_FINAL);
333: }
334:
335: /**
336: * Is the method private visibility.
337: */
338: public function isPrivate(): bool
339: {
340: return (bool) ($this->modifiers & CoreReflectionMethod::IS_PRIVATE);
341: }
342:
343: /**
344: * Is the method protected visibility.
345: */
346: public function isProtected(): bool
347: {
348: return (bool) ($this->modifiers & CoreReflectionMethod::IS_PROTECTED);
349: }
350:
351: /**
352: * Is the method public visibility.
353: */
354: public function isPublic(): bool
355: {
356: return (bool) ($this->modifiers & CoreReflectionMethod::IS_PUBLIC);
357: }
358:
359: /**
360: * Is the method static.
361: */
362: public function isStatic(): bool
363: {
364: return (bool) ($this->modifiers & CoreReflectionMethod::IS_STATIC);
365: }
366:
367: /**
368: * Is the method a constructor.
369: */
370: public function isConstructor(): bool
371: {
372: if (strtolower($this->getName()) === '__construct') {
373: return true;
374: }
375:
376: $declaringClass = $this->getDeclaringClass();
377: if ($declaringClass->inNamespace()) {
378: return false;
379: }
380:
381: return strtolower($this->getName()) === strtolower($declaringClass->getShortName());
382: }
383:
384: /**
385: * Is the method a destructor.
386: */
387: public function isDestructor(): bool
388: {
389: return strtolower($this->getName()) === '__destruct';
390: }
391:
392: /**
393: * Get the class that declares this method.
394: */
395: public function getDeclaringClass(): ReflectionClass
396: {
397: return $this->declaringClass;
398: }
399:
400: /**
401: * Get the class that implemented the method based on trait use.
402: */
403: public function getImplementingClass(): ReflectionClass
404: {
405: return $this->implementingClass;
406: }
407:
408: /**
409: * Get the current reflected class.
410: *
411: * @internal
412: */
413: public function getCurrentClass(): ReflectionClass
414: {
415: return $this->currentClass;
416: }
417:
418: /**
419: * @throws ClassDoesNotExist
420: * @throws NoObjectProvided
421: * @throws ObjectNotInstanceOfClass
422: */
423: public function getClosure(?object $object = null): Closure
424: {
425: $declaringClassName = $this->getDeclaringClass()->getName();
426:
427: if ($this->isStatic()) {
428: $this->assertClassExist($declaringClassName);
429:
430: return fn (...$args) => $this->callStaticMethod($args);
431: }
432:
433: $instance = $this->assertObject($object);
434:
435: return fn (...$args) => $this->callObjectMethod($instance, $args);
436: }
437:
438: /** @psalm-assert-if-true !null $this->getHookProperty() */
439: public function isHook(): bool
440: {
441: return $this->hookProperty !== null;
442: }
443:
444: public function getHookProperty(): ?\PHPStan\BetterReflection\Reflection\ReflectionProperty
445: {
446: return $this->hookProperty;
447: }
448:
449: /**
450: * @throws ClassDoesNotExist
451: * @throws NoObjectProvided
452: * @throws ObjectNotInstanceOfClass
453: * @param mixed ...$args
454: * @return mixed
455: */
456: public function invoke(?object $object = null, ...$args)
457: {
458: return $this->invokeArgs($object, $args);
459: }
460:
461: /**
462: * @param array<mixed> $args
463: *
464: * @throws ClassDoesNotExist
465: * @throws NoObjectProvided
466: * @throws ObjectNotInstanceOfClass
467: * @return mixed
468: */
469: public function invokeArgs(?object $object = null, array $args = [])
470: {
471: $implementingClassName = $this->getImplementingClass()->getName();
472:
473: if ($this->isStatic()) {
474: $this->assertClassExist($implementingClassName);
475:
476: return $this->callStaticMethod($args);
477: }
478:
479: return $this->callObjectMethod($this->assertObject($object), $args);
480: }
481:
482: /** @param array<mixed> $args
483: * @return mixed */
484: private function callStaticMethod(array $args)
485: {
486: $implementingClassName = $this->getImplementingClass()->getName();
487:
488: /** @psalm-suppress InvalidStringClass */
489: $closure = Closure::bind(fn (string $implementingClassName, string $_methodName, array $methodArgs) => $implementingClassName::{$_methodName}(...$methodArgs), null, $implementingClassName);
490:
491: /** @phpstan-ignore function.alreadyNarrowedType, instanceof.alwaysTrue */
492: assert($closure instanceof Closure);
493:
494: return $closure->__invoke($implementingClassName, $this->getName(), $args);
495: }
496:
497: /** @param array<mixed> $args
498: * @return mixed */
499: private function callObjectMethod(object $object, array $args)
500: {
501: /** @psalm-suppress MixedMethodCall */
502: $closure = Closure::bind(fn (object $object, string $methodName, array $methodArgs) => $object->{$methodName}(...$methodArgs), $object, $this->getImplementingClass()->getName());
503:
504: /** @phpstan-ignore function.alreadyNarrowedType, instanceof.alwaysTrue */
505: assert($closure instanceof Closure);
506:
507: return $closure->__invoke($object, $this->getName(), $args);
508: }
509:
510: /** @throws ClassDoesNotExist */
511: private function assertClassExist(string $className): void
512: {
513: if (! ClassExistenceChecker::classExists($className, true) && ! ClassExistenceChecker::traitExists($className, true)) {
514: throw new ClassDoesNotExist(sprintf('Method of class %s cannot be used as the class does not exist', $className));
515: }
516: }
517:
518: /**
519: * @throws NoObjectProvided
520: * @throws ObjectNotInstanceOfClass
521: */
522: private function assertObject(?object $object): object
523: {
524: if ($object === null) {
525: throw NoObjectProvided::create();
526: }
527:
528: $implementingClassName = $this->getImplementingClass()->getName();
529:
530: if (get_class($object) !== $implementingClassName) {
531: throw ObjectNotInstanceOfClass::fromClassName($implementingClassName);
532: }
533:
534: return $object;
535: }
536: }
537: