Completed
Push — master ( c556fa...aaba1e )
by Mr
02:39
created

Router::getMatches()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 22
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 22
rs 9.2
c 0
b 0
f 0
cc 2
eloc 10
nc 1
nop 0
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
11
 * @method Router get(string $pattern, callable $callable): Router
12
 * @method Router post(string $pattern, callable $callable): Router
13
 * @method Router put(string $pattern, callable $callable): Router
14
 * @method Router delete(string $pattern, callable $callable): Router
15
 * @method Router option(string $pattern, callable $callable): Router
16
 * @since 3.0.0
17
 */
18
class Router implements RouterInterface
19
{
20
    /**
21
     * Array with all available routes
22
     * @var array
23
     */
24
    private $_routes = [];
25
26
    /**
27
     * Class with error inside
28
     * @var callable|string
29
     */
30
    private $_error = Error::class;
31
32
    /**
33
     * @var ServerRequestInterface
34
     */
35
    private $_request;
36
37
    /**
38
     * @var ResponseInterface
39
     */
40
    private $_response;
41
42
    /**
43
     * Router constructor.
44
     *
45
     * @param   ServerRequestInterface $request
46
     * @param   ResponseInterface $response
47
     */
48
    public function __construct(ServerRequestInterface $request, ResponseInterface $response)
49
    {
50
        $this
51
            ->setRequest($request)
52
            ->setResponse($response);
53
    }
54
55
    /**
56
     * Some kind of magic ;) details in header of this class, in 'methods'
57
     *
58
     * @param   string $method
59
     * @param   array $args
60
     * @return  RouterInterface
61
     */
62
    public function __call(string $method, array $args): RouterInterface
63
    {
64
        if (\in_array($method, self::METHODS, false)) {
65
            $this->set($method, $args);
66
        }
67
        return $this;
68
    }
69
70
    /**
71
     * Abstraction of setter
72
     *
73
     * @param   string $method
74
     * @param   array $args
75
     * @return  RouterInterface
76
     */
77
    private function set(string $method, array $args): RouterInterface
78
    {
79
        list($pattern, $callable) = $args;
80
        $route = new Route($method, $pattern, $callable, $this->getRequest(), $this->getResponse());
81
        $this->setRoute($route);
82
        return $this;
83
    }
84
85
    /**
86
     * Callable must be only selected methods
87
     *
88
     * @param   array $methods
89
     * @param   string $pattern
90
     * @param   callable|string $callable
91
     * @return  RouterInterface
92
     */
93
    public function map(array $methods, string $pattern, $callable): RouterInterface
94
    {
95
        array_map(
96
            function($method) use ($pattern, $callable) {
97
                $method = strtolower($method);
98
99
                try {
100
                    if (!\in_array($method, self::METHODS, false)) {
101
                        throw new Exception("Value \"$method\" is not in array");
102
                    }
103
                } catch (Exception $e) {
104
                    // Catch empty because __construct overloaded
105
                }
106
107
                $this->$method($pattern, $callable);
108
            },
109
            $methods
110
        );
111
        return $this;
112
    }
113
114
    /**
115
     * Any method should be callable
116
     *
117
     * @param   string $pattern
118
     * @param   callable|string $callable
119
     * @return  RouterInterface
120
     */
121
    public function any(string $pattern, $callable): RouterInterface
122
    {
123
        return $this->map(self::METHODS, $pattern, $callable);
124
    }
125
126
    /**
127
     * @param   mixed $request
128
     * @return  RouterInterface
129
     */
130
    public function setRequest($request): RouterInterface
131
    {
132
        $this->_request = $request;
133
        return $this;
134
    }
135
136
    /**
137
     * @return ServerRequestInterface
138
     */
139
    public function getRequest(): ServerRequestInterface
140
    {
141
        return $this->_request;
142
    }
143
144
    /**
145
     * @param   mixed $response
146
     * @return  RouterInterface
147
     */
148
    public function setResponse($response): RouterInterface
149
    {
150
        $this->_response = $response;
151
        return $this;
152
    }
153
154
    /**
155
     * @return ResponseInterface
156
     */
157
    public function getResponse(): ResponseInterface
158
    {
159
        return $this->_response;
160
    }
161
162
    /**
163
     * Overwrite default error class
164
     *
165
     * @param   callable|string $error
166
     * @return  RouterInterface
167
     */
168
    public function setError($error): RouterInterface
169
    {
170
        $this->_error = $error;
171
        return $this;
172
    }
173
174
    /**
175
     * Get error class or closure
176
     *
177
     * @return  callable|object
178
     */
179
    public function getError()
180
    {
181
        $error = $this->_error;
182
        // If string inside then we work on class
183
        if (\is_string($error)) {
184
            $error = new $error();
185
        }
186
        return $error;
187
    }
188
189
    /**
190
     * Add route into the array of routes
191
     *
192
     * @param   RouteInterface $route
193
     * @return  RouterInterface
194
     */
195
    public function setRoute(RouteInterface $route): RouterInterface
196
    {
197
        $regexp = $route->getRegexp();
198
        $this->_routes[$regexp] = $route;
199
        return $this;
200
    }
201
202
    /**
203
     * Find optimal route from array of routes by regexp and uri
204
     *
205
     * @return  array
206
     */
207
    private function getMatches(): array
208
    {
209
        // Extract URI of current query
210
        $uri = $this->getRequest()->getUri()->getPath();
211
212
        // Foreach emulation
213
        return array_map(
214
            function($regexp, $route) use ($uri) {
215
                $match = preg_match_all($regexp, $uri, $matches);
216
217
                // If something found
218
                if ($match) {
219
                    // Set array of variables
220
                    $route->setVariables($matches);
221
                    return $route;
222
                }
223
                return null;
224
            },
225
            // Array with keys
226
            $this->getRoutes(true),
227
            // Array with values
228
            $this->getRoutes()
229
        );
230
    }
231
232
    /**
233
     * Parse URI by Regexp from routes and return single route
234
     *
235
     * @return  RouteInterface
236
     */
237
    public function getRoute(): RouteInterface
238
    {
239
        // Find route by regexp and URI
240
        $matches = $this->getMatches();
241
242
        // Cleanup the array of matches, then reindex array
243
        $matches = array_values(array_filter($matches));
244
245
        // If we have some classes in result of regexp
246
        if (!empty($matches)) {
247
            // Take first from matches
248
            $result = $matches[0]; // Here the Route() object
249
        } else {
250
            // Create new object with error inside
251
            $result = $this->getError();
252
        }
253
254
        $result->setRequest($this->getRequest());
255
        $result->setResponse($this->getResponse());
256
257
        return $result;
258
    }
259
260
    /**
261
     * Get all available routes
262
     *
263
     * @param   bool $keys - Return only keys
264
     * @return  array
265
     */
266
    public function getRoutes(bool $keys = false): array
267
    {
268
        return $keys
269
            ? array_keys($this->_routes)
270
            : $this->_routes;
271
    }
272
273
}
274