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\Enums\ParameterType;
11: use LaravelUi5\Core\Exceptions\InvalidParameterDateException;
12: use LaravelUi5\Core\Exceptions\InvalidParameterException;
13: use LaravelUi5\Core\Exceptions\InvalidParameterTypeException;
14: use LaravelUi5\Core\Exceptions\InvalidParameterValueException;
15: use LaravelUi5\Core\Exceptions\InvalidPathException;
16: use LaravelUi5\Core\Exceptions\NoModelFoundForParameterException;
17: use ReflectionClass;
18: use Throwable;
19:
20: readonly class ParameterResolver implements ParameterResolverInterface
21: {
22:
23: public function __construct(private Request $request)
24: {
25: }
26:
27: public function resolve(object $target): array
28: {
29: $params = [];
30:
31: $route = $this->request->route();
32:
33: if (!$route) {
34: throw new InvalidPathException('No route bound to request.');
35: }
36:
37: $uri = $route->parameter('uri');
38: $segments = is_string($uri) ? explode('/', $uri) : [];
39:
40: $reflection = new ReflectionClass($target);
41: $attributes = $reflection->getAttributes(Parameter::class);
42:
43: // Reject empty segments (/a//b)
44: if (in_array('', $segments, true)) {
45: throw new InvalidPathException($uri);
46: }
47:
48: // Reject mismatch route <> definitions
49: if (count($segments) !== count($attributes)) {
50: throw new InvalidPathException(sprintf(
51: 'Expected %d path parameters, got %d.',
52: count($attributes),
53: count($segments)
54: ));
55: }
56:
57: foreach ($attributes as $index => $attribute) {
58:
59: /** @var Parameter $definition */
60: $definition = $attribute->newInstance();
61:
62: $raw = $segments[$index];
63:
64: $value = $this->cast(
65: $raw,
66: $definition->type,
67: $definition->model,
68: $definition->name
69: );
70:
71: if ($value === null) {
72: throw new InvalidParameterValueException(
73: $definition->name,
74: $definition->type->label()
75: );
76: }
77:
78: $params[$definition->name] = $value;
79: }
80: return $params;
81: }
82:
83: private function cast(mixed $value, ParameterType $type, ?string $modelClass, string $name): mixed
84: {
85: switch ($type) {
86: case ParameterType::Integer:
87: return filter_var($value, FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE);
88:
89: case ParameterType::Float:
90: return filter_var($value, FILTER_VALIDATE_FLOAT, FILTER_NULL_ON_FAILURE);
91:
92: case ParameterType::Boolean:
93: // Accepts "true/false/1/0/on/off/yes/no"
94: return filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
95:
96: case ParameterType::String:
97: return is_string($value) ? $value : (string)$value;
98:
99: case ParameterType::Date:
100: try {
101: return Carbon::parse($value);
102: } catch (Throwable) {
103: throw new InvalidParameterDateException($name);
104: }
105:
106: case ParameterType::Model:
107: if ($value instanceof $modelClass) {
108: return $value;
109: }
110: /** @var Model $modelClass */
111: if ($modelClass) {
112: $model = $modelClass::find($value);
113: if ($model) {
114: return $model;
115: }
116: throw new NoModelFoundForParameterException($name, $modelClass);
117: }
118: throw new InvalidParameterException($name);
119: }
120:
121: throw new InvalidParameterTypeException($name);
122: }
123: }
124: