1: <?php
2:
3: namespace LaravelUi5\Core\Services;
4:
5: use Illuminate\Database\Eloquent\Model;
6: use Illuminate\Http\Request;
7: use Illuminate\Support\Carbon;
8: use LaravelUi5\Core\Attributes\Parameter;
9: use LaravelUi5\Core\Contracts\ParameterResolverInterface;
10: use LaravelUi5\Core\Contracts\Ui5Args;
11: use LaravelUi5\Core\Enums\ParameterSource;
12: use LaravelUi5\Core\Enums\ValueType;
13: use LaravelUi5\Core\Exceptions\InvalidArrayParameterException;
14: use LaravelUi5\Core\Exceptions\InvalidArrayValueParameterException;
15: use LaravelUi5\Core\Exceptions\InvalidJsonParameterException;
16: use LaravelUi5\Core\Exceptions\InvalidParameterDateException;
17: use LaravelUi5\Core\Exceptions\InvalidParameterException;
18: use LaravelUi5\Core\Exceptions\InvalidParameterTypeException;
19: use LaravelUi5\Core\Exceptions\InvalidParameterValueException;
20: use LaravelUi5\Core\Exceptions\InvalidPathException;
21: use LaravelUi5\Core\Exceptions\MissingRequiredParameterException;
22: use LaravelUi5\Core\Exceptions\NoModelFoundForParameterException;
23: use LaravelUi5\Core\Ui5\Contracts\ParameterizableInterface;
24: use ReflectionClass;
25: use Throwable;
26:
27: readonly class ParameterResolver implements ParameterResolverInterface
28: {
29:
30: public function __construct(
31: private Request $request
32: )
33: {
34: }
35:
36: public function resolve(ParameterizableInterface $target): Ui5Args
37: {
38: $reflection = new ReflectionClass($target);
39: $attributes = $reflection->getAttributes(Parameter::class);
40: $uriKeys = $this->getUriKeys();
41: $out = [];
42: $index = 0;
43:
44: /** @var Parameter $attribute */
45: foreach ($attributes as $a) {
46: $attribute = $a->newInstance();
47: $name = $attribute->name;
48: $raw = null;
49:
50: // 1. Get the raw value
51: switch ($attribute->source) {
52: case ParameterSource::Path:
53: $raw = $uriKeys[$index] ?? null;
54: break;
55: case ParameterSource::Query:
56: $raw = $this->request->query($name);
57: break;
58: }
59:
60: // 2. Handle required, default, and nullable
61: if (null === $raw) {
62: if ($attribute->required && !$attribute->nullable && $attribute->default === null) {
63: throw new MissingRequiredParameterException($name);
64: }
65: $out[$name] = $attribute->nullable ? null : $attribute->default;
66: continue;
67: }
68:
69: // 3. Cast raw value
70: $casted = $this->cast($raw, $attribute->type, $attribute->model, $name);
71:
72: // 4. Validate casted value
73: if (null === $casted && !$attribute->nullable) {
74: throw new InvalidParameterValueException($name, $attribute->type->label());
75: }
76:
77: $out[$name] = $casted;
78: }
79:
80: return new Ui5Args($out);
81: }
82:
83: public function getUriKeys(): array
84: {
85: $route = $this->request->route();
86: $uri = $route?->parameter('uri');
87: $segments = is_string($uri) ? explode('/', $uri) : [];
88: if (in_array("", $segments, true)) {
89: throw new InvalidPathException($uri);
90: }
91: return $segments;
92: }
93:
94: private function cast(mixed $value, ValueType $type, ?string $modelClass, string $name): mixed
95: {
96: switch ($type) {
97: case ValueType::Integer:
98: return filter_var($value, FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE);
99:
100: case ValueType::Float:
101: return filter_var($value, FILTER_VALIDATE_FLOAT, FILTER_NULL_ON_FAILURE);
102:
103: case ValueType::Boolean:
104: // Accepts "true/false/1/0/on/off/yes/no"
105: return filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
106:
107: case ValueType::String:
108: return is_string($value) ? $value : (string)$value;
109:
110: case ValueType::Date:
111: try {
112: return Carbon::parse($value);
113: } catch (Throwable) {
114: throw new InvalidParameterDateException($name);
115: }
116:
117: case ValueType::IntegerArray:
118: return $this->normalizeArray($value, fn($v) => filter_var($v, FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE), $name);
119:
120: case ValueType::FloatArray:
121: return $this->normalizeArray($value, fn($v) => filter_var($v, FILTER_VALIDATE_FLOAT, FILTER_NULL_ON_FAILURE), $name);
122:
123: case ValueType::BooleanArray:
124: return $this->normalizeArray($value, fn($v) => filter_var($v, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE), $name);
125:
126: case ValueType::StringArray:
127: return $this->normalizeArray($value, fn($v) => (string)$v, $name);
128:
129: case ValueType::Model:
130: if ($value instanceof $modelClass) {
131: return $value;
132: }
133: /** @var Model $modelClass */
134: if ($modelClass) {
135: $model = $modelClass::find($value);
136: if ($model) {
137: return $model;
138: }
139: throw new NoModelFoundForParameterException($name, $modelClass);
140: }
141: throw new InvalidParameterException($name);
142: }
143:
144: throw new InvalidParameterTypeException($name);
145: }
146:
147: /**
148: * Normalize input into an array of the given type.
149: *
150: * @param mixed $value Raw value (array or JSON string)
151: * @param callable $caster Function to cast each element
152: * @param string $name Parameter name (for error reporting)
153: * @return array
154: */
155: private function normalizeArray(mixed $value, callable $caster, string $name): array
156: {
157: if (is_string($value)) {
158: $decoded = json_decode($value, true);
159: if (!is_array($decoded)) {
160: throw new InvalidJsonParameterException($name);
161: }
162: $value = $decoded;
163: }
164:
165: if (!is_array($value)) {
166: throw new InvalidArrayParameterException($name);
167: }
168:
169: $out = [];
170: foreach ($value as $v) {
171: $casted = $caster($v);
172: if ($casted === null) {
173: throw new InvalidArrayValueParameterException($name);
174: }
175: $out[] = $casted;
176: }
177: return $out;
178: }
179: }
180: