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: $methods = $this->immediateMethods;
1380: foreach ($this->getTraits() as $trait) {
1381: $methods += $trait->immediateMethods;
1382: }
1383:
1384: foreach (array_keys($methods) as $immediateMethodName) {
1385: if (strtolower($immediateMethodName) === '__tostring') {
1386: try {
1387: $stringableInterfaceReflection = $this->reflector->reflectClass($stringableClassName);
1388: $interfaces[$stringableClassName] = $stringableInterfaceReflection;
1389: } catch (IdentifierNotFound $exception) {
1390: // Stringable interface does not exist on target PHP version
1391: }
1392:
1393: // @infection-ignore-all Break_: There's no difference between break and continue - break is just optimization
1394: break;
1395: }
1396: }
1397:
1398: return $interfaces;
1399: }
1400:
1401: /**
1402: * @param array<class-string, self> $interfaces
1403: *
1404: * @return array<class-string, self>
1405: *
1406: * @psalm-suppress MoreSpecificReturnType
1407: */
1408: private function addEnumInterfaces(array $interfaces): array
1409: {
1410: assert($this->isEnum === true);
1411:
1412: $interfaces[UnitEnum::class] = $this->reflector->reflectClass(UnitEnum::class);
1413:
1414: if ($this->isBackedEnum) {
1415: $interfaces[BackedEnum::class] = $this->reflector->reflectClass(BackedEnum::class);
1416: }
1417:
1418: /** @psalm-suppress LessSpecificReturnStatement */
1419: return $interfaces;
1420: }
1421:
1422: /** @return list<trait-string> */
1423: public function getTraitClassNames(): array
1424: {
1425: return $this->traitClassNames;
1426: }
1427:
1428: /**
1429: * Get the names of the traits used as an array of strings, if any are
1430: * defined. If this class does not have any defined traits, this will
1431: * return an empty array.
1432: *
1433: * @return list<trait-string>
1434: */
1435: public function getTraitNames(): array
1436: {
1437: return array_map(
1438: static function (ReflectionClass $trait): string {
1439: /** @psalm-var trait-string $traitName */
1440: $traitName = $trait->getName();
1441:
1442: return $traitName;
1443: },
1444: $this->getTraits(),
1445: );
1446: }
1447:
1448: /**
1449: * Return a list of the aliases used when importing traits for this class.
1450: * The returned array is in key/value pair in this format:.
1451: *
1452: * 'aliasedMethodName' => 'ActualClass::actualMethod'
1453: *
1454: * @return array<non-empty-string, non-empty-string>
1455: *
1456: * @example
1457: * // When reflecting a class such as:
1458: * class Foo
1459: * {
1460: * use MyTrait {
1461: * myTraitMethod as myAliasedMethod;
1462: * }
1463: * }
1464: * // This method would return
1465: * // ['myAliasedMethod' => 'MyTrait::myTraitMethod']
1466: */
1467: public function getTraitAliases(): array
1468: {
1469: if ($this->traitsData['aliases'] === []) {
1470: return [];
1471: }
1472:
1473: $traits = [];
1474: foreach ($this->getTraits() as $trait) {
1475: $traits[$trait->getName()] = $trait;
1476: }
1477: $traitAliases = [];
1478:
1479: foreach ($this->traitsData['aliases'] as $traitClassName => $traitAliasDefinitions) {
1480: foreach ($traitAliasDefinitions as $traitAliasDefinition) {
1481: if (!array_key_exists($traitClassName, $traits)) {
1482: continue;
1483: }
1484: if (! $traits[$traitClassName]->hasMethod($traitAliasDefinition['method'])) {
1485: continue;
1486: }
1487:
1488: $traitAliases[$traitAliasDefinition['alias']] = $this->traitsData['hashes'][$traitAliasDefinition['hash']];
1489: }
1490: }
1491:
1492: return $traitAliases;
1493: }
1494:
1495: /**
1496: * Returns data when importing traits for this class:
1497: *
1498: * 'aliases': List of the aliases used when importing traits. In format:
1499: *
1500: * 'traitClassName' => ['alias' => 'aliasedMethodName', 'method' => 'actualMethodName', 'hash' => 'traitClassName::actualMethodName'],
1501: *
1502: * Example:
1503: * // When reflecting a code such as:
1504: *
1505: * use MyTrait {
1506: * myTraitMethod as myAliasedMethod;
1507: * }
1508: *
1509: * // This method would return
1510: * // ['MyTrait' => ['alias' => 'myAliasedMethod', 'method' => 'myTraitMethod', 'hash' => 'mytrait::mytraitmethod']]
1511: *
1512: * 'modifiers': Used modifiers when importing traits. In format:
1513: *
1514: * 'methodName' => 'modifier'
1515: *
1516: * Example:
1517: * // When reflecting a code such as:
1518: *
1519: * use MyTrait {
1520: * myTraitMethod as public;
1521: * }
1522: *
1523: * // This method would return
1524: * // ['myTraitMethod' => 1]
1525: *
1526: * 'precedences': Precedences used when importing traits. In format:
1527: *
1528: * 'Class::method' => 'Class::method'
1529: *
1530: * Example:
1531: * // When reflecting a code such as:
1532: *
1533: * use MyTrait, MyTrait2 {
1534: * MyTrait2::foo insteadof MyTrait1;
1535: * }
1536: *
1537: * // This method would return
1538: * // ['MyTrait1::foo' => 'MyTrait2::foo']
1539: *
1540: * @return array{
1541: * aliases: array<trait-string, list<array{alias: non-empty-string, method: non-empty-string, hash: non-empty-string}>>,
1542: * modifiers: array<non-empty-string, int-mask-of<ReflectionMethodAdapter::IS_*>>,
1543: * precedences: array<non-empty-string, non-empty-string>,
1544: * hashes: array<non-empty-string, non-empty-string>,
1545: * }
1546: * @param ClassNode|InterfaceNode|TraitNode|EnumNode $node
1547: */
1548: private function computeTraitsData($node): array
1549: {
1550: $traitsData = [
1551: 'aliases' => [],
1552: 'modifiers' => [],
1553: 'precedences' => [],
1554: 'hashes' => [],
1555: ];
1556:
1557: foreach ($node->getTraitUses() as $traitUsage) {
1558: foreach ($traitUsage->adaptations as $adaptation) {
1559: $usedTraits = $adaptation->trait !== null ? [$adaptation->trait] : $traitUsage->traits;
1560: $traitsData = $this->processTraitAdaptation($adaptation, $usedTraits, $traitsData);
1561: }
1562: }
1563:
1564: return $traitsData;
1565: }
1566:
1567: /**
1568: * @phpcs:disable Squiz.Commenting.FunctionComment.MissingParamName
1569: *
1570: * @param array<array-key, Node\Name> $usedTraits
1571: * @param array{
1572: * aliases: array<trait-string, list<array{alias: non-empty-string, method: non-empty-string, hash: non-empty-string}>>,
1573: * modifiers: array<non-empty-string, int-mask-of<ReflectionMethodAdapter::IS_*>>,
1574: * precedences: array<non-empty-string, non-empty-string>,
1575: * hashes: array<non-empty-string, non-empty-string>,
1576: * } $traitsData
1577: *
1578: * @return array{
1579: * aliases: array<trait-string, list<array{alias: non-empty-string, method: non-empty-string, hash: non-empty-string}>>,
1580: * modifiers: array<non-empty-string, int-mask-of<ReflectionMethodAdapter::IS_*>>,
1581: * precedences: array<non-empty-string, non-empty-string>,
1582: * hashes: array<non-empty-string, non-empty-string>,
1583: * }
1584: */
1585: private function processTraitAdaptation(Node\Stmt\TraitUseAdaptation $adaptation, array $usedTraits, array $traitsData): array
1586: {
1587: foreach ($usedTraits as $usedTrait) {
1588: $methodHash = $this->methodHash($usedTrait->toString(), $adaptation->method->toString());
1589: $lowerCasedMethodHash = $this->lowerCasedMethodHash($usedTrait->toString(), $adaptation->method->toString());
1590:
1591: $traitsData['hashes'][$lowerCasedMethodHash] = $methodHash;
1592:
1593: if ($adaptation instanceof Node\Stmt\TraitUseAdaptation\Alias) {
1594: if ($adaptation->newModifier !== null) {
1595: /** @var int-mask-of<ReflectionMethodAdapter::IS_*> $modifier */
1596: $modifier = $adaptation->newModifier;
1597: $traitsData['modifiers'][$lowerCasedMethodHash] = $modifier;
1598: }
1599:
1600: if ($adaptation->newName !== null) {
1601: // We need to save all possible combinations of trait and method names
1602: // The real aliases will be filtered in getters
1603: /** @var trait-string $usedTraitClassName */
1604: $usedTraitClassName = $usedTrait->toString();
1605: $traitsData['aliases'][$usedTraitClassName][] = [
1606: 'alias' => $adaptation->newName->name,
1607: 'method' => $adaptation->method->toString(),
1608: 'hash' => $lowerCasedMethodHash,
1609: ];
1610: }
1611: } elseif ($adaptation instanceof Node\Stmt\TraitUseAdaptation\Precedence) {
1612: foreach ($adaptation->insteadof as $insteadof) {
1613: $adaptationNameHash = $this->lowerCasedMethodHash($insteadof->toString(), $adaptation->method->toString());
1614:
1615: $traitsData['precedences'][$adaptationNameHash] = $lowerCasedMethodHash;
1616: }
1617: }
1618: }
1619:
1620: return $traitsData;
1621: }
1622:
1623: /**
1624: * @return non-empty-string
1625: *
1626: * @psalm-pure
1627: */
1628: private function methodHash(string $className, string $methodName): string
1629: {
1630: return sprintf(
1631: '%s::%s',
1632: $className,
1633: $methodName,
1634: );
1635: }
1636:
1637: /** @return non-empty-string */
1638: private function lowerCasedMethodHash(string $className, string $methodName): string
1639: {
1640: return strtolower($this->methodHash($className, $methodName));
1641: }
1642:
1643: /** @return list<class-string> */
1644: public function getInterfaceClassNames(): array
1645: {
1646: return $this->implementsClassNames;
1647: }
1648:
1649: /**
1650: * Gets the interfaces.
1651: *
1652: * @link https://php.net/manual/en/reflectionclass.getinterfaces.php
1653: *
1654: * @return array<class-string, self> An associative array of interfaces, with keys as interface names and the array
1655: * values as {@see ReflectionClass} objects.
1656: */
1657: public function getInterfaces(): array
1658: {
1659: if ($this->cachedInterfaces !== null) {
1660: return $this->cachedInterfaces;
1661: }
1662:
1663: $interfaces = array_merge(
1664: [$this->getCurrentClassImplementedInterfacesIndexedByName()],
1665: array_map(
1666: static fn (self $parentClass): array => $parentClass->getCurrentClassImplementedInterfacesIndexedByName(),
1667: $this->getParentClasses(),
1668: ),
1669: );
1670:
1671: return $this->cachedInterfaces = array_merge(...array_reverse($interfaces));
1672: }
1673:
1674: /**
1675: * Get only the interfaces that this class implements (i.e. do not search
1676: * up parent classes etc.)
1677: *
1678: * @return array<class-string, self>
1679: */
1680: public function getImmediateInterfaces(): array
1681: {
1682: if ($this->isTrait) {
1683: return [];
1684: }
1685:
1686: $interfaces = [];
1687: foreach ($this->implementsClassNames as $interfaceClassName) {
1688: try {
1689: $interfaces[$interfaceClassName] = $this->reflector->reflectClass($interfaceClassName);
1690: } catch (IdentifierNotFound $exception) {
1691: continue;
1692: }
1693: }
1694:
1695: if ($this->isEnum) {
1696: $interfaces = $this->addEnumInterfaces($interfaces);
1697: }
1698:
1699: return $this->addStringableInterface($interfaces);
1700: }
1701:
1702: /**
1703: * Gets the interface names.
1704: *
1705: * @link https://php.net/manual/en/reflectionclass.getinterfacenames.php
1706: *
1707: * @return list<class-string> A numerical array with interface names as the values.
1708: */
1709: public function getInterfaceNames(): array
1710: {
1711: if ($this->cachedInterfaceNames !== null) {
1712: return $this->cachedInterfaceNames;
1713: }
1714:
1715: return $this->cachedInterfaceNames = array_values(array_map(
1716: static fn (self $interface): string => $interface->getName(),
1717: $this->getInterfaces(),
1718: ));
1719: }
1720:
1721: /**
1722: * Checks whether the given object is an instance.
1723: *
1724: * @link https://php.net/manual/en/reflectionclass.isinstance.php
1725: */
1726: public function isInstance(object $object): bool
1727: {
1728: $className = $this->getName();
1729:
1730: // note: since $object was loaded, we can safely assume that $className is available in the current
1731: // php script execution context
1732: return $object instanceof $className;
1733: }
1734:
1735: /**
1736: * Checks whether the given class string is a subclass of this class.
1737: *
1738: * @link https://php.net/manual/en/reflectionclass.isinstance.php
1739: */
1740: public function isSubclassOf(string $className): bool
1741: {
1742: return in_array(
1743: ltrim($className, '\\'),
1744: $this->getParentClassNames(),
1745: true,
1746: );
1747: }
1748:
1749: /**
1750: * Checks whether this class implements the given interface.
1751: *
1752: * @link https://php.net/manual/en/reflectionclass.implementsinterface.php
1753: */
1754: public function implementsInterface(string $interfaceName): bool
1755: {
1756: return in_array(ltrim($interfaceName, '\\'), $this->getInterfaceNames(), true);
1757: }
1758:
1759: /**
1760: * Checks whether this reflection is an instantiable class
1761: *
1762: * @link https://php.net/manual/en/reflectionclass.isinstantiable.php
1763: */
1764: public function isInstantiable(): bool
1765: {
1766: // @TODO doesn't consider internal non-instantiable classes yet.
1767:
1768: if ($this->isAbstract()) {
1769: return false;
1770: }
1771:
1772: if ($this->isInterface()) {
1773: return false;
1774: }
1775:
1776: if ($this->isTrait()) {
1777: return false;
1778: }
1779:
1780: $constructor = $this->getConstructor();
1781:
1782: if ($constructor === null) {
1783: return true;
1784: }
1785:
1786: return $constructor->isPublic();
1787: }
1788:
1789: /**
1790: * Checks whether this is a reflection of a class that supports the clone operator
1791: *
1792: * @link https://php.net/manual/en/reflectionclass.iscloneable.php
1793: */
1794: public function isCloneable(): bool
1795: {
1796: if (! $this->isInstantiable()) {
1797: return false;
1798: }
1799:
1800: $cloneMethod = $this->getMethod('__clone');
1801:
1802: if ($cloneMethod === null) {
1803: return true;
1804: }
1805:
1806: return $cloneMethod->isPublic();
1807: }
1808:
1809: /**
1810: * Checks if iterateable
1811: *
1812: * @link https://php.net/manual/en/reflectionclass.isiterateable.php
1813: */
1814: public function isIterateable(): bool
1815: {
1816: return $this->isInstantiable() && $this->implementsInterface(Traversable::class);
1817: }
1818:
1819: public function isEnum(): bool
1820: {
1821: return $this->isEnum;
1822: }
1823:
1824: /** @return array<class-string, ReflectionClass> */
1825: private function getCurrentClassImplementedInterfacesIndexedByName(): array
1826: {
1827: if ($this->isTrait) {
1828: return [];
1829: }
1830:
1831: if ($this->isInterface) {
1832: // assumption: first key is the current interface
1833: return array_slice($this->getInterfacesHierarchy(AlreadyVisitedClasses::createEmpty()), 1);
1834: }
1835:
1836: $interfaces = [];
1837: foreach ($this->implementsClassNames as $name) {
1838: try {
1839: $interface = $this->reflector->reflectClass($name);
1840: foreach ($interface->getInterfacesHierarchy(AlreadyVisitedClasses::createEmpty()) as $n => $i) {
1841: $interfaces[$n] = $i;
1842: }
1843: } catch (IdentifierNotFound $exception) {
1844: continue;
1845: }
1846: }
1847:
1848: if ($this->isEnum) {
1849: $interfaces = $this->addEnumInterfaces($interfaces);
1850: }
1851:
1852: return $this->addStringableInterface($interfaces);
1853: }
1854:
1855: /**
1856: * This method allows us to retrieve all interfaces parent of this interface. Do not use on class nodes!
1857: *
1858: * @return array<class-string, ReflectionClass> parent interfaces of this interface
1859: */
1860: private function getInterfacesHierarchy(AlreadyVisitedClasses $alreadyVisitedClasses): array
1861: {
1862: if (! $this->isInterface) {
1863: return [];
1864: }
1865:
1866: $interfaceClassName = $this->getName();
1867: $alreadyVisitedClasses->push($interfaceClassName);
1868:
1869: /** @var array<class-string, self> $interfaces */
1870: $interfaces = [$interfaceClassName => $this];
1871: foreach ($this->getImmediateInterfaces() as $interface) {
1872: $alreadyVisitedClassesCopyForInterface = clone $alreadyVisitedClasses;
1873: foreach ($interface->getInterfacesHierarchy($alreadyVisitedClassesCopyForInterface) as $extendedInterfaceName => $extendedInterface) {
1874: $interfaces[$extendedInterfaceName] = $extendedInterface;
1875: }
1876: }
1877:
1878: return $this->addStringableInterface($interfaces);
1879: }
1880:
1881: /**
1882: * Get the value of a static property, if it exists. Throws a
1883: * PropertyDoesNotExist exception if it does not exist or is not static.
1884: * (note, differs very slightly from internal reflection behaviour)
1885: *
1886: * @param non-empty-string $propertyName
1887: *
1888: * @throws ClassDoesNotExist
1889: * @throws NoObjectProvided
1890: * @throws NotAnObject
1891: * @throws ObjectNotInstanceOfClass
1892: * @return mixed
1893: */
1894: public function getStaticPropertyValue(string $propertyName)
1895: {
1896: $property = $this->getProperty($propertyName);
1897:
1898: if (! $property || ! $property->isStatic()) {
1899: throw PropertyDoesNotExist::fromName($propertyName);
1900: }
1901:
1902: return $property->getValue();
1903: }
1904:
1905: /**
1906: * Set the value of a static property
1907: *
1908: * @param non-empty-string $propertyName
1909: *
1910: * @throws ClassDoesNotExist
1911: * @throws NoObjectProvided
1912: * @throws NotAnObject
1913: * @throws ObjectNotInstanceOfClass
1914: * @param mixed $value
1915: */
1916: public function setStaticPropertyValue(string $propertyName, $value): void
1917: {
1918: $property = $this->getProperty($propertyName);
1919:
1920: if (! $property || ! $property->isStatic()) {
1921: throw PropertyDoesNotExist::fromName($propertyName);
1922: }
1923:
1924: $property->setValue($value);
1925: }
1926:
1927: /** @return array<non-empty-string, mixed> */
1928: public function getStaticProperties(): array
1929: {
1930: $staticProperties = [];
1931:
1932: foreach ($this->getProperties() as $property) {
1933: if (! $property->isStatic()) {
1934: continue;
1935: }
1936:
1937: /** @psalm-suppress MixedAssignment */
1938: $staticProperties[$property->getName()] = $property->getValue();
1939: }
1940:
1941: return $staticProperties;
1942: }
1943:
1944: /** @return list<ReflectionAttribute> */
1945: public function getAttributes(): array
1946: {
1947: return $this->attributes;
1948: }
1949:
1950: /** @return list<ReflectionAttribute> */
1951: public function getAttributesByName(string $name): array
1952: {
1953: return ReflectionAttributeHelper::filterAttributesByName($this->getAttributes(), $name);
1954: }
1955:
1956: /**
1957: * @param class-string $className
1958: *
1959: * @return list<ReflectionAttribute>
1960: */
1961: public function getAttributesByInstance(string $className): array
1962: {
1963: return ReflectionAttributeHelper::filterAttributesByInstance($this->getAttributes(), $className);
1964: }
1965: }
1966: