Passed
Branch dev (8e1e05)
by Alex
03:51
created

Matcher::parsePath()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2.032

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 10
ccs 4
cts 5
cp 0.8
rs 9.4285
cc 2
eloc 5
nc 2
nop 1
crap 2.032
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\Http\MethodNotAllowedException;
14
use Codeburner\Router\Exceptions\Http\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
     * @var Parser $parser
34
     */
35
36
    protected $parser;
37
38
    /**
39
     * Define a basepath to all routes.
40
     *
41
     * @var string
42
     */
43
44
    protected $basepath = "";
45
46
    /**
47
     * Construct the route dispatcher.
48
     *
49
     * @param Collector $collector
50
     * @param string $basepath Define a Path prefix that must be excluded on matches.
51
     */
52
53 54
    public function __construct(Collector $collector, $basepath = "")
54
    {
55 54
        $this->collector = $collector;
56 54
        $this->basepath  = $basepath;
57 54
    }
58
59
    /**
60
     * Find a route that matches the given arguments.
61
     * 
62
     * @param string $httpMethod
63
     * @param string $path
64
     *
65
     * @throws NotFoundException
66
     * @throws MethodNotAllowedException
67
     *
68
     * @return Route
69
     */
70
71 48
    public function match($httpMethod, $path)
72
    {
73 48
        $path = $this->parsePath($path);
74 48
        $route = null;
0 ignored issues
show
Unused Code introduced by
$route is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
75
76 48
        if (!$route = $this->collector->findStaticRoute($httpMethod, $path)) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The expression $this->collector->findSt...te($httpMethod, $path); of type Codeburner\Router\Route|false adds false to the return on line 83 which is incompatible with the return type documented by Codeburner\Router\Matcher::match of type Codeburner\Router\Route. It seems like you forgot to handle an error condition.
Loading history...
77 35
            if (!$route = $this->matchDynamicRoute($httpMethod, $path)) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The expression $this->matchDynamicRoute($httpMethod, $path); of type Codeburner\Router\Route|false adds false to the return on line 83 which is incompatible with the return type documented by Codeburner\Router\Matcher::match of type Codeburner\Router\Route. It seems like you forgot to handle an error condition.
Loading history...
78 14
                $this->matchSimilarRoute($httpMethod, $path);
79
            }
80 27
        }
81
82 42
        $route->setMatcher($this);
83 42
        return $route;
84
    }
85
86
    /**
87
     * Find and return the request dynamic route based on the compiled data and Path.
88
     *
89
     * @param string $httpMethod
90
     * @param string $path
91
     *
92
     * @return Route|false If the request match an array with the action and parameters will
93
     *                     be returned otherwise a false will.
94
     */
95
96 35
    protected function matchDynamicRoute($httpMethod, $path)
