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