Completed
Push — master ( e9175e...c70469 )
by Mehmet
02:29
created

Router   A

Complexity

Total Complexity 31

Size/Duplication

Total Lines 378
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 2

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
wmc 31
lcom 1
cbo 2
dl 0
loc 378
ccs 0
cts 194
cp 0
rs 9.8
c 0
b 0
f 0

15 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 16 2
A extractFolder() 0 10 3
A add() 0 19 4
A __call() 0 12 1
A determineReturnType() 0 7 3
A checkRequestMethodIsValid() 0 7 2
A checkRequestMethodParameterType() 0 11 2
A dispatcher() 0 9 2
A simpleDispatcher() 0 16 1
B cachedDispatcher() 0 27 3
A addRoutes() 0 10 2
A setRouteClosures() 0 14 2
A getRoute() 0 11 1
A runDispatcher() 0 16 1
A getRouteData() 0 13 2
1
<?php
2
/**
3
 * Selami Router
4
 * PHP version 7.1+
5
 *
6
 * @license https://github.com/selamiphp/router/blob/master/LICENSE (MIT License)
7
 * @link https://github.com/selamiphp/router
8
 * @package router
9
 * @category library
10
 */
11
12
declare(strict_types = 1);
13
14
namespace Selami;
15
16
use FastRoute;
17
use InvalidArgumentException;
18
use UnexpectedValueException;
19
20
/**
21
 * Router
22
 *
23
 * This class is responsible for registering route objects,
24
 * determining aliases if available and finding requested route
25
 */
