Skip to content

Commit 92779f7

Browse files
committed
feat: Adds error handler
1 parent fcdaf3e commit 92779f7

File tree

8 files changed

+820
-346
lines changed

8 files changed

+820
-346
lines changed

composer.lock

Lines changed: 452 additions & 331 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/DI/StaticParameterResolver.php

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
namespace Attributes\Wp\FastEndpoints\DI;
1212

1313
use Attributes\Wp\FastEndpoints\Endpoint;
14+
use Exception;
1415
use Invoker\ParameterResolver\ParameterResolver;
1516
use ReflectionFunctionAbstract;
1617
use ReflectionNamedType;
@@ -52,13 +53,22 @@ public function getParameters(
5253
switch ($typeName) {
5354
case WP_REST_Request::class:
5455
$resolvedParameters[$index] = $providedParameters['request'];
55-
break;
56+
57+
continue 2;
5658
case WP_REST_Response::class:
57-
$resolvedParameters[$index] = $providedParameters['response'];
58-
break;
59+
if (isset($providedParameters['response'])) {
60+
$resolvedParameters[$index] = $providedParameters['response'];
61+
}
62+
63+
continue 2;
5964
case Endpoint::class:
6065
$resolvedParameters[$index] = $providedParameters['endpoint'];
61-
break;
66+
67+
continue 2;
68+
}
69+
70+
if (isset($providedParameters['exception']) && (is_subclass_of($typeName, Exception::class) || $typeName == Exception::class)) {
71+
$resolvedParameters[$index] = $providedParameters['exception'];
6272
}
6373
}
6474

src/Endpoint.php

Lines changed: 77 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,13 @@ class Endpoint implements EndpointInterface
103103
*/
104104
protected array $onResponseHandlers = [];
105105

106+
/**
107+
* Set of functions used to handle exceptions
108+
*
109+
* @var array<string,callable>
110+
*/
111+
protected array $onExceptionHandlers = [];
112+
106113
/**
107114
* Dependency injection
108115
*/
@@ -156,6 +163,9 @@ public function register(string $namespace, string $restBase): bool
156163
if (! $args) {
157164
return false;
158165
}
166+
167+
$this->registerDefaultExceptionHandlers();
168+
159169
$route = $this->getRoute($restBase);
160170
$this->fullRoute = "/$namespace/$route";
161171
register_rest_route($namespace, $route, $args, $this->override);
@@ -260,6 +270,26 @@ public function permission(callable $permissionCb): self
260270
return $this;
261271
}
262272

