-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathDispatcher.php
More file actions
381 lines (352 loc) · 12.9 KB
/
Dispatcher.php
File metadata and controls
381 lines (352 loc) · 12.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
<?php
declare(strict_types=1);
namespace Firehed\API;
use BadMethodCallException;
use DomainException;
use Firehed\API\Errors\HandlerInterface;
use Firehed\API\Interfaces\HandlesOwnErrorsInterface;
use Firehed\Common\ClassMapper;
use Firehed\Input\Containers\ParsedInput;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Throwable;
use OutOfBoundsException;
use UnexpectedValueException;
use Zend\Diactoros\ServerRequestFactory;
class Dispatcher implements RequestHandlerInterface
{
private $authenticationProvider;
private $authorizationProvider;
private $container;
private $endpoint_list;
private $error_handler;
private $parser_list;
private $psrMiddleware = [];
private $response_middleware = [];
private $request;
private $uri_data;
/**
* Add a callback to run on the response after controller executation (or
* error handling) has finished.
*
* This must be a callable with the following signature:
*
* function(ResponseInterface $response, callable $next): ResponseInterface
*
* The callback may modify the response, and either pass it off to the next
* handler (by using `return $next($response)`) or return it immediately,
* bypassing all future callbacks.
*
* The callbacks are treated as a queue (FIFO)
*
* @param callable $callback the callback to execute
* @return self
*/
public function addResponseMiddleware(callable $callback): self
{
$this->response_middleware[] = $callback;
return $this;
}
public function addMiddleware(MiddlewareInterface $mw): self
{
$this->psrMiddleware[] = $mw;
return $this;
}
/**
* Provide the authentication and authorization providers. These will be
* run after routing but before the endpoint is executed.
*
* @return $this
*/
public function setAuthProviders(
Authentication\ProviderInterface $authn,
Authorization\ProviderInterface $authz
): self {
$this->authenticationProvider = $authn;
$this->authorizationProvider = $authz;
return $this;
}
/**
* Provide a DI Container/Service Locator class or array. During
* dispatching, this structure will be queried for the routed endpoint by
* the fully-qualified class name. If the container has a class at that
* key, it will be used during execution; if not, the default behavior is
* to automatically instanciate it.
*
* @param ContainerInterface $container Container
* @return self
*/
public function setContainer(ContainerInterface $container = null): self
{
$this->container = $container;
if (!$container) {
return $this;
}
// Auto-detect auth components
if (!$this->authenticationProvider && !$this->authorizationProvider) {
if ($container->has(Authentication\ProviderInterface::class)
&& $container->has(Authorization\ProviderInterface::class)
) {
$this->setAuthProviders(
$container->get(Authentication\ProviderInterface::class),
$container->get(Authorization\ProviderInterface::class)
);
}
}
// Auto-detect error handler
if (!$this->error_handler && $container->has(HandlerInterface::class)) {
$this->setErrorHandler($container->get(HandlerInterface::class));
}
return $this;
}
/**
* Provide a default error handler. This will be used in the event that an
* endpoint does not define its own handler.
*
* @param HandlerInterface $handler
* @return self
*/
public function setErrorHandler(HandlerInterface $handler): self
{
$this->error_handler = $handler;
return $this;
}
/**
* Inject the request
*
* @param RequestInterface $request The request
* @return self
*/
public function setRequest(RequestInterface $request): self
{
if (!$request instanceof ServerRequestInterface) {
trigger_error(
sprintf(
'Providing %s is deprecated. Use %s instead',
RequestInterface::class,
ServerRequestInterface::class
),
E_USER_DEPRECATED
);
$request = $this->transformRequestToServerRequest($request);
}
$this->request = $request;
return $this;
}
private function transformRequestToServerRequest(RequestInterface $request): ServerRequestInterface
{
$serverRequest = ServerRequestFactory::fromGlobals()
->withUri($request->getUri())
->withMethod($request->getMethod())
->withBody($request->getBody());
foreach ($request->getHeaders() as $name => $values) {
$serverRequest = $serverRequest->withHeader($name, $values);
}
// ZD2 hints the return type of withHeader to MessageInterface not SRI
assert($serverRequest instanceof ServerRequestInterface);
return $serverRequest;
}
/**
* Set the parser list. Can be an array consumable by ClassMapper or
* a string representing a file parsable by same. The list must map
* MIME-types to Firehed\Input\ParserInterface class names.
*
* @param array|string $parser_list The parser list or its path
* @return self
*/
public function setParserList($parser_list): self
{
$this->parser_list = $parser_list;
return $this;
}
/**
* Set the endpoint list. Can be an array consumable by ClassMapper or
* a string representing a file parsable by same. The list must be
* filterable by HTTP method and map absolute URI path components to
* controller methods.
*
* @param array|string $endpoint_list The endpoint list or its path
* @return self
*/
public function setEndpointList($endpoint_list): self
{
$this->endpoint_list = $endpoint_list;
return $this;
}
/**
* PSR-15 Entrypoint
*
* This method is intended for internal use only, and should not be called
* outside of the context of a Middleware's RequestHandler parameter
*/
public function handle(ServerRequestInterface $request): ResponseInterface
{
if (!count($this->psrMiddleware)) {
return $this->doDispatch($request);
}
// Run MW as a queue
$mw = array_shift($this->psrMiddleware);
return $mw->process($request, $this);
}
/**
* Execute the request
*
* @throws TypeError if both execute and handleException have bad return
* types
* @throws LogicException if the dispatcher is misconfigured
* @throws RuntimeException on 404-type errors
* @return ResponseInterface the completed HTTP response
*/
public function dispatch(): ResponseInterface
{
if (null === $this->request ||
null === $this->parser_list ||
null === $this->endpoint_list) {
throw new BadMethodCallException(
'Set the request, parser list, and endpoint list before '.
'calling dispatch()',
500
);
}
$request = $this->request;
// Delegate to PSR-15 middleware when possible
return $this->handle($request);
}
private function doDispatch(ServerRequestInterface $request)
{
/** @var ?EndpointInterface */
$endpoint = null;
try {
$endpoint = $this->getEndpoint($request);
if ($this->authenticationProvider
&& $endpoint instanceof Interfaces\AuthenticatedEndpointInterface
) {
$auth = $this->authenticationProvider->authenticate($request);
$endpoint->setAuthentication($auth);
$this->authorizationProvider->authorize($endpoint, $auth);
}
$endpoint->authenticate($request);
$safe_input = $this->parseInput($request)
->addData($this->getUriData())
->addData($this->getQueryStringData($request))
->validate($endpoint);
$response = $endpoint->execute($safe_input);
} catch (Throwable $e) {
try {
if ($endpoint instanceof HandlesOwnErrorsInterface) {
$response = $endpoint->handleException($e);
} else {
throw $e;
}
} catch (Throwable $e) {
// If an application-wide handler has been defined, use the
// response that it generates. If not, just rethrow the
// exception for the system default (if defined) to handle.
if ($this->error_handler) {
$response = $this->error_handler->handle($request, $e);
} else {
throw $e;
}
}
}
return $this->executeResponseMiddleware($response);
}
/**
* Executes any provided response middleware callbacks previously added
* with `addResponseMiddleware()`. This wraps itself in a callback so that
* each successive callback may be executed. Each middleware may
* short-circuit all remaining callbacks, but still must return
* a ResponseInterface object
*
* @param ResponseInterface $response the response so far
* @return ResponseInterface the response after any additional processing
*/
private function executeResponseMiddleware(
ResponseInterface $response
): ResponseInterface {
// Out of middlewares to run
if (!$this->response_middleware) {
return $response;
}
// Get the next in line and dispatch
$middleware = array_shift($this->response_middleware);
return $middleware($response, function (ResponseInterface $response) {
return $this->executeResponseMiddleware($response);
});
}
/**
* Parse the raw input body based on the content type
*
* @param RequestInterface $request
* @return ParsedInput the parsed input data
*/
private function parseInput(RequestInterface $request): ParsedInput
{
$data = [];
// Presence of Content-type header indicates PUT/POST; parse it. We
// don't use $_POST because additional content types are supported.
// Since PSR-7 doesn't specify parsing the body of most MIME-types,
// we'll hand off to our own set of parsers.
$header = $request->getHeader('Content-type');
if ($header) {
$directives = explode(';', $header[0]);
if (!count($directives)) {
throw new OutOfBoundsException('Invalid Content-type header', 415);
}
$mediaType = array_shift($directives);
// Future: trim and format directives; e.g. ' charset=utf-8' =>
// ['charset' => 'utf-8']
list($parser_class) = (new ClassMapper($this->parser_list))
->search($mediaType);
if (!$parser_class) {
throw new OutOfBoundsException('Unsupported Content-type', 415);
}
$parser = new $parser_class;
$data = $parser->parse((string)$request->getBody());
}
return new ParsedInput($data);
}
/**
* Find and instanciate the endpoint based on the request.
*
* @return Interfaces\EndpointInterface the routed endpoint
*/
private function getEndpoint(RequestInterface $request): Interfaces\EndpointInterface
{
list($class, $uri_data) = (new ClassMapper($this->endpoint_list))
->filter(strtoupper($request->getMethod()))
->search($request->getUri()->getPath());
if (!$class) {
throw new OutOfBoundsException('Endpoint not found', 404);
}
// Conceivably, we could use reflection to ensure the found class
// adheres to the interface; in practice, the built route is already
// doing the filtering so this should be redundant.
$this->setUriData(new ParsedInput($uri_data));
if ($this->container && $this->container->has($class)) {
return $this->container->get($class);
}
return new $class;
}
private function setUriData(ParsedInput $uri_data): self
{
$this->uri_data = $uri_data;
return $this;
}
private function getUriData(): ParsedInput
{
return $this->uri_data;
}
private function getQueryStringData(RequestInterface $request): ParsedInput
{
$uri = $request->getUri();
$query = $uri->getQuery();
$data = [];
parse_str($query, $data);
return new ParsedInput($data);
}
}