Completed
Push — master ( e75169...2e3c52 )
by Mehmet
06:55 queued 02:33
created

Router::add()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 13
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
dl 0
loc 13
ccs 0
cts 13
cp 0
rs 9.2
c 0
b 0
f 0
cc 4
eloc 9
nc 6
nop 5
crap 20
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
     * @var string
58
     */
59
    private $defaultReturnType;
60
61
62
    /**
63
     * Translation array.
64
     * Make sures about return type.
65
     * @var array
66
     */
67
    private static $translations = [
68
        'h'     => 'html',
69
        'html'  => 'html',
70
        'r'     => 'redirect',
71
        'redirect' => 'redirect',
72
        'j'     => 'json',
73
        'json'  => 'json',
74
        't'     => 'text',
75
        'text'  => 'text',
76
        'd'     => 'download',
77
        'download'  => 'download'
78
    ];
79
80
    /**
81
     * Valid Request Methods array.
82
     * Make sures about requested methods.
83
     * @var array
84
     */
85
    private static $validRequestMethods = [
86
        'GET',
87
        'OPTIONS',
88
        'HEAD',
89
        'POST',
90
        'PUT',
91
        'DELETE',
92
        'PATCH'
93
    ];
94
95
96
    /**
97
     * Valid Request Methods array.
98
     * Make sures about return type.
99
     * Index 0 is also default value.
100
     * @var array
101
     */
102
    private static $validReturnTypes = [
103
        'html',
104
        'json',
105
        'text',
106
        'redirect',
107
        'download'
108
    ];
109
110
    /**
111
     * Router constructor.
112
     * Create new router.
113
     *
114
     * @param string $defaultReturnType
115
     * @param string $method
116
     * @param string $requestedPath
117
     * @param string $folder
118
     * @throws UnexpectedValueException
119
     */
120
    public function __construct(
121
        string $defaultReturnType,
122
        string $method,
123
        string $requestedPath,
124
        string $folder = ''
125
    ) {
126
        if (!in_array($method, self::$validRequestMethods, true)) {
127
            $message = sprintf('%s is not valid Http request method.', $method);
128
            throw new UnexpectedValueException($message);
129
        }
130
        $this->method   = $method;
131
        $this->requestedPath     = $this->extractFolder($requestedPath, $folder);
132
        $this->defaultReturnType = self::$translations[$defaultReturnType] ?? self::$validReturnTypes[0];
133
    }
134
135
    /**
136
     * Remove sub folder from requestedPath if defined
137
     * @param string $requestPath
138
     * @param string $folder
139
     * @return string
140
     */
141
    private function extractFolder(string $requestPath, string $folder) : string
142
    {
143
        if (!empty($folder)) {
144
            $requestPath = '/' . trim(preg_replace('#^/' . $folder . '#msi', '/', $requestPath), '/');
145
        }
146
        if ($requestPath === '') {
147
            $requestPath = '/';
148
        }
149
        return $requestPath;
150
    }
151
152
    /**
153
     * add route to routes list
154
     * @param string|array requestMethods
155
     * @param string $route
156
     * @param string $action
157
     * @param string $returnType
158
     * @param string $alias
159
     * @throws InvalidArgumentException
160
     * @throws UnexpectedValueException
161
     */
162
    public function add($requestMethods, string $route, string $action, ?string $returnType = null, ?string $alias = null)
163
    {
164
        $requestMethodsGiven = is_array($requestMethods) ? (array) $requestMethods : [0 => $requestMethods];
165
        $returnType = $this->determineReturnType($returnType);
166
        foreach ($requestMethodsGiven as $requestMethod) {
167
            $this->checkRequestMethodParameterType($requestMethod);
168
            $this->checkRequestMethodIsValid($requestMethod);
169
            if ($alias !== null) {
170
                $this->aliases[$alias] = $route;
171
            }
172
            $this->routes[] = [strtoupper($requestMethod), $route, $action, $returnType];
173
        }
174
    }
175
176
    /**
177
     * @param string $method
178
     * @param array $args
179
     * @throws UnexpectedValueException
180
     * @throws InvalidArgumentException
181
     */
182
    public function __call(string $method, array $args) : void
