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