Passed
Branch dev (8b2306)
by Alex
02:48
created

Matcher   A

Complexity

Total Complexity 26

Size/Duplication

Total Lines 279
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 88.31%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 26
c 1
b 0
f 0
lcom 1
cbo 4
dl 0
loc 279
ccs 68
cts 77
cp 0.8831
rs 10

14 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A match() 0 14 3
B matchDynamicRoute() 0 32 4
A buildRoute() 0 9 2
A buildGroup() 0 15 2
A parsePlaceholders() 0 12 3
A parsePath() 0 10 2
A matchSimilarRoute() 0 11 3
A checkStaticRouteInOtherMethods() 0 6 1
A checkDynamicRouteInOtherMethods() 0 6 1
A getHttpMethodsBut() 0 4 1
A getCollector() 0 4 1
A getBasePath() 0 4 1
A setBasePath() 0 4 1
1
<?php
2
3
/**
4
 * Codeburner Framework.
5
 *
6
 * @author Alex Rohleder <[email protected]>
7
 * @copyright 2016 Alex Rohleder
8
 * @license http://opensource.org/licenses/MIT
9
 */
10
11
namespace Codeburner\Router;
12
13
use Codeburner\Router\Exceptions\MethodNotAllowedException;
14
use Codeburner\Router\Exceptions\NotFoundException;
15
use Exception;
16
17
/**
18
 * The matcher class find the route for a given http method and path.
19
 *
20
 * @author Alex Rohleder <[email protected]>
21
 */
