Completed
Push — master ( 1145d3...021497 )
by Phil
02:05 queued 12s
created

Router::handle()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 0
cts 2
cp 0
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 2
1
<?php declare(strict_types=1);
2
3
namespace League\Route;
4
5
use FastRoute\{DataGenerator, RouteCollector, RouteParser};
6
use InvalidArgumentException;
7
use League\Route\Middleware\{MiddlewareAwareInterface, MiddlewareAwareTrait};
8
use League\Route\Strategy\{ApplicationStrategy, StrategyAwareInterface, StrategyAwareTrait};
9
use Psr\Http\Server\RequestHandlerInterface;
10
use Psr\Http\Message\{ResponseInterface, ServerRequestInterface};
11
12
class Router extends RouteCollector implements
13
    MiddlewareAwareInterface,
14
    RouteCollectionInterface,
15
    StrategyAwareInterface,
16
    RequestHandlerInterface
17
{
18
    use MiddlewareAwareTrait;
19
    use RouteCollectionTrait;
20
    use StrategyAwareTrait;
21
22
    /**
23
     * @var Route[]
24
     */
25
    protected $routes = [];
26
27
    /**
28
     * @var Route[]
29
     */
30
    protected $namedRoutes = [];
31
32
    /**
33
     * @var RouteGroup[]
34
     */
35
    protected $groups = [];
36
37
    /**
38
     * @var array
39
     */
40
    protected $patternMatchers = [
41
        '/{(.+?):number}/'        => '{$1:[0-9]+}',
42
        '/{(.+?):word}/'          => '{$1:[a-zA-Z]+}',
43
        '/{(.+?):alphanum_dash}/' => '{$1:[a-zA-Z0-9-_]+}',
44
        '/{(.+?):slug}/'          => '{$1:[a-z0-9-]+}',
45
        '/{(.+?):uuid}/'          => '{$1:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}+}'
46
    ];
47
48
    /**
49
     * Constructor
50
     *
51
     * @param RouteParser $parser
52
     * @param DataGenerator $generator
53
     */
54 63
    public function __construct(?RouteParser $parser = null, ?DataGenerator $generator = null)
55
    {
56
        // build parent route collector
57 63
        $parser    = $parser ?? new RouteParser\Std;
58 63
        $generator = $generator ?? new DataGenerator\GroupCountBased;
59 63
        parent::__construct($parser, $generator);
60 63
    }
61
62
    /**
63
     * {@inheritdoc}
64
     */
65 45
    public function map(string $method, string $path, $handler): Route
66
    {
67 45
        $path  = sprintf('/%s', ltrim($path, '/'));
68 45
        $route = new Route($method, $path, $handler);
69
70 45
        $this->routes[] = $route;
71
72 45
        return $route;
73
    }
74
75
    /**
76
     * Add a group of routes to the collection
77
     *
78
     * @param string   $prefix
79
     * @param callable $group
80
     *
81
     * @return RouteGroup
82
     */
83 12
    public function group(string $prefix, callable $group): RouteGroup
84
    {
85 12
        $group          = new RouteGroup($prefix, $group, $this);
86 12
        $this->groups[] = $group;
87
88 12
        return $group;
89
    }
90
91
    /**
92
     * {@inheritdoc}
93
     */
94 48
    public function dispatch(ServerRequestInterface $request): ResponseInterface
95
    {
96 48
        if ($this->getStrategy() === null) {
97 30
            $this->setStrategy(new ApplicationStrategy);
98
        }
99
100 48
        $this->prepRoutes($request);
101
102
        /** @var Dispatcher $dispatcher */
103 48
        $dispatcher = (new Dispatcher($this->getData()))->setStrategy($this->getStrategy());
104
105 48
        foreach ($this->getMiddlewareStack() as $middleware) {
106 3
            if (is_string($middleware)) {
107 3
                $dispatcher->lazyMiddleware($middleware);
108 3
                continue;
109
            }
110
111 3
            $dispatcher->middleware($middleware);
112
        }
113
114 48
        return $dispatcher->dispatchRequest($request);
115
    }
116
117
118
    /**
119
     * {@inheritdoc}
120
     */
121
    public function handle(ServerRequestInterface $request): ResponseInterface
122
    {
123
        return $this->dispatch($request);
124
    }
125
126
    /**
127
     * Prepare all routes, build name index and filter out none matching
128
     * routes before being passed off to the parser.
129
     *
130
     * @param ServerRequestInterface $request
131
     *
132
     * @return void
133
     */
134 48
    protected function prepRoutes(ServerRequestInterface $request): void
135
    {
136 48
        $this->processGroups($request);
137 48
        $this->buildNameIndex();
138
139 48
        $routes = array_merge(array_values($this->routes), array_values($this->namedRoutes));
140
141
        /** @var Route $route */
142 48
        foreach ($routes as $key => $route) {
143
            // check for scheme condition
144 39
            $scheme = $route->getScheme();
145 39
            if ($scheme !== null && $scheme !== $request->getUri()->getScheme()) {
146 3
                continue;
147
            }
148
149
            // check for domain condition
150 36
            $host = $route->getHost();
151 36
            if ($host !== null && $host !== $request->getUri()->getHost()) {
152 3
                continue;
153
            }
154
155
            // check for port condition
156 33
            $port = $route->getPort();
157 33
            if ($port !== null && $port !== $request->getUri()->getPort()) {
158 3
                continue;
159
            }
160
161 30
            if ($route->getStrategy() === null) {
162 21
                $route->setStrategy($this->getStrategy());
163
            }
164
165 30
            $this->addRoute($route->getMethod(), $this->parseRoutePath($route->getPath()), $route);
166
        }
167 48
    }
168
169
    /**
170
     * Build an index of named routes.
171
     *
172
     * @return void
173
     */
174 54
    protected function buildNameIndex(): void
175
    {
176 54
        foreach ($this->routes as $key => $route) {
177 42
            if ($route->getName() !== null) {
178 3
                unset($this->routes[$key]);
179 29
                $this->namedRoutes[$route->getName()] = $route;
180
            }
181
        }
182 54
    }
183
184
    /**
185
     * Process all groups
186
     *
187
     * Adds all of the group routes to the collection and determines if the group
188
     * strategy should be be used.
189
     *
190
     * @param ServerRequestInterface $request
191
     *
192
     * @return void
193
     */
194 48
    protected function processGroups(ServerRequestInterface $request): void
195
    {
196 48
        $activePath = $request->getUri()->getPath();
197
198 48
        foreach ($this->groups as $key => $group) {
199
            // we want to determine if we are technically in a group even if the
200
            // route is not matched so exceptions are handled correctly
201 9
            if ($group->getStrategy() !== null
202 9
                && strncmp($activePath, $group->getPrefix(), strlen($group->getPrefix())) === 0
203
            ) {
204 6
                $this->setStrategy($group->getStrategy());
205
            }
206
207 9
            unset($this->groups[$key]);
208 9
            $group();
209
        }
210 48
    }
211
212
    /**
213
     * Get a named route
214
     *
215
     * @param string $name
216
     *
217
     * @return Route
218
     *
219
     * @throws InvalidArgumentException when no route of the provided name exists
220
     */
221 6
    public function getNamedRoute(string $name): Route
222
    {
223 6
        $this->buildNameIndex();
224
225 6
        if (isset($this->namedRoutes[$name])) {
226 3
            return $this->namedRoutes[$name];
227
        }
228
229 3
        throw new InvalidArgumentException(sprintf('No route of the name (%s) exists', $name));
230
    }
231
232
    /**
233
     * Add a convenient pattern matcher to the internal array for use with all routes
234
     *
235
     * @param string $alias
236
     * @param string $regex
237
     *
238
     * @return self
239
     */
240 3
    public function addPatternMatcher(string $alias, string $regex): self
241
    {
242 3
        $pattern = '/{(.+?):' . $alias . '}/';
243 3
        $regex   = '{$1:' . $regex . '}';
244
245 3
        $this->patternMatchers[$pattern] = $regex;
246
247 3
        return $this;
248
    }
249
250
    /**
251
     * Replace word patterns with regex in route path
252
     *
253
     * @param string $path
254
     *
255
     * @return string
256
     */
257 30
    protected function parseRoutePath(string $path): string
258
    {
259 30
        return preg_replace(array_keys($this->patternMatchers), array_values($this->patternMatchers), $path);
260
    }
261
}
262