Passed
Branch dev (e7fa0b)
by Alex
02:32
created

Matcher::buildGroup()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 15
ccs 10
cts 10
cp 1
rs 9.4285
cc 2
eloc 9
nc 2
nop 1
crap 2
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 48
    public function __construct(Collector $collector, $basepath = "")
48
    {
49 48
        $this->collector = $collector;
50 48
        $this->basepath  = $basepath;
51 48
    }
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 43
    public function match($httpMethod, $path)
66
    {
67 43
        $path = $this->parsePath($path);
68
69 43
        if ($route = $this->collector->findStaticRoute($httpMethod, $path)) {
70 22
            return $route;
71
        }
72
73 31
        if ($route = $this->matchDynamicRoute($httpMethod, $path)) {
74 23
            return $route;
75
        }
76
77 14
        $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 31
    protected function matchDynamicRoute($httpMethod, $path)
91
    {
92 31
        if ($routes = $this->collector->findDynamicRoutes($httpMethod, $path)) {
93
            // chunk routes for smaller regex groups using the Sturges' Formula
94 25
            foreach (array_chunk($routes, round(1 + 3.3 * log(count($routes))), true) as $chunk) {
95 25
                array_map([$this, "buildRoute"], $chunk);
96 25
                list($pattern, $map) = $this->buildGroup($chunk);
97
98 25
                if (!preg_match($pattern, $path, $matches)) {
99 7
                    continue;
100
                }
101
102
                /** @var Route $route */
103 23
                $route = $map[count($matches)];
104 23
                unset($matches[0]);
105
106 23
                $route->setParams(array_combine($route->getParams(), array_filter($matches)));
107 23
                $route->setMatcher($this);
108
109 23
                return $route;
110 7
            }
111 7
        }
112
113 14
        return false;
114
    }
115
116
    /**
117
     * Parse the dynamic segments of the pattern and replace then for
118
     * corresponding regex.
119
     *
120
     * @param Route $route
121
     * @return Route
122
     */
123
124 25
    protected function buildRoute(Route $route)
125
    {
126 25
        if ($route->getBlock()) {
127 9
            return $route;
128
        }
129
130 25
        list($pattern, $params) = $this->parsePlaceholders($route->getPattern());
131 25
        return $route->setPatternWithoutReset($pattern)->setParams($params)->setBlock(true);
132
    }
133
134
    /**
135
     * Group several dynamic routes patterns into one big regex and maps
136
     * the routes to the pattern positions in the big regex.
137
     *
138
     * @param array $routes
139
     * @return array
140
     */
141
142 25
    protected function buildGroup(array $routes)
143
    {
144 25
        $groupCount = (int) $map = $regex = [];
145
146
        /** @var Route $route */
147 25
        foreach ($routes as $route) {
148 25
            $params           = $route->getParams();
149 25
            $paramsCount      = count($params);
150 25
            $groupCount       = max($groupCount, $paramsCount) + 1;
151 25
            $regex[]          = $route->getPattern() . str_repeat("()", $groupCount - $paramsCount - 1);
152 25
            $map[$groupCount] = $route;
153 25
        }
154
155 25
        return ["~^(?|" . implode("|", $regex) . ")$~", $map];
156
    }
157
158
    /**
159
     * Parse an route pattern seeking for parameters and build the route regex.
160
     *
161
     * @param string $pattern
162
     * @return array 0 => new route regex, 1 => map of parameter names
163
     */
164
165 25
    protected function parsePlaceholders($pattern)
166
    {
167 25
        $params = [];
168 25
        preg_match_all("~" . Collector::DYNAMIC_REGEX . "~x", $pattern, $matches, PREG_SET_ORDER);
169
170 25
        foreach ((array) $matches as $key => $match) {
171 25
            $pattern = str_replace($match[0], isset($match[2]) ? "({$match[2]})" : "([^/]+)", $pattern);
172 25
            $params[$key] = $match[1];
173 25
        }
174
175 25
        return [$pattern, $params];
176
    }
177
178
    /**
179
     * Get only the path of a given url.
180
     *
181
     * @param string $path The given URL
182
     *
183
     * @throws Exception
184
     * @return string
185
     */
186
187 43
    protected function parsePath($path)
188
    {
189 43
        $path = parse_url(substr(strstr(";" . $path, ";" . $this->basepath), strlen(";" . $this->basepath)), PHP_URL_PATH);
190
191 43
        if ($path === false) {
192
            throw new Exception("Seriously malformed URL passed to route matcher.");
193
        }
194
195 43
        return $path;
196
    }
197
198
    /**
199
     * Generate an HTTP error request with method not allowed or not found.
200
     *
201
     * @param string $httpMethod
202
     * @param string $path
203
     *
204
     * @throws NotFoundException
205
     * @throws MethodNotAllowedException
206
     */
207
208 14
    protected function matchSimilarRoute($httpMethod, $path)
209
    {
210 14
        $dm = [];
211
212 14
        if (($sm = $this->checkStaticRouteInOtherMethods($httpMethod, $path))
213 14
                || ($dm = $this->checkDynamicRouteInOtherMethods($httpMethod, $path))) {
214 5
            throw new MethodNotAllowedException($httpMethod, $path, array_merge((array) $sm, (array) $dm));
215
        }
216
217 9
        throw new NotFoundException($httpMethod, $path);
218
    }
219
220
    /**
221
     * Verify if a static route match in another method than the requested.
222
     *
223
     * @param string $targetHttpMethod The HTTP method that must not be checked
224
     * @param string $path              The Path that must be matched.
225
     *
226
     * @return array
227
     */
228
229 14
    protected function checkStaticRouteInOtherMethods($targetHttpMethod, $path)
230
    {
231
        return array_filter($this->getHttpMethodsBut($targetHttpMethod), function ($httpMethod) use ($path) {
232 14
            return (bool) $this->collector->findStaticRoute($httpMethod, $path);
233 14
        });
234
    }
235
236
    /**
237
     * Verify if a dynamic route match in another method than the requested.
238
     *
239
     * @param string $targetHttpMethod The HTTP method that must not be checked
240
     * @param string $path             The Path that must be matched.
241
     *
242
     * @return array
243
     */
244
245
    protected function checkDynamicRouteInOtherMethods($targetHttpMethod, $path)
246
    {
247 9
        return array_filter($this->getHttpMethodsBut($targetHttpMethod), function ($httpMethod) use ($path) {
248 9
            return (bool) $this->matchDynamicRoute($httpMethod, $path);
249 9
        });
250
    }
251
252
    /**
253
     * Strip the given http methods and return all the others.
254
     *
255
     * @param  array|string
256
     * @return array
257
     */
258
259 14
    protected function getHttpMethodsBut($targetHttpMethod)
260
    {
261 14
        return array_diff(explode(" ", Collector::HTTP_METHODS), (array) $targetHttpMethod);
262
    }
263
264
    /**
265
     * @return Collector
266
     */
267
268
    public function getCollector()
269
    {
270
        return $this->collector;
271
    }
272
273
    /**
274
     * @return string
275
     */
276
277 1
    public function getBasePath()
278
    {
279 1
        return $this->basepath;
280
    }
281
282
    /**
283
     * Set a new basepath, this will be a prefix that must be excluded in
284
     * every requested Path.
285
     *
286
     * @param string $basepath The new basepath
287
     */
288
    
289 1
    public function setBasePath($basepath)
290
    {
291 1
        $this->basepath = $basepath;
292 1
    }
293
294
}
295