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