Completed
Push — master ( da7117...d72119 )
by Mr
02:47
created

Router::error()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 6
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 1
1
<?php
2
3
namespace DrMVC\Router;
4
5
use Psr\Http\Message\ServerRequestInterface;
6
use Psr\Http\Message\ResponseInterface;
7
8
/**
9
 * Class Router
10
 * @package DrMVC\Router
11
 * @method Router options(string $pattern, callable $callable): Router
12
 * @method Router get(string $pattern, callable $callable): Router
13
 * @method Router head(string $pattern, callable $callable): Router
14
 * @method Router post(string $pattern, callable $callable): Router
15
 * @method Router put(string $pattern, callable $callable): Router
16
 * @method Router delete(string $pattern, callable $callable): Router
17
 * @method Router trace(string $pattern, callable $callable): Router
18
 * @method Router connect(string $pattern, callable $callable): Router
19
 * @since 3.0
20
 */
21
class Router implements RouterInterface, MethodsInterface
22
{
23
    /**
24
     * Array with all available routes
25
     * @var array
26
     */
27
    private $_routes = [];
28
29
    /**
30
     * Class with error inside
31
     * @var callable|string
32
     */
33
    private $_error = Error::class;
34
35
    /**
36
     * @var ServerRequestInterface
37
     */
38
    private $_request;
39
40
    /**
41
     * @var ResponseInterface
42
     */
43
    private $_response;
44
45
    /**
46
     * Router constructor.
47
     *
48
     * @param   ServerRequestInterface $request
49
     * @param   ResponseInterface $response
50
     */
51
    public function __construct(ServerRequestInterface $request, ResponseInterface $response)
52
    {
53
        $this
54
            ->setRequest($request)
55
            ->setResponse($response);
56
    }
57
58
    /**
59
     * Some kind of magic ;) details in header of this class, in 'methods'
60
     *
61
     * @param   string $method
62
     * @param   array $args
63
     * @return  RouterInterface
64
     */
65
    public function __call(string $method, array $args): RouterInterface
66
    {
67
        if (\in_array($method, self::METHODS, false)) {
68
            $this->set([$method], $args);
69
        }
70
        return $this;
71
    }
72
73
    /**
74
     * Abstraction of setter
75
     *
76
     * @param   array $methods
77
     * @param   array $args
78
     * @return  RouterInterface
79
     */
80
    private function set(array $methods, array $args): RouterInterface
81
    {
82
        list($pattern, $callable) = $args;
83
        $route = new Route($methods, $pattern, $callable, $this->getRequest(), $this->getResponse());
84
        $this->setRoute($route);
85
        return $this;
86
    }
87
88
    /**
89
     * Check if passed methods in allowed list
90
     *
91
     * @param   array $methods list of methods for check
92
     * @return  array
93
     */
94
    public function checkMethods(array $methods): array
95
    {
96
        return array_map(
97
            function($method) {
98
                $method = strtolower($method);
99
100
                try {
101
                    if (!\in_array($method, self::METHODS, false)) {
102
                        throw new Exception("Method \"$method\" is not in allowed list [" . implode(',',
103
                                self::METHODS) . ']');
104
                    }
105
                } catch (Exception $e) {
106
                    // Catch empty because __construct overloaded
107
                }
108
109
                return $method;
110
            },
111
            $methods
112
        );
113
    }
114
115
    /**
116
     * Callable must be only selected methods
117
     *
118
     * @param   array $methods
119
     * @param   string $pattern
120
     * @param   callable|string $callable
121
     * @return  RouterInterface
122
     */
123
    public function map(array $methods, string $pattern, $callable): RouterInterface
124
    {
125
        // Check if method in allowed list
126
        $methods = $this->checkMethods($methods);
127
128
        // Set new route with parameters
129
        $this->set($methods, [$pattern, $callable]);
130
131
        return $this;
132
    }
133
134
    /**
135
     * Any method should be callable
136
     *
137
     * @param   string $pattern
138
     * @param   callable|string $callable
139
     * @return  RouterInterface
140
     */
141
    public function any(string $pattern, $callable): RouterInterface
142
    {
143
        // Set new route with all methods
144
        $this->set(self::METHODS, [$pattern, $callable]);
145
146
        return $this;
147
    }
148
149
    /**
150
     * Set error method
151
     *
152
     * @param   callable|string $callable
153
     * @return  RouterInterface
154
     */
155
    public function error($callable): RouterInterface
156
    {
157
        // Set new route with all methods
158
        $this->set(['get'], ['error', $callable]);
159
160
        return $this;
161
    }
162
163
    /**
164
     * @param   mixed $request
165
     * @return  RouterInterface
166
     */
167
    public function setRequest($request): RouterInterface
168
    {
169
        $this->_request = $request;
170
        return $this;
171
    }
172
173
    /**
174
     * @return ServerRequestInterface
175
     */
176
    public function getRequest(): ServerRequestInterface
177
    {
178
        return $this->_request;
179
    }
180
181
    /**
182
     * @param   mixed $response
183
     * @return  RouterInterface
184
     */
185
    public function setResponse($response): RouterInterface
186
    {
187
        $this->_response = $response;
188
        return $this;
189
    }
190
191
    /**
192
     * @return ResponseInterface
193
     */
194
    public function getResponse(): ResponseInterface
195
    {
196
        return $this->_response;
197
    }
198
199
    /**
200
     * Overwrite default error class
201
     *
202
     * @param   callable|string $error
203
     * @return  RouterInterface
204
     */
205
    public function setError($error): RouterInterface
206
    {
207
        $this->_error = $error;
208
        return $this;
209
    }
210
211
    /**
212
     * Get error class or closure
213
     *
214
     * @return  callable|object
215
     */
216
    public function getError()
217
    {
218
        $error = $this->_error;
219
        // If string inside then we work on class
220
        if (\is_string($error)) {
221
            $error = new $error();
222
        }
223
        return $error;
224
    }
225
226
    /**
227
     * Add route into the array of routes
228
     *
229
     * @param   RouteInterface $route
230
     * @return  RouterInterface
231
     */
232
    public function setRoute(RouteInterface $route): RouterInterface
233
    {
234
        $regexp = $route->getRegexp();
235
        $this->_routes[$regexp] = $route;
236
        return $this;
237
    }
238
239
    /**
240
     * Find route object by URL nad method
241
     *
242
     * @param   string $uri
243
     * @param   string $method
244
     * @return  array
245
     */
246
    private function checkMatches(string $uri, string $method): array
247
    {
248
        return array_map(
249
            function($regexp, $route) use ($uri, $method) {
250
                $match = preg_match_all($regexp, $uri, $matches);
251
252
                // If something found and method is correct
253
                if ($match && $route->checkMethod($method)) {
254
                    // Set array of variables
255
                    $route->setVariables($matches);
256
                    return $route;
257
                }
258
                return null;
259
            },
260
            // Array with keys
261
            $this->getRoutes(true),
262
            // Array with values
263
            $this->getRoutes()
264
        );
265
    }
266
267
    /**
268
     * Find optimal route from array of routes by regexp and uri
269
     *
270
     * @return  array
271
     */
272
    private function getMatches(): array
273
    {
274
        // Extract URI of current query
275
        $uri = $this->getRequest()->getUri()->getPath();
276
277
        // Extract method of current request
278
        $method = $this->getRequest()->getMethod();
279
        $method = strtolower($method);
280
281
        // Foreach emulation
282
        return $this->checkMatches($uri, $method);
283
    }
284
285
    /**
286
     * Parse URI by Regexp from routes and return single route
287
     *
288
     * @return  RouteInterface
289
     */
290
    public function getRoute(): RouteInterface
291
    {
292
        // Find route by regexp and URI
293
        $matches = $this->getMatches();
294
295
        // Cleanup the array of matches, then reindex array
296
        $matches = array_values(array_filter($matches));
297
298
        // If we have some classes in result of regexp
299
        $result = !empty($matches)
300
            // Take first from matches
301
            ? $matches[0] // Here the Route() object
302
            // Create new object with error inside
303
            : $this->getError();
304
305
        $result->setRequest($this->getRequest());
306
        $result->setResponse($this->getResponse());
307
308
        return $result;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $result could return the type callable which is incompatible with the type-hinted return DrMVC\Router\RouteInterface. Consider adding an additional type-check to rule them out.
Loading history...
309
    }
310
311
    /**
312
     * Get all available routes
313
     *
314
     * @param   bool $keys - Return only keys
315
     * @return  array
316
     */
317
    public function getRoutes(bool $keys = false): array
318
    {
319
        return $keys
320
            ? array_keys($this->_routes)
321
            : $this->_routes;
322
    }
323
324
}
325