Completed
Pull Request — master (#271)
by Derek Stephen
21:32
created

Router::dispatch()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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