Passed
Push — master ( 2e3c52...e9175e )
by Mehmet
03:49
created

Router::checkRequestMethodIsValid()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 7
ccs 0
cts 7
cp 0
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 4
nc 2
nop 1
crap 6
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($requestMethods, string $route, string $action, ?string $returnType = null, ?string $alias = null)
177
    {
178
        $requestMethodsGiven = is_array($requestMethods) ? (array) $requestMethods : [0 => $requestMethods];
179
        $returnType = $this->determineReturnType($returnType);
180
        foreach ($requestMethodsGiven as $requestMethod) {
181
            $this->checkRequestMethodParameterType($requestMethod);
182
            $this->checkRequestMethodIsValid($requestMethod);
183
            if ($alias !== null) {
184
                $this->aliases[$alias] = $route;
185
            }
186
            $this->routes[] = [strtoupper($requestMethod), $route, $action, $returnType];
187
        }
188
    }
189
190
    /**
191
     * @param string $method
192
     * @param array $args
193
     * @throws UnexpectedValueException
194
     * @throws InvalidArgumentException
195
     */
196
    public function __call(string $method, array $args) : void
197
    {
198
199
        $this->checkRequestMethodIsValid($method);
200
        $defaults = [
201
            null,
202
            null,
203
            $this->defaultReturnType,
204
            null
205
        ];
206
        [$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...
207
        $this->add($method, $route, $action, $returnType, $alias);
208
    }
209
210
    /**
211
     * @param string|null $returnType
212
     * @return string
213
     */
214
    private function determineReturnType(?string $returnType) : string
215
    {
216
        if ($returnType === null) {
217
            return $this->defaultReturnType;
218
        }
219
        return in_array($returnType, self::$validReturnTypes, true) ? $returnType : $this->defaultReturnType;
220
    }
221
222
    /**
223
     * @param string $requestMethod
224
     * Checks if request method is valid
225
     * @throws UnexpectedValueException;
226
     */
227
    private function checkRequestMethodIsValid(string $requestMethod) : void
228
    {
229
        if (!in_array(strtoupper($requestMethod), self::$validRequestMethods, true)) {
230
            $message = sprintf('%s is not valid Http request method.', $requestMethod);
231
            throw new UnexpectedValueException($message);
232
        }
233
    }
234
235
    /**
236
     * @param $requestMethod
237
     * @throws InvalidArgumentException
238
     */
239
    private function checkRequestMethodParameterType($requestMethod) : void
240
    {
241
        $requestMethodParameterType = gettype($requestMethod);
242
        if (!in_array($requestMethodParameterType, ['array', 'string'], true)) {
243
            $message = sprintf(
244
                'Request method must be string or array but %s given.',
245
                $requestMethodParameterType
246
            );
247
            throw new InvalidArgumentException($message);
248
        }
249
    }
250
251
    /**
252
     * Dispatch against the provided HTTP method verb and URI.
253
     * @return FastRoute\Dispatcher
254
     */
255
    private function dispatcher() : FastRoute\Dispatcher
256
    {
257
        if ($this->cachedFile !== null) {
258
            return $this->cachedDispatcher();
259
        }
260
        return $this->simpleDispatcher();
261
    }
262
263
    private function simpleDispatcher()
264
    {
265
        $options = [
266
            'routeParser' => FastRoute\RouteParser\Std::class,
267
            'dataGenerator' => FastRoute\DataGenerator\GroupCountBased::class,
268
            'dispatcher' => FastRoute\Dispatcher\GroupCountBased::class,
269
            'routeCollector' => FastRoute\RouteCollector::class,
270
        ];
271
        /** @var RouteCollector $routeCollector */
272
        $routeCollector = new $options['routeCollector'](
273
            new $options['routeParser'], new $options['dataGenerator']
274
        );
275
        $this->addRoutes($routeCollector);
276
277
        return new $options['dispatcher']($routeCollector->getData());
278
    }
279
280
    private function cachedDispatcher()
281
    {
282
        $options = [
283
            'routeParser' => FastRoute\RouteParser\Std::class,
284
            'dataGenerator' => FastRoute\DataGenerator\GroupCountBased::class,
285
            'dispatcher' => FastRoute\Dispatcher\GroupCountBased::class,
286
            'routeCollector' => FastRoute\RouteCollector::class
287
        ];
288
        if (file_exists($this->cachedFile)) {
289
            $dispatchData = require $this->cachedFile;
290
            if (!is_array($dispatchData)) {
291
                throw new \RuntimeException('Invalid cache file "' . $options['cacheFile'] . '"');
292
            }
293
            return new $options['dispatcher']($dispatchData);
294
        }
295
        $routeCollector = new $options['routeCollector'](
296
            new $options['routeParser'], new $options['dataGenerator']
297
        );
298
        $this->addRoutes($routeCollector);
299
        /** @var RouteCollector $routeCollector */
300
        $dispatchData = $routeCollector->getData();
301
        file_put_contents(
302
            $this->cachedFile,
303
            '<?php return ' . var_export($dispatchData, true) . ';'
304
        );
305
        return new $options['dispatcher']($dispatchData );
306
    }
307
308
    /**
309
     * Define Closures for all routes that returns controller info to be used.
310
     * @param FastRoute\RouteCollector $route
311
     */
312
    private function addRoutes(FastRoute\RouteCollector $route) : void
313
    {
314
        $routeIndex=0;
315
        foreach ($this->routes as $definedRoute) {
316
            $definedRoute[3] = $definedRoute[3] ?? $this->defaultReturnType;
317
            $routeName = 'routeClosure'.$routeIndex;
318
            [$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...
319
            $returnType = Router::$translations[$returnType] ?? $this->defaultReturnType;
320
            $this->routerClosures[$routeName] = function($args) use ($controller, $returnType) {
321
                return  ['controller' => $controller, 'returnType'=> $returnType, 'args'=> $args];
322
            };
323
            $route->addRoute(strtoupper($definedRoute[0]), $definedRoute[1], $routeName);
324
            $routeIndex++;
325
        }
326
    }
327
328
329
330
    /**
331
     * Get router data that includes route info and aliases
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
    /**
348
     * Get route info for requested uri
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
     * @param array $routeInfo
372
     * @return array
373
     */
374
    private function getRouteData(array $routeInfo) : array
375
    {
376
        if ($routeInfo[0] === FastRoute\Dispatcher::FOUND) {
377
            [$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...
378
            return $this->routerClosures[$handler]($vars);
379
        }
380
        return [
381
            'status'        => 200,
382
            'returnType'    => 'html',
383
            'definedRoute'  => null,
384
            'args'          => []
385
        ];
386
    }
387
}
388