Passed
Push — master ( 5b6f7f...57f3ff )
by Melech
03:58
created

Matcher::getParameterForAgumentIndex()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 2
c 0
b 0
f 0
dl 0
loc 4
rs 10
cc 1
nc 1
nop 2
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the Valkyrja Framework package.
7
 *
8
 * (c) Melech Mizrachi <[email protected]>
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace Valkyrja\Http\Routing\Matcher;
15
16
use Override;
0 ignored issues
show
Bug introduced by
The type Override was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
17
use Valkyrja\Http\Message\Enum\RequestMethod;
0 ignored issues
show
Bug introduced by
The type Valkyrja\Http\Message\Enum\RequestMethod was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
18
use Valkyrja\Http\Routing\Collection\Collection as RouteCollection;
19
use Valkyrja\Http\Routing\Collection\Contract\Collection;
20
use Valkyrja\Http\Routing\Data\Contract\Parameter;
21
use Valkyrja\Http\Routing\Data\Contract\Route;
22
use Valkyrja\Http\Routing\Exception\InvalidRouteParameterException;
23
use Valkyrja\Http\Routing\Exception\InvalidRoutePathException;
24
use Valkyrja\Http\Routing\Matcher\Contract\Matcher as Contract;
25
use Valkyrja\Http\Routing\Support\Helpers;
26
use Valkyrja\Type\Data\Cast;
27
28
use function is_array;
29
use function preg_match;
30
31
/**
32
 * Class Matcher.
33
 *
34
 * @author Melech Mizrachi
35
 */
