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 declaring class of a method's prototype, falling back to the
66: * method's declaring class if it has no prototype
67: *
68: * @return ReflectionClass<object>
69: */
70: public static function getPrototypeClass(ReflectionMethod $method): ReflectionClass
71: {
72: try {
73: return $method->getPrototype()->getDeclaringClass();
74: } catch (ReflectionException $ex) {
75: return $method->getDeclaringClass();
76: }
77: }
78:
79: /**
80: * Get the properties of a class, including private parent properties
81: *
82: * @param ReflectionClass<object> $class
83: * @return ReflectionProperty[]
84: */
85: public static function getAllProperties(ReflectionClass $class): array
86: {
87: do {
88: foreach ($class->getProperties() as $property) {
89: $name = $property->getName();
90: if (isset($seen[$name])) {
91: continue;
92: }
93: $properties[] = $property;
94: $seen[$name] = true;
95: }
96: } while ($class = $class->getParentClass());
97:
98: return $properties ?? [];
99: }
100:
101: /**
102: * Get a list of types accepted by the given parameter of a function or
103: * callable
104: *
105: * @param ReflectionFunctionAbstract|callable $function
106: * @return ($skipBuiltins is true ? array<class-string[]|class-string> : array<string[]|string>)
107: * @throws InvalidArgumentException if `$function` has no parameter at the
108: * given position.
109: */
110: public static function getAcceptedTypes(
111: $function,
112: bool $skipBuiltins = false,
113: int $param = 0
114: ): array {
115: if (!$function instanceof ReflectionFunctionAbstract) {
116: if (!$function instanceof Closure) {
117: $function = Closure::fromCallable($function);
118: }
119: $function = new ReflectionFunction($function);
120: }
121:
122: $params = $function->getParameters();
123: if (!isset($params[$param])) {
124: throw new InvalidArgumentException(sprintf(
125: '$function has no parameter at position %d',
126: $param,
127: ));
128: }
129:
130: $types = self::normaliseType($params[$param]->getType());
131: foreach ($types as $type) {
132: $intersection = [];
133: foreach (Arr::wrap($type) as $type) {
134: if ($skipBuiltins && $type->isBuiltin()) {
135: continue 2;
136: }
137: $intersection[] = $type->getName();
138: }
139: $union[] = Arr::unwrap($intersection);
140: }
141:
142: /** @var array<class-string[]|class-string> */
143: return $union ?? [];
144: }
145:
146: /**
147: * Resolve a ReflectionType to an array of ReflectionNamedType instances
148: *
149: * PHP reflection methods like {@see ReflectionParameter::getType()} and
150: * {@see ReflectionFunctionAbstract::getReturnType()} can return any of the
151: * following:
152: *
153: * - {@see ReflectionType} (until PHP 8)
154: * - {@see ReflectionNamedType}
155: * - {@see ReflectionUnionType} comprised of {@see ReflectionNamedType} (PHP
156: * 8+) and {@see ReflectionIntersectionType} (PHP 8.2+)
157: * - {@see ReflectionIntersectionType} comprised of
158: * {@see ReflectionNamedType} (PHP 8.1+)
159: * - `null`
160: *
161: * This method normalises these to an array that represents an equivalent
162: * union type, where each element is either:
163: *
164: * - a {@see ReflectionNamedType} instance, or
165: * - a list of {@see ReflectionNamedType} instances that represent an
166: * intersection type
167: *
168: * @return array<ReflectionNamedType[]|ReflectionNamedType>
169: */
170: public static function normaliseType(?ReflectionType $type): array
171: {
172: if ($type === null) {
173: return [];
174: }
175:
176: return self::doNormaliseType($type);
177: }
178:
179: /**
180: * Get the types in a ReflectionType
181: *
182: * @return ReflectionNamedType[]
183: */
184: public static function getTypes(?ReflectionType $type): array
185: {
186: return self::doGetTypes($type, false);
187: }
188:
189: /**
190: * Get the name of each type in a ReflectionType
191: *
192: * @return string[]
193: */
194: public static function getTypeNames(?ReflectionType $type): array
195: {
196: return self::doGetTypes($type, true);
197: }
198:
199: /**
200: * @return ($names is true ? string[] : ReflectionNamedType[])
201: */
202: private static function doGetTypes(?ReflectionType $type, bool $names): array
203: {
204: if ($type === null) {
205: return [];
206: }
207:
208: foreach (Arr::flatten(self::doNormaliseType($type)) as $type) {
209: /** @var ReflectionNamedType $type */
210: $name = $type->getName();
211: if (isset($seen[$name])) {
212: continue;
213: }
214: $types[] = $names ? $name : $type;
215: $seen[$name] = true;
216: }
217:
218: return $types ?? [];
219: }
220:
221: /**
222: * @return array<ReflectionNamedType[]|ReflectionNamedType>
223: */
224: private static function doNormaliseType(ReflectionType $type): array
225: {
226: if ($type instanceof ReflectionUnionType) {
227: foreach ($type->getTypes() as $type) {
228: if ($type instanceof ReflectionIntersectionType) {
229: $types[] = $type->getTypes();
230: continue;
231: }
232: $types[] = $type;
233: }
234: /** @var array<ReflectionNamedType[]|ReflectionNamedType> */
235: return $types ?? [];
236: }
237:
238: if ($type instanceof ReflectionIntersectionType) {
239: $types = [$type->getTypes()];
240: /** @var array<ReflectionNamedType[]> */
241: return $types;
242: }
243:
244: /** @var ReflectionNamedType $type */
245: return self::expandNullableType($type);
246: }
247:
248: /**
249: * @param ReflectionNamedType $type
250: * @return array<ReflectionNamedType>
251: */
252: private static function expandNullableType(ReflectionType $type): array
253: {
254: if ($type->allowsNull() && (
255: !$type->isBuiltin()
256: || strcasecmp($type->getName(), 'null')
257: )) {
258: return [
259: new NamedType($type->getName(), $type->isBuiltin(), false),
260: new NamedType('null', true, true),
261: ];
262: }
263:
264: return [$type];
265: }
266:
267: /**
268: * Get the public constants of a class or interface, indexed by name
269: *
270: * @param ReflectionClass<object>|class-string $class
271: * @return array<string,mixed>
272: */
273: public static function getConstants($class): array
274: {
275: return self::$Constants[self::getClassName($class)]
276: ??= self::doGetConstants($class);
277: }
278:
279: /**
280: * @param ReflectionClass<object>|class-string $class
281: * @return array<string,mixed>
282: */
283: private static function doGetConstants($class): array
284: {
285: $class = self::getClass($class);
286: foreach ($class->getReflectionConstants() as $constant) {
287: if ($constant->isPublic()) {
288: $constants[$constant->getName()] = $constant->getValue();
289: }
290: }
291:
292: return $constants ?? [];
293: }
294:
295: /**
296: * Get the public constants of a class or interface, indexed by value
297: *
298: * If the value of a constant is not an integer or string, it is ignored.
299: * For any values used by multiple constants, an array is returned.
300: *
301: * @param ReflectionClass<object>|class-string $class
302: * @return array<int|string,string[]|string>
303: */
304: public static function getConstantsByValue($class): array
305: {
306: return self::$ConstantsByValue[self::getClassName($class)]
307: ??= self::doGetConstantsByValue($class);
308: }
309:
310: /**
311: * @param ReflectionClass<object>|class-string $class
312: * @return array<int|string,string[]|string>
313: */
314: private static function doGetConstantsByValue($class): array
315: {
316: foreach (self::getConstants($class) as $name => $value) {
317: if (!is_int($value) && !is_string($value)) {
318: continue;
319: }
320: if (!isset($constants[$value])) {
321: $constants[$value] = $name;
322: continue;
323: }
324: if (!is_array($constants[$value])) {
325: $constants[$value] = (array) $constants[$value];
326: }
327: $constants[$value][] = $name;
328: }
329:
330: return $constants ?? [];
331: }
332:
333: /**
334: * Check if a class or interface has a public constant with the given value
335: *
336: * @param ReflectionClass<object>|class-string $class
337: * @param mixed $value
338: */
339: public static function hasConstantWithValue($class, $value): bool
340: {
341: return in_array($value, self::getConstants($class), true);
342: }
343:
344: /**
345: * Get the name of a public constant with the given value from a class or
346: * interface
347: *
348: * @param ReflectionClass<object>|class-string $class
349: * @param mixed $value
350: * @throws InvalidArgumentException if `$value` is invalid or matches
351: * multiple constants.
352: */
353: public static function getConstantName($class, $value): string
354: {
355: foreach (self::getConstants($class) as $name => $_value) {
356: if ($_value === $value) {
357: $names[] = $name;
358: }
359: }
360:
361: if (!isset($names)) {
362: throw new InvalidArgumentException(sprintf(
363: 'Invalid value: %s',
364: Format::value($value),
365: ));
366: }
367:
368: if (count($names) > 1) {
369: throw new InvalidArgumentException(sprintf(
370: 'Value matches multiple constants: %s',
371: Format::value($value),
372: ));
373: }
374:
375: return $names[0];
376: }
377:
378: /**
379: * @template T of object
380: *
381: * @param ReflectionClass<T>|class-string<T> $class
382: * @return ReflectionClass<T>
383: */
384: private static function getClass($class): ReflectionClass
385: {
386: if ($class instanceof ReflectionClass) {
387: return $class;
388: }
389: return new ReflectionClass($class);
390: }
391:
392: /**
393: * @template T of object
394: *
395: * @param ReflectionClass<T>|class-string<T> $class
396: * @return class-string<T>
397: */
398: private static function getClassName($class): string
399: {
400: if ($class instanceof ReflectionClass) {
401: return $class->getName();
402: }
403: return $class;
404: }
405: }
406: