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