273+
/**
274+
* Adds a handler for a given exception.
275+
*
276+
* Handlers will be resolved on the following order: 1) by same exact exception or 2) by a parent class
277+
*
278+
* @internal
279+
*
280+
* @param string $exceptionClass The exception to look-up for
281+
*/
282+
public function onException(string $exceptionClass, callable $handler, bool $override = false): self
283+
{
284+
if (isset($this->onExceptionHandlers[$exceptionClass]) && ! $override) {
285+
return $this;
286+
}
287+
288+
$this->onExceptionHandlers[$exceptionClass] = $handler;
289+
290+
return $this;
291+
}
292+
263293
/**
264294
* WordPress function callback to handle this endpoint
265295
*
@@ -371,18 +401,33 @@ protected function replaceSpecialValue(WP_REST_Request $request, string $value):
371401
*
372402
* @throws InvocationException
373403
*/
374-
protected function runHandlers(array $allHandlers, array $dependencies, bool $isToReturnResult = false, bool $isPermissionCallback = false): mixed
404+
protected function runHandlers(array $allHandlers, array $dependencies, bool $isToReturnResult = false, bool $isPermissionCallback = false, array $handledExceptionHandlers = []): mixed
375405
{
376406
$result = null;
377407
foreach ($allHandlers as $handler) {
378408
try {
379409
$result = $this->getInvoker()->call($handler, $dependencies);
380-
} catch (ValidationException $e) {
381-
$result = new WpError(422, $e->getMessage(), ['errors' => $e->getErrors()]);
382-
$result = apply_filters('fastendpoints_request_error', $result, $e, $this);
383410
} catch (Exception $e) {
384-
$result = new WpError(500, $e->getMessage());
411+
$result = null;
412+
[$exceptionClass, $exceptionHandler] = $this->getExceptionHandler($e);
413+
if ($exceptionHandler) {
414+
if (isset($handledExceptionHandlers[$exceptionClass])) {
415+
return new WpError(500, "Bad exception handler for: $exceptionClass");
416+
}
417+
418+
$dependencies['exception'] = $e;
419+
$handledExceptionHandlers[$exceptionClass] = true;
420+
$result = $this->runHandlers([$exceptionHandler], $dependencies, true, false, $handledExceptionHandlers);
421+
}
422+
385423
$result = apply_filters('fastendpoints_request_error', $result, $e, $this);
424+
if (! is_wp_error($result) && ! ($result instanceof WP_REST_Response)) {
425+
$result = null;
426+
427+
continue;
428+
}
429+
430+
return $result;
386431
}
387432

388433
if (is_wp_error($result) || $result instanceof WP_REST_Response) {
@@ -397,6 +442,33 @@ protected function runHandlers(array $allHandlers, array $dependencies, bool $is
397442
return $isToReturnResult ? $result : null;
398443
}
399444

445+
/**
446+
* Retrieves the exception handler for a given exception, if exists.
447+
*
448+
* @param Exception $exception The exception to be handled.
449+
* @return ?callable Returns a callable to handle that exception or null if not found
450+
*/
451+
protected function getExceptionHandler(Exception $exception): array
452+
{
453+
if (isset($this->onExceptionHandlers[$exception::class])) {
454+
return [$exception::class, $this->onExceptionHandlers[$exception::class]];
455+
}
456+
457+
foreach ($this->onExceptionHandlers as $exceptionClass => $handler) {
458+
if (is_subclass_of($exception, $exceptionClass)) {
459+
return [$exceptionClass, $handler];
460+
}
461+
}
462+
463+
return [null, null];
464+
}
465+
466+
protected function registerDefaultExceptionHandlers(): void
467+
{
468+
$this->onException(ValidationException::class, fn (ValidationException $e) => new WpError(422, $e->getMessage(), ['errors' => $e->getErrors()]));
469+
$this->onException(Exception::class, fn (Exception $e) => new WpError(500, $e->getMessage()));
470+
}
471+
400472
/**
401473
* @internal
402474
*/

src/Router.php

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,13 @@ class Router implements RouterContract
8787
*/
8888
protected array $injectables = [];
8989

90+
/**
91+
* Set of functions used to handle exceptions
92+
*
93+
* @var array<string,callable>
94+
*/
95+
protected array $onExceptionHandlers = [];
96+
9097
/**
9198
* Creates a new Router instance
9299
*
@@ -220,6 +227,10 @@ public function register(): void
220227
$router->inject($name, $callable);
221228
}
222229

230+
foreach ($this->onExceptionHandlers as $exceptionClass => $handler) {
231+
$router->onException($exceptionClass, $handler);
232+
}
233+
223234
$router->register();
224235
}
225236

@@ -229,15 +240,43 @@ public function register(): void
229240
}
230241

231242
/**
232-
* Specifies a set of plugins that are needed by the endpoint
243+
* Adds a dependency which can then be injected in endpoints, middlewares or permission handlers.
244+
*
245+
* This should be useful to share common dependencies across multiple handlers e.g. database connection.
246+
* The dependency will be instantiated once, only!
247+
*
248+
*
249+
* @param string $name The dependency name.
250+
* @param callable $handler The handler which resolves the dependency.
251+
* @param bool $override If set, overrides any existent dependency. Default value: false.
233252
*/
234-
public function inject(string $name, callable $callable, bool $override = false): self
253+
public function inject(string $name, callable $handler, bool $override = false): self
235254
{
236255
if (isset($this->injectables[$name]) && ! $override) {
237256
return $this;
238257
}
239258

240-
$this->injectables[$name] = $callable;
259+
$this->injectables[$name] = $handler;
260+
261+
return $this;
262+
}
263+
264+
/**
265+
* Adds a handler for a given exception.
266+
*
267+
* Handlers will be resolved on the following order: 1) by same exact exception or 2) by a parent class
268+
*
269+
* @param string $exceptionClass The exception class to add a handler.
270+
* @param callable $handler The handler to resolve those types of exceptions.
271+
* @param bool $override If set, overrides any existent handlers. Default value: false.
272+
*/
273+
public function onException(string $exceptionClass, callable $handler, bool $override = false): self
274+
{
275+
if (isset($this->onExceptionHandlers[$exceptionClass]) && ! $override) {
276+
return $this;
277+
}
278+
279+
$this->onExceptionHandlers[$exceptionClass] = $handler;
241280

242281
return $this;
243282
}
@@ -256,6 +295,10 @@ public function registerEndpoints(): void
256295
$e->depends($this->plugins);
257296
}
258297

298+
foreach ($this->onExceptionHandlers as $exceptionClass => $handler) {
299+
$e->onException($exceptionClass, $handler);
300+
}
301+
259302
$e->register($namespace, $restBase);
260303
$e->getInvoker()->setInjectables($this->injectables);
261304
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
<?php
2+
3+
/**
4+
* Holds tests for testing custom exception handling
5+
*
6+
* @license MIT
7+
*/
8+
9+
declare(strict_types=1);
10+
11+
namespace Attributes\Wp\FastEndpoints\Tests\Integration;
12+
13+
use Attributes\Wp\FastEndpoints\Tests\Helpers\Helpers;
14+
use WP_REST_Request;
15+
use WP_REST_Server;
16+
use Yoast\WPTestUtils\WPIntegration\TestCase;
17+
18+
if (! Helpers::isIntegrationTest()) {
19+
return;
20+
}
21+
22+
/*
23+
* We need to provide the base test class to every integration test.
24+
* This will enable us to use all the WordPress test goodies, such as
25+
* factories and proper test cleanup.
26+
*/
27+
uses(TestCase::class);
28+
29+
beforeEach(function () {
30+
parent::setUp();
31+
32+
// Set up a REST server instance.
33+
global $wp_rest_server;
34+
35+
$this->server = $wp_rest_server = new WP_REST_Server;
36+
$router = Helpers::getRouter('ErrorHandlerRouter.php');
37+
$router->register();
38+
do_action('rest_api_init', $this->server);
39+
});
40+
41+
afterEach(function () {
42+
global $wp_rest_server;
43+
$wp_rest_server = null;
44+
45+
parent::tearDown();
46+
});
47+
48+
// Common
49+
50+
test('Ignores all errors', function (string $endpoint) {
51+
$request = new WP_REST_Request('POST', $endpoint);
52+
$request->set_header('content-type', 'application/json');
53+
54+
$response = $this->server->dispatch($request);
55+
expect($response->get_status())->toBe(200);
56+
$data = $response->get_data();
57+
expect($data)->toBeNull();
58+
})
59+
->with(['/error-handler/v1/handler/ignore-all-errors', '/error-handler/v1/permission/ignore-all-errors'])
60+
->group('error-handler');
61+
62+
// Handler
63+
64+
test('Handle handler exceptions', function (string $endpoint) {
65+
$request = new WP_REST_Request('POST', $endpoint);
66+
$request->set_header('content-type', 'application/json');
67+
68+
$response = $this->server->dispatch($request);
69+
expect($response->get_status())->toBe(570);
70+
$data = $response->get_data();
71+
expect($data)
72+
->toBeArray()
73+
->toBe([
74+
'code' => 570,
75+
'message' => 'Exception handler',
76+
'data' => ['status' => 570],
77+
]);
78+
})
79+
->with(['/error-handler/v1/handler', '/error-handler/v1/handler/custom-exception'])
80+
->group('error-handler', 'handler');
81+
82+
// Permission callback
83+
84+
test('Handle permission handler exceptions', function (string $endpoint) {
85+
$request = new WP_REST_Request('POST', $endpoint);
86+
$request->set_header('content-type', 'application/json');
87+
88+
$response = $this->server->dispatch($request);
89+
expect($response->get_status())->toBe(570);
90+
$data = $response->get_data();
91+
expect($data)
92+
->toBeArray()
93+
->toBe([
94+
'code' => 570,
95+
'message' => 'Exception handler',
96+
'data' => ['status' => 570],
97+
]);
98+
})
99+
->with(['/error-handler/v1/permission', '/error-handler/v1/permission/custom-exception'])
100+
->group('error-handler', 'permission');
101+
102+
// Exception handler
103+
104+
test('Handle exceptions in exception handler', function () {
105+
$request = new WP_REST_Request('POST', '/error-handler/v1/exception-handler');
106+
$request->set_header('content-type', 'application/json');
107+
108+
$response = $this->server->dispatch($request);
109+
expect($response->get_status())->toBe(500);
110+
$data = $response->get_data();
111+
expect($data)
112+
->toBeArray()
113+
->toBe([
114+
'code' => 500,
115+
'message' => 'Bad exception handler for: Exception',
116+
'data' => ['status' => 500],
117+
]);
118+
})
119+
->group('error-handler', 'exception-handler');
120+
121+
test('Handle different exceptions in exception handler', function () {
122+
$request = new WP_REST_Request('POST', '/error-handler/v1/exception-handler/missing-field');
123+
$request->set_header('content-type', 'application/json');
124+
125+
$response = $this->server->dispatch($request);
126+
expect($response->get_status())->toBe(422);
127+
$data = $response->get_data();
128+
expect($data)
129+
->toBeArray()
130+
->toBe([
131+
'code' => 422,
132+
'message' => 'Invalid data',
133+
'data' => [
134+
'status' => 422,
135+
'errors' => [[
136+
'field' => 'missingField',
137+
'reason' => 'Missing required argument \'missingField\'',
138+
]],
139+
],
140+
]);
141+
})
142+
->group('error-handler', 'exception-handler');

0 commit comments

Comments
 (0)