22
23
class Matcher
24
{
25
26
    /**
27
     * @var Collector
28
     */
29
30
    protected $collector;
31
32
    /**
33
     * Define a basepath to all routes.
34
     *
35
     * @var string
36
     */
37
38
    protected $basepath = "";
39
40
    /**
41
     * Construct the route dispatcher.
42
     *
43
     * @param Collector $collector
44
     * @param string $basepath Define a Path prefix that must be excluded on matches.
45
     */
46
47 28
    public function __construct(Collector $collector, $basepath = "")
48
    {
49 28
        $this->collector = $collector;
50 28
        $this->basepath  = $basepath;
51 28
    }
52
53
    /**
54
     * Find a route that matches the given arguments.
55
     * 
56
     * @param string $httpMethod
57
     * @param string $path
58
     *
59
     * @throws NotFoundException
60
     * @throws MethodNotAllowedException
61
     *
62
     * @return Route
63
     */
64
65 27
    public function match($httpMethod, $path)
66
    {
67 27
        $path = $this->parsePath($path);
68
69 27
        if ($route = $this->collector->findStaticRoute($httpMethod, $path)) {
70 14
            return $route;
71
        }
72
73 22
        if ($route = $this->matchDynamicRoute($httpMethod, $path)) {
74 15
            return $route;
75
        }
76
77 11
        $this->matchSimilarRoute($httpMethod, $path);
78
    }
79
80
    /**
81
     * Find and return the request dynamic route based on the compiled data and Path.
82
     *
83
     * @param string $httpMethod
84
     * @param string $path
85
     *
86
     * @return Route|false If the request match an array with the action and parameters will
87
     *                     be returned otherwise a false will.
88
     */
89
90 22
    protected function matchDynamicRoute($httpMethod, $path)
91
    {
92 22
        $routes = $this->collector->findDynamicRoutes($httpMethod, $path);
93
94 22
        if (!$routes) {
95 11
            return false;
96
        }
97
98
        // chunk routes for smaller regex groups using the Sturges' Formula
99 17
        foreach (array_chunk($routes, round(1 + 3.3 * log(count($routes))), true) as $chunk)
100
        {
101 17
            array_map([$this, "buildRoute"], $chunk);
102 17
            list($pattern, $map) = $this->buildGroup($chunk);
103
104 17
            if (!preg_match($pattern, $path, $matches)) {
105 5
                continue;
106
            }
107
108
            /** @var Route $route */
109 15
            $route = $map[count($matches)];
110
            // removing the Path from array.
111 15
            unset($matches[0]);
112
            // sometimes null values come with the matches so the array_filter must be called first.
113 15
            $route->setParams(array_combine($route->getParams(), array_filter($matches)));
114
            // route must know who match them
115 15
            $route->setMatcher($this);
116
117 15
            return $route;
118 5
        }
119
120 5
        return false;
121
    }
122
123
    /**
124
     * Parse the dynamic segments of the pattern and replace then for
125
     * corresponding regex.
126
     *
127
     * @param Route $route
128
     * @return Route
129
     */
130
131 17
    protected function buildRoute(Route $route)
132
    {
133 17
        if ($route->blocked()) {
134 4
            return $route;
135
        }
136
        
137 17
        list($pattern, $params) = $this->parsePlaceholders($route->getPattern());
138 17
        return  $route->setPatternWithoutReset($pattern)->setParams($params)->block();
139
    }
140
141
    /**
142
     * Group several dynamic routes patterns into one big regex and maps
143
     * the routes to the pattern positions in the big regex.
144
     *
145
     * @param array $routes
146
     * @return array
147
     */
148
149 17
    protected function buildGroup(array $routes)
150
    {
151 17
        $groupCount = (int) $map = $regex = [];
152
153
        /** @var Route $route */
154 17
        foreach ($routes as $route) {
155 17
            $params           = $route->getParams();
156 17
            $paramsCount      = count($params);
157 17
            $groupCount       = max($groupCount, $paramsCount) + 1;
158 17
            $regex[]          = $route->getPattern() . str_repeat("()", $groupCount - $paramsCount - 1);
159 17
            $map[$groupCount] = $route;
160 17
        }
161
162 17
        return ["~^(?|" . implode("|", $regex) . ")$~", $map];
163
    }
164
165
    /**
166
     * Parse an route pattern seeking for parameters and build the route regex.
167
     *
168
     * @param string $pattern
169
     * @return array 0 => new route regex, 1 => map of parameter names
170
     */
171
172 17
    protected function parsePlaceholders($pattern)
173
    {
174 17
        $params = [];
175 17
        preg_match_all("~" . Collector::DYNAMIC_REGEX . "~x", $pattern, $matches, PREG_SET_ORDER);
176
177 17
        foreach ((array) $matches as $key => $match) {
178 17
            $pattern = str_replace($match[0], isset($match[2]) ? "({$match[2]})" : "([^/]+)", $pattern);
179 17
            $params[$key] = $match[1];
180 17
        }
181
182 17
        return [$pattern, $params];
183
    }
184
185
    /**
186
     * Get only the path of a given url.
187
     *
188
     * @param string $path The given URL
189
     *
190
     * @throws Exception
191
     * @return string
192
     */
193
194 27
    protected function parsePath($path)
195
    {
196 27
        $path = parse_url(substr(strstr(";" . $path, ";" . $this->basepath), strlen(";" . $this->basepath)), PHP_URL_PATH);
197
198 27
        if ($path === false) {
199
            throw new Exception("Seriously malformed URL passed to route dispatcher.");
200
        }
201
202 27
        return $path;
203
    }
204
205
    /**
206
     * Generate an HTTP error request with method not allowed or not found.
207
     *
208
     * @param string $httpMethod
209
     * @param string $path
210
     *
211
     * @throws NotFoundException
212
     * @throws MethodNotAllowedException
213
     */
214
215 11
    protected function matchSimilarRoute($httpMethod, $path)
216
    {
217 11
        $dm = [];
218
219 11
        if (($sm = $this->checkStaticRouteInOtherMethods($httpMethod, $path))
220 11
                || ($dm = $this->checkDynamicRouteInOtherMethods($httpMethod, $path))) {
221 4
            throw new MethodNotAllowedException($httpMethod, $path, array_merge((array) $sm, (array) $dm));
222
        }
223
224 7
        throw new NotFoundException($httpMethod, $path);
225
    }
226
227
    /**
228
     * Verify if a static route match in another method than the requested.
229
     *
230
     * @param string $targetHttpMethod The HTTP method that must not be checked
231
     * @param string $path              The Path that must be matched.
232
     *
233
     * @return array
234
     */
235
236 11
    protected function checkStaticRouteInOtherMethods($targetHttpMethod, $path)
237
    {
238
        return array_filter($this->getHttpMethodsBut($targetHttpMethod), function ($httpMethod) use ($path) {
239 11
            return (bool) $this->collector->findStaticRoute($httpMethod, $path);
240 11
        });
241
    }
242
243
    /**
244
     * Verify if a dynamic route match in another method than the requested.
245
     *
246
     * @param string $targetHttpMethod The HTTP method that must not be checked
247
     * @param string $path             The Path that must be matched.
248
     *
249
     * @return array
250
     */
251
252
    protected function checkDynamicRouteInOtherMethods($targetHttpMethod, $path)
253
    {
254 7
        return array_filter($this->getHttpMethodsBut($targetHttpMethod), function ($httpMethod) use ($path) {
255 7
            return (bool) $this->matchDynamicRoute($httpMethod, $path);
256 7
        });
257
    }
258
259
    /**
260
     * Strip the given http methods and return all the others.
261
     *
262
     * @param  array|string
263
     * @return array
264
     */
265
266 11
    protected function getHttpMethodsBut($targetHttpMethod)
267
    {
268 11
        return array_diff(explode(" ", Collector::HTTP_METHODS), (array) $targetHttpMethod);
269
    }
270
271
    /**
272
     * @return Collector
273
     */
274
275
    public function getCollector()
276
    {
277
        return $this->collector;
278
    }
279
280
    /**
281
     * @return string
282
     */
283
284
    public function getBasePath()
285
    {
286
        return $this->basepath;
287
    }
288
289
    /**
290
     * Set a new basepath, this will be a prefix that must be excluded in
291
     * every requested Path.
292
     *
293
     * @param string $basepath The new basepath
294
     */
295
    
296
    public function setBasePath($basepath)
297
    {
298
        $this->basepath = $basepath;
299
    }
300
301
}
302