Passed
Push — master ( 035a69...ef3bdc )
by Kirill
03:22
created

Router::addRoute()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 1
c 1
b 0
f 0
dl 0
loc 4
rs 10
cc 1
nc 1
nop 2
1
<?php
2
3
/**
4
 * Spiral Framework.
5
 *
6
 * @license   MIT
7
 * @author    Anton Titov (Wolfy-J)
8
 */
9
10
declare(strict_types=1);
11
12
namespace Spiral\Router;
13
14
use Psr\Container\ContainerInterface;
15
use Psr\Http\Message\ResponseInterface;
16
use Psr\Http\Message\ServerRequestInterface;
17
use Psr\Http\Message\UriInterface;
18
use Spiral\Router\Exception\RouteException;
19
use Spiral\Router\Exception\RouteNotFoundException;
20
use Spiral\Router\Exception\RouterException;
21
use Spiral\Router\Exception\UndefinedRouteException;
22
23
/**
24
 * Manages set of routes.
25
 */
26
final class Router implements RouterInterface
27
{
28
    // attribute to store active route in request
29
    public const ROUTE_ATTRIBUTE = 'route';
30
31
    // attribute to store active route in request
32
    public const ROUTE_NAME = 'routeName';
33
34
    // attribute to store active route in request
35
    public const ROUTE_MATCHES = 'matches';
36
37
    /** @var string */
38
    private $basePath = '/';
39
40
    /** @var RouteInterface[] */
41
    private $routes = [];
42
43
    /** @var RouteInterface */
44
    private $default = null;
45
46
    /** @var UriHandler */
47
    private $uriHandler;
48
49
    /** @var ContainerInterface */
50
    private $container;
51
52
    /**
53
     * @param string             $basePath
54
     * @param UriHandler         $uriHandler
55
     * @param ContainerInterface $container
56
     */
57
    public function __construct(string $basePath, UriHandler $uriHandler, ContainerInterface $container)
58
    {
59
        $this->basePath = '/' . ltrim($basePath, '/');
60
        $this->uriHandler = $uriHandler;
61
        $this->container = $container;
62
    }
63
64
    /**
65
     * @inheritdoc
66
     *
67
     * @throws RouteNotFoundException
68
     * @throws RouterException
69
     */
70
    public function handle(ServerRequestInterface $request): ResponseInterface
71
    {
72
        try {
73
            $route = $this->matchRoute($request, $routeName);
74
        } catch (RouteException $e) {
75
            throw new RouterException('Invalid route definition', $e->getCode(), $e);
76
        }
77
78
        if ($route === null) {
79
            throw new RouteNotFoundException($request->getUri());
80
        }
81
82
        return $route->handle(
83
            $request
84
                ->withAttribute(self::ROUTE_ATTRIBUTE, $route)
85
                ->withAttribute(self::ROUTE_NAME, $routeName)
86
                ->withAttribute(self::ROUTE_MATCHES, $route->getMatches() ?? [])
87
        );
88
    }
89
90
    /**
91
     * @inheritdoc
92
     *
93
     * @deprecated see setRoute()
94
     */
95
    public function addRoute(string $name, RouteInterface $route): void
96
    {
97
        //Each added route must inherit basePath prefix
98
        $this->setRoute($name, $route);
99
    }
100
101
    /**
102
     * @inheritdoc
103
     */
104
    public function setRoute(string $name, RouteInterface $route): void
105
    {
106
        // each route must inherit basePath prefix
107
        $this->routes[$name] = $this->configure($route);
108
    }
109
110
    /**
111
     * @inheritdoc
112
     */
113
    public function setDefault(RouteInterface $route): void
114
    {
115
        $this->default = $this->configure($route);
116
    }
117
118
    /**
119
     * @inheritdoc
120
     */
121
    public function getRoute(string $name): RouteInterface
122
    {
123
        if (isset($this->routes[$name])) {
124
            return $this->routes[$name];
125
        }
126
127
        throw new UndefinedRouteException("Undefined route `{$name}`");
128
    }
129
130
    /**
131
     * @inheritdoc
132
     */
133
    public function getRoutes(): array
134
    {
135
        if (!empty($this->default)) {
136
            return $this->routes + [null => $this->default];
137
        }
138
139
        return $this->routes;
140
    }
141
142
    /**
143
     * @inheritdoc
144
     */
145
    public function uri(string $route, $parameters = []): UriInterface
146
    {
147
        try {
148
            return $this->getRoute($route)->uri($parameters);
149
        } catch (UndefinedRouteException $e) {
150
            //In some cases route name can be provided as controller:action pair, we can try to
151
            //generate such route automatically based on our default/fallback route
152
            return $this->castRoute($route)->uri($parameters);
153
        }
154
    }
155
156
    /**
157
     * Find route matched for given request.
158
     *
159
     * @param ServerRequestInterface $request
160
     * @return null|RouteInterface
161
     */
162
    protected function matchRoute(ServerRequestInterface $request, string &$routeName = null): ?RouteInterface
163
    {
164
        foreach ($this->routes as $name => $route) {
165
            // Matched route will return new route instance with matched parameters
166
            $matched = $route->match($request);
167
168
            if ($matched !== null) {
169
                $routeName = $name;
170
                return $matched;
171
            }
172
        }
173
174
        if ($this->default !== null) {
175
            return $this->default->match($request);
176
        }
177
178
        // unable to match any route
179
        return null;
180
    }
181
182
    /**
183
     * Configure route with needed dependencies.
184
     *
185
     * @param RouteInterface $route
186
     * @return RouteInterface
187
     */
188
    protected function configure(RouteInterface $route): RouteInterface
189
    {
190
        if ($route instanceof ContainerizedInterface && !$route->hasContainer()) {
191
            // isolating route in a given container
192
            $route = $route->withContainer($this->container);
193
        }
194
195
        return $route->withUriHandler($this->uriHandler->withPrefix($this->basePath));
196
    }
197
198
    /**
199
     * Locates appropriate route by name. Support dynamic route allocation using following pattern:
200
     * Named route:   `name/controller:action`
201
     * Default route: `controller:action`
202
     * Only action:   `name/action`
203
     *
204
     * @param string $route
205
     * @return RouteInterface
206
     *
207
     * @throws UndefinedRouteException
208
     */
209
    protected function castRoute(string $route): RouteInterface
210
    {
211
        if (
212
            !preg_match(
213
                '/^(?:(?P<name>[^\/]+)\/)?(?:(?P<controller>[^:]+):+)?(?P<action>[a-z_\-]+)$/i',
214
                $route,
215
                $matches
216
            )
217
        ) {
218
            throw new UndefinedRouteException(
219
                "Unable to locate route or use default route with 'name/controller:action' pattern"
220
            );
221
        }
222
223
        if (!empty($matches['name'])) {
224
            $routeObject = $this->getRoute($matches['name']);
225
        } elseif ($this->default !== null) {
226
            $routeObject = $this->default;
227
        } else {
228
            throw new UndefinedRouteException("Unable to locate route candidate for `{$route}`");
229
        }
230
231
        return $routeObject->withDefaults(
232
            [
233
                'controller' => $matches['controller'],
234
                'action'     => $matches['action']
235
            ]
236
        );
237
    }
238
}
239