Passed
Branch master (60a40e)
by Alex
03:08
created

Matcher::getBasePath()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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