Regex::routeMatch()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 11
ccs 6
cts 6
cp 1
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 6
nc 2
nop 2
crap 2
1
<?php
2
declare(strict_types=1);
3
4
namespace Kambo\Router\Matcher;
5
6
// \spl
7
use InvalidArgumentException;
8
9
// \Psr\Http\Message
10
use Psr\Http\Message\ServerRequestInterface as ServerRequest;
11
12
// \Kambo\Router
13
use Kambo\Router\Matcher;
14
15
// \Kambo\Router\Route
16
use Kambo\Router\Route\Collection;
17
use Kambo\Router\Route\Route\Parsed;
18
19
// \Kambo\Router\Enum
20
use Kambo\Router\Enum\Method;
21
use Kambo\Router\Enum\RouteMode;
22
23
/**
24
 * Match provided request object with all defined routes in route collection
25
 * using regular expresions. If some of routes match a data in provided request
26
 * An instance of matched route is returned. If nothing is matched false value
27
 * is returned.
28
 *
29
 * @package Kambo\Router\Matcher
30
 * @author  Bohuslav Simek <[email protected]>
31
 * @license MIT
32
 */
33
class Regex implements Matcher
34
{
35
    /**
36
     * Regex for getting URL variables
37
     *
38
     * @var string
39
     */
40
    const VARIABLE_REGEX =
41
        "~\{
42
            \s* ([a-zA-Z0-9_]*) \s*
43
            (?:
44
                : \s* ([^{]+(?:\{.*?\})?)
45
            )?
46
        \}\??~x";
47
48
    /**
49
     * Shortcuts for regex.
50
     *
51
     * @var array
52
     */
53
    private $regexShortcuts = [
54
        ':i}'  => ':[0-9]+}',
55
        ':a}'  => ':[0-9A-Za-z]+}',
56
        ':h}'  => ':[0-9A-Fa-f]+}',
57
        ':c}'  => ':[a-zA-Z0-9+_\-\.]+}'
58
    ];
59
60
    /**
61
     * Flag for enabling support of mode rewrite.
62
     *
63
     * @var string
64
     */
65
    private $urlFormat = RouteMode::PATH_FORMAT;
66
67
    /**
68
     * Name of GET parameter from which the route will be get
69
     * if url format is set to GET_FORMAT.
70
     *
71
     * @var string
72
     */
73
    private $modeRewriteParameter = 'r';
74
75
    /**
76
     * Instance of route collection
77
     *
78
     * @var \Kambo\Router\Route\Collection
79
     */
80
    private $routeCollection;
81
82
    /**
83
     * Constructor
84
     *
85
     * @param \Kambo\Router\Route\Collection $routeCollection
86
     *
87
     */
88 43
    public function __construct(
89
        Collection $routeCollection
90
    ) {
91 43
        $this->routeCollection = $routeCollection;
92 43
    }
93
94
    /**
95
     * Match request with provided routes.
96
     * Get method and url from provided request and start matching.
97
     *
98
     * @param ServerRequest $request instance of PSR 7 compatible request object
99
     *
100
     * @return mixed
101
     */
102 23
    public function matchRequest(ServerRequest $request)
103
    {
104 23
        return $this->getMatchRoute(
105 23
            $request->getMethod(),
106 23
            $this->getUrl($request)
107
        );
108
    }
109
110
    /**
111
     * Match url and method with provided routes.
112
     *
113
     * @param string $method http method
114
     * @param string $url    url
115
     *
116
     * @return mixed
117
     */
118 16
    public function matchPathAndMethod(string $method, string $url)
119
    {
120 16
        return $this->getMatchRoute($method, $url);
121
    }
122
123
    /**
124
     * Set format for URL resolving.
125
     * If the path mode is set to a path a web server must be properly
126
     * configurated, defualt value is PATH_FORMAT.
127
     *
128
     * @param string $urlFormat value from RouteMode enum
129
     *
130
     * @return self for fluent interface
131
     */
132 8
    public function setUrlFormat(string $urlFormat) : Matcher
133
    {
134 8
        if (RouteMode::inEnum($urlFormat)) {
135 6
            $this->urlFormat = $urlFormat;
136
137 6
            return $this;
138
        }
139
140 2
        throw new InvalidArgumentException(
141 2
            'Value of urlFormat must be from RouteMode enum.'
142
        );
143
    }
144
145
    /**
146
     * Get format for URL resolving.
147
     *
148
     * @return string value from RouteMode enum
149
     */
150 2
    public function getUrlFormat() : string
151
    {
152 2
        return $this->urlFormat;
153
    }
154
155
    // ------------ PRIVATE METHODS
156
157
    /**
158
     * Match method and route with provided routes.
159
     * If route and method match a route is dispatch using provided dispatcher.
160
     *
161
     * @param string $method http method
162
     * @param string $url    url
163
     *
164
     * @return mixed
165
     */
166 39
    private function getMatchRoute(string $method, string $url)
167
    {
168 39
        $parsedRoutes = $this->parseRoutes($this->routeCollection);
169 39
        foreach ($parsedRoutes as $singleParsedRoute) {
170 36
            list($routeRegex, $route) = $singleParsedRoute;
171 36
            $matchedParameters        = $this->routeMatch($routeRegex, $url);
172
173 36
            if ($matchedParameters !== false) {
174 31
                $routeMethod = $route->getMethod();
175 31
                if ($routeMethod === $method || $routeMethod === Method::ANY) {
176 31
                    $route->setParameters($matchedParameters);
177
178 36
                    return $route;
179
                }
180
            }
181
        }
182
183 8
        return false;
184
    }
185
186
    /**
187
     * Match route by provided regex.
188
     *
189
     * @param string $routeRegex
190
     * @param string $route
191
     *
192
     * @return mixed
193
     */
194 36
    private function routeMatch(string $routeRegex, string $route)
195
    {
196 36
        $matches = [];
197 36
        if (preg_match($routeRegex, $route, $matches)) {
198 31
            unset($matches[0]);
199
200 31
            return array_values($matches);
201
        }
202
203 5
        return false;
204
    }
205
206
    /**
207
     * Prepare regex and parameters for each of routes.
208
     *
209
     * @param array $routes array with instances of route object
210
     *
211
     * @return array transformed routes
212
     */
213 39
    private function parseRoutes(Collection $routes) : array
214
    {
215 39
        $parsedRoutes = [];
216 39
        foreach ($routes as $route) {
217 36
            $routeUrl = strtr($route->getUrl(), $this->regexShortcuts);
218
219 36
            list($routeRegex, $parameters) = $this->transformRoute($routeUrl);
220
221 36
            $parsedRoute = new Parsed($route);
222 36
            $parsedRoute->setPlaceholders($parameters);
223
224 36
            $parsedRoutes[] = [$routeRegex, $parsedRoute];
225
        }
226
227 39
        return $parsedRoutes;
228
    }
229
230
    /**
231
     * Get route from request object.
232
     * Method expect an instance of PSR 7 compatible request object.
233
     *
234
     * @param object $request
235
     *
236
     * @return string
237
     */
238 23
    private function getUrl($request) : string
239
    {
240 23
        if ($this->urlFormat === RouteMode::PATH_FORMAT) {
241 19
            return $request->getUri()->getPath();
242
        }
243
244 4
        $queryParams = $request->getQueryParams();
245 4
        $route       = '';
246
247 4
        if (isset($queryParams[$this->modeRewriteParameter])) {
248 4
            $route = $queryParams[$this->modeRewriteParameter];
249
        }
250
251 4
        $path = '/'.$route;
252
253 4
        return $path;
254
    }
255
256
    /**
257
     * Prepare regex for resolving route a extract variables from route.
258
     *
259
     * @param string $route
260
     *
261
     * @return array regex and parameters
262
     */
263 36
    private function transformRoute(string $route) : array
264
    {
265 36
        $parameters = $this->extractVariableRouteParts($route);
266 36
        foreach ($parameters as $variables) {
267 28
            $route = $this->transformRouteVariables($route, $variables);
268
        }
269
270 36
        $route = '~^'.$route.'$~';
271
272 36
        return [$route, $parameters];
273
    }
274
275
    /**
276
     * Prepare regex for resolving route a extract variables from route.
277
     *
278
     * @param string $route     Route
279
     * @param array  $variables Variables
280
     *
281
     * @return string Transformed route
282
     */
283 28
    private function transformRouteVariables(string $route, array $variables) : string
284
    {
285 28
        list($valueToReplace, , $parametersVariables) = array_pad(
286 28
            $variables,
287 28
            3,
288 28
            null
289
        );
290
291 28
        if (isset($parametersVariables)) {
292 28
            return str_replace(
293 28
                $valueToReplace,
294 28
                '('.reset($parametersVariables).')',
295 28
                $route
296
            );
297
        }
298
299 24
        return str_replace($valueToReplace, '([^/]+)', $route);
300
    }
301
302
    /**
303
     * Extract variables from the route
304
     *
305
     * @param string $route
306
     *
307
     * @return array
308
     */
309 36
    private function extractVariableRouteParts(string $route) : array
310
    {
311 36
        $matches = [];
312 36
        preg_match_all(
313 36
            self::VARIABLE_REGEX,
314 36
            $route,
315 36
            $matches,
316 36
            PREG_OFFSET_CAPTURE | PREG_SET_ORDER
317
        );
318
319 36
        return $matches;
320
    }
321
}
322