26
final class Router
27
{
28
    /**
29
     * routes array to be registered.
30
     * Some routes may have aliases to be used in templating system
31
     * Route item can be defined using array key as an alias key.
32
     * @var array
33
     */
34
    private $routes = [];
35
36
    /**
37
     * aliases array to be registered.
38
     * Each route item is an array has items respectively : Request Method, Request Uri, Controller/Action, Return Type.
39
     * @var array
40
     */
41
    private $aliases = [];
42
43
    /**
44
     * HTTP request Method
45
     * @var string
46
     */
47
    private $method;
48
49
    /**
50
     * Request Uri
51
     * @var string
52
     */
53
    private $requestedPath;
54
55
    /**
56
     * Default return type if not noted in the $routes
57
     *
58
     * @var string
59
     */
60
    private $defaultReturnType;
61
62
    /**
63
     * @var null|string
64
     */
65
    private $cachedFile;
66
67
    /**
68
     * @var array
69
     */
70
    private $routerClosures = [];
71
72
    /**
73
     * Translation array.
74
     * Make sures about return type.
75
     *
76
     * @var array
77
     */
78
    private static $translations = [
79
        'h'     => 'html',
80
        'html'  => 'html',
81
        'r'     => 'redirect',
82
        'redirect' => 'redirect',
83
        'j'     => 'json',
84
        'json'  => 'json',
85
        't'     => 'text',
86
        'text'  => 'text',
87
        'd'     => 'download',
88
        'download'  => 'download'
89
    ];
90
91
    /**
92
     * Valid Request Methods array.
93
     * Make sures about requested methods.
94
     * @var array
95
     */
96
    private static $validRequestMethods = [
97
        'GET',
98
        'OPTIONS',
99
        'HEAD',
100
        'POST',
101
        'PUT',
102
        'DELETE',
103
        'PATCH'
104
    ];
105
106
107
    /**
108
     * Valid Request Methods array.
109
     * Make sures about return type.
110
     * Index 0 is also default value.
111
     * @var array
112
     */
113
    private static $validReturnTypes = [
114
        'html',
115
        'json',
116
        'text',
117
        'redirect',
118
        'download'
119
    ];
120
121
    /**
122
     * Router constructor.
123
     * Create new router.
124
     *
125
     * @param string $defaultReturnType
126
     * @param string $method
127
     * @param string $requestedPath
128
     * @param string $folder
129
     * @param string $cachedFile
130
     * @throws UnexpectedValueException
131
     */
132
    public function __construct(
133
        string $defaultReturnType,
134
        string $method,
135
        string $requestedPath,
136
        string $folder = '',
137
        ?string $cachedFile = null
138
    ) {
139
        if (!in_array($method, self::$validRequestMethods, true)) {
140
            $message = sprintf('%s is not valid Http request method.', $method);
141
            throw new UnexpectedValueException($message);
142
        }
143
        $this->method = $method;
144
        $this->requestedPath = $this->extractFolder($requestedPath, $folder);
145
        $this->defaultReturnType = self::$translations[$defaultReturnType] ?? self::$validReturnTypes[0];
146
        $this->cachedFile = $cachedFile;
147
    }
148
149
    /**
150
     * Remove sub folder from requestedPath if defined
151
     * @param string $requestPath
152
     * @param string $folder
153
     * @return string
154
     */
155
    private function extractFolder(string $requestPath, string $folder) : string
156
    {
157
        if (!empty($folder)) {
158
            $requestPath = '/' . trim(preg_replace('#^/' . $folder . '#msi', '/', $requestPath), '/');
159
        }
160
        if ($requestPath === '') {
161
            $requestPath = '/';
162
        }
163
        return $requestPath;
164
    }
165
166
    /**
167
     * add route to routes list
168
     * @param string|array requestMethods
169
     * @param string $route
170
     * @param string $action
171
     * @param string $returnType
172
     * @param string $alias
173
     * @throws InvalidArgumentException
174
     * @throws UnexpectedValueException
175
     */
176
    public function add(
177
        $requestMethods,
178
        string $route,
179
        string $action,
180
        ?string $returnType = null,
181
        ?string $alias = null
182
    ) : void
183
    {
184
        $requestMethodsGiven = is_array($requestMethods) ? (array) $requestMethods : [0 => $requestMethods];
185
        $returnType = $this->determineReturnType($returnType);
186
        foreach ($requestMethodsGiven as $requestMethod) {
187
            $this->checkRequestMethodParameterType($requestMethod);
188
            $this->checkRequestMethodIsValid($requestMethod);
189
            if ($alias !== null) {
190
                $this->aliases[$alias] = $route;
191
            }
192
            $this->routes[] = [strtoupper($requestMethod), $route, $action, $returnType];
193
        }
194
    }
195
196
    /**
197
     * @param string $method
198
     * @param array $args
199
     * @throws UnexpectedValueException
200
     * @throws InvalidArgumentException
201
     */
202
    public function __call(string $method, array $args) : void
203
    {
204
        $this->checkRequestMethodIsValid($method);
205
        $defaults = [
206
            null,
207
            null,
208
            $this->defaultReturnType,
209
            null
210
        ];
211
        [$route, $action, $returnType, $alias] = array_merge($args, $defaults);
4 ignored issues
show
Bug introduced by
The variable $route does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $action does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $returnType does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $alias does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
212
        $this->add($method, $route, $action, $returnType, $alias);
213
    }
214
215
    /**
216
     * @param string|null $returnType
217
     * @return string
218
     */
219
    private function determineReturnType(?string $returnType) : string
220
    {
221
        if ($returnType === null) {
222
            return $this->defaultReturnType;
223
        }
224
        return in_array($returnType, self::$validReturnTypes, true) ? $returnType : $this->defaultReturnType;
225
    }
226
227
    /**
228
     * @param string $requestMethod
229
     * Checks if request method is valid
230
     * @throws UnexpectedValueException;
231
     */
232
    private function checkRequestMethodIsValid(string $requestMethod) : void
233
    {
234
        if (!in_array(strtoupper($requestMethod), self::$validRequestMethods, true)) {
235
            $message = sprintf('%s is not valid Http request method.', $requestMethod);
236
            throw new UnexpectedValueException($message);
237
        }
238
    }
239
240
    /**
241
     * @param $requestMethod
242
     * @throws InvalidArgumentException
243
     */
244
    private function checkRequestMethodParameterType($requestMethod) : void
245
    {
246
        $requestMethodParameterType = gettype($requestMethod);
247
        if (!in_array($requestMethodParameterType, ['array', 'string'], true)) {
248
            $message = sprintf(
249
                'Request method must be string or array but %s given.',
250
                $requestMethodParameterType
251
            );
252
            throw new InvalidArgumentException($message);
253
        }
254
    }
255
256
    /**
257
     * Dispatch against the provided HTTP method verb and URI.
258
     * @return FastRoute\Dispatcher
259
     */
260
    private function dispatcher() : FastRoute\Dispatcher
261
    {
262
263
        $this->setRouteClosures();
264
        if ($this->cachedFile !== null) {
265
            return $this->cachedDispatcher();
266
        }
267
        return $this->simpleDispatcher();
268
    }
269
270
    private function simpleDispatcher() : FastRoute\Dispatcher\GroupCountBased
271
    {
272
        $options = [
273
            'routeParser' => FastRoute\RouteParser\Std::class,
274
            'dataGenerator' => FastRoute\DataGenerator\GroupCountBased::class,
275
            'dispatcher' => FastRoute\Dispatcher\GroupCountBased::class,
276
            'routeCollector' => FastRoute\RouteCollector::class,
277
        ];
278
        /** @var RouteCollector $routeCollector */
279
        $routeCollector = new $options['routeCollector'](
280
            new $options['routeParser'], new $options['dataGenerator']
281
        );
282
        $this->addRoutes($routeCollector);
283
284
        return new $options['dispatcher']($routeCollector->getData());
285
    }
286
287
    private function cachedDispatcher() : FastRoute\Dispatcher\GroupCountBased
288
    {
289
        $options = [
290
            'routeParser' => FastRoute\RouteParser\Std::class,
291
            'dataGenerator' => FastRoute\DataGenerator\GroupCountBased::class,
292
            'dispatcher' => FastRoute\Dispatcher\GroupCountBased::class,
293
            'routeCollector' => FastRoute\RouteCollector::class
294
        ];
295
        if (file_exists($this->cachedFile)) {
296
            $dispatchData = require $this->cachedFile;
297
            if (!is_array($dispatchData)) {
298
                throw new \RuntimeException('Invalid cache file "' . $options['cacheFile'] . '"');
299
            }
300
            return new $options['dispatcher']($dispatchData);
301
        }
302
        $routeCollector = new $options['routeCollector'](
303
            new $options['routeParser'], new $options['dataGenerator']
304
        );
305
        $this->addRoutes($routeCollector);
306
        /** @var RouteCollector $routeCollector */
307
        $dispatchData = $routeCollector->getData();
308
        file_put_contents(
309
            $this->cachedFile,
310
            '<?php return ' . var_export($dispatchData, true) . ';'
311
        );
312
        return new $options['dispatcher']($dispatchData );
313
    }
314
315
    /**
316
     * Define Closures for all routes that returns controller info to be used.
317
     * @param FastRoute\RouteCollector $route
318
     */
319
    private function addRoutes(FastRoute\RouteCollector $route) : void
320
    {
321
        $routeIndex=0;
322
        foreach ($this->routes as $definedRoute) {
323
            $definedRoute[3] = $definedRoute[3] ?? $this->defaultReturnType;
324
            $routeName = 'routeClosure'.$routeIndex;
325
            $route->addRoute(strtoupper($definedRoute[0]), $definedRoute[1], $routeName);
326
            $routeIndex++;
327
        }
328
    }
329
    private function setRouteClosures() : void
330
    {
331
        $routeIndex=0;
332
        foreach ($this->routes as $definedRoute) {
333
            $definedRoute[3] = $definedRoute[3] ?? $this->defaultReturnType;
334
            $routeName = 'routeClosure'.$routeIndex;
335
            [$null1, $null2, $controller, $returnType] = $definedRoute;
0 ignored issues
show
Bug introduced by
The variable $null1 does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $null2 does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $controller does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $returnType does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
336
            $returnType = Router::$translations[$returnType] ?? $this->defaultReturnType;
337
            $this->routerClosures[$routeName]= function($args) use ($controller, $returnType) {
338
                return  ['controller' => $controller, 'returnType'=> $returnType, 'args'=> $args];
339
            };
340
            $routeIndex++;
341
        }
342
    }
343
344
345
346
    /**
347
     * Get router data that includes route info and aliases
348
     * @return array
349
     */
350
    public function getRoute() : array
351
    {
352
        $dispatcher = $this->dispatcher();
353
        $routeInfo  = $dispatcher->dispatch($this->method, $this->requestedPath);
354
        $route = $this->runDispatcher($routeInfo);
355
        $routerData = [
356
            'route'     => $route,
357
            'aliases'   => $this->aliases
358
        ];
359
        return $routerData;
360
    }
361
362
363
    /**
364
     * Get route info for requested uri
365
     * @param array $routeInfo
366
     * @return array $routerData
367
     */
368
    private function runDispatcher(array $routeInfo) : array
369
    {
370
        $routeData = $this->getRouteData($routeInfo);
371
        $dispatchResults = [
372
            FastRoute\Dispatcher::METHOD_NOT_ALLOWED => [
373
                'status' => 405
374
            ],
375
            FastRoute\Dispatcher::FOUND => [
376
                'status'  => 200
377
            ],
378
            FastRoute\Dispatcher::NOT_FOUND => [
379
                'status' => 404
380
            ]
381
        ];
382
        return array_merge($routeData, $dispatchResults[$routeInfo[0]]);
383
    }
384
385
    /**
386
     * Get routeData according to dispatcher's results
387
     * @param array $routeInfo
388
     * @return array
389
     */
390
    private function getRouteData(array $routeInfo) : array
391
    {
392
        if ($routeInfo[0] === FastRoute\Dispatcher::FOUND) {
393
            [$null1, $handler, $vars] = $routeInfo;
0 ignored issues
show
Bug introduced by
The variable $null1 does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $handler does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $vars does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
394
            return $this->routerClosures[$handler]($vars);
395
        }
396
        return [
397
            'status'        => 200,
398
            'returnType'    => 'html',
399
            'definedRoute'  => null,
400
            'args'          => []
401
        ];
402
    }
403
}
404