Passed
Push — master ( 6d3166...a738c4 )
by Alex
02:19 queued 10s
created

Mezon/Router/UrlParser.php (1 issue)

Labels
Severity
1
<?php
2
namespace Mezon\Router;
3
4
use Mezon\Router\Types\BaseType;
5
6
trait UrlParser
7
{
8
9
    /**
10
     * Parsed parameters of the calling router
11
     *
12
     * @var array
13
     */
14
    protected $parameters = [];
15
16
    /**
17
     * Called route
18
     *
19
     * @var string
20
     */
21
    protected $calledRoute = '';
22
23
    /**
24
     * Cache for regular expressions
25
     *
26
     * @var array
27
     */
28
    private $cachedRegExps = [];
29
30
    /**
31
     * Cached parameters for route
32
     *
33
     * @var array
34
     */
35
    private $cachedParameters = [];
36
37
    /**
38
     * Middleware for routes processing
39
     *
40
     * @var array
41
     */
42
    private $middleware = [];
43
44
    /**
45
     * Method compiles route pattern string in regex string.
46
     * For example [i:id]/some-str in ([\[0-9\]])/some-str
47
     *
48
     * @param string $routerPattern
49
     *            router pattern
50
     * @return string regexp pattern
51
     */
52
    private function _getRouteMatcherRegExPattern(string $routerPattern): string
53
    {
54
        $key = $routerPattern;
55
56
        // try read from cache
57
        if (isset($this->cachedRegExps[$key])) {
58
            return $this->cachedRegExps[$key];
59
        }
60
61
        // parsing routes
62
        $compiledRouterPattern = $routerPattern;
63
        foreach ($this->types as $typeClass) {
64
            $compiledRouterPattern = preg_replace('/' . $typeClass::searchRegExp() . '/', '(' . $typeClass::parserRegExp() . ')', $compiledRouterPattern);
65
        }
66
67
        // final setup + save in cache
68
        $this->cachedRegExps[$key] = $compiledRouterPattern;
69
70
        return $compiledRouterPattern;
71
    }
72
73
    /**
74
     * Method returns all parameter names in the route
75
     *
76
     * @param string $routerPattern
77
     *            route
78
     * @return array names
79
     */
80
    private function _getParameterNames(string $routerPattern): array
81
    {
82
        if (isset($this->cachedParameters[$routerPattern])) {
83
            return $this->cachedParameters[$routerPattern];
84
        }
85
86
        $regExPattern = [];
87
88
        foreach (array_keys($this->types) as $typeName) {
89
            $regExPattern[] = $typeName;
90
        }
91
92
        $regExPattern = '\[(' . implode('|', $regExPattern) . '):(' . BaseType::PARAMETER_NAME_REGEXP . ')\]';
93
94
        $names = [];
95
        preg_match_all('/' . str_replace('/', '\\/', $regExPattern) . '/', $routerPattern, $names);
96
97
        $return = [];
98
99
        foreach ($names[2] as $name) {
100
            $return[] = $name;
101
        }
102
103
        $this->cachedParameters[$routerPattern] = $return;
104
105
        return $return;
106
    }
107
108
    /**
109
     * Method warms cache
110
     */
111
    public function warmCache(): void
112
    {
113
        foreach (self::getListOfSupportedRequestMethods() as $requestMethod) {
114
            foreach ($this->paramRoutes[$requestMethod] as $bunch) {
115
                foreach ($bunch['bunch'] as $route) {
116
                    $this->_getRouteMatcherRegExPattern($route['pattern']);
117
118
                    $this->_getParameterNames($route['pattern']);
119
                }
120
            }
121
        }
122
123
        $this->compileRegexpForBunches();
124
    }
125
126
    /**
127
     * Method searches dynamic route processor
128
     *
129
     * @param string $route
130
     *            Route
131
     * @param string $requestMethod
132
     *            Request method
133
     * @return array|callable|bool route's handler or false in case the handler was not found
134
     */
135
    protected function getDynamicRouteProcessor(string $route, string $requestMethod = '')
136
    {
137
        $bunches = $this->paramRoutes[$requestMethod == '' ? $_SERVER['REQUEST_METHOD'] : $requestMethod];
138
139
        foreach ($bunches as $bunch) {
140
            $matches = [];
141
142
            if (preg_match($bunch['regexp'], $route, $matches)) {
143
                $routeData = $bunch['bunch'][count($matches)];
144
145
                $names = $this->_getParameterNames($routeData['pattern']);
146
147
                $this->parameters = [];
148
                foreach ($names as $i => $name) {
149
                    $this->parameters[$name] = $matches[$i + 1];
150
                }
151
152
                $this->calledRoute = $routeData['pattern'];
153
154
                return $routeData['callback'];
155
            }
156
        }
157
158
        // match was not found
159
        return false;
160
    }
161
162
    /**
163
     * Method searches dynamic route processor
164
     *
165
     * @param string $route
166
     *            Route
167
     * @return string|bool Result of the router'scall or false if any error occured
168
     */
169
    public function findDynamicRouteProcessor(string $route)
170
    {
171
        $processor = $this->getDynamicRouteProcessor($route);
172
173
        if ($processor === false) {
174
            return false;
175
        }
176
177
        return $this->executeHandler($processor, $route);
178
    }
179
180
    /**
181
     * Checking that method exists
182
     *
183
     * @param mixed $processor
184
     *            callback object
185
     * @param ?string $functionName
186
     *            callback method
187
     * @return bool true if method does not exists
188
     */
189
    private function methodDoesNotExists($processor, ?string $functionName): bool
190
    {
191
        return isset($processor[0]) && method_exists($processor[0], $functionName) === false;
192
    }
193
194
    /**
195
     * Checking that handler can be called
196
     *
197
     * @param object|array|callable $processor
198
     *            callback object
199
     * @param ?string $functionName
200
     *            callback method
201
     * @return bool
202
     */
203
    private function canBeCalled($processor, ?string $functionName): bool
204
    {
205
        return is_callable($processor) && (method_exists($processor[0], $functionName) || isset($processor[0]->$functionName));
0 ignored issues
show
It seems like $functionName can also be of type null; however, parameter $method of method_exists() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

205
        return is_callable($processor) && (method_exists($processor[0], /** @scrutinizer ignore-type */ $functionName) || isset($processor[0]->$functionName));
Loading history...
206
    }
207
208
    /**
209
     * Checking that processor can be called as function
210
     *
211
     * @param mixed $processor
212
     *            route processor
213
     * @return bool true if the $processor can be called as function
214
     */
215
    private function isFunction($processor): bool
216
    {
217
        return is_callable($processor) && is_array($processor) === false;
218
    }
219
220
    /**
221
     * Method returns either universal hanler if it fits or normal handler
222
     *
223
     * @param array $processors
224
     *            list of routes and handlers
225
     * @param string $route
226
     *            calling route
227
     * @return mixed processor
228
     */
229
    protected function getExactRouteHandlerOrUniversal(&$processors, string $route)
230
    {
231
        $this->calledRoute = $route;
232
233
        if ($this->universalRouteWasAdded) {
234
            $allRoutes = array_keys($processors);
235
236
            if (array_search('*', $allRoutes) <= array_search($route, $allRoutes)) {
237
                $processor = $processors['*'];
238
                $this->calledRoute = '*';
239
            } else {
240
                $processor = $processors[$route];
241
            }
242
        } else {
243
            $processor = $processors[$route];
244
        }
245
246
        return $processor;
247
    }
248
249
    /**
250
     * Method registeres middleware for the router
251
     *
252
     * @param callable $middleware
253
     *            middleware
254
     */
255
    public function registerMiddleware(string $router, callable $middleware): void
256
    {
257
        $routerTrimmed = trim($router, '/');
258
259
        if (! isset($this->middleware[$routerTrimmed])) {
260
            $this->middleware[$routerTrimmed] = [];
261
        }
262
263
        $this->middleware[$routerTrimmed][] = $middleware;
264
    }
265
266
    /**
267
     * Method returns middleware processing result
268
     *
269
     * @param string $route
270
     *            processed route
271
     * @return array middleware result
272
     */
273
    private function getMiddlewareResult(string $route): array
274
    {
275
        $middleWares = [];
276
277
        if (isset($this->middleware['*'])) {
278
            $middleWares = $this->middleware['*'];
279
        }
280
281
        if ($this->calledRoute !== '*' && isset($this->middleware[$this->calledRoute])) {
282
            $middleWares = array_merge($middleWares, $this->middleware[$this->calledRoute]);
283
        }
284
285
        $result = [
286
            $route,
287
            $this->parameters
288
        ];
289
290
        if (! count($middleWares)) {
291
            return $result;
292
        }
293
294
        foreach ($middleWares as $middleWare) {
295
            $result = call_user_func($middleWare, $route, $this->parameters);
296
297
            if (is_array($result)) {
298
                if (array_key_exists(0, $result)) {
299
                    $route = $result[0];
300
                }
301
302
                if (array_key_exists(1, $result)) {
303
                    $this->parameters = $result[1];
304
                }
305
            }
306
        }
307
308
        return [
309
            $route,
310
            $this->parameters
311
        ];
312
    }
313
314
    /**
315
     * Method executes route handler
316
     *
317
     * @param mixed $processor
318
     * @param string $route
319
     * @return mixed route handler execution result
320
     */
321
    protected function executeHandler($processor, string $route)
322
    {
323
        if ($this->isFunction($processor)) {
324
            return call_user_func_array($processor, $this->getMiddlewareResult($route));
325
        }
326
327
        $functionName = $processor[1] ?? null;
328
329
        if ($this->canBeCalled($processor, $functionName)) {
330
            // passing route path and parameters
331
            return call_user_func_array($processor, $this->getMiddlewareResult($route));
332
        } else {
333
            $callableDescription = Utils::getCallableDescription($processor);
334
335
            if ($this->methodDoesNotExists($processor, $functionName)) {
336
                throw (new \Exception("'$callableDescription' does not exists"));
337
            } else {
338
                throw (new \Exception("'$callableDescription' must be callable entity"));
339
            }
340
        }
341
    }
342
343
    /**
344
     * Method returns route handler
345
     *
346
     * @param string $route
347
     *            Route
348
     * @return array|callable|bool route handler
349
     */
350
    protected function getStaticRouteProcessor(string $route)
351
    {
352
        $processors = $this->staticRoutes[$_SERVER['REQUEST_METHOD']];
353
354
        if (isset($processors[$route])) {
355
            $processor = $this->getExactRouteHandlerOrUniversal($processors, $route);
356
        } elseif (isset($processors['*'])) {
357
            $processor = $processors['*'];
358
        } else {
359
            return false;
360
        }
361
362
        return $processor;
363
    }
364
365
    /**
366
     * Method searches route processor
367
     *
368
     * @param string $route
369
     *            Route
370
     * @return mixed Result of the router processor
371
     */
372
    public function findStaticRouteProcessor(string $route)
373
    {
374
        $processor = $this->getStaticRouteProcessor($route);
375
376
        if ($processor === false) {
377
            return false;
378
        }
379
380
        return $this->executeHandler($processor, $route);
381
    }
382
383
    /**
384
     * Method returns route parameter
385
     *
386
     * @param string $name
387
     *            Route parameter
388
     * @return string Route parameter
389
     */
390
    public function getParam(string $name): string
391
    {
392
        if (isset($this->parameters[$name]) === false) {
393
            throw (new \Exception('Parameter ' . $name . ' was not found in route', - 1));
394
        }
395
396
        return $this->parameters[$name];
397
    }
398
399
    /**
400
     * Does parameter exists
401
     *
402
     * @param string $name
403
     *            Param name
404
     * @return bool True if the parameter exists
405
     */
406
    public function hasParam(string $name): bool
407
    {
408
        return isset($this->parameters[$name]);
409
    }
410
411
    /**
412
     * Getting route by name
413
     *
414
     * @param string $routeName
415
     *            route's name
416
     * @return string route
417
     */
418
    public abstract function getRouteByName(string $routeName): string;
419
420
    /**
421
     * Compiling route into URL
422
     *
423
     * @param string $routeName
424
     *            route name
425
     * @param array $parameters
426
     *            parameters to use in URL
427
     * @return string compiled route
428
     */
429
    public function reverse(string $routeName, array $parameters = []): string
430
    {
431
        $route = $this->getRouteByName($routeName);
432
433
        foreach ($parameters as $name => $value) {
434
            $route = preg_replace('/\[([A-Za-z_\-])\:' . $name . ']/', $value, $route);
435
        }
436
437
        return $route;
438
    }
439
}
440