Skip to content

Commit

Permalink
Merge pull request #14 from dimitriBouteille/develop
Browse files Browse the repository at this point in the history
Release 1.0.1
  • Loading branch information
dimitriBouteille authored Mar 3, 2024
2 parents b5e5f1b + e513706 commit 982dc84
Show file tree
Hide file tree
Showing 13 changed files with 279 additions and 39 deletions.
2 changes: 2 additions & 0 deletions doc/error-handling.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ If you want to return a 500 error, just use `\Exception`. By default, 500 error
}
```

> 💡 You can enable [debug](../doc/options.md#debug) mode to return the exception in the response.
### Custom error

You can return a custom error using the exception `\Dbout\WpRestApi\Exceptions\RouteException` :
Expand Down
26 changes: 24 additions & 2 deletions doc/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ The [RouteLoader](../src/RouteLoader.php) takes 2 arguments:
- A [RouteLoaderOptions](../src/RouteLoaderOptions.php) object that contains options

```php
<?php

use Dbout\WpRestApi\RouteLoader;
use Dbout\WpRestApi\RouteLoaderOptions;

Expand Down Expand Up @@ -55,4 +53,28 @@ $options = new RouteLoaderOptions(
cache: $cache,
cacheKey: 'my_cache_key'
);
```

## Debug

In development mode, it may be interesting to enable debug mode so that errors (500) are visible. When mode is enabled, the full exception is returned in the response.

```php
use Dbout\WpRestApi\RouteLoaderOptions;

