Completed
Push — master ( 6e365b...fbe9d6 )
by Woody
15s queued 11s
created

Router   A

Complexity

Total Complexity 24

Size/Duplication

Total Lines 185
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 24
eloc 48
dl 0
loc 185
ccs 68
cts 68
cp 1
rs 10
c 0
b 0
f 0

15 Methods

Rating   Name   Duplication   Size   Complexity  
A post() 0 3 1
A delete() 0 3 1
A __construct() 0 8 1
A head() 0 3 1
A patch() 0 3 1
A makeDispatcher() 0 5 2
A get() 0 3 1
A options() 0 3 1
A findMissingParameters() 0 6 1
A routes() 0 3 1
A add() 0 3 1
A put() 0 3 1
A handle() 0 3 1
A uri() 0 36 6
A process() 0 21 4
1
<?php
2
declare(strict_types=1);
3
4
namespace Northwoods\Router;
5
6
use FastRoute\Dispatcher;
7
use FastRoute\RouteCollector;
8
use FastRoute\RouteParser;
9
use Fig\Http\Message\RequestMethodInterface;
10
use Fig\Http\Message\StatusCodeInterface;
11
use Http\Factory\Discovery\HttpFactory;
12
use Psr\Http\Message\ServerRequestInterface;
13
use Psr\Http\Message\ResponseFactoryInterface;
14
use Psr\Http\Message\ResponseInterface;
15
use Psr\Http\Server\MiddlewareInterface;
16
use Psr\Http\Server\RequestHandlerInterface;
17
18
use function FastRoute\simpleDispatcher;
19
20
class Router implements
21
    MiddlewareInterface,
22
    RequestHandlerInterface,
23
    RequestMethodInterface,
24
    StatusCodeInterface
