1: <?php declare(strict_types=1);
2:
3: namespace Salient\Core\Reflection;
4:
5: use Salient\Utility\Reflect;
6: use Closure;
7: use DateTimeInterface;
8: use ReflectionException;
9: use ReflectionMethod;
10: use ReflectionNamedType;
11: use ReflectionParameter;
12:
13: /**
14: * @api
15: */
16: class MethodReflection extends ReflectionMethod
17: {
18: /** @var class-string */
19: private string $ClassUnderReflection;
20:
21: /**
22: * @api
23: *
24: * @param object|class-string $objectOrClass
25: */
26: public function __construct($objectOrClass, string $method)
27: {
28: $this->ClassUnderReflection = is_object($objectOrClass)
29: ? get_class($objectOrClass)
30: : $objectOrClass;
31:
32: parent::__construct($objectOrClass, $method);
33: }
34:
35: /**
36: * Check if a parameter has a type hint and accepts values of a given type
37: *
38: * Limitations:
39: * - Intersection types are ignored
40: * - Relative class types are not resolved
41: *
42: * @param int<0,max> $position
43: */
44: public function accepts(string $typeName, bool $isBuiltin = false, int $position = 0): bool
45: {
46: $types = Reflect::normaliseType($this->getParameter($position)->getType());
47: foreach ($types as $type) {
48: if (
49: !is_array($type)
50: && $type->isBuiltin() === $isBuiltin
51: && (
52: !strcasecmp($name = $type->getName(), $typeName)
53: || (!$isBuiltin && is_a($typeName, $name, true))
54: )
55: ) {
56: return true;
57: }
58: }
59: return false;
60: }
61:
62: /**
63: * Check if the method has a type hint and returns a given type
64: *
65: * Limitations:
66: * - Always returns `false` for intersection types
67: * - Relative class types are not resolved
68: */
69: public function returns(string $typeName, bool $isBuiltin = false, bool $allowNull = true): bool
70: {
71: $types = Reflect::normaliseType($this->getReturnType());
72: if (!$types) {
73: return false;
74: }
75: foreach ($types as $type) {
76: if (is_array($type) || !(
77: (
78: ($builtin = $type->isBuiltin()) === $isBuiltin
79: && (
80: !strcasecmp($name = $type->getName(), $typeName)
81: || (!$isBuiltin && is_a($name, $typeName, true))
82: )
83: ) || (
84: $allowNull
85: && $builtin
86: && !strcasecmp($type->getName(), 'null')
87: )
88: )) {
89: return false;
90: }
91: }
92: return true;
93: }
94:
95: /**
96: * Get the parameter at the given position
97: *
98: * @param int<0,max> $position
99: * @throws ReflectionException if there is no parameter at `$position`.
100: */
101: public function getParameter(int $position): ReflectionParameter
102: {
103: if ($position >= $this->getNumberOfParameters()) {
104: throw new ReflectionException(sprintf(
105: 'Method %s does not have a parameter at position %d',
106: $this->name,
107: $position,
108: ));
109: }
110: return $this->getParameters()[$position];
111: }
112:
113: /**
114: * Get a parameter index for the method
115: *
116: * @param (Closure(string $name, bool $fromData=): string)|null $normaliser
117: */
118: public function getParameterIndex(?Closure $normaliser = null): ParameterIndex
119: {
120: $class = new ClassReflection($this->ClassUnderReflection);
121: $normaliser ??= $class->getNormaliser();
122:
123: foreach ($this->getParameters() as $param) {
124: $name = $normaliser
125: ? $normaliser($param->name, false)
126: : $param->name;
127: $type = $param->getType();
128: $isOptional = $param->isOptional();
129:
130: $names[$name] = $param->name;
131:
132: if (!$param->isVariadic()) {
133: $defaultArgs[] = $isOptional && $param->isDefaultValueAvailable()
134: ? $param->getDefaultValue()
135: : null;
136: }
137:
138: if (!$param->allowsNull()) {
139: $notNullable[$name] = $param->name;
140: if (!$isOptional) {
141: $required[$name] = $param->name;
142: }
143: }
144:
145: if ($param->isPassedByReference()) {
146: $byRef[$name] = $param->name;
147: }
148:
149: if ($type instanceof ReflectionNamedType) {
150: $typeName = $type->getName();
151: if ($type->isBuiltin()) {
152: $builtins[$name] = $typeName;
153: } else {
154: /** @var class-string $typeName */
155: $services[$name] = $typeName;
156: if (!strcasecmp($typeName, DateTimeInterface::class)) {
157: $date[$name] = $param->name;
158: }
159: }
160: }
161: }
162:
163: return new ParameterIndex(
164: $names ?? [],
165: array_flip(array_values($names ?? [])),
166: $defaultArgs ?? [],
167: $notNullable ?? [],
168: $required ?? [],
169: $byRef ?? [],
170: $date ?? [],
171: $builtins ?? [],
172: $services ?? [],
173: $this->getNumberOfRequiredParameters(),
174: );
175: }
176: }
177: