Completed
Push — master ( 1798cf...75b2e6 )
by Bohuslav
01:48
created

Matcher::routeMatch()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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