1: <?php
2:
3: declare(strict_types=1);
4:
5: namespace PHPStan\BetterReflection\Reflection;
6:
7: use BackedEnum;
8: use PhpParser\Modifiers;
9: use PhpParser\Node;
10: use PhpParser\Node\Stmt\Class_ as ClassNode;
11: use PhpParser\Node\Stmt\ClassMethod;
12: use PhpParser\Node\Stmt\Enum_ as EnumNode;
13: use PhpParser\Node\Stmt\Interface_ as InterfaceNode;
14: use PhpParser\Node\Stmt\Trait_ as TraitNode;
15: use PhpParser\Node\Stmt\TraitUse;
16: use ReflectionClass as CoreReflectionClass;
17: use ReflectionException;
18: use ReflectionMethod as CoreReflectionMethod;
19: use PHPStan\BetterReflection\BetterReflection;
20: use PHPStan\BetterReflection\Reflection\Adapter\ReflectionClass as ReflectionClassAdapter;
21: use PHPStan\BetterReflection\Reflection\Adapter\ReflectionClassConstant as ReflectionClassConstantAdapter;
22: use PHPStan\BetterReflection\Reflection\Adapter\ReflectionMethod as ReflectionMethodAdapter;
23: use PHPStan\BetterReflection\Reflection\Adapter\ReflectionProperty as ReflectionPropertyAdapter;
24: use PHPStan\BetterReflection\Reflection\Attribute\ReflectionAttributeHelper;
25: use PHPStan\BetterReflection\Reflection\Deprecated\DeprecatedHelper;
26: use PHPStan\BetterReflection\Reflection\Exception\CircularReference;
27: use PHPStan\BetterReflection\Reflection\Exception\ClassDoesNotExist;
28: use PHPStan\BetterReflection\Reflection\Exception\NoObjectProvided;
29: use PHPStan\BetterReflection\Reflection\Exception\NotAnObject;
30: use PHPStan\BetterReflection\Reflection\Exception\ObjectNotInstanceOfClass;
31: use PHPStan\BetterReflection\Reflection\Exception\PropertyDoesNotExist;
32: use PHPStan\BetterReflection\Reflection\StringCast\ReflectionClassStringCast;
33: use PHPStan\BetterReflection\Reflection\Support\AlreadyVisitedClasses;
34: use PHPStan\BetterReflection\Reflector\Exception\IdentifierNotFound;
35: use PHPStan\BetterReflection\Reflector\Reflector;
36: use PHPStan\BetterReflection\SourceLocator\Located\InternalLocatedSource;
37: use PHPStan\BetterReflection\SourceLocator\Located\LocatedSource;
38: use PHPStan\BetterReflection\Util\CalculateReflectionColumn;
39: use PHPStan\BetterReflection\Util\GetLastDocComment;
40: use Stringable;
41: use Traversable;
42: use UnitEnum;
43:
44: use function array_filter;
45: use function array_key_exists;
46: use function array_keys;
47: use function array_map;
48: use function array_merge;
49: use function array_reverse;
50: use function array_slice;
51: use function array_values;
52: use function assert;
53: use function in_array;
54: use function is_int;
55: use function is_string;
56: use function ltrim;
57: use function sha1;
58: use function sprintf;
59: use function strtolower;
60:
61: /** @psalm-immutable */
62: class ReflectionClass implements Reflection
63: {
64: private Reflector $reflector;
65: private LocatedSource $locatedSource;
66: /**
67: * @var non-empty-string|null
68: */
69: private $namespace = null;
70: public const ANONYMOUS_CLASS_NAME_PREFIX = 'class@anonymous';
71: public const ANONYMOUS_CLASS_NAME_PREFIX_REGEXP = '~^(?:class|[\w\\\\]+)@anonymous~';
72: private const ANONYMOUS_CLASS_NAME_SUFFIX = '@anonymous';
73:
74: /** @var class-string|trait-string|null */
75: private $name;
76:
77: /** @var non-empty-string|null */
78: private $shortName;
79:
80: private bool $isInterface;
81: private bool $isTrait;
82: private bool $isEnum;
83: private bool $isBackedEnum;
84:
85: /** @var int-mask-of<ReflectionClassAdapter::IS_*> */
86: private int $modifiers;
87:
88: /** @var non-empty-string|null */
89: private $docComment;
90:
91: /** @var list<ReflectionAttribute> */
92: private array $attributes;
93:
94: /** @var positive-int */
95: private int $startLine;
96:
97: /** @var positive-int */
98: private int $endLine;
99:
100: /** @var positive-int */
101: private int $startColumn;
102:
103: /** @var positive-int */
104: private int $endColumn;
105:
106: /** @var class-string|null */
107: private $parentClassName;
108:
109: /** @var list<class-string> */
110: private array $implementsClassNames;
111:
112: /** @var list<trait-string> */
113: private array $traitClassNames;
114:
115: /** @var array<non-empty-string, ReflectionClassConstant> */
116: private array $immediateConstants;
117:
118: /** @var array<non-empty-string, ReflectionProperty> */
119: private array $immediateProperties;
120:
121: /** @var array<non-empty-string, ReflectionMethod> */
122: private array $immediateMethods;
123:
124: /** @var array{
125: * aliases: array<trait-string, list<array{alias: non-empty-string, method: non-empty-string, hash: non-empty-string}>>,
126: * modifiers: array<non-empty-string, int-mask-of<ReflectionMethodAdapter::IS_*>>,
127: * precedences: array<non-empty-string, non-empty-string>,
128: * hashes: array<non-empty-string, non-empty-string>,
129: * }
130: */
131: private array $traitsData;
132:
133: /**
134: * @var array<non-empty-string, ReflectionClassConstant>|null
135: * @psalm-allow-private-mutation
136: */
137: private $cachedConstants = null;
138:
139: /**
140: * @var array<non-empty-string, ReflectionProperty>|null
141: * @psalm-allow-private-mutation
142: */
143: private $cachedProperties = null;
144:
145: /** @var array<class-string, ReflectionClass>|null */
146: private $cachedInterfaces = null;
147:
148: /** @var list<class-string>|null */
149: private $cachedInterfaceNames = null;
150:
151: /** @var list<ReflectionClass>|null */
152: private $cachedTraits = null;
153:
154: /**
155: * @var \PHPStan\BetterReflection\Reflection\ReflectionMethod|null
156: */
157: private $cachedConstructor = null;
158:
159: /**
160: * @var string|null
161: */
162: private $cachedName = null;
163:
164: /**
165: * @psalm-allow-private-mutation
166: * @var array<lowercase-string, ReflectionMethod>|null
167: */
168: private $cachedMethods = null;
169:
170: /**
171: * @var list<ReflectionClass>|null
172: * @psalm-allow-private-mutation
173: */
174: private $cachedParentClasses = null;
175:
176: /**
177: * @internal
178: *
179: * @param non-empty-string|null $namespace
180: * @param ClassNode|InterfaceNode|TraitNode|EnumNode $node
181: */
182: protected function __construct(Reflector $reflector, $node, LocatedSource $locatedSource, ?string $namespace = null)
183: {
184: $this->reflector = $reflector;
185: $this->locatedSource = $locatedSource;
186: $this->namespace = $namespace;
187: $name = null;
188: $shortName = null;
189: if ($node->name instanceof Node\Identifier) {
190: $namespacedName = $node->namespacedName;
191: if ($namespacedName === null) {
192: /** @psalm-var class-string|trait-string */
193: $name = $node->name->name;
194: } else {
195: /** @psalm-var class-string|trait-string */
196: $name = $namespacedName->toString();
197: }
198:
199: $shortName = $node->name->name;
200: }
201: $this->name = $name;
202: $this->shortName = $shortName;
203: $this->isInterface = $node instanceof InterfaceNode;
204: $this->isTrait = $node instanceof TraitNode;
205: $this->isEnum = $node instanceof EnumNode;
206: $this->isBackedEnum = $node instanceof EnumNode && $node->scalarType !== null;
207: $this->modifiers = $this->computeModifiers($node);
208: $this->docComment = GetLastDocComment::forNode($node);
209: $this->attributes = ReflectionAttributeHelper::createAttributes($reflector, $this, $node->attrGroups);
210: $startLine = $node->getStartLine();
211: assert($startLine > 0);
212: $endLine = $node->getEndLine();
213: assert($endLine > 0);
214: $this->startLine = $startLine;
215: $this->endLine = $endLine;
216: $this->startColumn = CalculateReflectionColumn::getStartColumn($locatedSource->getSource(), $node);
217: $this->endColumn = CalculateReflectionColumn::getEndColumn($locatedSource->getSource(), $node);
218: /** @var class-string|null $parentClassName */
219: $parentClassName = $node instanceof ClassNode ? ($nullsafeVariable1 = $node->extends) ? $nullsafeVariable1->toString() : null : null;
220: $this->parentClassName = $parentClassName;
221: // @infection-ignore-all UnwrapArrayMap: It works without array_map() as well but this is less magical
222: /** @var list<class-string> $implementsClassNames */
223: $implementsClassNames = array_map(
224: static fn (Node\Name $name): string => $name->toString(),
225: $node instanceof TraitNode ? [] : ($node instanceof InterfaceNode ? $node->extends : $node->implements),
226: );
227: $this->implementsClassNames = $implementsClassNames;
228: /** @var list<trait-string> $traitClassNames */
229: $traitClassNames = array_merge(
230: [],
231: ...array_map(
232: // @infection-ignore-all UnwrapArrayMap: It works without array_map() as well but this is less magical
233: static fn (TraitUse $traitUse): array => array_map(static fn (Node\Name $traitName): string => $traitName->toString(), $traitUse->traits),
234: $node->getTraitUses(),
235: ),
236: );
237: $this->traitClassNames = $traitClassNames;
238: $this->immediateConstants = $this->createImmediateConstants($node, $reflector);
239: $this->immediateProperties = $this->createImmediateProperties($node, $reflector);
240: $this->immediateMethods = $this->createImmediateMethods($node, $reflector);
241: $this->traitsData = $this->computeTraitsData($node);
242: }
243:
244: /** @return non-empty-string */
245: public function __toString(): string
246: {
247: return ReflectionClassStringCast::toString($this);
248: }
249:
250: /**
251: * Create a ReflectionClass from an instance, using default reflectors etc.
252: *
253: * This is simply a helper method that calls ReflectionObject::createFromInstance().
254: *
255: * @see ReflectionObject::createFromInstance
256: *
257: * @throws IdentifierNotFound
258: * @throws ReflectionException
259: */
260: public static function createFromInstance(object $instance): self
261: {
262: return ReflectionObject::createFromInstance($instance);
263: }
264:
265: /**
266: * Create from a Class Node.
267: *
268: * @internal
269: *
270: * @param ClassNode|InterfaceNode|TraitNode|EnumNode $node Node has to be processed by the PhpParser\NodeVisitor\NameResolver
271: * @param non-empty-string|null $namespace optional - if omitted, we assume it is global namespaced class
272: */
273: public static function createFromNode(Reflector $reflector, $node, LocatedSource $locatedSource, ?string $namespace = null): self
274: {
275: return new self($reflector, $node, $locatedSource, $namespace);
276: }
277:
278: /**
279: * Get the "short" name of the class (e.g. for A\B\Foo, this will return
280: * "Foo").
281: *
282: * @return non-empty-string
283: */
284: public function getShortName(): string
285: {
286: if ($this->shortName !== null) {
287: return $this->shortName;
288: }
289:
290: $fileName = $this->getFileName();
291:
292: if ($fileName === null) {
293: $fileName = sha1($this->locatedSource->getSource());
294: }
295:
296: return sprintf('%s%s%c%s(%d)', $this->getAnonymousClassNamePrefix(), self::ANONYMOUS_CLASS_NAME_SUFFIX, "\0", $fileName, $this->getStartLine());
297: }
298:
299: /**
300: * PHP creates the name of the anonymous class based on first parent
301: * or implemented interface.
302: */
303: private function getAnonymousClassNamePrefix(): string
304: {
305: if ($this->parentClassName !== null) {
306: return $this->parentClassName;
307: }
308:
309: if ($this->implementsClassNames !== []) {
310: return $this->implementsClassNames[0];
311: }
312:
313: return 'class';
314: }
315:
316: /**
317: * Get the "full" name of the class (e.g. for A\B\Foo, this will return
318: * "A\B\Foo").
319: *
320: * @return class-string|trait-string
321: */
322: public function getName(): string
323: {
324: if ($this->cachedName !== null) {
325: return $this->cachedName;
326: }
327:
328: if (! $this->inNamespace()) {
329: /** @psalm-var class-string|trait-string */
330: return $this->cachedName = $this->getShortName();
331: }
332:
333: assert($this->name !== null);
334:
335: return $this->cachedName = $this->name;
336: }
337:
338: /** @return class-string|null */
339: public function getParentClassName(): ?string
340: {
341: return $this->parentClassName;
342: }
343:
344: /**
345: * Get the "namespace" name of the class (e.g. for A\B\Foo, this will
346: * return "A\B").
347: *
348: * @return non-empty-string|null
349: */
350: public function getNamespaceName(): ?string
351: {
352: return $this->namespace;
353: }
354:
355: /**
356: * Decide if this class is part of a namespace. Returns false if the class
357: * is in the global namespace or does not have a specified namespace.
358: */
359: public function inNamespace(): bool
360: {
361: return $this->namespace !== null;
362: }
363:
364: /** @return non-empty-string|null */
365: public function getExtensionName(): ?string
366: {
367: return $this->locatedSource->getExtensionName();
368: }
369:
370: /** @return list<ReflectionMethod> */
371: private function createMethodsFromTrait(ReflectionMethod $method): array
372: {
373: $methodModifiers = $method->getModifiers();
374: $lowerCasedMethodHash = $this->lowerCasedMethodHash($method->getImplementingClass()->getName(), $method->getName());
375:
376: if (array_key_exists($lowerCasedMethodHash, $this->traitsData['modifiers'])) {
377: $newModifierAst = $this->traitsData['modifiers'][$lowerCasedMethodHash];
378: if ($this->traitsData['modifiers'][$lowerCasedMethodHash] & ClassNode::VISIBILITY_MODIFIER_MASK) {
379: $methodModifiersWithoutVisibility = $methodModifiers;
380: if (($methodModifiers & CoreReflectionMethod::IS_PUBLIC) === CoreReflectionMethod::IS_PUBLIC) {
381: $methodModifiersWithoutVisibility -= CoreReflectionMethod::IS_PUBLIC;
382: }
383: if (($methodModifiers & CoreReflectionMethod::IS_PROTECTED) === CoreReflectionMethod::IS_PROTECTED) {
384: $methodModifiersWithoutVisibility -= CoreReflectionMethod::IS_PROTECTED;
385: }
386: if (($methodModifiers & CoreReflectionMethod::IS_PRIVATE) === CoreReflectionMethod::IS_PRIVATE) {
387: $methodModifiersWithoutVisibility -= CoreReflectionMethod::IS_PRIVATE;
388: }
389: $newModifier = 0;
390: if (($newModifierAst & Modifiers::PUBLIC) === Modifiers::PUBLIC) {
391: $newModifier = CoreReflectionMethod::IS_PUBLIC;
392: }
393: if (($newModifierAst & Modifiers::PROTECTED) === Modifiers::PROTECTED) {
394: $newModifier = CoreReflectionMethod::IS_PROTECTED;
395: }
396: if (($newModifierAst & Modifiers::PRIVATE) === Modifiers::PRIVATE) {
397: $newModifier = CoreReflectionMethod::IS_PRIVATE;
398: }
399: $methodModifiers = $methodModifiersWithoutVisibility | $newModifier;
400: }
401: if (($newModifierAst & Modifiers::FINAL) === Modifiers::FINAL) {
402: $methodModifiers |= CoreReflectionMethod::IS_FINAL;
403: }
404: }
405:
406: $createMethod = function (?string $aliasMethodName) use ($method, $methodModifiers): ReflectionMethod {
407: assert($aliasMethodName === null || $aliasMethodName !== '');
408:
409: /** @phpstan-ignore argument.type */
410: return $method->withImplementingClass($this, $aliasMethodName, $methodModifiers);
411: };
412:
413: $methods = [];
414:
415: if (! array_key_exists($lowerCasedMethodHash, $this->traitsData['precedences'])) {
416: $methods[] = $createMethod($method->getAliasName());
417: }
418:
419: if ($this->traitsData['aliases'] !== []) {
420: $traits = array_combine($this->traitClassNames, $this->getTraits());
421:
422: foreach ($this->traitsData['aliases'] as $traitClassName => $traitAliasDefinitions) {
423: foreach ($traitAliasDefinitions as $traitAliasDefinition) {
424: if ($lowerCasedMethodHash !== $traitAliasDefinition['hash']) {
425: continue;
426: }
427:
428: if (! $traits[$traitClassName]->hasMethod($traitAliasDefinition['method'])) {
429: continue;
430: }
431:
432: $methods[] = $createMethod($traitAliasDefinition['alias']);
433: }
434: }
435: }
436:
437: return $methods;
438: }
439:
440: /**
441: * Construct a flat list of all methods in this precise order from:
442: * - current class
443: * - parent class
444: * - traits used in parent class
445: * - interfaces implemented in parent class
446: * - traits used in current class
447: * - interfaces implemented in current class
448: *
449: * Methods are not merged via their name as array index, since internal PHP method
450: * sorting does not follow `\array_merge()` semantics.
451: *
452: * @return array<lowercase-string, ReflectionMethod> indexed by method name
453: */
454: private function getMethodsIndexedByLowercasedName(AlreadyVisitedClasses $alreadyVisitedClasses): array
455: {
456: if ($this->cachedMethods !== null) {
457: return $this->cachedMethods;
458: }
459:
460: $alreadyVisitedClasses->push($this->getName());
461:
462: $immediateMethods = $this->getImmediateMethods();
463: $className = $this->getName();
464:
465: $methods = array_combine(
466: array_map(static fn (ReflectionMethod $method): string => strtolower($method->getName()), $immediateMethods),
467: $immediateMethods,
468: );
469:
470: $parentClass = $this->getParentClass();
471: if ($parentClass !== null) {
472: foreach ($parentClass->getMethodsIndexedByLowercasedName($alreadyVisitedClasses) as $lowercasedMethodName => $method) {
473: if (array_key_exists($lowercasedMethodName, $methods)) {
474: continue;
475: }
476:
477: $methods[$lowercasedMethodName] = $method->withCurrentClass($this);
478: }
479: }
480:
481: foreach ($this->getTraits() as $trait) {
482: $alreadyVisitedClassesCopy = clone $alreadyVisitedClasses;
483: foreach ($trait->getMethodsIndexedByLowercasedName($alreadyVisitedClassesCopy) as $method) {
484: foreach ($this->createMethodsFromTrait($method) as $traitMethod) {
485: $lowercasedMethodName = strtolower($traitMethod->getName());
486:
487: if (! array_key_exists($lowercasedMethodName, $methods)) {
488: $methods[$lowercasedMethodName] = $traitMethod;
489: continue;
490: }
491:
492: if ($traitMethod->isAbstract()) {
493: continue;
494: }
495:
496: // Non-abstract trait method can overwrite existing method:
497: // - when existing method comes from parent class
498: // - when existing method comes from trait and is abstract
499:
500: $existingMethod = $methods[$lowercasedMethodName];
501:
502: if (
503: $existingMethod->getDeclaringClass()->getName() === $className
504: && ! (
505: $existingMethod->isAbstract()
506: && $existingMethod->getDeclaringClass()->isTrait()
507: )
508: ) {
509: continue;
510: }
511:
512: $methods[$lowercasedMethodName] = $traitMethod;
513: }
514: }
515: }
516:
517: foreach ($this->getImmediateInterfaces() as $interface) {
518: $alreadyVisitedClassesCopy = clone $alreadyVisitedClasses;
519: foreach ($interface->getMethodsIndexedByLowercasedName($alreadyVisitedClassesCopy) as $lowercasedMethodName => $method) {
520: if (array_key_exists($lowercasedMethodName, $methods)) {
521: continue;
522: }
523:
524: $methods[$lowercasedMethodName] = $method;
525: }
526: }
527:
528: $this->cachedMethods = $methods;
529:
530: return $this->cachedMethods;
531: }
532:
533: /**
534: * Fetch an array of all methods for this class.
535: *
536: * Filter the results to include only methods with certain attributes. Defaults
537: * to no filtering.
538: * Any combination of \ReflectionMethod::IS_STATIC,
539: * \ReflectionMethod::IS_PUBLIC,
540: * \ReflectionMethod::IS_PROTECTED,
541: * \ReflectionMethod::IS_PRIVATE,
542: * \ReflectionMethod::IS_ABSTRACT,
543: * \ReflectionMethod::IS_FINAL.
544: * For example if $filter = \ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_FINAL
545: * the only the final public methods will be returned
546: *
547: * @param int-mask-of<CoreReflectionMethod::IS_*> $filter
548: *
549: * @return array<non-empty-string, ReflectionMethod>
550: */
551: public function getMethods(int $filter = 0): array
552: {
553: $methods = $this->getMethodsIndexedByLowercasedName(AlreadyVisitedClasses::createEmpty());
554:
555: if ($filter !== 0) {
556: $methods = array_filter(
557: $methods,
558: static fn (ReflectionMethod $method): bool => (bool) ($filter & $method->getModifiers()),
559: );
560: }
561:
562: return array_combine(
563: array_map(static fn (ReflectionMethod $method): string => $method->getName(), $methods),
564: $methods,
565: );
566: }
567:
568: /**
569: * Get only the methods that this class implements (i.e. do not search
570: * up parent classes etc.)
571: *
572: * @see ReflectionClass::getMethods for the usage of $filter
573: *
574: * @param int-mask-of<CoreReflectionMethod::IS_*> $filter
575: *
576: * @return array<non-empty-string, ReflectionMethod>
577: */
578: public function getImmediateMethods(int $filter = 0): array
579: {
580: if ($filter === 0) {
581: return $this->immediateMethods;
582: }
583:
584: return array_filter(
585: $this->immediateMethods,
586: static fn (ReflectionMethod $method): bool => (bool) ($filter & $method->getModifiers()),
587: );
588: }
589:
590: /** @return array<non-empty-string, ReflectionMethod>
591: * @param ClassNode|InterfaceNode|TraitNode|EnumNode $node */
592: private function createImmediateMethods($node, Reflector $reflector): array
593: {
594: $methods = [];
595:
596: foreach ($node->getMethods() as $methodNode) {
597: $method = ReflectionMethod::createFromMethodNode(
598: $reflector,
599: $methodNode,
600: $this->locatedSource,
601: $this->getNamespaceName(),
602: $this,
603: $this,
604: $this,
605: );
606:
607: if (array_key_exists($method->getName(), $methods)) {
608: continue;
609: }
610:
611: $methods[$method->getName()] = $method;
612: }
613:
614: if ($node instanceof EnumNode) {
615: $methods = $this->addEnumMethods($node, $methods);
616: }
617:
618: return $methods;
619: }
620:
621: /**
622: * @param array<non-empty-string, ReflectionMethod> $methods
623: *
624: * @return array<non-empty-string, ReflectionMethod>
625: */
626: private function addEnumMethods(EnumNode $node, array $methods): array
627: {
628: $internalLocatedSource = new InternalLocatedSource('', $this->getName(), 'Core', $this->getFileName());
629: $createMethod = function (string $name, array $params, $returnType) use ($internalLocatedSource): ReflectionMethod {
630: assert($name !== '');
631:
632: /** @var array{flags: int, params: Node\Param[], returnType: Node\Identifier|Node\NullableType} $classMethodSubnodes */
633: $classMethodSubnodes = [
634: 'flags' => Modifiers::PUBLIC | Modifiers::STATIC,
635: 'params' => $params,
636: 'returnType' => $returnType,
637: ];
638:
639: return ReflectionMethod::createFromMethodNode(
640: $this->reflector,
641: new ClassMethod(
642: new Node\Identifier($name),
643: $classMethodSubnodes,
644: ),
645: $internalLocatedSource,
646: $this->getNamespaceName(),
647: $this,
648: $this,
649: $this,
650: );
651: };
652:
653: $methods['cases'] = $createMethod('cases', [], new Node\Identifier('array'));
654:
655: if ($node->scalarType === null) {
656: return $methods;
657: }
658:
659: $valueParameter = new Node\Param(
660: new Node\Expr\Variable('value'),
661: null,
662: new Node\UnionType([new Node\Identifier('string'), new Node\Identifier('int')]),
663: );
664:
665: $methods['from'] = $createMethod(
666: 'from',
667: [$valueParameter],
668: new Node\Identifier('static'),
669: );
670:
671: $methods['tryFrom'] = $createMethod(
672: 'tryFrom',
673: [$valueParameter],
674: new Node\NullableType(new Node\Identifier('static')),
675: );
676:
677: return $methods;
678: }
679:
680: /**
681: * Get a single method with the name $methodName.
682: *
683: * @param non-empty-string $methodName
684: */
685: public function getMethod(string $methodName): ?\PHPStan\BetterReflection\Reflection\ReflectionMethod
686: {
687: $lowercaseMethodName = strtolower($methodName);
688: $methods = $this->getMethodsIndexedByLowercasedName(AlreadyVisitedClasses::createEmpty());
689:
690: return $methods[$lowercaseMethodName] ?? null;
691: }
692:
693: /**
694: * Does the class have the specified method?
695: *
696: * @param non-empty-string $methodName
697: */
698: public function hasMethod(string $methodName): bool
699: {
700: return $this->getMethod($methodName) !== null;
701: }
702:
703: /**
704: * Get an associative array of only the constants for this specific class (i.e. do not search
705: * up parent classes etc.), with keys as constant names and values as {@see ReflectionClassConstant} objects.
706: *
707: * @param int-mask-of<ReflectionClassConstantAdapter::IS_*> $filter
708: *
709: * @return array<non-empty-string, ReflectionClassConstant> indexed by name
710: */
711: public function getImmediateConstants(int $filter = 0): array
712: {
713: if ($filter === 0) {
714: return $this->immediateConstants;
715: }
716:
717: return array_filter(
718: $this->immediateConstants,
719: static fn (ReflectionClassConstant $constant): bool => (bool) ($filter & $constant->getModifiers()),
720: );
721: }
722:
723: /**
724: * Does this class have the specified constant?
725: *
726: * @param non-empty-string $name
727: */
728: public function hasConstant(string $name): bool
729: {
730: return $this->getConstant($name) !== null;
731: }
732:
733: /**
734: * Get the reflection object of the specified class constant.
735: *
736: * Returns null if not specified.
737: *
738: * @param non-empty-string $name
739: */
740: public function getConstant(string $name): ?\PHPStan\BetterReflection\Reflection\ReflectionClassConstant
741: {
742: return $this->getConstants()[$name] ?? null;
743: }
744:
745: /** @return array<non-empty-string, ReflectionClassConstant>
746: * @param ClassNode|InterfaceNode|TraitNode|EnumNode $node */
747: private function createImmediateConstants($node, Reflector $reflector): array
748: {
749: $constants = [];
750:
751: foreach ($node->getConstants() as $constantsNode) {
752: foreach (array_keys($constantsNode->consts) as $constantPositionInNode) {
753: assert(is_int($constantPositionInNode));
754: $constant = ReflectionClassConstant::createFromNode($reflector, $constantsNode, $constantPositionInNode, $this, $this);
755:
756: $constants[$constant->getName()] = $constant;
757: }
758: }
759:
760: return $constants;
761: }
762:
763: /**
764: * Get an associative array of the defined constants in this class,
765: * with keys as constant names and values as {@see ReflectionClassConstant} objects.
766: *
767: * @param int-mask-of<ReflectionClassConstantAdapter::IS_*> $filter
768: *
769: * @return array<non-empty-string, ReflectionClassConstant> indexed by name
770: */
771: public function getConstants(int $filter = 0): array
772: {
773: $constants = $this->getConstantsConsideringAlreadyVisitedClasses(AlreadyVisitedClasses::createEmpty());
774:
775: if ($filter === 0) {
776: return $constants;
777: }
778:
779: return array_filter(
780: $constants,
781: static fn (ReflectionClassConstant $constant): bool => (bool) ($filter & $constant->getModifiers()),
782: );
783: }
784:
785: /** @return array<non-empty-string, ReflectionClassConstant> indexed by name */
786: private function getConstantsConsideringAlreadyVisitedClasses(AlreadyVisitedClasses $alreadyVisitedClasses): array
787: {
788: if ($this->cachedConstants !== null) {
789: return $this->cachedConstants;
790: }
791:
792: $alreadyVisitedClasses->push($this->getName());
793:
794: // Note: constants are not merged via their name as array index, since internal PHP constant
795: // sorting does not follow `\array_merge()` semantics
796:
797: $constants = $this->getImmediateConstants();
798:
799: $parentClass = $this->getParentClass();
800: if ($parentClass !== null) {
801: foreach ($parentClass->getConstantsConsideringAlreadyVisitedClasses($alreadyVisitedClasses) as $constantName => $constant) {
802: if ($constant->isPrivate()) {
803: continue;
804: }
805:
806: if (array_key_exists($constantName, $constants)) {
807: continue;
808: }
809:
810: $constants[$constantName] = $constant;
811: }
812: }
813:
814: foreach ($this->getTraits() as $trait) {
815: foreach ($trait->getConstantsConsideringAlreadyVisitedClasses($alreadyVisitedClasses) as $constantName => $constant) {
816: if (array_key_exists($constantName, $constants)) {
817: continue;
818: }
819:
820: $constants[$constantName] = $constant->withImplementingClass($this);
821: }
822: }
823:
824: foreach ($this->getImmediateInterfaces() as $interface) {
825: $alreadyVisitedClassesCopy = clone $alreadyVisitedClasses;
826: foreach ($interface->getConstantsConsideringAlreadyVisitedClasses($alreadyVisitedClassesCopy) as $constantName => $constant) {
827: if (array_key_exists($constantName, $constants)) {
828: continue;
829: }
830:
831: $constants[$constantName] = $constant;
832: }
833: }
834:
835: $this->cachedConstants = $constants;
836:
837: return $this->cachedConstants;
838: }
839:
840: /**
841: * Get the constructor method for this class.
842: */
843: public function getConstructor(): ?\PHPStan\BetterReflection\Reflection\ReflectionMethod
844: {
845: if ($this->cachedConstructor !== null) {
846: return $this->cachedConstructor;
847: }
848:
849: $constructors = array_values(array_filter($this->getMethods(), static fn (ReflectionMethod $method): bool => $method->isConstructor()));
850:
851: return $this->cachedConstructor = $constructors[0] ?? null;
852: }
853:
854: /**
855: * Get only the properties for this specific class (i.e. do not search
856: * up parent classes etc.)
857: *
858: * @see ReflectionClass::getProperties() for the usage of filter
859: *
860: * @param int-mask-of<ReflectionPropertyAdapter::IS_*> $filter
861: *
862: * @return array<non-empty-string, ReflectionProperty>
863: */
864: public function getImmediateProperties(int $filter = 0): array
865: {
866: if ($filter === 0) {
867: return $this->immediateProperties;
868: }
869:
870: return array_filter(
871: $this->immediateProperties,
872: static fn (ReflectionProperty $property): bool => (bool) ($filter & $property->getModifiers()),
873: );
874: }
875:
876: /** @return array<non-empty-string, ReflectionProperty>
877: * @param ClassNode|InterfaceNode|TraitNode|EnumNode $node */
878: private function createImmediateProperties($node, Reflector $reflector): array
879: {
880: $properties = [];
881:
882: foreach ($node->getProperties() as $propertiesNode) {
883: foreach ($propertiesNode->props as $propertyPropertyNode) {
884: $property = ReflectionProperty::createFromNode(
885: $reflector,
886: $propertiesNode,
887: $propertyPropertyNode,
888: $this,
889: $this,
890: );
891: $properties[$property->getName()] = $property;
892: }
893: }
894:
895: foreach ($node->getMethods() as $methodNode) {
896: if ($methodNode->name->toLowerString() !== '__construct') {
897: continue;
898: }
899:
900: foreach ($methodNode->params as $parameterNode) {
901: if ($parameterNode->flags === 0 && $parameterNode->hooks === []) {
902: // No flags, no promotion
903: continue;
904: }
905:
906: $parameterNameNode = $parameterNode->var;
907: assert($parameterNameNode instanceof Node\Expr\Variable);
908: assert(is_string($parameterNameNode->name));
909:
910: $propertyNode = new Node\Stmt\Property(
911: $parameterNode->flags,
912: [new Node\PropertyItem($parameterNameNode->name)],
913: $parameterNode->getAttributes(),
914: $parameterNode->type,
915: $parameterNode->attrGroups,
916: $parameterNode->hooks,
917: );
918: $property = ReflectionProperty::createFromNode(
919: $reflector,
920: $propertyNode,
921: $propertyNode->props[0],
922: $this,
923: $this,
924: true,
925: );
926: $properties[$property->getName()] = $property;
927: }
928: }
929:
930: if ($node instanceof EnumNode || $node instanceof InterfaceNode) {
931: $properties = $this->addEnumProperties($properties, $node, $reflector);
932: }
933:
934: return $properties;
935: }
936:
937: /**
938: * @param array<non-empty-string, ReflectionProperty> $properties
939: *
940: * @return array<non-empty-string, ReflectionProperty>
941: * @param EnumNode|InterfaceNode $node
942: */
943: private function addEnumProperties(array $properties, $node, Reflector $reflector): array
944: {
945: $createProperty = function (string $name, $type) use ($reflector): ReflectionProperty {
946: $propertyNode = new Node\Stmt\Property(
947: Modifiers::PUBLIC | Modifiers::READONLY,
948: [new Node\PropertyItem($name)],
949: [],
950: $type,
951: );
952:
953: return ReflectionProperty::createFromNode(
954: $reflector,
955: $propertyNode,
956: $propertyNode->props[0],
957: $this,
958: $this,
959: );
960: };
961:
962: if ($node instanceof InterfaceNode) {
963: $interfaceName = $this->getName();
964: if ($interfaceName === 'UnitEnum') {
965: $properties['name'] = $createProperty('name', new Node\Identifier('string'));
966: }
967:
968: if ($interfaceName === 'BackedEnum') {
969: $properties['value'] = $createProperty('value', new Node\UnionType([
970: new Node\Identifier('int'),
971: new Node\Identifier('string'),
972: ]));
973: }
974: } else {
975: $properties['name'] = $createProperty('name', new Node\Identifier('string'));
976:
977: if ($node->scalarType !== null) {
978: $properties['value'] = $createProperty('value', $node->scalarType);
979: }
980: }
981:
982: return $properties;
983: }
984:
985: /**
986: * Get the properties for this class.
987: *
988: * Filter the results to include only properties with certain attributes. Defaults
989: * to no filtering.
990: * Any combination of \ReflectionProperty::IS_STATIC,
991: * \ReflectionProperty::IS_PUBLIC,
992: * \ReflectionProperty::IS_PROTECTED,
993: * \ReflectionProperty::IS_PRIVATE.
994: * For example if $filter = \ReflectionProperty::IS_STATIC | \ReflectionProperty::IS_PUBLIC
995: * only the static public properties will be returned
996: *
997: * @param int-mask-of<ReflectionPropertyAdapter::IS_*> $filter
998: *
999: * @return array<non-empty-string, ReflectionProperty>
1000: */
1001: public function getProperties(int $filter = 0): array
1002: {
1003: $properties = $this->getPropertiesConsideringAlreadyVisitedClasses(AlreadyVisitedClasses::createEmpty());
1004:
1005: if ($filter === 0) {
1006: return $properties;
1007: }
1008:
1009: return array_filter(
1010: $properties,
1011: static fn (ReflectionProperty $property): bool => (bool) ($filter & $property->getModifiers()),
1012: );
1013: }
1014:
1015: /** @return array<non-empty-string, ReflectionProperty> */
1016: private function getPropertiesConsideringAlreadyVisitedClasses(AlreadyVisitedClasses $alreadyVisitedClasses): array
1017: {
1018: if ($this->cachedProperties !== null) {
1019: return $this->cachedProperties;
1020: }
1021:
1022: $alreadyVisitedClasses->push($this->getName());
1023:
1024: $immediateProperties = $this->getImmediateProperties();
1025:
1026: // Merging together properties from parent class, interfaces, traits, current class (in this precise order)
1027:
1028: $properties = array_merge(
1029: array_filter(
1030: (($nullsafeVariable2 = $this->getParentClass()) ? $nullsafeVariable2->getPropertiesConsideringAlreadyVisitedClasses($alreadyVisitedClasses) : null) ?? [],
1031: static fn (ReflectionProperty $property) => ! $property->isPrivate(),
1032: ),
1033: ...array_map(
1034: static fn (ReflectionClass $ancestor): array => $ancestor->getPropertiesConsideringAlreadyVisitedClasses(clone $alreadyVisitedClasses),
1035: array_values($this->getImmediateInterfaces()),
1036: ),
1037: );
1038:
1039: foreach ($this->getTraits() as $trait) {
1040: foreach ($trait->getPropertiesConsideringAlreadyVisitedClasses($alreadyVisitedClasses) as $traitProperty) {
1041: $traitPropertyName = $traitProperty->getName();
1042:
1043: if (
1044: array_key_exists($traitPropertyName, $properties)
1045: || array_key_exists($traitPropertyName, $immediateProperties)
1046: ) {
1047: continue;
1048: }
1049:
1050: $properties[$traitPropertyName] = $traitProperty->withImplementingClass($this);
1051: }
1052: }
1053:
1054: // Merge immediate properties last to get the required order
1055: $properties = array_merge($properties, $immediateProperties);
1056:
1057: $this->cachedProperties = $properties;
1058:
1059: return $this->cachedProperties;
1060: }
1061:
1062: /**
1063: * Get the property called $name.
1064: *
1065: * Returns null if property does not exist.
1066: *
1067: * @param non-empty-string $name
1068: */
1069: public function getProperty(string $name): ?\PHPStan\BetterReflection\Reflection\ReflectionProperty
1070: {
1071: $properties = $this->getProperties();
1072:
1073: if (! isset($properties[$name])) {
1074: return null;
1075: }
1076:
1077: return $properties[$name];
1078: }
1079:
1080: /**
1081: * Does this class have the specified property?
1082: *
1083: * @param non-empty-string $name
1084: */
1085: public function hasProperty(string $name): bool
1086: {
1087: return $this->getProperty($name) !== null;
1088: }
1089:
1090: /** @return array<non-empty-string, mixed> */
1091: public function getDefaultProperties(): array
1092: {
1093: return array_map(
1094: static fn (ReflectionProperty $property) => $property->getDefaultValue(),
1095: $this->getProperties(),
1096: );
1097: }
1098:
1099: /** @return non-empty-string|null */
1100: public function getFileName(): ?string
1101: {
1102: return $this->locatedSource->getFileName();
1103: }
1104:
1105: public function getLocatedSource(): LocatedSource
1106: {
1107: return $this->locatedSource;
1108: }
1109:
1110: /**
1111: * Get the line number that this class starts on.
1112: *
1113: * @return positive-int
1114: */
1115: public function getStartLine(): int
1116: {
1117: return $this->startLine;
1118: }
1119:
1120: /**
1121: * Get the line number that this class ends on.
1122: *
1123: * @return positive-int
1124: */
1125: public function getEndLine(): int
1126: {
1127: return $this->endLine;
1128: }
1129:
1130: /** @return positive-int */
1131: public function getStartColumn(): int
1132: {
1133: return $this->startColumn;
1134: }
1135:
1136: /** @return positive-int */
1137: public function getEndColumn(): int
1138: {
1139: return $this->endColumn;
1140: }
1141:
1142: /**
1143: * Get the parent class, if it is defined.
1144: */
1145: public function getParentClass(): ?\PHPStan\BetterReflection\Reflection\ReflectionClass
1146: {
1147: $parentClassName = $this->getParentClassName();
1148: if ($parentClassName === null) {
1149: return null;
1150: }
1151:
1152: if ($this->name === $parentClassName) {
1153: throw CircularReference::fromClassName($parentClassName);
1154: }
1155:
1156: try {
1157: return $this->reflector->reflectClass($parentClassName);
1158: } catch (IdentifierNotFound $exception) {
1159: return null;
1160: }
1161: }
1162:
1163: /**
1164: * Gets the parent class names.
1165: *
1166: * @return list<class-string> A numerical array with parent class names as the values.
1167: */
1168: public function getParentClassNames(): array
1169: {
1170: return array_map(static fn (self $parentClass): string => $parentClass->getName(), $this->getParentClasses());
1171: }
1172:
1173: /** @return list<ReflectionClass> */
1174: private function getParentClasses(): array
1175: {
1176: if ($this->cachedParentClasses === null) {
1177: $parentClasses = [];
1178:
1179: $parentClassName = $this->parentClassName;
1180: while ($parentClassName !== null) {
1181: try {
1182: $parentClass = $this->reflector->reflectClass($parentClassName);
1183: } catch (IdentifierNotFound $exception) {
1184: break;
1185: }
1186:
1187: if (
1188: $this->name === $parentClassName
1189: || array_key_exists($parentClassName, $parentClasses)
1190: ) {
1191: throw CircularReference::fromClassName($parentClassName);
1192: }
1193:
1194: $parentClasses[$parentClassName] = $parentClass;
1195:
1196: $parentClassName = $parentClass->parentClassName;
1197: }
1198:
1199: $this->cachedParentClasses = array_values($parentClasses);
1200: }
1201:
1202: return $this->cachedParentClasses;
1203: }
1204:
1205: /** @return non-empty-string|null */
1206: public function getDocComment(): ?string
1207: {
1208: return $this->docComment;
1209: }
1210:
1211: public function isAnonymous(): bool
1212: {
1213: return $this->name === null;
1214: }
1215:
1216: /**
1217: * Is this an internal class?
1218: */
1219: public function isInternal(): bool
1220: {
1221: return $this->locatedSource->isInternal();
1222: }
1223:
1224: /**
1225: * Is this a user-defined function (will always return the opposite of
1226: * whatever isInternal returns).
1227: */
1228: public function isUserDefined(): bool
1229: {
1230: return ! $this->isInternal();
1231: }
1232:
1233: public function isDeprecated(): bool
1234: {
1235: return DeprecatedHelper::isDeprecated($this);
1236: }
1237:
1238: /**
1239: * Is this class an abstract class.
1240: */
1241: public function isAbstract(): bool
1242: {
1243: return (bool) ($this->modifiers & CoreReflectionClass::IS_EXPLICIT_ABSTRACT);
1244: }
1245:
1246: /**
1247: * Is this class a final class.
1248: */
1249: public function isFinal(): bool
1250: {
1251: if ($this->isEnum) {
1252: return true;
1253: }
1254:
1255: return (bool) ($this->modifiers & CoreReflectionClass::IS_FINAL);
1256: }
1257:
1258: public function isReadOnly(): bool
1259: {
1260: return (bool) ($this->modifiers & ReflectionClassAdapter::IS_READONLY_COMPATIBILITY);
1261: }
1262:
1263: /**
1264: * Get the core-reflection-compatible modifier values.
1265: *
1266: * @return int-mask-of<ReflectionClassAdapter::IS_*>
1267: */
1268: public function getModifiers(): int
1269: {
1270: return $this->modifiers;
1271: }
1272:
1273: /**
1274: * @return int-mask-of<ReflectionClassAdapter::IS_*>
1275: *
1276: * @phpstan-ignore-next-line return.unusedType
1277: * @param ClassNode|InterfaceNode|TraitNode|EnumNode $node
1278: */
1279: private function computeModifiers($node): int
1280: {
1281: if (! $node instanceof ClassNode) {
1282: return 0;
1283: }
1284:
1285: $modifiers = $node->isAbstract() ? CoreReflectionClass::IS_EXPLICIT_ABSTRACT : 0;
1286: $modifiers += $node->isFinal() ? CoreReflectionClass::IS_FINAL : 0;
1287: $modifiers += $node->isReadonly() ? ReflectionClassAdapter::IS_READONLY_COMPATIBILITY : 0;
1288:
1289: return $modifiers;
1290: }
1291:
1292: /**
1293: * Is this reflection a trait?
1294: */
1295: public function isTrait(): bool
1296: {
1297: return $this->isTrait;
1298: }
1299:
1300: /**
1301: * Is this reflection an interface?
1302: */
1303: public function isInterface(): bool
1304: {
1305: return $this->isInterface;
1306: }
1307:
1308: /**
1309: * Get the traits used, if any are defined. If this class does not have any
1310: * defined traits, this will return an empty array.
1311: *
1312: * @return list<ReflectionClass>
1313: */
1314: public function getTraits(): array
1315: {
1316: if ($this->cachedTraits !== null) {
1317: return $this->cachedTraits;
1318: }
1319:
1320: $traits = [];
1321: foreach ($this->traitClassNames as $traitClassName) {
1322: try {
1323: $traits[] = $this->reflector->reflectClass($traitClassName);
1324: } catch (IdentifierNotFound $exception) {
1325: // pass
1326: }
1327: }
1328:
1329: return $this->cachedTraits = $traits;
1330: }
1331:
1332: /**
1333: * @param array<class-string, self> $interfaces
1334: *
1335: * @return array<class-string, self>
1336: */
1337: private function addStringableInterface(array $interfaces): array
1338: {
1339: if (BetterReflection::$phpVersion < 80000) {
1340: return $interfaces;
1341: }
1342:
1343: /** @psalm-var class-string $stringableClassName */
1344: $stringableClassName = Stringable::class;
1345:
1346: if (array_key_exists($stringableClassName, $interfaces) || ($this->isInterface && $this->getName() === $stringableClassName)) {
1347: return $interfaces;
1348: }
1349:
1350: foreach (array_keys($this->immediateMethods) as $immediateMethodName) {
1351: if (strtolower($immediateMethodName) === '__tostring') {
1352: try {
1353: $stringableInterfaceReflection = $this->reflector->reflectClass($stringableClassName);
1354: $interfaces[$stringableClassName] = $stringableInterfaceReflection;
1355: } catch (IdentifierNotFound $exception) {
1356: // Stringable interface does not exist on target PHP version
1357: }
1358:
1359: // @infection-ignore-all Break_: There's no difference between break and continue - break is just optimization
1360: break;
1361: }
1362: }
1363:
1364: return $interfaces;
1365: }
1366:
1367: /**
1368: * @param array<class-string, self> $interfaces
1369: *
1370: * @return array<class-string, self>
1371: *
1372: * @psalm-suppress MoreSpecificReturnType
1373: */
1374: private function addEnumInterfaces(array $interfaces): array
1375: {
1376: assert($this->isEnum === true);
1377:
1378: $interfaces[UnitEnum::class] = $this->reflector->reflectClass(UnitEnum::class);
1379:
1380: if ($this->isBackedEnum) {
1381: $interfaces[BackedEnum::class] = $this->reflector->reflectClass(BackedEnum::class);
1382: }
1383:
1384: /** @psalm-suppress LessSpecificReturnStatement */
1385: return $interfaces;
1386: }
1387:
1388: /** @return list<trait-string> */
1389: public function getTraitClassNames(): array
1390: {
1391: return $this->traitClassNames;
1392: }
1393:
1394: /**
1395: * Get the names of the traits used as an array of strings, if any are
1396: * defined. If this class does not have any defined traits, this will
1397: * return an empty array.
1398: *
1399: * @return list<trait-string>
1400: */
1401: public function getTraitNames(): array
1402: {
1403: return array_map(
1404: static function (ReflectionClass $trait): string {
1405: /** @psalm-var trait-string $traitName */
1406: $traitName = $trait->getName();
1407:
1408: return $traitName;
1409: },
1410: $this->getTraits(),
1411: );
1412: }
1413:
1414: /**
1415: * Return a list of the aliases used when importing traits for this class.
1416: * The returned array is in key/value pair in this format:.
1417: *
1418: * 'aliasedMethodName' => 'ActualClass::actualMethod'
1419: *
1420: * @return array<non-empty-string, non-empty-string>
1421: *
1422: * @example
1423: * // When reflecting a class such as:
1424: * class Foo
1425: * {
1426: * use MyTrait {
1427: * myTraitMethod as myAliasedMethod;
1428: * }
1429: * }
1430: * // This method would return
1431: * // ['myAliasedMethod' => 'MyTrait::myTraitMethod']
1432: */
1433: public function getTraitAliases(): array
1434: {
1435: if ($this->traitsData['aliases'] === []) {
1436: return [];
1437: }
1438:
1439: $traits = array_combine($this->traitClassNames, $this->getTraits());
1440: $traitAliases = [];
1441:
1442: foreach ($this->traitsData['aliases'] as $traitClassName => $traitAliasDefinitions) {
1443: foreach ($traitAliasDefinitions as $traitAliasDefinition) {
1444: if (! $traits[$traitClassName]->hasMethod($traitAliasDefinition['method'])) {
1445: continue;
1446: }
1447:
1448: $traitAliases[$traitAliasDefinition['alias']] = $this->traitsData['hashes'][$traitAliasDefinition['hash']];
1449: }
1450: }
1451:
1452: return $traitAliases;
1453: }
1454:
1455: /**
1456: * Returns data when importing traits for this class:
1457: *
1458: * 'aliases': List of the aliases used when importing traits. In format:
1459: *
1460: * 'traitClassName' => ['alias' => 'aliasedMethodName', 'method' => 'actualMethodName', 'hash' => 'traitClassName::actualMethodName'],
1461: *
1462: * Example:
1463: * // When reflecting a code such as:
1464: *
1465: * use MyTrait {
1466: * myTraitMethod as myAliasedMethod;
1467: * }
1468: *
1469: * // This method would return
1470: * // ['MyTrait' => ['alias' => 'myAliasedMethod', 'method' => 'myTraitMethod', 'hash' => 'mytrait::mytraitmethod']]
1471: *
1472: * 'modifiers': Used modifiers when importing traits. In format:
1473: *
1474: * 'methodName' => 'modifier'
1475: *
1476: * Example:
1477: * // When reflecting a code such as:
1478: *
1479: * use MyTrait {
1480: * myTraitMethod as public;
1481: * }
1482: *
1483: * // This method would return
1484: * // ['myTraitMethod' => 1]
1485: *
1486: * 'precedences': Precedences used when importing traits. In format:
1487: *
1488: * 'Class::method' => 'Class::method'
1489: *
1490: * Example:
1491: * // When reflecting a code such as:
1492: *
1493: * use MyTrait, MyTrait2 {
1494: * MyTrait2::foo insteadof MyTrait1;
1495: * }
1496: *
1497: * // This method would return
1498: * // ['MyTrait1::foo' => 'MyTrait2::foo']
1499: *
1500: * @return array{
1501: * aliases: array<trait-string, list<array{alias: non-empty-string, method: non-empty-string, hash: non-empty-string}>>,
1502: * modifiers: array<non-empty-string, int-mask-of<ReflectionMethodAdapter::IS_*>>,
1503: * precedences: array<non-empty-string, non-empty-string>,
1504: * hashes: array<non-empty-string, non-empty-string>,
1505: * }
1506: * @param ClassNode|InterfaceNode|TraitNode|EnumNode $node
1507: */
1508: private function computeTraitsData($node): array
1509: {
1510: $traitsData = [
1511: 'aliases' => [],
1512: 'modifiers' => [],
1513: 'precedences' => [],
1514: 'hashes' => [],
1515: ];
1516:
1517: foreach ($node->getTraitUses() as $traitUsage) {
1518: foreach ($traitUsage->adaptations as $adaptation) {
1519: $usedTraits = $adaptation->trait !== null ? [$adaptation->trait] : $traitUsage->traits;
1520: $traitsData = $this->processTraitAdaptation($adaptation, $usedTraits, $traitsData);
1521: }
1522: }
1523:
1524: return $traitsData;
1525: }
1526:
1527: /**
1528: * @phpcs:disable Squiz.Commenting.FunctionComment.MissingParamName
1529: *
1530: * @param array<array-key, Node\Name> $usedTraits
1531: * @param array{
1532: * aliases: array<trait-string, list<array{alias: non-empty-string, method: non-empty-string, hash: non-empty-string}>>,
1533: * modifiers: array<non-empty-string, int-mask-of<ReflectionMethodAdapter::IS_*>>,
1534: * precedences: array<non-empty-string, non-empty-string>,
1535: * hashes: array<non-empty-string, non-empty-string>,
1536: * } $traitsData
1537: *
1538: * @return array{
1539: * aliases: array<trait-string, list<array{alias: non-empty-string, method: non-empty-string, hash: non-empty-string}>>,
1540: * modifiers: array<non-empty-string, int-mask-of<ReflectionMethodAdapter::IS_*>>,
1541: * precedences: array<non-empty-string, non-empty-string>,
1542: * hashes: array<non-empty-string, non-empty-string>,
1543: * }
1544: */
1545: private function processTraitAdaptation(Node\Stmt\TraitUseAdaptation $adaptation, array $usedTraits, array $traitsData): array
1546: {
1547: foreach ($usedTraits as $usedTrait) {
1548: $methodHash = $this->methodHash($usedTrait->toString(), $adaptation->method->toString());
1549: $lowerCasedMethodHash = $this->lowerCasedMethodHash($usedTrait->toString(), $adaptation->method->toString());
1550:
1551: $traitsData['hashes'][$lowerCasedMethodHash] = $methodHash;
1552:
1553: if ($adaptation instanceof Node\Stmt\TraitUseAdaptation\Alias) {
1554: if ($adaptation->newModifier !== null) {
1555: /** @var int-mask-of<ReflectionMethodAdapter::IS_*> $modifier */
1556: $modifier = $adaptation->newModifier;
1557: $traitsData['modifiers'][$lowerCasedMethodHash] = $modifier;
1558: }
1559:
1560: if ($adaptation->newName !== null) {
1561: // We need to save all possible combinations of trait and method names
1562: // The real aliases will be filtered in getters
1563: /** @var trait-string $usedTraitClassName */
1564: $usedTraitClassName = $usedTrait->toString();
1565: $traitsData['aliases'][$usedTraitClassName][] = [
1566: 'alias' => $adaptation->newName->name,
1567: 'method' => $adaptation->method->toString(),
1568: 'hash' => $lowerCasedMethodHash,
1569: ];
1570: }
1571: } elseif ($adaptation instanceof Node\Stmt\TraitUseAdaptation\Precedence) {
1572: foreach ($adaptation->insteadof as $insteadof) {
1573: $adaptationNameHash = $this->lowerCasedMethodHash($insteadof->toString(), $adaptation->method->toString());
1574:
1575: $traitsData['precedences'][$adaptationNameHash] = $lowerCasedMethodHash;
1576: }
1577: }
1578: }
1579:
1580: return $traitsData;
1581: }
1582:
1583: /**
1584: * @return non-empty-string
1585: *
1586: * @psalm-pure
1587: */
1588: private function methodHash(string $className, string $methodName): string
1589: {
1590: return sprintf(
1591: '%s::%s',
1592: $className,
1593: $methodName,
1594: );
1595: }
1596:
1597: /** @return non-empty-string */
1598: private function lowerCasedMethodHash(string $className, string $methodName): string
1599: {
1600: return strtolower($this->methodHash($className, $methodName));
1601: }
1602:
1603: /** @return list<class-string> */
1604: public function getInterfaceClassNames(): array
1605: {
1606: return $this->implementsClassNames;
1607: }
1608:
1609: /**
1610: * Gets the interfaces.
1611: *
1612: * @link https://php.net/manual/en/reflectionclass.getinterfaces.php
1613: *
1614: * @return array<class-string, self> An associative array of interfaces, with keys as interface names and the array
1615: * values as {@see ReflectionClass} objects.
1616: */
1617: public function getInterfaces(): array
1618: {
1619: if ($this->cachedInterfaces !== null) {
1620: return $this->cachedInterfaces;
1621: }
1622:
1623: $interfaces = array_merge(
1624: [$this->getCurrentClassImplementedInterfacesIndexedByName()],
1625: array_map(
1626: static fn (self $parentClass): array => $parentClass->getCurrentClassImplementedInterfacesIndexedByName(),
1627: $this->getParentClasses(),
1628: ),
1629: );
1630:
1631: return $this->cachedInterfaces = array_merge(...array_reverse($interfaces));
1632: }
1633:
1634: /**
1635: * Get only the interfaces that this class implements (i.e. do not search
1636: * up parent classes etc.)
1637: *
1638: * @return array<class-string, self>
1639: */
1640: public function getImmediateInterfaces(): array
1641: {
1642: if ($this->isTrait) {
1643: return [];
1644: }
1645:
1646: $interfaces = [];
1647: foreach ($this->implementsClassNames as $interfaceClassName) {
1648: try {
1649: $interfaces[$interfaceClassName] = $this->reflector->reflectClass($interfaceClassName);
1650: } catch (IdentifierNotFound $exception) {
1651: continue;
1652: }
1653: }
1654:
1655: if ($this->isEnum) {
1656: $interfaces = $this->addEnumInterfaces($interfaces);
1657: }
1658:
1659: return $this->addStringableInterface($interfaces);
1660: }
1661:
1662: /**
1663: * Gets the interface names.
1664: *
1665: * @link https://php.net/manual/en/reflectionclass.getinterfacenames.php
1666: *
1667: * @return list<class-string> A numerical array with interface names as the values.
1668: */
1669: public function getInterfaceNames(): array
1670: {
1671: if ($this->cachedInterfaceNames !== null) {
1672: return $this->cachedInterfaceNames;
1673: }
1674:
1675: return $this->cachedInterfaceNames = array_values(array_map(
1676: static fn (self $interface): string => $interface->getName(),
1677: $this->getInterfaces(),
1678: ));
1679: }
1680:
1681: /**
1682: * Checks whether the given object is an instance.
1683: *
1684: * @link https://php.net/manual/en/reflectionclass.isinstance.php
1685: */
1686: public function isInstance(object $object): bool
1687: {
1688: $className = $this->getName();
1689:
1690: // note: since $object was loaded, we can safely assume that $className is available in the current
1691: // php script execution context
1692: return $object instanceof $className;
1693: }
1694:
1695: /**
1696: * Checks whether the given class string is a subclass of this class.
1697: *
1698: * @link https://php.net/manual/en/reflectionclass.isinstance.php
1699: */
1700: public function isSubclassOf(string $className): bool
1701: {
1702: return in_array(
1703: ltrim($className, '\\'),
1704: $this->getParentClassNames(),
1705: true,
1706: );
1707: }
1708:
1709: /**
1710: * Checks whether this class implements the given interface.
1711: *
1712: * @link https://php.net/manual/en/reflectionclass.implementsinterface.php
1713: */
1714: public function implementsInterface(string $interfaceName): bool
1715: {
1716: return in_array(ltrim($interfaceName, '\\'), $this->getInterfaceNames(), true);
1717: }
1718:
1719: /**
1720: * Checks whether this reflection is an instantiable class
1721: *
1722: * @link https://php.net/manual/en/reflectionclass.isinstantiable.php
1723: */
1724: public function isInstantiable(): bool
1725: {
1726: // @TODO doesn't consider internal non-instantiable classes yet.
1727:
1728: if ($this->isAbstract()) {
1729: return false;
1730: }
1731:
1732: if ($this->isInterface()) {
1733: return false;
1734: }
1735:
1736: if ($this->isTrait()) {
1737: return false;
1738: }
1739:
1740: $constructor = $this->getConstructor();
1741:
1742: if ($constructor === null) {
1743: return true;
1744: }
1745:
1746: return $constructor->isPublic();
1747: }
1748:
1749: /**
1750: * Checks whether this is a reflection of a class that supports the clone operator
1751: *
1752: * @link https://php.net/manual/en/reflectionclass.iscloneable.php
1753: */
1754: public function isCloneable(): bool
1755: {
1756: if (! $this->isInstantiable()) {
1757: return false;
1758: }
1759:
1760: $cloneMethod = $this->getMethod('__clone');
1761:
1762: if ($cloneMethod === null) {
1763: return true;
1764: }
1765:
1766: return $cloneMethod->isPublic();
1767: }
1768:
1769: /**
1770: * Checks if iterateable
1771: *
1772: * @link https://php.net/manual/en/reflectionclass.isiterateable.php
1773: */
1774: public function isIterateable(): bool
1775: {
1776: return $this->isInstantiable() && $this->implementsInterface(Traversable::class);
1777: }
1778:
1779: public function isEnum(): bool
1780: {
1781: return $this->isEnum;
1782: }
1783:
1784: /** @return array<class-string, ReflectionClass> */
1785: private function getCurrentClassImplementedInterfacesIndexedByName(): array
1786: {
1787: if ($this->isTrait) {
1788: return [];
1789: }
1790:
1791: if ($this->isInterface) {
1792: // assumption: first key is the current interface
1793: return array_slice($this->getInterfacesHierarchy(AlreadyVisitedClasses::createEmpty()), 1);
1794: }
1795:
1796: $interfaces = [];
1797: foreach ($this->implementsClassNames as $name) {
1798: try {
1799: $interface = $this->reflector->reflectClass($name);
1800: foreach ($interface->getInterfacesHierarchy(AlreadyVisitedClasses::createEmpty()) as $n => $i) {
1801: $interfaces[$n] = $i;
1802: }
1803: } catch (IdentifierNotFound $exception) {
1804: continue;
1805: }
1806: }
1807:
1808: if ($this->isEnum) {
1809: $interfaces = $this->addEnumInterfaces($interfaces);
1810: }
1811:
1812: return $this->addStringableInterface($interfaces);
1813: }
1814:
1815: /**
1816: * This method allows us to retrieve all interfaces parent of this interface. Do not use on class nodes!
1817: *
1818: * @return array<class-string, ReflectionClass> parent interfaces of this interface
1819: */
1820: private function getInterfacesHierarchy(AlreadyVisitedClasses $alreadyVisitedClasses): array
1821: {
1822: if (! $this->isInterface) {
1823: return [];
1824: }
1825:
1826: $interfaceClassName = $this->getName();
1827: $alreadyVisitedClasses->push($interfaceClassName);
1828:
1829: /** @var array<class-string, self> $interfaces */
1830: $interfaces = [$interfaceClassName => $this];
1831: foreach ($this->getImmediateInterfaces() as $interface) {
1832: $alreadyVisitedClassesCopyForInterface = clone $alreadyVisitedClasses;
1833: foreach ($interface->getInterfacesHierarchy($alreadyVisitedClassesCopyForInterface) as $extendedInterfaceName => $extendedInterface) {
1834: $interfaces[$extendedInterfaceName] = $extendedInterface;
1835: }
1836: }
1837:
1838: return $this->addStringableInterface($interfaces);
1839: }
1840:
1841: /**
1842: * Get the value of a static property, if it exists. Throws a
1843: * PropertyDoesNotExist exception if it does not exist or is not static.
1844: * (note, differs very slightly from internal reflection behaviour)
1845: *
1846: * @param non-empty-string $propertyName
1847: *
1848: * @throws ClassDoesNotExist
1849: * @throws NoObjectProvided
1850: * @throws NotAnObject
1851: * @throws ObjectNotInstanceOfClass
1852: * @return mixed
1853: */
1854: public function getStaticPropertyValue(string $propertyName)
1855: {
1856: $property = $this->getProperty($propertyName);
1857:
1858: if (! $property || ! $property->isStatic()) {
1859: throw PropertyDoesNotExist::fromName($propertyName);
1860: }
1861:
1862: return $property->getValue();
1863: }
1864:
1865: /**
1866: * Set the value of a static property
1867: *
1868: * @param non-empty-string $propertyName
1869: *
1870: * @throws ClassDoesNotExist
1871: * @throws NoObjectProvided
1872: * @throws NotAnObject
1873: * @throws ObjectNotInstanceOfClass
1874: * @param mixed $value
1875: */
1876: public function setStaticPropertyValue(string $propertyName, $value): void
1877: {
1878: $property = $this->getProperty($propertyName);
1879:
1880: if (! $property || ! $property->isStatic()) {
1881: throw PropertyDoesNotExist::fromName($propertyName);
1882: }
1883:
1884: $property->setValue($value);
1885: }
1886:
1887: /** @return array<non-empty-string, mixed> */
1888: public function getStaticProperties(): array
1889: {
1890: $staticProperties = [];
1891:
1892: foreach ($this->getProperties() as $property) {
1893: if (! $property->isStatic()) {
1894: continue;
1895: }
1896:
1897: /** @psalm-suppress MixedAssignment */
1898: $staticProperties[$property->getName()] = $property->getValue();
1899: }
1900:
1901: return $staticProperties;
1902: }
1903:
1904: /** @return list<ReflectionAttribute> */
1905: public function getAttributes(): array
1906: {
1907: return $this->attributes;
1908: }
1909:
1910: /** @return list<ReflectionAttribute> */
1911: public function getAttributesByName(string $name): array
1912: {
1913: return ReflectionAttributeHelper::filterAttributesByName($this->getAttributes(), $name);
1914: }
1915:
1916: /**
1917: * @param class-string $className
1918: *
1919: * @return list<ReflectionAttribute>
1920: */
1921: public function getAttributesByInstance(string $className): array
1922: {
1923: return ReflectionAttributeHelper::filterAttributesByInstance($this->getAttributes(), $className);
1924: }
1925: }
1926: