Test Failed
Branch dev (494019)
by Alex
03:13
created

Matcher::buildRoute()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 9
rs 9.6666
cc 2
eloc 5
nc 2
nop 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
    public function __construct(Collector $collector, $basepath = "")
48
    {
49
        $this->collector = $collector;
50
        $this->basepath  = $basepath;
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
    public function match($httpMethod, $path)
66
    {
67
        $path = $this->parsePath($path);
68
69
        if ($route = $this->collector->findStaticRoute($httpMethod, $path)) {
70
            return $route;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $route; (array) is incompatible with the return type documented by Codeburner\Router\Matcher::match of type Codeburner\Router\Route|null.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
71
        }
72
73
        if ($route = $this->matchDynamicRoute($httpMethod, $path)) {
74
            return $route;
75
        }
76
77
        $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
    protected function matchDynamicRoute($httpMethod, $path)
91
    {
92
        $routes = $this->collector->findDynamicRoutes($httpMethod, $path);
93
94
        if (!$routes) {
95
            return false;
96
        }
97
98
        // chunk routes for smaller regex groups using the Sturges' Formula
99
        foreach (array_chunk($routes, round(1 + 3.3 * log(count($routes))), true) as $chunk)
100
        {
101
            array_map([$this, "buildRoute"], $chunk);
102
            list($pattern, $map) = $this->buildGroup($chunk);
103
104
            if (!preg_match($pattern, $path, $matches)) {
105
                continue;
106
            }
107
108
            /** @var Route $route */
109
            $route = $map[count($matches)];
110
            // removing the Path from array.
111
            unset($matches[0]);
112
            // sometimes null values come with the matches so the array_filter must be called first.
113
            $route->setParams(array_combine($route->getParams(), array_filter($matches)));
114
            // route must know who match them
115
            $route->setMatcher($this);
116
117
            return $route;
118
        }
119
120
        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
    protected function buildRoute(Route $route)
132
    {
133
        if ($route->blocked()) {
134
            return $route;
135
        }
136
        
137
        list($pattern, $params) = $this->parsePlaceholders($route->getPattern());
138
        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
    protected function buildGroup(array $routes)
150
    {
151
        $groupCount = (int) $map = $regex = [];
152
153
        /** @var Route $route */
154
        foreach ($routes as $route) {
155
            $params           = $route->getParams();
156
            $paramsCount      = count($params);
157
            $groupCount       = max($groupCount, $paramsCount) + 1;
158
            $regex[]          = $route->getPattern() . str_repeat("()", $groupCount - $paramsCount - 1);
159
            $map[$groupCount] = $route;
160
        }
161
162
        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
    protected function parsePlaceholders($pattern)
173
    {
174
        $params = [];
175
        preg_match_all("~" . Collector::DYNAMIC_REGEX . "~x", $pattern, $matches, PREG_SET_ORDER);
176
177
        foreach ((array) $matches as $key => $match) {
178
            $pattern = str_replace($match[0], isset($match[2]) ? "({$match[2]})" : "([^/]+)", $pattern);
179
            $params[$key] = $match[1];
180
        }
181
182
        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
    protected function parsePath($path)
195
    {
196
        $path = parse_url(substr(strstr(";" . $path, ";" . $this->basepath), strlen(";" . $this->basepath)), PHP_URL_PATH);
197
198
        if ($path === false) {
199
            throw new Exception("Seriously malformed URL passed to route dispatcher.");
200
        }
201
202
        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
    protected function matchSimilarRoute($httpMethod, $path)
216
    {
217
        $sm = $dm = [];
0 ignored issues
show
Unused Code introduced by
$sm 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...
218
219
        if ($sm = ($this->checkStaticRouteInOtherMethods($httpMethod, $path)) 
220
                || $dm = ($this->checkDynamicRouteInOtherMethods($httpMethod, $path))) {
221
            throw new MethodNotAllowedException($httpMethod, $path, array_merge((array) $sm, (array) $dm));
222
        }
223
224
        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
    protected function checkStaticRouteInOtherMethods($targetHttpMethod, $path)
237
    {
238
        return array_filter($this->getHttpMethodsBut($targetHttpMethod), function ($httpMethod) use ($path) {
239
            return (bool) $this->collector->findStaticRoute($httpMethod, $path);
240
        });
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
        return array_filter($this->getHttpMethodsBut($targetHttpMethod), function ($httpMethod) use ($path) {
255
            return (bool) $this->matchDynamicRoute($httpMethod, $path);
256
        });
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
    protected function getHttpMethodsBut($targetHttpMethod)
267
    {
268
        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