Completed
Push — master ( a10276...87baef )
by Mehmet
02:38
created

Router::__call()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 11

Duplication

Lines 4
Ratio 26.67 %

Code Coverage

Tests 9
CRAP Score 2

Importance

Changes 0
Metric Value
dl 4
loc 15
ccs 9
cts 9
cp 1
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 11
nc 2
nop 2
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 = $returnType === null ? $this->defaultReturnType : self::$validReturnTypes[$returnType] ?? $this->defaultReturnType;
166 9
        foreach ($requestMethodsGiven as $requestMethod) {
167 9
            $requestMethodParameterType = gettype($requestMethod);
168 9
            if (!in_array($requestMethodParameterType, ['array', 'string'], true)) {
169 1
                $message = sprintf(
170 1
                    'Request method must be string or array but %s given.',
171
                    $requestMethodParameterType);
172 1
                throw new InvalidArgumentException($message);
173
            }
174 8 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...
175 1
                $message = sprintf('%s is not valid Http request method.', $requestMethod);
176 1
                throw new UnexpectedValueException($message);
177
            }
178 7
            if ($alias !== null) {
179 7
                $this->aliases[$alias] = $route;
180
            }
181 7
            $this->routes[] = [strtoupper($requestMethod), $route, $action, $returnType];
182
        }
183 7
    }
184
185
    /**
186
     * @param string $method
187
     * @param array $args
188
     * @throws UnexpectedValueException
189
     */
190 2
    public function __call(string $method, array $args)
191
    {
192 2 View Code Duplication
        if (!in_array(strtoupper($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...
193 1
            $message = sprintf('%s is not valid Http request method.', $method);
194 1
            throw new UnexpectedValueException($message);
195
        }
196
        $defaults = [
197 1
            null,
198
            null,
199 1
            $this->defaultReturnType,
200
            null
201
        ];
202 1
        list($route, $action, $returnType, $alias) = array_merge($args, $defaults);
203 1
        $this->add($method, $route, $action, $returnType, $alias);
204 1
    }
205
206
    /**
207
     * Dispatch against the provided HTTP method verb and URI.
208
     * @return array
209
     */
210 3
    private function dispatcher()
211
    {
212
        $options = [
213 3
            'routeParser'   => FastRoute\RouteParser\Std::class,
214
            'dataGenerator' => FastRoute\DataGenerator\GroupCountBased::class,
215
            'dispatcher'    => FastRoute\Dispatcher\GroupCountBased::class,
216
            'routeCollector' => FastRoute\RouteCollector::class,
217
        ];
218
        /** @var RouteCollector $routeCollector */
219 3
        $routeCollector = new $options['routeCollector'](
220 3
            new $options['routeParser'], new $options['dataGenerator']
221
        );
222 3
        $this->addRoutes($routeCollector);
223 3
        return new $options['dispatcher']($routeCollector->getData());
224
    }
225
226
    /**
227
     * Define Closures for all routes that returns controller info to be used.
228
     * @param FastRoute\RouteCollector $route
229
     */
230 3
    private function addRoutes(FastRoute\RouteCollector $route)
231
    {
232 3
        foreach ($this->routes as $definedRoute) {
233 3
            $definedRoute[3] = $definedRoute[3] ?? $this->defaultReturnType;
234 3
            $route->addRoute(strtoupper($definedRoute[0]), $definedRoute[1], function ($args) use ($definedRoute) {
235 1
                list(,,$controller, $returnType) = $definedRoute;
236 1
                $returnType = Router::$translations[$returnType] ?? $this->defaultReturnType;
237 1
                return  ['controller' => $controller, 'returnType'=> $returnType, 'args'=> $args];
238 3
            });
239
        }
240 3
    }
241
242
243
244
    /**
245
     * Get router data that includes route info and aliases
246
     */
247 3
    public function getRoute()
248
    {
249 3
        $dispatcher = $this->dispatcher();
250 3
        $routeInfo  = $dispatcher->dispatch($this->method, $this->requestedPath);
251
        $routerData = [
252 3
            'route'     => $this->runDispatcher($routeInfo),
253 3
            'aliases'   => $this->aliases
254
        ];
255 3
        return $routerData;
256
    }
257
258
259
    /**
260
     * Get route info for requested uri
261
     * @param array $routeInfo
262
     * @return array $routerData
263
     */
264 3
    private function runDispatcher(array $routeInfo)
265
    {
266 3
        $routeData = $this->getRouteData($routeInfo);
267
        $dispatchResults = [
268 3
            FastRoute\Dispatcher::METHOD_NOT_ALLOWED => [
269
                'status' => 405
270 3
            ],
271 3
            FastRoute\Dispatcher::FOUND => [
272
                'status'  => 200
273
            ],
274 3
            FastRoute\Dispatcher::NOT_FOUND => [
275
                'status' => 404
276
            ]
277
        ];
278 3
        return array_merge($routeData, $dispatchResults[$routeInfo[0]]);
279
    }
280
281
    /**
282
     * Get routeData according to dispatcher's results
283
     * @param array $routeInfo
284
     * @return array
285
     */
286 3
    private function getRouteData(array $routeInfo)
287
    {
288 3
        if ($routeInfo[0] === FastRoute\Dispatcher::FOUND) {
289 1
            list(, $handler, $vars) = $routeInfo;
290 1
            return $handler($vars);
291
        }
292
        return [
293 2
            'status'        => 200,
294
            'returnType'    => 'html',
295
            'definedRoute'  => null,
296
            'args'          => []
297
        ];
298
    }
299
}
300