Completed
Push — master ( 71996f...11de1b )
by Alex
14s queued 11s
created

UrlParser::getMiddlewareResult()   B

Complexity

Conditions 9
Paths 28

Size

Total Lines 33
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 9
eloc 16
c 1
b 0
f 0
nc 28
nop 1
dl 0
loc 33
rs 8.0555
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, bool $addBraces = true): string
53
    {
54
        $key = $routerPattern . ($addBraces ? '1' : '0');
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
                ($addBraces ? '(' : '') . $typeClass::parserRegExp() . ($addBraces ? ')' : ''),
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
            $routesForMethod = $this->paramRoutes[$requestMethod];
118
119
            foreach (array_keys($routesForMethod) as $routerPattern) {
120
                $this->_getRouteMatcherRegExPattern($routerPattern);
121
122
                $this->_getParameterNames($routerPattern);
123
            }
124
        }
125
    }
126
127
    /**
128
     * Method searches dynamic route processor
129
     *
130
     * @param string $route
131
     *            Route
132
     * @param string $requestMethod
133
     *            Request method
134
     * @return array|callable|bool route's handler or false in case the handler was not found
135
     */
136
    protected function getDynamicRouteProcessor(string $route, string $requestMethod = '')
137
    {
138
        $bunches = $this->paramRoutes[$requestMethod == '' ? $_SERVER['REQUEST_METHOD'] : $requestMethod];
139
140
        foreach ($bunches as $bunch) {
141
            $matches = [];
142
143
            if (preg_match($bunch['regexp'], $route, $matches)) {
144
                $routeData = $bunch['bunch'][count($matches)];
145
                $regExPattern = $this->_getRouteMatcherRegExPattern($routeData['pattern']);
146
147
                // try match
148
                $values = [];
149
                if (preg_match('/^' . str_replace('/', '\\/', $regExPattern) . '$/', $route, $values)) {
150
                    // fetch parameter names
151
                    $names = $this->_getParameterNames($routeData['pattern']);
152
153
                    $this->parameters = [];
154
                    foreach ($names as $i => $name) {
155
                        $this->parameters[$name] = $values[$i + 1];
156
                    }
157
158
                    $this->calledRoute = $routeData['pattern'];
159
160
                    return $routeData['callback'];
161
                }
162
            }
163
        }
164
165
        // match was not found
166
        return false;
167
    }
168
169
    /**
170
     * Method searches dynamic route processor
171
     *
172
     * @param string $route
173
     *            Route
174
     * @return string|bool Result of the router'scall or false if any error occured
175
     */
176
    public function findDynamicRouteProcessor(string $route)
177
    {
178
        $processor = $this->getDynamicRouteProcessor($route);
179
180
        if ($processor === false) {
181
            return false;
182
        }
183
184
        return $this->executeHandler($processor, $route);
185
    }
186
187
    /**
188
     * Checking that method exists
189
     *
190
     * @param mixed $processor
191
     *            callback object
192
     * @param ?string $functionName
193
     *            callback method
194
     * @return bool true if method does not exists
195
     */
196
    private function methodDoesNotExists($processor, ?string $functionName): bool
197
    {
198
        return isset($processor[0]) && method_exists($processor[0], $functionName) === false;
199
    }
200
201
    /**
202
     * Checking that handler can be called
203
     *
204
     * @param object|array|callable $processor
205
     *            callback object
206
     * @param ?string $functionName
207
     *            callback method
208
     * @return bool
209
     */
210
    private function canBeCalled($processor, ?string $functionName): bool
211
    {
212
        return is_callable($processor) &&
213
            (method_exists($processor[0], $functionName) || isset($processor[0]->$functionName));
214
    }
215
216
    /**
217
     * Checking that processor can be called as function
218
     *
219
     * @param mixed $processor
220
     *            route processor
221
     * @return bool true if the $processor can be called as function
222
     */
223
    private function isFunction($processor): bool
224
    {
225
        return is_callable($processor) && is_array($processor) === false;
226
    }
227
228
    /**
229
     * Method returns either universal hanler if it fits or normal handler
230
     *
231
     * @param array $processors
232
     *            list of routes and handlers
233
     * @param string $route
234
     *            calling route
235
     * @return mixed processor
236
     */
237
    protected function getExactRouteHandlerOrUniversal(&$processors, string $route)
238
    {
239
        $this->calledRoute = $route;
240
241
        if ($this->universalRouteWasAdded) {
242
            $allRoutes = array_keys($processors);
243
244
            if (array_search('*', $allRoutes) <= array_search($route, $allRoutes)) {
245
                $processor = $processors['*'];
246
                $this->calledRoute = '*';
247
            } else {
248
                $processor = $processors[$route];
249
            }
250
        } else {
251
            $processor = $processors[$route];
252
        }
253
254
        return $processor;
255
    }
256
257
    /**
258
     * Method registeres middleware for the router
259
     *
260
     * @param callable $middleware
261
     *            middleware
262
     */
263
    public function registerMiddleware(string $router, callable $middleware): void