183
    {
184
185
        $this->checkRequestMethodIsValid($method);
186
        $defaults = [
187
            null,
188
            null,
189
            $this->defaultReturnType,
190
            null
191
        ];
192
        [$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...
193
        $this->add($method, $route, $action, $returnType, $alias);
194
    }
195
196
    /**
197
     * @param string|null $returnType
198
     * @return string
199
     */
200
    private function determineReturnType(?string $returnType) : string
201
    {
202
        if ($returnType === null) {
203
            return $this->defaultReturnType;
204
        }
205
        return in_array($returnType, self::$validReturnTypes, true) ? $returnType : $this->defaultReturnType;
206
    }
207
208
    /**
209
     * @param string $requestMethod
210
     * Checks if request method is valid
211
     * @throws UnexpectedValueException;
212
     */
213
    private function checkRequestMethodIsValid(string $requestMethod) : void
214
    {
215
        if (!in_array(strtoupper($requestMethod), self::$validRequestMethods, true)) {
216
            $message = sprintf('%s is not valid Http request method.', $requestMethod);
217
            throw new UnexpectedValueException($message);
218
        }
219
    }
220
221
    /**
222
     * @param $requestMethod
223
     * @throws InvalidArgumentException
224
     */
225
    private function checkRequestMethodParameterType($requestMethod) : void
226
    {
227
        $requestMethodParameterType = gettype($requestMethod);
228
        if (!in_array($requestMethodParameterType, ['array', 'string'], true)) {
229
            $message = sprintf(
230
                'Request method must be string or array but %s given.',
231
                $requestMethodParameterType
232
            );
233
            throw new InvalidArgumentException($message);
234
        }
235
    }
236
237
    /**
238
     * Dispatch against the provided HTTP method verb and URI.
239
     * @return array
240
     */
241
    private function dispatcher()
242
    {
243
        $options = [
244
            'routeParser'   => FastRoute\RouteParser\Std::class,
245
            'dataGenerator' => FastRoute\DataGenerator\GroupCountBased::class,
246
            'dispatcher'    => FastRoute\Dispatcher\GroupCountBased::class,
247
            'routeCollector' => FastRoute\RouteCollector::class,
248
        ];
249
        /** @var RouteCollector $routeCollector */
250
        $routeCollector = new $options['routeCollector'](
251
            new $options['routeParser'], new $options['dataGenerator']
252
        );
253
        $this->addRoutes($routeCollector);
254
        return new $options['dispatcher']($routeCollector->getData());
255
    }
256
257
    /**
258
     * Define Closures for all routes that returns controller info to be used.
259
     * @param FastRoute\RouteCollector $route
260
     */
261
    private function addRoutes(FastRoute\RouteCollector $route)
262
    {
263
        foreach ($this->routes as $definedRoute) {
264
            $definedRoute[3] = $definedRoute[3] ?? $this->defaultReturnType;
265
            $route->addRoute(strtoupper($definedRoute[0]), $definedRoute[1], function ($args) use ($definedRoute) {
266
                [$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 seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
267
                $returnType = Router::$translations[$returnType] ?? $this->defaultReturnType;
268
                return  ['controller' => $controller, 'returnType'=> $returnType, 'args'=> $args];
269
            });
270
        }
271
    }
272
273
274
275
    /**
276
     * Get router data that includes route info and aliases
277
     * @return array
278
     */
279
    public function getRoute() : array
280
    {
281
        $dispatcher = $this->dispatcher();
282
        $routeInfo  = $dispatcher->dispatch($this->method, $this->requestedPath);
283
        $routerData = [
284
            'route'     => $this->runDispatcher($routeInfo),
285
            'aliases'   => $this->aliases
286
        ];
287
        return $routerData;
288
    }
289
290
291
    /**
292
     * Get route info for requested uri
293
     * @param array $routeInfo
294
     * @return array $routerData
295
     */
296
    private function runDispatcher(array $routeInfo) : array
297
    {
298
        $routeData = $this->getRouteData($routeInfo);
299
        $dispatchResults = [
300
            FastRoute\Dispatcher::METHOD_NOT_ALLOWED => [
301
                'status' => 405
302
            ],
303
            FastRoute\Dispatcher::FOUND => [
304
                'status'  => 200
305
            ],
306
            FastRoute\Dispatcher::NOT_FOUND => [
307
                'status' => 404
308
            ]
309
        ];
310
        return array_merge($routeData, $dispatchResults[$routeInfo[0]]);
311
    }
312
313
    /**
314
     * Get routeData according to dispatcher's results
315
     * @param array $routeInfo
316
     * @return array
317
     */
318
    private function getRouteData(array $routeInfo) : array
319
    {
320
        if ($routeInfo[0] === FastRoute\Dispatcher::FOUND) {
321
            [$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...
322
            return $handler($vars);
323
        }
324
        return [
325
            'status'        => 200,
326
            'returnType'    => 'html',
327
            'definedRoute'  => null,
328
            'args'          => []
329
        ];
330
    }
331
}
332