97
    {
98 35
        if ($routes = $this->collector->findDynamicRoutes($httpMethod, $path)) {
99
            // cache the parser reference
100 29
            $this->parser = $this->collector->getParser();
101
            // chunk routes for smaller regex groups using the Sturges' Formula
102 29
            foreach (array_chunk($routes, round(1 + 3.3 * log(count($routes))), true) as $chunk) {
103 29
                array_map([$this, "buildRoute"], $chunk);
104 29
                list($pattern, $map) = $this->buildGroup($chunk);
105
106 29
                if (!preg_match($pattern, $path, $matches)) {
107 7
                    continue;
108
                }
109
110
                /** @var Route $route */
111 27
                $route = $map[count($matches)];
112 27
                unset($matches[0]);
113
114 27
                $route->setParams(array_combine($route->getParams(), array_filter($matches)));
115
116 27
                return $route;
117 7
            }
118 7
        }
119
120 14
        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 29
    protected function buildRoute(Route $route)
132
    {
133 29
        if ($route->getBlock()) {
134 9
            return $route;
135
        }
136
137 29
        list($pattern, $params) = $this->parsePlaceholders($route->getPattern());
138 29
        return $route->setPatternWithoutReset($pattern)->setParams($params)->setBlock(true);
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 Route[] $routes
146
     * @return array
147
     */
148
149 29
    protected function buildGroup(array $routes)
150
    {
151 29
        $groupCount = (int) $map = $regex = [];
152
153 29
        foreach ($routes as $route) {
154 29
            $params           = $route->getParams();
155 29
            $paramsCount      = count($params);
156 29
            $groupCount       = max($groupCount, $paramsCount) + 1;
157 29
            $regex[]          = $route->getPattern() . str_repeat("()", $groupCount - $paramsCount - 1);
158 29
            $map[$groupCount] = $route;
159 29
        }
160
161 29
        return ["~^(?|" . implode("|", $regex) . ")$~", $map];
162
    }
163
164
    /**
165
     * Parse an route pattern seeking for parameters and build the route regex.
166
     *
167
     * @param string $pattern
168
     * @return array 0 => new route regex, 1 => map of parameter names
169
     */
170
171 29
    protected function parsePlaceholders($pattern)
172
    {
173 29
        $params = [];
174 29
        $parser = $this->parser;
175
176 29
        preg_match_all("~" . $parser::DYNAMIC_REGEX . "~x", $pattern, $matches, PREG_SET_ORDER);
177
178 29
        foreach ((array) $matches as $key => $match) {
179 29
            $pattern = str_replace($match[0], isset($match[2]) ? "({$match[2]})" : "([^/]+)", $pattern);
180 29
            $params[$key] = $match[1];
181 29
        }
182
183 29
        return [$pattern, $params];
184
    }
185
186
    /**
187
     * Get only the path of a given url.
188
     *
189
     * @param string $path The given URL
190
     *
191
     * @throws Exception
192
     * @return string
193
     */
194
195 48
    protected function parsePath($path)
196
    {
197 48
        $path = parse_url(substr(strstr(";" . $path, ";" . $this->basepath), strlen(";" . $this->basepath)), PHP_URL_PATH);
198
199 48
        if ($path === false) {
200
            throw new Exception("Seriously malformed URL passed to route matcher.");
201
        }
202
203 48
        return $path;
204
    }
205
206
    /**
207
     * Generate an HTTP error request with method not allowed or not found.
208
     *
209
     * @param string $httpMethod
210
     * @param string $path
211
     *
212
     * @throws NotFoundException
213
     * @throws MethodNotAllowedException
214
     */
215
216 14
    protected function matchSimilarRoute($httpMethod, $path)
217
    {
218 14
        $dm = [];
219
220 14
        if (($sm = $this->checkStaticRouteInOtherMethods($httpMethod, $path))
221 14
                || ($dm = $this->checkDynamicRouteInOtherMethods($httpMethod, $path))) {
222 5
            throw new MethodNotAllowedException($httpMethod, $path, array_merge((array) $sm, (array) $dm));
223
        }
224
225 9
        throw new NotFoundException;
226
    }
227
228
    /**
229
     * Verify if a static route match in another method than the requested.
230
     *
231
     * @param string $targetHttpMethod The HTTP method that must not be checked
232
     * @param string $path              The Path that must be matched.
233
     *
234
     * @return array
235
     */
236
237 14
    protected function checkStaticRouteInOtherMethods($targetHttpMethod, $path)
238
    {
239
        return array_filter($this->getHttpMethodsBut($targetHttpMethod), function ($httpMethod) use ($path) {
240 14
            return (bool) $this->collector->findStaticRoute($httpMethod, $path);
241 14
        });
242
    }
243
244
    /**
245
     * Verify if a dynamic route match in another method than the requested.
246
     *
247
     * @param string $targetHttpMethod The HTTP method that must not be checked
248
     * @param string $path             The Path that must be matched.
249
     *
250
     * @return array
251
     */
252
253
    protected function checkDynamicRouteInOtherMethods($targetHttpMethod, $path)
254
    {
255 9
        return array_filter($this->getHttpMethodsBut($targetHttpMethod), function ($httpMethod) use ($path) {
256 9
            return (bool) $this->matchDynamicRoute($httpMethod, $path);
257 9
        });
258
    }
259
260
    /**
261
     * Strip the given http methods and return all the others.
262
     *
263
     * @param string|string[]
264
     * @return array
265
     */
266
267 14
    protected function getHttpMethodsBut($targetHttpMethod)
268
    {
269 14
        return array_diff(explode(" ", Collector::HTTP_METHODS), (array) $targetHttpMethod);
270
    }
271
272
    /**
273
     * @return Collector
274
     */
275
276
    public function getCollector()
277
    {
278
        return $this->collector;
279
    }
280
281
    /**
282
     * @return string
283
     */
284
285 1
    public function getBasePath()
286
    {
287 1
        return $this->basepath;
288
    }
289
290
    /**
291
     * Set a new basepath, this will be a prefix that must be excluded in
292
     * every requested Path.
293
     *
294
     * @param string $basepath The new basepath
295
     */
296
    
297 1
    public function setBasePath($basepath)
298
    {
299 1
        $this->basepath = $basepath;
300 1
    }
301
302
}
303