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