25
{
26
    /** @var RouteCollection */
27
    private $routes;
28
29
    /** @var RouteParser */
30
    private $parser;
31
32
    /** @var ResponseFactoryInterface */
33
    private $responseFactory;
34
35 13
    public function __construct(
36
        ?RouteCollection $routes = null,
37
        ?RouteParser $parser = null,
38
        ?ResponseFactoryInterface $responseFactory = null
39
    ) {
40 13
        $this->routes = $routes ?? new RouteCollection();
41 13
        $this->parser = $parser ?? new RouteParser\Std();
42 13
        $this->responseFactory = $responseFactory ?? HttpFactory::responseFactory();
43 13
    }
44
45
    /**
46
     * Add a named route.
47
     */
48 11
    public function add(string $name, Route $route): void
49
    {
50 11
        $this->routes->set($name, $route);
51 11
    }
52
53
    /**
54
     * Add a named route for a GET request.
55
     */
56 2
    public function get(string $name, string $pattern, RequestHandlerInterface $handler): void
57
    {
58 2
        $this->add($name, new Route(self::METHOD_GET, $pattern, $handler));
59 2
    }
60
61
    /**
62
     * Add a named route for a POST request.
63
     */
64 3
    public function post(string $name, string $pattern, RequestHandlerInterface $handler): void
65
    {
66 3
        $this->add($name, new Route(self::METHOD_POST, $pattern, $handler));
67 3
    }
68
69
    /**
70
     * Add a named route for a PUT request.
71
     */
72 1
    public function put(string $name, string $pattern, RequestHandlerInterface $handler): void
73
    {
74 1
        $this->add($name, new Route(self::METHOD_PUT, $pattern, $handler));
75 1
    }
76
77
    /**
78
     * Add a named route for a PATCH request.
79
     */
80 3
    public function patch(string $name, string $pattern, RequestHandlerInterface $handler): void
81
    {
82 3
        $this->add($name, new Route(self::METHOD_PATCH, $pattern, $handler));
83 3
    }
84
85
    /**
86
     * Add a named route for a DELETE request.
87
     */
88 1
    public function delete(string $name, string $pattern, RequestHandlerInterface $handler): void
89
    {
90 1
        $this->add($name, new Route(self::METHOD_DELETE, $pattern, $handler));
91 1
    }
92
93
    /**
94
     * Add a named route for a HEAD request.
95
     */
96 1
    public function head(string $name, string $pattern, RequestHandlerInterface $handler): void
97
    {
98 1
        $this->add($name, new Route(self::METHOD_HEAD, $pattern, $handler));
99 1
    }
100
101
    /**
102
     * Add a named route for a OPTIONS request.
103
     */
104 1
    public function options(string $name, string $pattern, RequestHandlerInterface $handler): void
105
    {
106 1
        $this->add($name, new Route(self::METHOD_OPTIONS, $pattern, $handler));
107 1
    }
108
109
    /**
110
     * Get the route collection.
111
     */
112 1
    public function routes(): RouteCollection
113
    {
114 1
        return $this->routes;
115
    }
116
117
    /**
118
     * Generate a URI for a route.
119
     *
120
     * @throws Error\UriParameterInvalidException If a parameter has an invalid value.
121
     * @throws Error\UriParametersMissingException If required parameters are missing.
122
     */
123 3
    public function uri(string $name, array $params = []): string
124
    {
125
        // Use the route parser to generate a URI for a named route:
126
        // https://github.com/nikic/FastRoute/issues/66
127 3
        $routes = $this->parser->parse($this->routes->get($name)->pattern());
128 3
        $missing = [];
129
130 3
        foreach (array_reverse($routes) as $parts) {
131 3
            $missing = $this->findMissingParameters($parts, $params);
132
133
            // Try the next route
134 3
            if (count($missing) > 0) {
135 1
                continue;
136
            }
137
138 2
            $path = '';
139 2
            foreach ($parts as $part) {
140
                // Fixed segment
141 2
                if (is_string($part)) {
142 2
                    $path .= $part;
143 2
                    continue;
144
                }
145
146
                // Check if the parameter can be matched
147 2
                if (! preg_match("~^{$part[1]}$~", strval($params[$part[0]]))) {
148 1
                    throw Error\UriParameterInvalidException::from($part[0], $part[1]);
149
                }
150
151
                // Variable segment
152 1
                $path .= $params[$part[0]];
153
            }
154
155 1
            return $path;
156
        }
157
158 1
        throw Error\UriParametersMissingException::from($name, $missing, array_keys($params));
159
    }
160
161
    // MiddlewareInterface
162 9
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
163
    {
164
        // https://github.com/nikic/FastRoute#usage
165 9
        $dispatcher = $this->makeDispatcher();
166 9
        $match = $dispatcher->dispatch($request->getMethod(), $request->getUri()->getPath());
167
168 9
        if ($match[0] === Dispatcher::NOT_FOUND) {
169 1
            return $handler->handle($request);
170
        }
171
172 8
        if ($match[0] === Dispatcher::METHOD_NOT_ALLOWED) {
173 1
            return $this->responseFactory
174 1
                        ->createResponse(self::STATUS_METHOD_NOT_ALLOWED)
175 1
                        ->withHeader('allow', implode(',', $match[1]));
176
        }
177
178 7
        foreach ($match[2] as $param => $value) {
179 2
            $request = $request->withAttribute($param, $value);
180
        }
181
182 7
        return $match[1]->handler()->handle($request);
183
    }
184
185
    // RequestHandlerInterface
186 9
    public function handle(ServerRequestInterface $request): ResponseInterface
187
    {
188 9
        return $this->process($request, new NotFoundHandler($this->responseFactory));
189
    }
190
191
    /** @return string[] */
192 3
    private function findMissingParameters(array $parts, array $params): array
193
    {
194
        // Remove all fixed segments, get named parts
195 3
        $missing = array_column(array_filter($parts, 'is_array'), 0);
196
197 3
        return array_diff($missing, array_keys($params));
198
    }
199
200 9
    private function makeDispatcher(): Dispatcher
201
    {
202
        return simpleDispatcher(function (RouteCollector $collector): void {
203 9
            foreach ($this->routes as $route) {
204 8
                $collector->addRoute($route->method(), $route->pattern(), $route);
205
            }
206 9
        });
207
    }
208
}
209