$options = new RouteLoaderOptions(
debug: true,
);
```

```json
{
"error": {
"code": "fatal-error",
"message": "The exception message.",
"data": {
"exception": "The exception full trace"
}
}
}
```
37 changes: 19 additions & 18 deletions rector.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php
/**
* Copyright (c) 2024 Dimitri BOUTEILLE (https://github.com/dimitriBouteille)
* Copyright (c) Dimitri BOUTEILLE (https://github.com/dimitriBouteille)
* See LICENSE.txt for license details.
*
* Author: Dimitri BOUTEILLE <[email protected]>
Expand All @@ -13,23 +13,24 @@
use Rector\Set\ValueObject\SetList;
use Rector\TypeDeclaration\Rector\Property\TypedPropertyFromStrictConstructorRector;

return static function (RectorConfig $rectorConfig): void {
$rectorConfig->paths([
return RectorConfig::configure()
->withPaths([
__DIR__ . '/src',
]);

// register single rule
$rectorConfig->rule(TypedPropertyFromStrictConstructorRector::class);

$rectorConfig
->skip([
SimplifyBoolIdenticalTrueRector::class,
CallableThisArrayToAnonymousFunctionRector::class,
SimplifyIfReturnBoolRector::class,
]);

$rectorConfig->sets([
SetList::CODE_QUALITY,
__DIR__ . '/tests',
])
->withRules([
TypedPropertyFromStrictConstructorRector::class,
])
->withPreparedSets(
codeQuality: true,
typeDeclarations: true,
instanceOf: true,
)
->withSets([
SetList::PHP_81,
])
->withSkip([
SimplifyBoolIdenticalTrueRector::class,
CallableThisArrayToAnonymousFunctionRector::class,
SimplifyIfReturnBoolRector::class,
]);
};
7 changes: 5 additions & 2 deletions src/Exceptions/RouteException.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,19 @@ class RouteException extends \Exception
* @param string|null $errorCode
* @param int $httpStatusCode
* @param array<string, mixed> $additionalData
* @param \Throwable|null $previous
*/
public function __construct(
string $message,
protected ?string $errorCode = null,
public int $httpStatusCode = 400,
public array $additionalData = []
public array $additionalData = [],
?\Throwable $previous = null,
) {
parent::__construct(
$message,
$httpStatusCode
$httpStatusCode,
$previous,
);
}

Expand Down
4 changes: 2 additions & 2 deletions src/Loaders/AnnotationDirectoryLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,14 @@ public function load(mixed $resource): array
$files = iterator_to_array(new \RecursiveIteratorIterator(
new \RecursiveCallbackFilterIterator(
new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS),
function (\SplFileInfo $current) {
function (\SplFileInfo $current): bool {
return !str_starts_with($current->getBasename(), '.');
}
),
\RecursiveIteratorIterator::LEAVES_ONLY
));

usort($files, function (\SplFileInfo $a, \SplFileInfo $b) {
usort($files, function (\SplFileInfo $a, \SplFileInfo $b): int {
return (string) $a > (string) $b ? 1 : -1;
});

Expand Down
5 changes: 3 additions & 2 deletions src/RouteLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ protected function checkRoutes(array $routes): void
public function register(): void
{
$routes = $this->getRoutes();
add_action('rest_api_init', function () use ($routes) {
add_action('rest_api_init', function () use ($routes): void {
foreach ($routes as $route) {
register_rest_route(
$route->namespace,
Expand All @@ -141,10 +141,11 @@ public function register(): void
protected function buildRouteArgs(Route $route): array
{
$actions = [];
$isDebug = $this->options?->debug ?? false;
foreach ($route->actions as $action) {
$actions[] = [
'methods' => $action->methods,
'callback' => [new RestWrapper($action), 'execute'],
'callback' => [new RestWrapper($action, $isDebug), 'execute'],
'permission_callback' => [new PermissionWrapper($action), 'execute'],
'args' => [],
];
Expand Down
2 changes: 2 additions & 0 deletions src/RouteLoaderOptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ class RouteLoaderOptions
/**
* @param CacheItemPoolInterface|null $cache
* @param string $cacheKey
* @param bool $debug
*/
public function __construct(
public ?CacheItemPoolInterface $cache = null,
public string $cacheKey = self::DEFAULT_CACHE_KEY,
public bool $debug = false,
) {
}
}
50 changes: 37 additions & 13 deletions src/Wrappers/RestWrapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ class RestWrapper

/**
* @param RouteAction $action
* @param bool $debug
*/
public function __construct(
protected RouteAction $action,
protected bool $debug = false,
) {
}

Expand All @@ -36,18 +38,7 @@ public function execute(\WP_REST_Request $request): \WP_REST_Response
$dependencies = $this->collectDependencies($method, $request);
$response = $method->invoke($classRef->newInstance(), ...$dependencies);
} catch (\Exception $exception) {
if (!$exception instanceof RouteException) {
$exception = new RouteException(
message: 'Something went wrong. Please try again.',
errorCode: 'fatal-error',
httpStatusCode: self::DEFAULT_EXCEPTION_HTTP_CODE
);
}

return $this->parseErrorToRestResponse(
$this->buildResponseError($exception),
$exception->getHttpStatusCode()
);
return $this->onError($exception);
}

if (is_wp_error($response)) {
Expand All @@ -60,6 +51,39 @@ public function execute(\WP_REST_Request $request): \WP_REST_Response
return $response;
}

/**
* @param \Exception $exception
* @return \WP_REST_Response
*/
protected function onError(\Exception $exception): \WP_REST_Response
{
$rootException = $exception;
if (!$exception instanceof RouteException) {
$exception = new RouteException(
message: 'Something went wrong. Please try again.',
errorCode: 'fatal-error',
httpStatusCode: self::DEFAULT_EXCEPTION_HTTP_CODE,
);
}

if ($this->debug === true) {
$exception = new RouteException(
message: $rootException->getMessage(),
errorCode: $exception->getErrorCode(),
httpStatusCode: $exception->getHttpStatusCode(),
additionalData: [
'exception' => $rootException->getTraceAsString(),
],
previous: $rootException,
);
}

return $this->parseErrorToRestResponse(
$this->buildResponseError($exception),
$exception->getHttpStatusCode()
);
}

/**
* @param \ReflectionMethod $method
* @param \WP_REST_Request $request
Expand Down Expand Up @@ -114,7 +138,7 @@ protected function castRequestArgument(\ReflectionNamedType $type, mixed $value)
* @return \WP_Error
*/
protected function buildResponseError(
RouteException $exception
RouteException $exception,
): \WP_Error {
return new \WP_Error(
$exception->getErrorCode() ?? self::DEFAULT_EXCEPTION_CODE,
Expand Down
120 changes: 120 additions & 0 deletions tests/Wrappers/RestWrapperTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<?php
/**
* Copyright (c) Dimitri BOUTEILLE (https://github.com/dimitriBouteille)
* See LICENSE.txt for license details.
*
* Author: Dimitri BOUTEILLE <[email protected]>
*/

namespace Dbout\WpRestApi\Tests\Wrappers;

use Dbout\WpRestApi\RouteAction;
use Dbout\WpRestApi\Tests\fixtures\RouteWithException;
use Dbout\WpRestApi\Tests\fixtures\RouteWithNotFoundException;
use Dbout\WpRestApi\Tests\fixtures\RouteWithRouteException;
use Dbout\WpRestApi\Wrappers\RestWrapper;
use PHPUnit\Framework\TestCase;

/**
* @coversDefaultClass \Dbout\WpRestApi\Wrappers\RestWrapper
*/
class RestWrapperTest extends TestCase
{
/**
* @param string $className
* @param bool $debug
* @param string $expectedMessage
* @param int $expectedHttpCode
* @return void
* @dataProvider providerActionThrowException
* @covers ::execute
* @covers ::onError
*/
public function testActionThrowException(
string $className,
bool $debug,
string $expectedMessage,
int $expectedHttpCode
): void {
$action = new RouteAction($className, 'execute', ['GET'], null);
$wrapper = new RestWrapper($action, $debug);

$response = $wrapper->execute(new \WP_REST_Request());
$error = $response->get_data()['error'] ?? null;
$this->exceptionAsserts($response, $expectedMessage, $expectedHttpCode);
if ($debug === true) {
$data = $error['data'] ?? [];
$this->assertArrayHasKey('exception', $data, 'Key error.data.exception not found.');
}
}

/**
* @return \Generator
*/
public static function providerActionThrowException(): \Generator
{
yield 'With \Exception and debug mode' => [
RouteWithException::class,
true,
'My custom exception.',
500,
];

yield 'With \Exception and without debug mode' => [
RouteWithException::class,
false,
'Something went wrong. Please try again.',
500,
];

yield 'With RouteException and debug mode' => [
RouteWithRouteException::class,
true,
'My route exception.',
400,
];

yield 'With RouteException and without debug mode' => [
RouteWithRouteException::class,
false,
'My route exception.',
400,
];
}

/**
* @return void
* @covers ::execute
*/
public function testNotFoundException(): void
{
$action = new RouteAction(
RouteWithNotFoundException::class,
'execute',
['GET'],
null
);

$wrapper = new RestWrapper($action);

$response = $wrapper->execute(new \WP_REST_Request());
$this->exceptionAsserts($response, 'Object not found.', 404);
}

/**
* @param \WP_REST_Response $response
* @param string $expectedMessage
* @param int $expectedHttpCode
* @return void
*/
protected function exceptionAsserts(
\WP_REST_Response $response,
string $expectedMessage,
int $expectedHttpCode
): void {
$error = $response->get_data()['error'] ?? null;
$this->assertInstanceOf(\WP_REST_Response::class, $response);
$this->assertEquals($expectedHttpCode, $response->get_status());
$this->assertEquals($expectedMessage, $error['message'] ?? null);
}
}
4 changes: 4 additions & 0 deletions tests/bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,19 @@
* Author: Dimitri BOUTEILLE <[email protected]>
*/

define( 'WPINC', 'wp-includes' );
define('ABSPATH', __DIR__ . '/../web/wordpress/');
$includeDirectory = __DIR__ . '/../web/wordpress/wp-includes';

$paths = [
'/functions.php',
'/plugin.php',
'/class-wp-http-response.php',
'/rest-api.php',
'/rest-api/class-wp-rest-server.php',
'/rest-api/class-wp-rest-response.php',
'/rest-api/class-wp-rest-request.php',
'/class-wp-error.php'
];

foreach ($paths as $path) {
Expand Down
Loading

0 comments on commit 982dc84

Please sign in to comment.