1: <?php declare(strict_types=1);
2:
3: namespace Salient\Utility;
4:
5: use Salient\Utility\Internal\NamedType;
6: use Closure;
7: use InvalidArgumentException;
8: use ReflectionAttribute;
9: use ReflectionClass;
10: use ReflectionClassConstant;
11: use ReflectionConstant;
12: use ReflectionException;
13: use ReflectionExtension;
14: use ReflectionFunction;
15: use ReflectionFunctionAbstract;
16: use ReflectionIntersectionType;
17: use ReflectionMethod;
18: use ReflectionNamedType;
19: use ReflectionParameter;
20: use ReflectionProperty;
21: use ReflectionType;
22: use ReflectionUnionType;
23: use ReflectionZendExtension;
24:
25: /**
26: * Work with PHP's reflection API
27: *
28: * @api
29: */
30: final class Reflect extends AbstractUtility
31: {
32: /** @var array<class-string,array<string,mixed>> */
33: private static array $Constants = [];
34: /** @var array<class-string,array<int|string,string[]|string>> */
35: private static array $ConstantsByValue = [];
36:
37: /**
38: * Get the names of the given reflectors
39: *
40: * @param array<ReflectionAttribute<*>|ReflectionClass<*>|ReflectionClassConstant|ReflectionConstant|ReflectionExtension|ReflectionFunctionAbstract|ReflectionNamedType|ReflectionParameter|ReflectionProperty|ReflectionZendExtension> $reflectors
41: * @return string[]
42: */
43: public static function getNames(array $reflectors, bool $preserveKeys = true): array
44: {
45: foreach ($reflectors as $key => $reflector) {
46: $name = $reflector->getName();
47: if ($preserveKeys) {
48: $names[$key] = $name;
49: } else {
50: $names[] = $name;
51: }
52: }
53: return $names ?? [];
54: }
55:
56: /**
57: * Get the file name of a reflector, or null if its file name is unknown
58: *
59: * @param ReflectionClass<*>|ReflectionClassConstant|ReflectionFunctionAbstract|ReflectionParameter|ReflectionProperty $reflector
60: */
61: public static function getFileName($reflector): ?string
62: {
63: $filename = self::getDeclaring($reflector)->getFileName();
64: return $filename === false
65: ? null
66: : $filename;
67: }
68:
69: /**
70: * Get the namespace name of a reflector
71: *
72: * @param ReflectionClass<*>|ReflectionClassConstant|ReflectionFunctionAbstract|ReflectionParameter|ReflectionProperty $reflector
73: */
74: public static function getNamespaceName($reflector): string
75: {
76: return self::getDeclaring($reflector)->getNamespaceName();
77: }
78:
79: /**
80: * Get the declaring class or function of a reflector
81: *
82: * @param ReflectionClass<*>|ReflectionClassConstant|ReflectionFunctionAbstract|ReflectionParameter|ReflectionProperty $reflector
83: * @return ReflectionClass<*>|ReflectionFunctionAbstract
84: */
85: public static function getDeclaring($reflector)
86: {
87: return $reflector instanceof ReflectionParameter
88: ? $reflector->getDeclaringFunction()
89: : ($reflector instanceof ReflectionProperty
90: || $reflector instanceof ReflectionClassConstant
91: ? $reflector->getDeclaringClass()
92: : $reflector);
93: }
94:
95: /**
96: * Follow the parents of a class to its base class
97: *
98: * @param ReflectionClass<*> $class
99: * @return ReflectionClass<*>
100: */
101: public static function getBaseClass(ReflectionClass $class): ReflectionClass
102: {
103: while ($parent = $class->getParentClass()) {
104: $class = $parent;
105: }
106: return $class;
107: }
108:
109: /**
110: * Get the prototype of a method, or null if it has no prototype
111: */
112: public static function getPrototype(ReflectionMethod $method): ?ReflectionMethod
113: {
114: try {
115: return $method->getPrototype();
116: } catch (ReflectionException $ex) {
117: if (\PHP_VERSION_ID >= 80200 || $method->isPrivate()) {
118: return null;
119: }
120: // Work around issue where PHP does not return a prototype for
121: // methods inserted from traits
122: $class = $method->getDeclaringClass();
123: $name = $method->getName();
124: if ($method->isPublic()) {
125: foreach ($class->getInterfaces() as $interface) {
126: if ($interface->hasMethod($name)) {
127: return $interface->getMethod($name);
128: }
129: }
130: }
131: $class = $class->getParentClass();
132: if ($class && $class->hasMethod($name)) {
133: return $class->getMethod($name);
134: }
135: return null;
136: }
137: }
138:
139: /**
140: * Get the declaring class of a method's prototype, or the method's
141: * declaring class if it has no prototype
142: *
143: * @return ReflectionClass<*>
144: */
145: public static function getPrototypeClass(ReflectionMethod $method): ReflectionClass
146: {
147: return (self::getPrototype($method) ?? $method)->getDeclaringClass();
148: }
149:
150: /**
151: * Check if a method is declared in a class
152: *
153: * Returns `false` if the method:
154: *
155: * - does not belong to the given class
156: * - is declared in a parent of the given class
157: * - is inserted by a trait
158: *
159: * Returns `null` if the check cannot be performed, e.g. if the class is
160: * internal.
161: *
162: * @param ReflectionClass<*> $class
163: * @throws ReflectionException if the check cannot be completed because
164: * there are multiple declarations on the same line.
165: */
166: public static function isMethodInClass(
167: ReflectionMethod $method,
168: ReflectionClass $class,
169: ?string $name = null
170: ): ?bool {
171: if (
172: $method->getDeclaringClass()->getName() !== $class->getName()
173: || ($method->isInternal() && !$class->isInternal()) // e.g. `UnitEnum::cases()`
174: ) {
175: return false;
176: }
177:
178: if (!($traits = $class->getTraits())) {
179: return true;
180: }
181:
182: if (
183: ($file = $method->getFileName()) === false
184: || ($line = $method->getStartLine()) === false
185: || ($start = $class->getStartLine()) === false
186: || ($end = $class->getEndLine()) === false
187: ) {
188: return null;
189: }
190:
191: if (
192: $file !== $class->getFileName()
193: || $line < $start
194: || $line > $end
195: ) {
196: return false;
197: }
198:
199: if ($line > $start && $line < $end) {
200: return true;
201: }
202:
203: // Check if the method belongs to an adjacent trait on the same line
204: $name ??= $method->getName();
205: if ($inserted = Reflect::getTraitAliases($class)[$name] ?? null) {
206: $traits = array_intersect_key($traits, [$inserted[0] => null]);
207: $name = $inserted[1];
208: }
209: foreach ($traits as $trait) {
210: if (
211: $trait->hasMethod($name)
212: && ($traitMethod = $trait->getMethod($name))->getFileName() === $file
213: && $traitMethod->getStartLine() === $line
214: ) {
215: throw new ReflectionException(sprintf(
216: 'Unable to check location of %s::%s(): %s::%s() declared on same line',
217: $class->getName(),
218: $method->getName(),
219: $traitMethod->getDeclaringClass()->getName(),
220: $name,
221: ));
222: }
223: }
224:
225: return true;
226: }
227:
228: /**
229: * Get the trait method inserted into a class with the given name
230: *
231: * @param ReflectionClass<*> $class
232: */
233: public static function getTraitMethod(
234: ReflectionClass $class,
235: string $methodName,
236: bool $recursive = false
237: ): ?ReflectionMethod {
238: $lastMethod = null;
239: $method = null;
240: do {
241: if ($inserted = self::getTraitAliases($class)[$methodName] ?? null) {
242: $method = new ReflectionMethod(...$inserted);
243: } else {
244: foreach ($class->getTraits() as $trait) {
245: if ($trait->hasMethod($methodName)) {
246: $method = $trait->getMethod($methodName);
247: break;
248: }
249: }
250: }
251: if (!$recursive || !$method || $method === $lastMethod) {
252: return $method;
253: }
254: $class = $method->getDeclaringClass();
255: $methodName = $inserted ? $inserted[1] : $methodName;
256: $lastMethod = $method;
257: } while (true);
258: }
259:
260: /**
261: * Get the trait method aliases of a class as an array that maps aliases to
262: * [ trait, method ] arrays
263: *
264: * @param ReflectionClass<*> $class
265: * @return array<string,array{class-string,string}>
266: */
267: public static function getTraitAliases(ReflectionClass $class): array
268: {
269: foreach ($class->getTraitAliases() as $alias => $original) {
270: /** @var array{class-string,string} */
271: $original = explode('::', $original, 2);
272: $aliases[$alias] = $original;
273: }
274:
275: return $aliases ?? [];
276: }
277:
278: /**
279: * Get the trait property inserted into a class with the given name
280: *
281: * @param ReflectionClass<*> $class
282: */
283: public static function getTraitProperty(
284: ReflectionClass $class,
285: string $propertyName
286: ): ?ReflectionProperty {
287: foreach ($class->getTraits() as $trait) {
288: if ($trait->hasProperty($propertyName)) {
289: return $trait->getProperty($propertyName);
290: }
291: }
292:
293: return null;
294: }
295:
296: /**
297: * Get the trait constant inserted into a class with the given name
298: *
299: * @param ReflectionClass<*> $class
300: */
301: public static function getTraitConstant(
302: ReflectionClass $class,
303: string $constantName
304: ): ?ReflectionClassConstant {
305: if (\PHP_VERSION_ID < 80200) {
306: return null;
307: }
308:
309: foreach ($class->getTraits() as $trait) {
310: if (
311: $trait->hasConstant($constantName)
312: && ($constant = $trait->getReflectionConstant($constantName))
313: ) {
314: return $constant;
315: }
316: }
317:
318: return null;
319: }
320:
321: /**
322: * Get traits inserted by a class, including any traits they insert
323: *
324: * If `$recursive` is `true`, traits inserted by parent classes are also
325: * returned.
326: *
327: * @param ReflectionClass<*> $class
328: * @return array<string,ReflectionClass<*>>
329: */
330: public static function getAllTraits(ReflectionClass $class, bool $recursive = false): array
331: {
332: $traits = $class->getTraits();
333: foreach ($traits as $trait) {
334: $traits = array_merge($traits, self::getAllTraits($trait));
335: }
336: if ($recursive) {
337: while ($class = $class->getParentClass()) {
338: $traits = array_merge($traits, self::getAllTraits($class));
339: }
340: }
341: return $traits;
342: }
343:
344: /**
345: * Get the properties of a class, including private parent properties
346: *
347: * @param ReflectionClass<*> $class
348: * @return ReflectionProperty[]
349: */
350: public static function getAllProperties(ReflectionClass $class): array
351: {
352: do {
353: foreach ($class->getProperties() as $property) {
354: $name = $property->getDeclaringClass()->getName()
355: . "\0" . $property->getName();
356: if (isset($seen[$name])) {
357: continue;
358: }
359: $properties[] = $property;
360: $seen[$name] = true;
361: }
362: } while ($class = $class->getParentClass());
363:
364: return $properties ?? [];
365: }
366:
367: /**
368: * Get a list of types accepted by the given parameter of a function or
369: * callable
370: *
371: * @param ReflectionFunctionAbstract|callable $function
372: * @return ($discardBuiltins is true ? array<class-string[]|class-string> : array<class-string[]|string>)
373: * @throws InvalidArgumentException if `$function` has no parameter at the
374: * given position.
375: */
376: public static function getAcceptedTypes(
377: $function,
378: bool $discardBuiltins = false,
379: int $position = 0
380: ): array {
381: if (!$function instanceof ReflectionFunctionAbstract) {
382: if (!$function instanceof Closure) {
383: $function = Closure::fromCallable($function);
384: }
385: $function = new ReflectionFunction($function);
386: }
387:
388: $params = $function->getParameters();
389: if (!isset($params[$position])) {
390: throw new InvalidArgumentException(sprintf(
391: '%s has no parameter at position %d',
392: $function->name,
393: $position,
394: ));
395: }
396:
397: $types = self::normaliseType($params[$position]->getType());
398: foreach ($types as $type) {
399: $intersection = [];
400: foreach (Arr::wrap($type) as $type) {
401: if ($discardBuiltins && $type->isBuiltin()) {
402: continue 2;
403: }
404: $intersection[] = $type->getName();
405: }
406: $union[] = Arr::unwrap($intersection);
407: }
408:
409: /** @var array<class-string[]|class-string>|array<class-string[]|string> */
410: return $union ?? [];
411: }
412:
413: /**
414: * Resolve a ReflectionType to an array of ReflectionNamedType instances
415: *
416: * PHP reflection methods like {@see ReflectionParameter::getType()} and
417: * {@see ReflectionFunctionAbstract::getReturnType()} can return any of the
418: * following:
419: *
420: * - {@see ReflectionType} (until PHP 8)
421: * - {@see ReflectionNamedType}
422: * - {@see ReflectionUnionType} comprised of {@see ReflectionNamedType} (PHP
423: * 8+) and {@see ReflectionIntersectionType} (PHP 8.2+)
424: * - {@see ReflectionIntersectionType} comprised of
425: * {@see ReflectionNamedType} (PHP 8.1+)
426: * - `null`
427: *
428: * This method normalises these to an array that represents an equivalent
429: * union type, where each element is either:
430: *
431: * - a {@see ReflectionNamedType} instance, or
432: * - a list of {@see ReflectionNamedType} instances that represent an
433: * intersection type
434: *
435: * @return array<ReflectionNamedType[]|ReflectionNamedType>
436: */
437: public static function normaliseType(?ReflectionType $type): array
438: {
439: return $type === null
440: ? []
441: : self::doNormaliseType($type);
442: }
443:
444: /**
445: * Get the types in a ReflectionType
446: *
447: * @return ReflectionNamedType[]
448: */
449: public static function getTypes(?ReflectionType $type): array
450: {
451: return self::doGetTypes($type, false);
452: }
453:
454: /**
455: * Get the name of each type in a ReflectionType
456: *
457: * @return string[]
458: */
459: public static function getTypeNames(?ReflectionType $type): array
460: {
461: return self::doGetTypes($type, true);
462: }
463:
464: /**
465: * @return ($names is true ? string[] : ReflectionNamedType[])
466: */
467: private static function doGetTypes(?ReflectionType $type, bool $names): array
468: {
469: if ($type === null) {
470: return [];
471: }
472:
473: /** @var ReflectionNamedType $type */
474: foreach (Arr::flatten(self::doNormaliseType($type)) as $type) {
475: $name = $type->getName();
476: if (isset($seen[$name])) {
477: continue;
478: }
479: $types[] = $names ? $name : $type;
480: $seen[$name] = true;
481: }
482:
483: return $types ?? [];
484: }
485:
486: /**
487: * @return array<ReflectionNamedType[]|ReflectionNamedType>
488: */
489: private static function doNormaliseType(ReflectionType $type): array
490: {
491: if ($type instanceof ReflectionUnionType) {
492: foreach ($type->getTypes() as $type) {
493: if ($type instanceof ReflectionIntersectionType) {
494: $types[] = $type->getTypes();
495: } else {
496: $types[] = $type;
497: }
498: }
499: /** @var array<ReflectionNamedType[]|ReflectionNamedType> */
500: return $types ?? [];
501: }
502:
503: if ($type instanceof ReflectionIntersectionType) {
504: /** @var array<ReflectionNamedType[]> */
505: return [$type->getTypes()];
506: }
507:
508: /** @var ReflectionNamedType $type */
509: return self::expandNullableType($type);
510: }
511:
512: /**
513: * @param ReflectionNamedType $type
514: * @return array<ReflectionNamedType>
515: */
516: private static function expandNullableType(ReflectionType $type): array
517: {
518: return $type->allowsNull() && (
519: !$type->isBuiltin()
520: || strcasecmp($type->getName(), 'null')
521: )
522: ? [
523: new NamedType($type->getName(), $type->isBuiltin(), false),
524: new NamedType('null', true, true),
525: ]
526: : [$type];
527: }
528:
529: /**
530: * Get the public constants of a class or interface, indexed by name
531: *
532: * @param ReflectionClass<*>|class-string $class
533: * @return array<string,mixed>
534: */
535: public static function getConstants($class): array
536: {
537: return self::$Constants[self::getClassName($class)] ??=
538: self::doGetConstants($class);
539: }
540:
541: /**
542: * @param ReflectionClass<*>|class-string $class
543: * @return array<string,mixed>
544: */
545: private static function doGetConstants($class): array
546: {
547: $class = self::getClass($class);
548: foreach ($class->getReflectionConstants() as $constant) {
549: if ($constant->isPublic()) {
550: $constants[$constant->getName()] = $constant->getValue();
551: }
552: }
553:
554: return $constants ?? [];
555: }
556:
557: /**
558: * Get the public constants of a class or interface, indexed by value
559: *
560: * If the value of a constant is not an integer or string, it is ignored.
561: * For any values used by multiple constants, an array is returned.
562: *
563: * @param ReflectionClass<*>|class-string $class
564: * @return array<int|string,string[]|string>
565: */
566: public static function getConstantsByValue($class): array
567: {
568: return self::$ConstantsByValue[self::getClassName($class)] ??=
569: self::doGetConstantsByValue($class);
570: }
571:
572: /**
573: * @param ReflectionClass<*>|class-string $class
574: * @return array<int|string,string[]|string>
575: */
576: private static function doGetConstantsByValue($class): array
577: {
578: foreach (self::getConstants($class) as $name => $value) {
579: if (is_int($value) || is_string($value)) {
580: if (!isset($constants[$value])) {
581: $constants[$value] = $name;
582: } else {
583: if (!is_array($constants[$value])) {
584: $constants[$value] = (array) $constants[$value];
585: }
586: $constants[$value][] = $name;
587: }
588: }
589: }
590:
591: return $constants ?? [];
592: }
593:
594: /**
595: * Check if a class or interface has a public constant with the given value
596: *
597: * @param ReflectionClass<*>|class-string $class
598: * @param mixed $value
599: */
600: public static function hasConstantWithValue($class, $value): bool
601: {
602: return in_array($value, self::getConstants($class), true);
603: }
604:
605: /**
606: * Get the name of a public constant with the given value from a class or
607: * interface
608: *
609: * @param ReflectionClass<*>|class-string $class
610: * @param mixed $value
611: * @throws InvalidArgumentException if `$value` is invalid or matches
612: * multiple constants.
613: */
614: public static function getConstantName($class, $value): string
615: {
616: $names = [];
617: foreach (self::getConstants($class) as $name => $_value) {
618: if ($_value === $value) {
619: $names[] = $name;
620: }
621: }
622:
623: if (!$names) {
624: throw new InvalidArgumentException(sprintf(
625: 'Invalid value: %s',
626: Format::value($value),
627: ));
628: }
629:
630: if (count($names) > 1) {
631: throw new InvalidArgumentException(sprintf(
632: 'Value matches multiple constants: %s',
633: Format::value($value),
634: ));
635: }
636:
637: return $names[0];
638: }
639:
640: /**
641: * Get the value of a public constant with the given name from a class or
642: * interface
643: *
644: * @param ReflectionClass<*>|class-string $class
645: * @return mixed
646: * @throws InvalidArgumentException if `$name` is invalid.
647: */
648: public static function getConstantValue($class, string $name, bool $ignoreCase = false)
649: {
650: $constants = self::getConstants($class);
651: if (array_key_exists($name, $constants)) {
652: return $constants[$name];
653: }
654:
655: if ($ignoreCase) {
656: $constants = array_change_key_case($constants, \CASE_UPPER);
657: if (array_key_exists($upper = Str::upper($name), $constants)) {
658: return $constants[$upper];
659: }
660: }
661:
662: throw new InvalidArgumentException(sprintf('Invalid name: %s', $name));
663: }
664:
665: /**
666: * @param ReflectionClass<*>|class-string $class
667: * @return ReflectionClass<*>
668: */
669: private static function getClass($class): ReflectionClass
670: {
671: return $class instanceof ReflectionClass
672: ? $class
673: : new ReflectionClass($class);
674: }
675:
676: /**
677: * @param ReflectionClass<*>|class-string $class
678: * @return class-string
679: */
680: private static function getClassName($class): string
681: {
682: return $class instanceof ReflectionClass
683: ? $class->getName()
684: : $class;
685: }
686: }
687: