Completed
Push — master ( 87baef...b8526c )
by Mehmet
03:05
created

Router   A

Complexity

Total Complexity 22

Size/Duplication

Total Lines 291
Duplicated Lines 2.75 %

Coupling/Cohesion

Components 1
Dependencies 1

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 22
lcom 1
cbo 1
dl 8
loc 291
ccs 76
cts 76
cp 1
rs 10
c 0
b 0
f 0

11 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 4 14 2
A extractFolder() 0 10 3
B add() 0 13 5
A __call() 0 13 1
A checkRequestMethodIsValid() 4 7 2
A checkRequestMethodParameterType() 0 10 2
A dispatcher() 0 15 1
A addRoutes() 0 11 2
A getRoute() 0 10 1
A runDispatcher() 0 16 1
A getRouteData() 0 13 2

How to fix   Duplicated Code   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

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