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