36
class Matcher implements Contract
37
{
38
    /**
39
     * Matcher constructor.
40
     *
41
     * @param Collection $collection The collection
42
     */
43
    public function __construct(
44
        protected Collection $collection = new RouteCollection()
45
    ) {
46
    }
47
48
    /**
49
     * @inheritDoc
50
     *
51
     * @throws InvalidRoutePathException
52
     * @throws InvalidRouteParameterException
53
     */
54
    #[Override]
55
    public function match(string $path, RequestMethod|null $requestMethod = null): Route|null
56
    {
57
        $path  = Helpers::trimPath($path);
58
        $route = $this->matchStatic($path, $requestMethod);
59
60
        return $route
61
            ?? $this->matchDynamic($path, $requestMethod);
62
    }
63
64
    /**
65
     * @inheritDoc
66
     */
67
    #[Override]
68
    public function matchStatic(string $path, RequestMethod|null $requestMethod = null): Route|null
69
    {
70
        $route = $this->collection->getStatic($path, $requestMethod);
71
72
        if ($route !== null) {
73
            return clone $route;
74
        }
75
76
        return null;
77
    }
78
79
    /**
80
     * @inheritDoc
81
     *
82
     * @throws InvalidRoutePathException
83
     * @throws InvalidRouteParameterException
84
     */
85
    #[Override]
86
    public function matchDynamic(string $path, RequestMethod|null $requestMethod = null): Route|null
87
    {
88
        return $this->matchDynamicFromArray($this->collection->allDynamic($requestMethod), $path);
89
    }
90
91
    /**
92
     * Match a dynamic route by path from a given array.
93
     *
94
     * @param array<string, Route>|array<string, array<string, Route>> $routes The routes
95
     * @param string                                                   $path   The path
96
     *
97
     * @throws InvalidRoutePathException
98
     * @throws InvalidRouteParameterException
99
     *
100
     * @return Route|null
101
     */
102
    protected function matchDynamicFromArray(array $routes, string $path): Route|null
103
    {
104
        // Attempt to find a match using dynamic routes that are set
105
        foreach ($routes as $regex => $route) {
106
            if (($match = $this->matchDynamicFromRouteOrArray($route, $path, $regex)) !== null) {
107
                return $match;
108
            }
109
        }
110
111
        return null;
112
    }
113
114
    /**
115
     * Match a dynamic route by path from a given route or array.
116
     *
117
     * @param Route|array<string, Route> $route The route
118
     * @param string                     $path  The path
119
     * @param string                     $regex The regex
120
     *
121
     * @throws InvalidRoutePathException
122
     * @throws InvalidRouteParameterException
123
     *
124
     * @return Route|null
125
     */
126
    protected function matchDynamicFromRouteOrArray(Route|array $route, string $path, string $regex): Route|null
127
    {
128
        if (is_array($route)) {
0 ignored issues
show
introduced by
The condition is_array($route) is always true.
Loading history...
129
            return $this->matchDynamicFromArray($route, $path);
130
        }
131
132
        // If the preg match is successful, we've found our route!
133
        if ($regex !== '' && preg_match($regex, $path, $matches)) {
134
            /** @var array<int, string> $matches */
135
            return $this->applyArgumentsToRoute($route, $matches);
136
        }
137
138
        return null;
139
    }
140
141
    /**
142
     * Get a matched dynamic route.
143
     *
144
     * @param Route              $route   The route
145
     * @param array<int, string> $matches The regex matches
146
     *
147
     * @throws InvalidRoutePathException
148
     *
149
     * @return Route
150
     */
151
    protected function applyArgumentsToRoute(Route $route, array $matches): Route
152
    {
153
        // Clone the route to avoid changing the one set in the master array
154
        $route = clone $route;
155
156
        return $this->processArguments($route, $matches);
157
    }
158
159
    /**
160
     * Process matches for a dynamic route.
161
     *
162
     * @param Route              $route   The route
163
     * @param array<int, string> $matches The regex matches
164
     *
165
     * @throws InvalidRoutePathException
166
     *
167
     * @return Route
168
     */
169
    protected function processArguments(Route $route, array $matches): Route
170
    {
171
        $dispatch = $route->getDispatch();
172
173
        // The first match is the path itself, the rest could be empty.
174
        array_shift($matches);
175
176
        // Get the parameters
177
        $parameters = $route->getParameters();
178
179
        if ($parameters === []) {
180
            throw new InvalidRoutePathException('Route parameters must not be empty');
181
        }
182
183
        // Parameters aren't guaranteed to be int indexed
184
        $index = 0;
185
186
        // Iterate through the matches
187
        foreach ($parameters as $parameter) {
188
            $match = $matches[$index]
189
                ??= $parameter->getDefault();
190
191
            $matches = $this->checkAndCastMatchValue(
192
                parameter: $parameter,
193
                matches: $matches,
194
                index: $index,
195
                match: $match
196
            );
197
198
            $index++;
199
        }
200
201
        return $route->withDispatch($dispatch->withArguments($matches));
202
    }
203
204
    /**
205
     * @param Parameter                                           $parameter The parameter
206
     * @param array<int, array<scalar|object>|scalar|object|null> $matches   The regex matches
207
     * @param int                                                 $index     The index for this match
208
     * @param array<scalar|object>|scalar|object|null             $match     The match
209
     *
210
     * @return array<int, array<scalar|object>|scalar|object|null>
211
     */
212
    protected function checkAndCastMatchValue(
213
        Parameter $parameter,
214
        array $matches,
215
        int $index,
216
        array|string|int|bool|float|object|null $match
217
    ): array {
218
        $cast = $parameter->getCast();
219
220
        if ($cast !== null) {
221
            $matches[$index] = $this->castMatchValue(
222
                cast: $cast,
223
                match: $match
224
            );
225
        }
226
227
        return $matches;
228
    }
229
230
    /**
231
     * Get a match value for the given cast type.
232
     *
233
     * @param Cast                                    $cast  The cast
234
     * @param array<scalar|object>|scalar|object|null $match The match value
235
     *
236
     * @return array<scalar|object>|scalar|object|null
237
     */
238
    protected function castMatchValue(
239
        Cast $cast,
240
        array|string|int|bool|float|object|null $match
241
    ): array|string|int|bool|float|object|null {
242
        $type = $cast->type::fromValue($match);
243
244
        if ($cast->convert) {
245
            /** @var array<scalar|object>|scalar|object|null $type */
246
            $type = $type->asValue();
247
        }
248
249
        return $type;
250
    }
251
}
252