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

UrlParser::findUniversalRouteProcessor()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
c 0
b 0
f 0
nc 2
nop 1
dl 0
loc 9
rs 10
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(
65
                '/' . $typeClass::searchRegExp() . '/',
66
                '(' . $typeClass::parserRegExp() . ')',
67
                $compiledRouterPattern);
68
        }
69
70
        // final setup + save in cache
71
        $this->cachedRegExps[$key] = $compiledRouterPattern;
72
73
        return $compiledRouterPattern;
74
    }
75
76
    /**
77
     * Method returns all parameter names in the route
78
     *
79
     * @param string $routerPattern
80
     *            route
81
     * @return array names
82
     */
83
    private function _getParameterNames(string $routerPattern): array
84
    {
85
        if (isset($this->cachedParameters[$routerPattern])) {
86
            return $this->cachedParameters[$routerPattern];
87
        }
88
89
        $regExPattern = [];
90
91
        foreach (array_keys($this->types) as $typeName) {
92
            $regExPattern[] = $typeName;
93
        }
94
95
        $regExPattern = '\[(' . implode('|', $regExPattern) . '):(' . BaseType::PARAMETER_NAME_REGEXP . ')\]';
96
97
        $names = [];
98
        preg_match_all('/' . str_replace('/', '\\/', $regExPattern) . '/', $routerPattern, $names);
99
100
        $return = [];
101
102
        foreach ($names[2] as $name) {
103
            $return[] = $name;
104
        }
105
106
        $this->cachedParameters[$routerPattern] = $return;
107
108
        return $return;
109
    }
110
111
    /**
112
     * Method warms cache
113
     */
114
    public function warmCache(): void
115
    {
116
        foreach (self::getListOfSupportedRequestMethods() as $requestMethod) {
117
            foreach ($this->paramRoutes[$requestMethod] as $bunch) {
118
                foreach ($bunch['bunch'] as $route) {
119
                    $this->_getRouteMatcherRegExPattern($route['pattern']);
120
121
                    $this->_getParameterNames($route['pattern']);
122
                }
123
            }
124
        }
125
126
        $this->compileRegexpForBunches();
0 ignored issues
show
Bug introduced by
It seems like compileRegexpForBunches() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

126
        $this->/** @scrutinizer ignore-call */ 
127
               compileRegexpForBunches();
Loading history...
127
    }
128
129
    /**
130
     * Method searches dynamic route processor
131
     *
132
     * @param string $route
133
     *            Route
134
     * @param string $requestMethod
135
     *            Request method
136
     * @return array|callable|bool route's handler or false in case the handler was not found
137
     */
138
    protected function getDynamicRouteProcessor(string $route, string $requestMethod = '')
139
    {
140
        $bunches = $this->paramRoutes[$requestMethod == '' ? $_SERVER['REQUEST_METHOD'] : $requestMethod];
141
142
        foreach ($bunches as $bunch) {
143
            $matches = [];
144
145
            if (preg_match($bunch['regexp'], $route, $matches)) {
146
                $routeData = $bunch['bunch'][count($matches)];
147
148
                $names = $this->_getParameterNames($routeData['pattern']);
149
150
                $this->parameters = [];
151
                foreach ($names as $i => $name) {
152
                    $this->parameters[$name] = $matches[$i + 1];
153
                }
154
155
                $this->calledRoute = $routeData['pattern'];
156
157
                return $routeData['callback'];
158
            }
159
        }
160
161
        // match was not found
162
        return false;
163
    }
164
165
    /**
166
     * Method searches dynamic route processor
167
     *
168
     * @param string $route
169
     *            Route
170
     * @return string|bool Result of the router'scall or false if any error occured
171
     */
172
    public function findDynamicRouteProcessor(string $route)
173
    {
174
        $processor = $this->getDynamicRouteProcessor($route);
175
176
        if ($processor === false) {
177
            return false;
178
        }
179
180
        return $this->executeHandler($processor, $route);
181
    }
182
183
    /**
184
     * Checking that method exists
185
     *
186
     * @param mixed $processor
187
     *            callback object
188
     * @param ?string $functionName
189
     *            callback method
190
     * @return bool true if method does not exists
191
     */
192
    private function methodDoesNotExists($processor, ?string $functionName): bool
193
    {
194
        return isset($processor[0]) && method_exists($processor[0], $functionName) === false;
0 ignored issues
show
Bug introduced by
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

194
        return isset($processor[0]) && method_exists($processor[0], /** @scrutinizer ignore-type */ $functionName) === false;
Loading history...
195
    }
196
197
    /**
198
     * Checking that handler can be called
199
     *
200
     * @param object|array|callable $processor
201
     *            callback object
202
     * @param ?string $functionName
203
     *            callback method
204
     * @return bool
205
     */
206
    private function canBeCalled($processor, ?string $functionName): bool
207
    {
208
        return is_callable($processor) &&
209
            (method_exists($processor[0], $functionName) || isset($processor[0]->$functionName));
0 ignored issues
show
Bug introduced by
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

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