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: | |
27: | |
28: | |
29: | |
30: | final class Reflect extends AbstractUtility |
31: | { |
32: | |
33: | private static array $Constants = []; |
34: | |
35: | private static array $ConstantsByValue = []; |
36: | |
37: | |
38: | |
39: | |
40: | |
41: | |
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: | |
58: | |
59: | |
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: | |
71: | |
72: | |
73: | |
74: | public static function getNamespaceName($reflector): string |
75: | { |
76: | return self::getDeclaring($reflector)->getNamespaceName(); |
77: | } |
78: | |
79: | |
80: | |
81: | |
82: | |
83: | |
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: | |
97: | |
98: | |
99: | |
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: | |
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: | |
121: | |
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: | |
141: | |
142: | |
143: | |
144: | |
145: | public static function getPrototypeClass(ReflectionMethod $method): ReflectionClass |
146: | { |
147: | return (self::getPrototype($method) ?? $method)->getDeclaringClass(); |
148: | } |
149: | |
150: | |
151: | |
152: | |
153: | |
154: | |
155: | |
156: | |
157: | |
158: | |
159: | |
160: | |
161: | |
162: | |
163: | |
164: | |
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()) |
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: | |
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: | |
230: | |
231: | |
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: | |
262: | |
263: | |
264: | |
265: | |
266: | |
267: | public static function getTraitAliases(ReflectionClass $class): array |
268: | { |
269: | foreach ($class->getTraitAliases() as $alias => $original) { |
270: | |
271: | $original = explode('::', $original, 2); |
272: | $aliases[$alias] = $original; |
273: | } |
274: | |
275: | return $aliases ?? []; |
276: | } |
277: | |
278: | |
279: | |
280: | |
281: | |
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: | |
298: | |
299: | |
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: | |
323: | |
324: | |
325: | |
326: | |
327: | |
328: | |
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: | |
346: | |
347: | |
348: | |
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: | |
369: | |
370: | |
371: | |
372: | |
373: | |
374: | |
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: | |
410: | return $union ?? []; |
411: | } |
412: | |
413: | |
414: | |
415: | |
416: | |
417: | |
418: | |
419: | |
420: | |
421: | |
422: | |
423: | |
424: | |
425: | |
426: | |
427: | |
428: | |
429: | |
430: | |
431: | |
432: | |
433: | |
434: | |
435: | |
436: | |
437: | public static function normaliseType(?ReflectionType $type): array |
438: | { |
439: | return $type === null |
440: | ? [] |
441: | : self::doNormaliseType($type); |
442: | } |
443: | |
444: | |
445: | |
446: | |
447: | |
448: | |
449: | public static function getTypes(?ReflectionType $type): array |
450: | { |
451: | return self::doGetTypes($type, false); |
452: | } |
453: | |
454: | |
455: | |
456: | |
457: | |
458: | |
459: | public static function getTypeNames(?ReflectionType $type): array |
460: | { |
461: | return self::doGetTypes($type, true); |
462: | } |
463: | |
464: | |
465: | |
466: | |
467: | private static function doGetTypes(?ReflectionType $type, bool $names): array |
468: | { |
469: | if ($type === null) { |
470: | return []; |
471: | } |
472: | |
473: | |
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: | |
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: | |
500: | return $types ?? []; |
501: | } |
502: | |
503: | if ($type instanceof ReflectionIntersectionType) { |
504: | |
505: | return [$type->getTypes()]; |
506: | } |
507: | |
508: | |
509: | return self::expandNullableType($type); |
510: | } |
511: | |
512: | |
513: | |
514: | |
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: | |
531: | |
532: | |
533: | |
534: | |
535: | public static function getConstants($class): array |
536: | { |
537: | return self::$Constants[self::getClassName($class)] ??= |
538: | self::doGetConstants($class); |
539: | } |
540: | |
541: | |
542: | |
543: | |
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: | |
559: | |
560: | |
561: | |
562: | |
563: | |
564: | |
565: | |
566: | public static function getConstantsByValue($class): array |
567: | { |
568: | return self::$ConstantsByValue[self::getClassName($class)] ??= |
569: | self::doGetConstantsByValue($class); |
570: | } |
571: | |
572: | |
573: | |
574: | |
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: | |
596: | |
597: | |
598: | |
599: | |
600: | public static function hasConstantWithValue($class, $value): bool |
601: | { |
602: | return in_array($value, self::getConstants($class), true); |
603: | } |
604: | |
605: | |
606: | |
607: | |
608: | |
609: | |
610: | |
611: | |
612: | |
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: | |
642: | |
643: | |
644: | |
645: | |
646: | |
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: | |
667: | |
668: | |
669: | private static function getClass($class): ReflectionClass |
670: | { |
671: | return $class instanceof ReflectionClass |
672: | ? $class |
673: | : new ReflectionClass($class); |
674: | } |
675: | |
676: | |
677: | |
678: | |
679: | |
680: | private static function getClassName($class): string |
681: | { |
682: | return $class instanceof ReflectionClass |
683: | ? $class->getName() |
684: | : $class; |
685: | } |
686: | } |
687: | |