264
    {
265
        $routerTrimmed = trim($router, '/');
266
267
        if (!isset($this->middleware[$routerTrimmed])) {
268
            $this->middleware[$routerTrimmed] = [];
269
        }
270
271
        $this->middleware[$routerTrimmed][] = $middleware;
272
    }
273
274
    /**
275
     * Method returns middleware processing result
276
     *
277
     * @param string $route
278
     *            processed route
279
     * @return array middleware result
280
     */
281
    private function getMiddlewareResult(string $route): array
282
    {
283
        $middleWares = [];
284
285
        if (isset($this->middleware['*'])) {
286
            $middleWares = $this->middleware['*'];
287
        }
288
289
        if ($this->calledRoute !== '*' && isset($this->middleware[$this->calledRoute])) {
290
            $middleWares = array_merge($middleWares, $this->middleware[$this->calledRoute]);
291
        }
292
293
        $result = [$route, $this->parameters];
294
295
        if (!count($middleWares)) {
296
            return $result;
297
        }
298
299
        foreach ($middleWares as $middleWare) {
300
            $result = call_user_func($middleWare, $route, $this->parameters);
301
302
            if (is_array($result)) {
303
                if (array_key_exists(0, $result)) {
304
                    $route = $result[0];
305
                }
306
307
                if (array_key_exists(1, $result)) {
308
                    $this->parameters = $result[1];
309
                }
310
            }
311
        }
312
313
        return [$route, $this->parameters];
314
    }
315
316
    /**
317
     * Method executes route handler
318
     *
319
     * @param mixed $processor
320
     * @param string $route
321
     * @return mixed route handler execution result
322
     */
323
    protected function executeHandler($processor, string $route)
324
    {
325
        if ($this->isFunction($processor)) {
326
            return call_user_func_array($processor, $this->getMiddlewareResult($route));
327
        }
328
329
        $functionName = $processor[1] ?? null;
330
331
        if ($this->canBeCalled($processor, $functionName)) {
332
            // passing route path and parameters
333
            return call_user_func_array($processor, $this->getMiddlewareResult($route));
334
        } else {
335
            $callableDescription = Utils::getCallableDescription($processor);
336
337
            if ($this->methodDoesNotExists($processor, $functionName)) {
338
                throw (new \Exception("'$callableDescription' does not exists"));
339
            } else {
340
                throw (new \Exception("'$callableDescription' must be callable entity"));
341
            }
342
        }
343
    }
344
345
    /**
346
     * Method returns route handler
347
     *
348
     * @param string $route
349
     *            Route
350
     * @return array|callable|bool route handler
351
     */
352
    protected function getStaticRouteProcessor(string $route)
353
    {
354
        $processors = $this->staticRoutes[$_SERVER['REQUEST_METHOD']];
355
356
        if (isset($processors[$route])) {
357
            $processor = $this->getExactRouteHandlerOrUniversal($processors, $route);
358
        } elseif (isset($processors['*'])) {
359
            $processor = $processors['*'];
360
        } else {
361
            return false;
362
        }
363
364
        return $processor;
365
    }
366
367
    /**
368
     * Method searches route processor
369
     *
370
     * @param string $route
371
     *            Route
372
     * @return mixed Result of the router processor
373
     */
374
    public function findStaticRouteProcessor(string $route)
375
    {
376
        $processor = $this->getStaticRouteProcessor($route);
377
378
        if ($processor === false) {
379
            return false;
380
        }
381
382
        return $this->executeHandler($processor, $route);
383
    }
384
385
    /**
386
     * Method returns route parameter
387
     *
388
     * @param string $name
389
     *            Route parameter
390
     * @return string Route parameter
391
     */
392
    public function getParam(string $name): string
393
    {
394
        if (isset($this->parameters[$name]) === false) {
395
            throw (new \Exception('Parameter ' . $name . ' was not found in route', - 1));
396
        }
397
398
        return $this->parameters[$name];
399
    }
400
401
    /**
402
     * Does parameter exists
403
     *
404
     * @param string $name
405
     *            Param name
406
     * @return bool True if the parameter exists
407
     */
408
    public function hasParam(string $name): bool
409
    {
410
        return isset($this->parameters[$name]);
411
    }
412
413
    /**
414
     * Getting route by name
415
     *
416
     * @param string $routeName
417
     *            route's name
418
     * @return string route
419
     */
420
    public abstract function getRouteByName(string $routeName): string;
421
422
    /**
423
     * Compiling route into URL
424
     *
425
     * @param string $routeName
426
     *            route name
427
     * @param array $parameters
428
     *            parameters to use in URL
429
     * @return string compiled route
430
     */
431
    public function reverse(string $routeName, array $parameters = []): string
432
    {
433
        $route = $this->getRouteByName($routeName);
434
435
        foreach ($parameters as $name => $value) {
436
            $route = preg_replace('/\[([A-Za-z_\-])\:' . $name . ']/', $value, $route);
437
        }
438
439
        return $route;
440
    }
441
}
442