Test Failed
Branch develop (ab83c3)
by Bohuslav
02:47
created

Regex   A

Complexity

Total Complexity 23

Size/Duplication

Total Lines 275
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 2

Importance

Changes 0
Metric Value
dl 0
loc 275
c 0
b 0
f 0
wmc 23
lcom 1
cbo 2
rs 10

11 Methods

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