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 ReflectionException;
12: use ReflectionExtension;
13: use ReflectionFunction;
14: use ReflectionFunctionAbstract;
15: use ReflectionIntersectionType;
16: use ReflectionMethod;
17: use ReflectionNamedType;
18: use ReflectionParameter;
19: use ReflectionProperty;
20: use ReflectionType;
21: use ReflectionUnionType;
22: use ReflectionZendExtension;
23:
24: /**
25: * Work with PHP's reflection API
26: *
27: * @api
28: */
29: final class Reflect extends AbstractUtility
30: {
31: /** @var array<class-string,array<string,mixed>> */
32: private static array $Constants = [];
33: /** @var array<class-string,array<int|string,string[]|string>> */
34: private static array $ConstantsByValue = [];
35:
36: /**
37: * Get a list of names from a list of reflectors
38: *
39: * @param array<ReflectionAttribute<object>|ReflectionClass<object>|ReflectionClassConstant|ReflectionExtension|ReflectionFunctionAbstract|ReflectionNamedType|ReflectionParameter|ReflectionProperty|ReflectionZendExtension> $reflectors
40: * @return string[]
41: */
42: public static function getNames(array $reflectors): array
43: {
44: foreach ($reflectors as $reflector) {
45: $names[] = $reflector->getName();
46: }
47: return $names ?? [];
48: }
49:
50: /**
51: * Follow parents of a class to the root class
52: *
53: * @param ReflectionClass<object> $class
54: * @return ReflectionClass<object>
55: */
56: public static function getBaseClass(ReflectionClass $class): ReflectionClass
57: {
58: while ($parent = $class->getParentClass()) {
59: $class = $parent;
60: }
61: return $class;
62: }
63:
64: /**
65: * Get the prototype of a method, or null if it has no prototype
66: */
67: public static function getPrototype(ReflectionMethod $method): ?ReflectionMethod
68: {
69: try {
70: return $method->getPrototype();
71: } catch (ReflectionException $ex) {
72: if (\PHP_VERSION_ID >= 80200 || $method->isPrivate()) {
73: return null;
74: }
75: // Work around issue where PHP does not return a prototype for
76: // methods inserted from traits
77: $class = $method->getDeclaringClass();
78: $name = $method->getName();
79: if ($method->isPublic()) {
80: foreach ($class->getInterfaces() as $interface) {
81: if ($interface->hasMethod($name)) {
82: return $interface->getMethod($name);
83: }
84: }
85: }
86: $class = $class->getParentClass();
87: if ($class && $class->hasMethod($name)) {
88: return $class->getMethod($name);
89: }
90: return null;
91: }
92: }
93:
94: /**
95: * Get the declaring class of a method's prototype, falling back to the
96: * method's declaring class if it has no prototype
97: *
98: * @return ReflectionClass<object>
99: */
100: public static function getPrototypeClass(ReflectionMethod $method): ReflectionClass
101: {
102: return (self::getPrototype($method) ?? $method)->getDeclaringClass();
103: }
104:
105: /**
106: * Get the trait method inserted into a class with the given name
107: *
108: * @param ReflectionClass<object> $class
109: */
110: public static function getTraitMethod(
111: ReflectionClass $class,
112: string $methodName
113: ): ?ReflectionMethod {
114: if ($inserted = self::getTraitAliases($class)[$methodName] ?? null) {
115: return new ReflectionMethod(...$inserted);
116: }
117:
118: foreach ($class->getTraits() as $trait) {
119: if ($trait->hasMethod($methodName)) {
120: return $trait->getMethod($methodName);
121: }
122: }
123:
124: return null;
125: }
126:
127: /**
128: * Get the trait method aliases of a class as an array that maps aliases to
129: * [ trait, method ] arrays
130: *
131: * @param ReflectionClass<object> $class
132: * @return array<string,array{class-string,string}>
133: */
134: public static function getTraitAliases(ReflectionClass $class): array
135: {
136: foreach ($class->getTraitAliases() as $alias => $original) {
137: /** @var array{class-string,string} */
138: $original = explode('::', $original, 2);
139: $aliases[$alias] = $original;
140: }
141:
142: return $aliases ?? [];
143: }
144:
145: /**
146: * Get the trait property inserted into a class with the given name
147: *
148: * @param ReflectionClass<object> $class
149: */
150: public static function getTraitProperty(
151: ReflectionClass $class,
152: string $propertyName
153: ): ?ReflectionProperty {
154: foreach ($class->getTraits() as $trait) {
155: if ($trait->hasProperty($propertyName)) {
156: return $trait->getProperty($propertyName);
157: }
158: }
159:
160: return null;
161: }
162:
163: /**
164: * Get the trait constant inserted into a class with the given name
165: *
166: * @param ReflectionClass<object> $class
167: */
168: public static function getTraitConstant(
169: ReflectionClass $class,
170: string $constantName
171: ): ?ReflectionClassConstant {
172: if (\PHP_VERSION_ID < 80200) {
173: return null;
174: }
175:
176: foreach ($class->getTraits() as $trait) {
177: if (
178: $trait->hasConstant($constantName)
179: && ($constant = $trait->getReflectionConstant($constantName))
180: ) {
181: return $constant;
182: }
183: }
184:
185: return null;
186: }
187:
188: /**
189: * Get the properties of a class, including private parent properties
190: *
191: * @param ReflectionClass<object> $class
192: * @return ReflectionProperty[]
193: */
194: public static function getAllProperties(ReflectionClass $class): array
195: {
196: do {
197: foreach ($class->getProperties() as $property) {
198: $name = $property->getName();
199: if (isset($seen[$name])) {
200: continue;
201: }
202: $properties[] = $property;
203: $seen[$name] = true;
204: }
205: } while ($class = $class->getParentClass());
206:
207: return $properties ?? [];
208: }
209:
210: /**
211: * Get a list of types accepted by the given parameter of a function or
212: * callable
213: *
214: * @param ReflectionFunctionAbstract|callable $function
215: * @return ($skipBuiltins is true ? array<class-string[]|class-string> : array<string[]|string>)
216: * @throws InvalidArgumentException if `$function` has no parameter at the
217: * given position.
218: */
219: public static function getAcceptedTypes(
220: $function,
221: bool $skipBuiltins = false,
222: int $param = 0
223: ): array {
224: if (!$function instanceof ReflectionFunctionAbstract) {
225: if (!$function instanceof Closure) {
226: $function = Closure::fromCallable($function);
227: }
228: $function = new ReflectionFunction($function);
229: }
230:
231: $params = $function->getParameters();
232: if (!isset($params[$param])) {
233: throw new InvalidArgumentException(sprintf(
234: '$function has no parameter at position %d',
235: $param,
236: ));
237: }
238:
239: $types = self::normaliseType($params[$param]->getType());
240: foreach ($types as $type) {
241: $intersection = [];
242: foreach (Arr::wrap($type) as $type) {
243: if ($skipBuiltins && $type->isBuiltin()) {
244: continue 2;
245: }
246: $intersection[] = $type->getName();
247: }
248: $union[] = Arr::unwrap($intersection);
249: }
250:
251: /** @var array<class-string[]|class-string> */
252: return $union ?? [];
253: }
254:
255: /**
256: * Resolve a ReflectionType to an array of ReflectionNamedType instances
257: *
258: * PHP reflection methods like {@see ReflectionParameter::getType()} and
259: * {@see ReflectionFunctionAbstract::getReturnType()} can return any of the
260: * following:
261: *
262: * - {@see ReflectionType} (until PHP 8)
263: * - {@see ReflectionNamedType}
264: * - {@see ReflectionUnionType} comprised of {@see ReflectionNamedType} (PHP
265: * 8+) and {@see ReflectionIntersectionType} (PHP 8.2+)
266: * - {@see ReflectionIntersectionType} comprised of
267: * {@see ReflectionNamedType} (PHP 8.1+)
268: * - `null`
269: *
270: * This method normalises these to an array that represents an equivalent
271: * union type, where each element is either:
272: *
273: * - a {@see ReflectionNamedType} instance, or
274: * - a list of {@see ReflectionNamedType} instances that represent an
275: * intersection type
276: *
277: * @return array<ReflectionNamedType[]|ReflectionNamedType>
278: */
279: public static function normaliseType(?ReflectionType $type): array
280: {
281: if ($type === null) {
282: return [];
283: }
284:
285: return self::doNormaliseType($type);
286: }
287:
288: /**
289: * Get the types in a ReflectionType
290: *
291: * @return ReflectionNamedType[]
292: */
293: public static function getTypes(?ReflectionType $type): array
294: {
295: return self::doGetTypes($type, false);
296: }
297:
298: /**
299: * Get the name of each type in a ReflectionType
300: *
301: * @return string[]
302: */
303: public static function getTypeNames(?ReflectionType $type): array
304: {
305: return self::doGetTypes($type, true);
306: }
307:
308: /**
309: * @return ($names is true ? string[] : ReflectionNamedType[])
310: */
311: private static function doGetTypes(?ReflectionType $type, bool $names): array
312: {
313: if ($type === null) {
314: return [];
315: }
316:
317: foreach (Arr::flatten(self::doNormaliseType($type)) as $type) {
318: /** @var ReflectionNamedType $type */
319: $name = $type->getName();
320: if (isset($seen[$name])) {
321: continue;
322: }
323: $types[] = $names ? $name : $type;
324: $seen[$name] = true;
325: }
326:
327: return $types ?? [];
328: }
329:
330: /**
331: * @return array<ReflectionNamedType[]|ReflectionNamedType>
332: */
333: private static function doNormaliseType(ReflectionType $type): array
334: {
335: if ($type instanceof ReflectionUnionType) {
336: foreach ($type->getTypes() as $type) {
337: if ($type instanceof ReflectionIntersectionType) {
338: $types[] = $type->getTypes();
339: continue;
340: }
341: $types[] = $type;
342: }
343: /** @var array<ReflectionNamedType[]|ReflectionNamedType> */
344: return $types ?? [];
345: }
346:
347: if ($type instanceof ReflectionIntersectionType) {
348: $types = [$type->getTypes()];
349: /** @var array<ReflectionNamedType[]> */
350: return $types;
351: }
352:
353: /** @var ReflectionNamedType $type */
354: return self::expandNullableType($type);
355: }
356:
357: /**
358: * @param ReflectionNamedType $type
359: * @return array<ReflectionNamedType>
360: */
361: private static function expandNullableType(ReflectionType $type): array
362: {
363: if ($type->allowsNull() && (
364: !$type->isBuiltin()
365: || strcasecmp($type->getName(), 'null')
366: )) {
367: return [
368: new NamedType($type->getName(), $type->isBuiltin(), false),
369: new NamedType('null', true, true),
370: ];
371: }
372:
373: return [$type];
374: }
375:
376: /**
377: * Get the public constants of a class or interface, indexed by name
378: *
379: * @param ReflectionClass<object>|class-string $class
380: * @return array<string,mixed>
381: */
382: public static function getConstants($class): array
383: {
384: return self::$Constants[self::getClassName($class)]
385: ??= self::doGetConstants($class);
386: }
387:
388: /**
389: * @param ReflectionClass<object>|class-string $class
390: * @return array<string,mixed>
391: */
392: private static function doGetConstants($class): array
393: {
394: $class = self::getClass($class);
395: foreach ($class->getReflectionConstants() as $constant) {
396: if ($constant->isPublic()) {
397: $constants[$constant->getName()] = $constant->getValue();
398: }
399: }
400:
401: return $constants ?? [];
402: }
403:
404: /**
405: * Get the public constants of a class or interface, indexed by value
406: *
407: * If the value of a constant is not an integer or string, it is ignored.
408: * For any values used by multiple constants, an array is returned.
409: *
410: * @param ReflectionClass<object>|class-string $class
411: * @return array<int|string,string[]|string>
412: */
413: public static function getConstantsByValue($class): array
414: {
415: return self::$ConstantsByValue[self::getClassName($class)]
416: ??= self::doGetConstantsByValue($class);
417: }
418:
419: /**
420: * @param ReflectionClass<object>|class-string $class
421: * @return array<int|string,string[]|string>
422: */
423: private static function doGetConstantsByValue($class): array
424: {
425: foreach (self::getConstants($class) as $name => $value) {
426: if (!is_int($value) && !is_string($value)) {
427: continue;
428: }
429: if (!isset($constants[$value])) {
430: $constants[$value] = $name;
431: continue;
432: }
433: if (!is_array($constants[$value])) {
434: $constants[$value] = (array) $constants[$value];
435: }
436: $constants[$value][] = $name;
437: }
438:
439: return $constants ?? [];
440: }
441:
442: /**
443: * Check if a class or interface has a public constant with the given value
444: *
445: * @param ReflectionClass<object>|class-string $class
446: * @param mixed $value
447: */
448: public static function hasConstantWithValue($class, $value): bool
449: {
450: return in_array($value, self::getConstants($class), true);
451: }
452:
453: /**
454: * Get the name of a public constant with the given value from a class or
455: * interface
456: *
457: * @param ReflectionClass<object>|class-string $class
458: * @param mixed $value
459: * @throws InvalidArgumentException if `$value` is invalid or matches
460: * multiple constants.
461: */
462: public static function getConstantName($class, $value): string
463: {
464: foreach (self::getConstants($class) as $name => $_value) {
465: if ($_value === $value) {
466: $names[] = $name;
467: }
468: }
469:
470: if (!isset($names)) {
471: throw new InvalidArgumentException(sprintf(
472: 'Invalid value: %s',
473: Format::value($value),
474: ));
475: }
476:
477: if (count($names) > 1) {
478: throw new InvalidArgumentException(sprintf(
479: 'Value matches multiple constants: %s',
480: Format::value($value),
481: ));
482: }
483:
484: return $names[0];
485: }
486:
487: /**
488: * Get the value of a public constant with the given name from a class or
489: * interface
490: *
491: * @param ReflectionClass<object>|class-string $class
492: * @return mixed
493: * @throws InvalidArgumentException if `$name` is invalid.
494: */
495: public static function getConstantValue($class, string $name, bool $ignoreCase = false)
496: {
497: $constants = self::getConstants($class);
498: if (array_key_exists($name, $constants)) {
499: return $constants[$name];
500: }
501:
502: if ($ignoreCase) {
503: $constants = array_change_key_case($constants, \CASE_UPPER);
504: if (array_key_exists($upper = Str::upper($name), $constants)) {
505: return $constants[$upper];
506: }
507: }
508:
509: throw new InvalidArgumentException(sprintf('Invalid name: %s', $name));
510: }
511:
512: /**
513: * @template T of object
514: *
515: * @param ReflectionClass<T>|class-string<T> $class
516: * @return ReflectionClass<T>
517: */
518: private static function getClass($class): ReflectionClass
519: {
520: if ($class instanceof ReflectionClass) {
521: return $class;
522: }
523: return new ReflectionClass($class);
524: }
525:
526: /**
527: * @template T of object
528: *
529: * @param ReflectionClass<T>|class-string<T> $class
530: * @return class-string<T>
531: */
532: private static function getClassName($class): string
533: {
534: if ($class instanceof ReflectionClass) {
535: return $class->getName();
536: }
537: return $class;
538: }
539: }
540: