Passed
Branch dev (709f37)
by Alex
03:14
created

Matcher::match()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3.0175

Importance

Changes 3
Bugs 1 Features 0
Metric Value
c 3
b 1
f 0
dl 0
loc 13
ccs 7
cts 8
cp 0.875
rs 9.4285
cc 3
eloc 7
nc 2
nop 2
crap 3.0175
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 57
    public function __construct(Collector $collector, $basepath = "")
54
    {
55 57
        $this->collector = $collector;
56 57
        $this->basepath  = $basepath;
57 57
    }
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 51
    public function match($httpMethod, $path)
72
    {
73 51
        $path = $this->parsePath($path);
74
75 51
        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 79 which is incompatible with the return type documented by Codeburner\Router\Matcher::match of type Codeburner\Router\Route|null. It seems like you forgot to handle an error condition.
Loading history...
76 51
            ($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 79 which is incompatible with the return type documented by Codeburner\Router\Matcher::match of type Codeburner\Router\Route|null. It seems like you forgot to handle an error condition.
Loading history...
77 45
             $route->setMatcher($this);
78
79 45
            return $route;
80
        }
81
82 14
        $this->matchSimilarRoute($httpMethod, $path);
83
    }
84
85
    /**
86
     * Find and return the request dynamic route based on the compiled data and Path.
87
     *
88
     * @param string $httpMethod
89
     * @param string $path
90
     *
91
     * @return Route|false If the request match an array with the action and parameters will
92
     *                     be returned otherwise a false will.
93
     */
94
95 38
    protected function matchDynamicRoute($httpMethod, $path)
96
    {
97 38
        if ($routes = $this->collector->findDynamicRoutes($httpMethod, $path)) {
98
            // cache the parser reference
99 32
            $this->parser = $this->collector->getParser();
100
            // chunk routes for smaller regex groups using the Sturges' Formula
101 32
            foreach (array_chunk($routes, round(1 + 3.3 * log(count($routes))), true) as $chunk) {
102 32
                array_map([$this, "buildRoute"], $chunk);
103 32
                list($pattern, $map) = $this->buildGroup($chunk);
104
105 32
                if (!preg_match($pattern, $path, $matches)) {
106 7
                    continue;
107
                }
108
109
                /** @var Route $route */
110 30
                $route = $map[count($matches)];
111 30
                unset($matches[0]);
112
113 30
                $route->setParams(array_combine($route->getParams(), array_filter($matches)));
114
115 30
                return $route;
116 7
            }
117 7
        }
118
119 14
        return false;
120
    }
121
122
    /**
123
     * Parse the dynamic segments of the pattern and replace then for
124
     * corresponding regex.
125
     *
126
     * @param Route $route
127
     * @return Route
128
     */
129
130 32
    protected function buildRoute(Route $route)
131
    {
132 32
        if ($route->getBlock()) {
133 9
            return $route;
134
        }
135
136 32
        list($pattern, $params) = $this->parsePlaceholders($route->getPattern());
137 32
        return $route->setPatternWithoutReset($pattern)->setParams($params)->setBlock(true);
138
    }
139
140
    /**
141
     * Group several dynamic routes patterns into one big regex and maps
142
     * the routes to the pattern positions in the big regex.
143
     *
144
     * @param Route[] $routes
145
     * @return array
146
     */
147
148 32
    protected function buildGroup(array $routes)
149
    {
150 32
        $groupCount = (int) $map = $regex = [];
151
152 32
        foreach ($routes as $route) {
153 32
            $params           = $route->getParams();
154 32
            $paramsCount      = count($params);
155 32
            $groupCount       = max($groupCount, $paramsCount) + 1;
156 32
            $regex[]          = $route->getPattern() . str_repeat("()", $groupCount - $paramsCount - 1);
157 32
            $map[$groupCount] = $route;
158 32
        }
159
160 32
        return ["~^(?|" . implode("|", $regex) . ")$~", $map];
161
    }
162
163
    /**
164
     * Parse an route pattern seeking for parameters and build the route regex.
165
     *
166
     * @param string $pattern
167
     * @return array 0 => new route regex, 1 => map of parameter names
168
     */
169
170 32
    protected function parsePlaceholders($pattern)
171
    {
172 32
        $params = [];
173 32
        $parser = $this->parser;
174
175 32
        preg_match_all("~" . $parser::DYNAMIC_REGEX . "~x", $pattern, $matches, PREG_SET_ORDER);
176
177 32
        foreach ((array) $matches as $key => $match) {
178 32
            $pattern = str_replace($match[0], isset($match[2]) ? "({$match[2]})" : "([^/]+)", $pattern);
179 32
            $params[$key] = $match[1];
180 32
        }
181
182 32
        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 51
    protected function parsePath($path)
195
    {
196 51
        $path = parse_url(substr(strstr(";" . $path, ";" . $this->basepath), strlen(";" . $this->basepath)), PHP_URL_PATH);
197
198 51
        if ($path === false) {
199
            throw new Exception("Seriously malformed URL passed to route matcher.");
200
        }
201
202 51
        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 14
    protected function matchSimilarRoute($httpMethod, $path)
216
    {
217 14
        $dm = [];
218
219 14
        if (($sm = $this->checkStaticRouteInOtherMethods($httpMethod, $path))
220 14
                || ($dm = $this->checkDynamicRouteInOtherMethods($httpMethod, $path))) {
221 5
            throw new MethodNotAllowedException($httpMethod, $path, array_merge((array) $sm, (array) $dm));
222
        }
223
224 9
        throw new NotFoundException;
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 14
    protected function checkStaticRouteInOtherMethods($targetHttpMethod, $path)
237
    {
238
        return array_filter($this->getHttpMethodsBut($targetHttpMethod), function ($httpMethod) use ($path) {
239 14
            return (bool) $this->collector->findStaticRoute($httpMethod, $path);
240 14
        });
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 9
        return array_filter($this->getHttpMethodsBut($targetHttpMethod), function ($httpMethod) use ($path) {
255 9
            return (bool) $this->matchDynamicRoute($httpMethod, $path);
256 9
        });
257
    }
258
259
    /**
260
     * Strip the given http methods and return all the others.
261
     *
262
     * @param string|string[]
263
     * @return array
264
     */
265
266 14
    protected function getHttpMethodsBut($targetHttpMethod)
267
    {
268 14
        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 1
    public function getBasePath()
285
    {
286 1
        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 1
    public function setBasePath($basepath)
297
    {
298 1
        $this->basepath = $basepath;
299 1
    }
300
301
}
302