Completed
Push — master ( a98a47...63879d )
by Mehmet
01:55
created

Router::getRoute()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 11
ccs 0
cts 11
cp 0
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 8
nc 1
nop 0
crap 2
1
<?php
2
/**
3
 * Selami Router
4
 * PHP version 7.1+
5
 *
6
 * @category Library
7
 * @package  Router
8
 * @author   Mehmet Korkmaz <[email protected]>
9
 * @license  https://github.com/selamiphp/router/blob/master/LICENSE (MIT License)
10
 * @link     https://github.com/selamiphp/router
11
 */
12
13
declare(strict_types = 1);
14
15
namespace Selami;
16
17
use FastRoute;
18
use InvalidArgumentException;
19
use UnexpectedValueException;
20
use RuntimeException;
21
22
/**
23
 * Router
24
 *
25
 * This class is responsible for registering route objects,
26
 * determining aliases if available and finding requested route
27
 */
28
final class Router
29
{
30
    const HTML = 1;
31
    const JSON = 2;
32
    const TEXT = 3;
33
    const XML = 4;
34
    const REDIRECT = 5;
35
    const DOWNLOAD = 6;
36
    const CUSTOM = 7;
37
38
    const OPTIONS = 'OPTIONS';
39
    const HEAD = 'HEAD';
40
    const GET = 'GET';
41
    const POST = 'POST';
42
    const PUT = 'PUT';
43
    const DELETE = 'DELETE';
44
    const PATCH = 'PATCH';
45
46
    /**
47
     * Routes array to be registered.
48
     * Some routes may have aliases to be used in templating system
49
     * Route item can be defined using array key as an alias key.
50
     * Each route item is an array has items respectively: Request Method, Request Uri, Controller/Action, Return Type.
51
     *
52
     * @var array
53
     */
54
    private $routes = [];
55
56
    /**
57
     * Aliases array to be registered.
58
     *
59
     * @var array
60
     */
61
    private $aliases = [];
62
63
    /**
64
     * HTTP request Method
65
     *
66
     * @var string
67
     */
68
    private $method;
69
70
    /**
71
     * Request Uri
72
     *
73
     * @var string
74
     */
75
    private $requestedPath;
76
77
    /**
78
     * Default return type if not noted in the $routes
79
     *
80
     * @var string
81
     */
82
    private $defaultReturnType;
83
84
    /**
85
     * @var null|string
86
     */
87
    private $cachedFile;
88
89
    /**
90
     * @var array
91
     */
92
    private $routerClosures = [];
93
94
    /**
95
     * Valid Request Methods array.
96
     * Make sures about requested methods.
97
     *
98
     * @var array
99
     */
100
    private static $validRequestMethods = [
101
        'GET',
102
        'OPTIONS',
103
        'HEAD',
104
        'POST',
105
        'PUT',
106
        'DELETE',
107
        'PATCH'
108
    ];
109
110
    /**
111
     * Router constructor.
112
     * Create new router.
113
     *
114
     * @param  int    $defaultReturnType
115
     * @param  string $method
116
     * @param  string $requestedPath
117
     * @param  string $folder
118
     * @param  string $cachedFile
119
     * @throws UnexpectedValueException
120
     */
121
    public function __construct(
122
        int $defaultReturnType,
123
        string $method,
124
        string $requestedPath,
125
        string $folder = '',
126
        ?string $cachedFile = null
127
    ) {
128
        if (!in_array($method, self::$validRequestMethods, true)) {
129
            $message = sprintf('%s is not valid Http request method.', $method);
130
            throw new UnexpectedValueException($message);
131
        }
132
        $this->method = $method;
133
        $this->requestedPath = $this->extractFolder($requestedPath, $folder);
134
        $this->defaultReturnType = ($defaultReturnType >=1 && $defaultReturnType <=7) ? $defaultReturnType : self::HTML;
0 ignored issues
show
Documentation Bug introduced by
The property $defaultReturnType was declared of type string, but $defaultReturnType >= 1 ...ReturnType : self::HTML is of type integer. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
135
        $this->cachedFile = $cachedFile;
136
    }
137
138
    /**
139
     * Remove sub folder from requestedPath if defined
140
     *
141
     * @param  string $requestPath
142
     * @param  string $folder
143
     * @return string
144
     */
145
    private function extractFolder(string $requestPath, string $folder) : string
146
    {
147
        if (!empty($folder)) {
148
            $requestPath = '/' . trim(preg_replace('#^/' . $folder . '#msi', '/', $requestPath), '/');
149
        }
150
        if ($requestPath === '') {
151
            $requestPath = '/';
152
        }
153
        return $requestPath;
154
    }
155
156
    /**
157
     * Add route to routes list
158
     *
159
     * @param  string|array requestMethods
160
     * @param  string                      $route
161
     * @param  string                      $action
162
     * @param  int                         $returnType
163
     * @param  string                      $alias
164
     * @throws InvalidArgumentException
165
     * @throws UnexpectedValueException
166
     */
167
    public function add(
168
        $requestMethods,
169
        string $route,
170
        string $action,
171
        ?int $returnType = null,
172
        ?string $alias = null
173
    ) : void {
174
    
175
        $requestMethodsGiven = is_array($requestMethods) ? (array) $requestMethods : [0 => $requestMethods];
176
        $returnType = $this->determineReturnType($returnType);
177
        foreach ($requestMethodsGiven as $requestMethod) {
178
            $this->checkRequestMethodParameterType($requestMethod);
179
            $this->checkRequestMethodIsValid($requestMethod);
180
            if ($alias !== null) {
181
                $this->aliases[$alias] = $route;
182
            }
183
            $this->routes[] = [strtoupper($requestMethod), $route, $action, $returnType];
184
        }
185
    }
186
187
    /**
188
     * @param string $method
189
     * @param array  $args
190
     * @throws UnexpectedValueException
191
     * @throws InvalidArgumentException
192
     */
193
    public function __call(string $method, array $args) : void
194
    {
195
        $this->checkRequestMethodIsValid($method);
196
        $defaults = [
197
            null,
198
            null,
199
            $this->defaultReturnType,
200
            null
201
        ];
202
        [$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...
203
        $this->add($method, $route, $action, $returnType, $alias);
204
    }
205
206
    /**
207
     * @param int|null $returnType
208
     * @return int
209
     */
210
    private function determineReturnType(?int $returnType) : int
211
    {
212
        if ($returnType === null) {
213
            return $this->defaultReturnType;
214
        }
215
        return ($returnType >=1 && $returnType <=7) ? $returnType : self::HTML;
216
    }
217
218
    /**
219
     * @param string $requestMethod
220
     * Checks if request method is valid
221
     * @throws UnexpectedValueException;
222
     */
223
    private function checkRequestMethodIsValid(string $requestMethod) : void
224
    {
225
        if (!in_array(strtoupper($requestMethod), self::$validRequestMethods, true)) {
226
            $message = sprintf('%s is not valid Http request method.', $requestMethod);
227
            throw new UnexpectedValueException($message);
228
        }
229
    }
230
231
    /**
232
     * @param $requestMethod
233
     * @throws InvalidArgumentException
234
     */
235
    private function checkRequestMethodParameterType($requestMethod) : void
236
    {
237
        $requestMethodParameterType = gettype($requestMethod);
238
        if (!in_array($requestMethodParameterType, ['array', 'string'], true)) {
239
            $message = sprintf(
240
                'Request method must be string or array but %s given.',
241
                $requestMethodParameterType
242
            );
243
            throw new InvalidArgumentException($message);
244
        }
245
    }
246
247
    /**
248
     * Dispatch against the provided HTTP method verb and URI.
249
     *
250
     * @return FastRoute\Dispatcher
251
     * @throws RuntimeException;
252
     */
253
    private function dispatcher() : FastRoute\Dispatcher
254
    {
255
        $this->setRouteClosures();
256
        if ($this->cachedFile !== null && file_exists($this->cachedFile)) {
257
            return $this->cachedDispatcher();
258
        }
259
        return $this->simpleDispatcher();
260
    }
261
262
    private function simpleDispatcher() : FastRoute\Dispatcher\GroupCountBased
263
    {
264
        /**
265
         * @var \FastRoute\RouteCollector $routeCollector
266
         */
267
        $routeCollector = new FastRoute\RouteCollector(
268
            new FastRoute\RouteParser\Std, new FastRoute\DataGenerator\GroupCountBased
269
        );
270
        $this->addRoutes($routeCollector);
271
        $this->createCachedRoute($routeCollector);
272
        return new FastRoute\Dispatcher\GroupCountBased($routeCollector->getData());
273
    }
274
275
    private function createCachedRoute($routeCollector ) : void
276
    {
277
        if ($this->cachedFile !== null && !file_exists($this->cachedFile)) {
278
            /**
279
             * @var FastRoute\RouteCollector $routeCollector
280
             */
281
            $dispatchData = $routeCollector->getData();
282
            file_put_contents($this->cachedFile, '<?php return ' . var_export($dispatchData, true) . ';');
283
        }
284
    }
285
286
    /**
287
     * @return FastRoute\Dispatcher\GroupCountBased
288
     * @throws RuntimeException
289
     */
290
    private function cachedDispatcher() : FastRoute\Dispatcher\GroupCountBased
291
    {
292
        $dispatchData = include $this->cachedFile;
293
        if (!is_array($dispatchData)) {
294
            throw new RuntimeException('Invalid cache file "' . $this->cachedFile . '"');
295
        }
296
        return new FastRoute\Dispatcher\GroupCountBased($dispatchData);
297
    }
298
299
    /**
300
     * Define Closures for all routes that returns controller info to be used.
301
     *
302
     * @param FastRoute\RouteCollector $route
303
     */
304
    private function addRoutes(FastRoute\RouteCollector $route) : void
305
    {
306
        $routeIndex=0;
307
        foreach ($this->routes as $definedRoute) {
308
            $definedRoute[3] = $definedRoute[3] ?? $this->defaultReturnType;
309
            $routeName = 'routeClosure'.$routeIndex;
310
            $route->addRoute(strtoupper($definedRoute[0]), $definedRoute[1], $routeName);
311
            $routeIndex++;
312
        }
313
    }
314
    private function setRouteClosures() : void
315
    {
316
        $routeIndex=0;
317
        foreach ($this->routes as $definedRoute) {
318
            $definedRoute[3] = $definedRoute[3] ?? $this->defaultReturnType;
319
            $routeName = 'routeClosure'.$routeIndex;
320
            [$requestMedhod, $url, $controller, $returnType] = $definedRoute;
4 ignored issues
show
Bug introduced by
The variable $requestMedhod 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 $url 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...
321
            $returnType = ($returnType >=1 && $returnType <=7) ? $returnType : $this->defaultReturnType;
322
            $this->routerClosures[$routeName]= function ($args) use ($controller, $returnType) {
323
                return  ['controller' => $controller, 'returnType'=> $returnType, 'args'=> $args];
324
            };
325
            $routeIndex++;
326
        }
327
    }
328
329
    /**
330
     * Get router data that includes route info and aliases
331
     *
332
     * @return array
333
     */
334
    public function getRoute() : array
335
    {
336
        $dispatcher = $this->dispatcher();
337
        $routeInfo  = $dispatcher->dispatch($this->method, $this->requestedPath);
338
        $route = $this->runDispatcher($routeInfo);
339
        $routerData = [
340
            'route'     => $route,
341
            'aliases'   => $this->aliases
342
        ];
343
        return $routerData;
344
    }
345
346
    /**
347
     * Get route info for requested uri
348
     *
349
     * @param  array $routeInfo
350
     * @return array $routerData
351
     */
352
    private function runDispatcher(array $routeInfo) : array
353
    {
354
        $routeData = $this->getRouteData($routeInfo);
355
        $dispatchResults = [
356
            FastRoute\Dispatcher::METHOD_NOT_ALLOWED => [
357
                'status' => 405
358
            ],
359
            FastRoute\Dispatcher::FOUND => [
360
                'status'  => 200
361
            ],
362
            FastRoute\Dispatcher::NOT_FOUND => [
363
                'status' => 404
364
            ]
365
        ];
366
        return array_merge($routeData, $dispatchResults[$routeInfo[0]]);
367
    }
368
369
    /**
370
     * Get routeData according to dispatcher's results
371
     *
372
     * @param  array $routeInfo
373
     * @return array
374
     */
375
    private function getRouteData(array $routeInfo) : array
376
    {
377
        if ($routeInfo[0] === FastRoute\Dispatcher::FOUND) {
378
            [$dispatcher, $handler, $vars] = $routeInfo;
3 ignored issues
show
Bug introduced by
The variable $dispatcher 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...
379
            return $this->routerClosures[$handler]($vars);
380
        }
381
        return [
382
            'status'        => 200,
383
            'returnType'    => Router::HTML,
384
            'definedRoute'  => null,
385
            'args'          => []
386
        ];
387
    }
388
}
389