Completed
Push — master ( b8526c...2a0f4d )
by Mehmet
04:39
created

Router::checkRequestMethodParameterType()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 10
ccs 7
cts 7
cp 1
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 7
nc 2
nop 1
crap 2
1
<?php
2
/**
3
 * Selami Router
4
 * PHP version 7+
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 11
    public function __construct(
121
        string $defaultReturnType,
122
        string $method,
123
        string $requestedPath,
124
        string $folder = ''
125
    ) {
126 11 View Code Duplication
        if (!in_array($method, self::$validRequestMethods, true)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
127 1
            $message = sprintf('%s is not valid Http request method.', $method);
128 1
            throw new UnexpectedValueException($message);
129
        }
130 10
        $this->method   = $method;
131 10
        $this->requestedPath     = $this->extractFolder($requestedPath, $folder);
132 10
        $this->defaultReturnType = self::$translations[$defaultReturnType] ?? self::$validReturnTypes[0];
133 10
    }
134
135
    /**
136
     * Remove sub folder from requestedPath if defined
137
     * @param string $requestPath
138
     * @param string $folder
139
     * @return string
140
     */
141 10
    private function extractFolder(string $requestPath, string $folder)
142
    {
143 10
        if (!empty($folder)) {
144 1
            $requestPath = '/' . trim(preg_replace('#^/' . $folder . '#msi', '/', $requestPath), '/');
145
        }
146 10
        if ($requestPath === '') {
147 1
            $requestPath = '/';
148
        }
149 10
        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 9
    public function add($requestMethods, string $route, string $action, string $returnType=null, string $alias=null)
163
    {
164 9
        $requestMethodsGiven = is_array($requestMethods) ? (array) $requestMethods : [0 => $requestMethods];
165 9
        $returnType = $this->checkReturnType($returnType);
166 9
        foreach ($requestMethodsGiven as $requestMethod) {
167 9
            $this->checkRequestMethodParameterType($requestMethod);
168 8
            $this->checkRequestMethodIsValid($requestMethod);
169 7
            if ($alias !== null) {
170 7
                $this->aliases[$alias] = $route;
171
            }
172 7
            $this->routes[] = [strtoupper($requestMethod), $route, $action, $returnType];
173
        }
174 7
    }
175
176
    /**
177
     * @param string $method
178
     * @param array $args
179
     * @throws UnexpectedValueException
180
     */
181 2
    public function __call(string $method, array $args)
182
    {
183
184 2
        $this->checkRequestMethodIsValid($method);
185
        $defaults = [
186 1
            null,
187
            null,
188 1
            $this->defaultReturnType,
189
            null
190
        ];
191 1
        list($route, $action, $returnType, $alias) = array_merge($args, $defaults);
192 1
        $this->add($method, $route, $action, $returnType, $alias);
193 1
    }
194
195
    /**
196
     * @param string|null $returnType
197
     * @return string
198
     */
199 9
    private function checkReturnType($returnType) {
200 9
        return $returnType === null ? $this->defaultReturnType : self::$validReturnTypes[$returnType] ?? $this->defaultReturnType;
201
    }
202
203
    /**
204
     * @param string $requestMethod
205
     * Checks if request method is valid
206
     * @throws UnexpectedValueException;
207
     */
208 9
    private function checkRequestMethodIsValid(string $requestMethod)
209
    {
210 9 View Code Duplication
        if (!in_array(strtoupper($requestMethod), self::$validRequestMethods, true)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
211 2
            $message = sprintf('%s is not valid Http request method.', $requestMethod);
212 2
            throw new UnexpectedValueException($message);
213
        }
214 7
    }
215
216
    /**
217
     * @param $requestMethod
218
     * @throws InvalidArgumentException
219
     */
220 9
    private function checkRequestMethodParameterType($requestMethod)
221
    {
222 9
        $requestMethodParameterType = gettype($requestMethod);
223 9
        if (!in_array($requestMethodParameterType, ['array', 'string'], true)) {
224 1
            $message = sprintf(
225 1
                'Request method must be string or array but %s given.',
226
                $requestMethodParameterType);
227 1
            throw new InvalidArgumentException($message);
228
        }
229 8
    }
230
231
    /**
232
     * Dispatch against the provided HTTP method verb and URI.
233
     * @return array
234
     */
235 3
    private function dispatcher()
236
    {
237
        $options = [
238 3
            'routeParser'   => FastRoute\RouteParser\Std::class,
239
            'dataGenerator' => FastRoute\DataGenerator\GroupCountBased::class,
240
            'dispatcher'    => FastRoute\Dispatcher\GroupCountBased::class,
241
            'routeCollector' => FastRoute\RouteCollector::class,
242
        ];
243
        /** @var RouteCollector $routeCollector */
244 3
        $routeCollector = new $options['routeCollector'](
245 3
            new $options['routeParser'], new $options['dataGenerator']
246
        );
247 3
        $this->addRoutes($routeCollector);
248 3
        return new $options['dispatcher']($routeCollector->getData());
249
    }
250
251
    /**
252
     * Define Closures for all routes that returns controller info to be used.
253
     * @param FastRoute\RouteCollector $route
254
     */
255 3
    private function addRoutes(FastRoute\RouteCollector $route)
256
    {
257 3
        foreach ($this->routes as $definedRoute) {
258 3
            $definedRoute[3] = $definedRoute[3] ?? $this->defaultReturnType;
259 3
            $route->addRoute(strtoupper($definedRoute[0]), $definedRoute[1], function ($args) use ($definedRoute) {
260 1
                list(,,$controller, $returnType) = $definedRoute;
261 1
                $returnType = Router::$translations[$returnType] ?? $this->defaultReturnType;
262 1
                return  ['controller' => $controller, 'returnType'=> $returnType, 'args'=> $args];
263 3
            });
264
        }
265 3
    }
266
267
268
269
    /**
270
     * Get router data that includes route info and aliases
271
     */
272 3
    public function getRoute()
273
    {
274 3
        $dispatcher = $this->dispatcher();
275 3
        $routeInfo  = $dispatcher->dispatch($this->method, $this->requestedPath);
276
        $routerData = [
277 3
            'route'     => $this->runDispatcher($routeInfo),
278 3
            'aliases'   => $this->aliases
279
        ];
280 3
        return $routerData;
281
    }
282
283
284
    /**
285
     * Get route info for requested uri
286
     * @param array $routeInfo
287
     * @return array $routerData
288
     */
289 3
    private function runDispatcher(array $routeInfo)
290
    {
291 3
        $routeData = $this->getRouteData($routeInfo);
292
        $dispatchResults = [
293 3
            FastRoute\Dispatcher::METHOD_NOT_ALLOWED => [
294
                'status' => 405
295 3
            ],
296 3
            FastRoute\Dispatcher::FOUND => [
297
                'status'  => 200
298
            ],
299 3
            FastRoute\Dispatcher::NOT_FOUND => [
300
                'status' => 404
301
            ]
302
        ];
303 3
        return array_merge($routeData, $dispatchResults[$routeInfo[0]]);
304
    }
305
306
    /**
307
     * Get routeData according to dispatcher's results
308
     * @param array $routeInfo
309
     * @return array
310
     */
311 3
    private function getRouteData(array $routeInfo)
312
    {
313 3
        if ($routeInfo[0] === FastRoute\Dispatcher::FOUND) {
314 1
            list(, $handler, $vars) = $routeInfo;
315 1
            return $handler($vars);
316
        }
317
        return [
318 2
            'status'        => 200,
319
            'returnType'    => 'html',
320
            'definedRoute'  => null,
321
            'args'          => []
322
        ];
323
    }
324
}
325