diff --git a/framework/core/src/Foundation/ErrorHandling/ExceptionHandler/IlluminateValidationExceptionHandler.php b/framework/core/src/Foundation/ErrorHandling/ExceptionHandler/IlluminateValidationExceptionHandler.php new file mode 100644 index 000000000..922a8cdd4 --- /dev/null +++ b/framework/core/src/Foundation/ErrorHandling/ExceptionHandler/IlluminateValidationExceptionHandler.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Foundation\ErrorHandling\ExceptionHandler; + +use Flarum\Foundation\ErrorHandling\HandledError; +use Illuminate\Validation\ValidationException; + +class IlluminateValidationExceptionHandler +{ + public function handle(ValidationException $e): HandledError + { + return (new HandledError( + $e, 'validation_error', 422 + ))->withDetails($this->errorDetails($e)); + } + + protected function errorDetails(ValidationException $e): array + { + $errors = $e->errors(); + + return array_map(function ($field, $messages) { + return [ + 'detail' => implode("\n", $messages), + 'source' => ['pointer' => "/data/attributes/$field"] + ]; + }, array_keys($errors), $errors); + } +} diff --git a/framework/core/src/Foundation/ErrorHandling/ExceptionHandler/ValidationExceptionHandler.php b/framework/core/src/Foundation/ErrorHandling/ExceptionHandler/ValidationExceptionHandler.php new file mode 100644 index 000000000..224143766 --- /dev/null +++ b/framework/core/src/Foundation/ErrorHandling/ExceptionHandler/ValidationExceptionHandler.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Foundation\ErrorHandling\ExceptionHandler; + +use Flarum\Foundation\ErrorHandling\HandledError; +use Flarum\Foundation\ValidationException; + +class ValidationExceptionHandler +{ + public function handle(ValidationException $e) + { + return (new HandledError( + $e, 'validation_error', 422 + ))->withDetails(array_merge( + $this->buildDetails($e->getAttributes(), '/data/attributes'), + $this->buildDetails($e->getRelationships(), '/data/relationships') + )); + } + + private function buildDetails(array $messages, $pointer): array + { + return array_map(function ($path, $detail) use ($pointer) { + return [ + 'detail' => $detail, + 'source' => ['pointer' => $pointer.'/'.$path] + ]; + }, array_keys($messages), $messages); + } +} diff --git a/framework/core/src/Foundation/ErrorServiceProvider.php b/framework/core/src/Foundation/ErrorServiceProvider.php new file mode 100644 index 000000000..2a1deba21 --- /dev/null +++ b/framework/core/src/Foundation/ErrorServiceProvider.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Foundation; + +use Flarum\Foundation\ErrorHandling\ExceptionHandler; +use Flarum\Foundation\ErrorHandling\LogReporter; +use Flarum\Foundation\ErrorHandling\Registry; +use Flarum\Foundation\ErrorHandling\Reporter; +use Illuminate\Database\Eloquent\ModelNotFoundException; +use Illuminate\Validation\ValidationException as IlluminateValidationException; +use Tobscure\JsonApi\Exception\InvalidParameterException; + +class ErrorServiceProvider extends AbstractServiceProvider +{ + public function register() + { + $this->app->singleton('flarum.error.statuses', function () { + return [ + // 400 Bad Request + 'csrf_token_mismatch' => 400, + 'invalid_parameter' => 400, + + // 401 Unauthorized + 'invalid_access_token' => 401, + + // 403 Forbidden + 'forbidden' => 403, + 'invalid_confirmation_token' => 403, + 'permission_denied' => 403, + + // 404 Not Found + 'model_not_found' => 404, + 'route_not_found' => 404, + + // 405 Method Not Allowed + 'method_not_allowed' => 405, + + // 429 Too Many Requests + 'too_many_requests' => 429, + ]; + }); + + $this->app->singleton('flarum.error.classes', function () { + return [ + InvalidParameterException::class => 'invalid_parameter', + ModelNotFoundException::class => 'model_not_found', + ]; + }); + + $this->app->singleton('flarum.error.handlers', function () { + return [ + IlluminateValidationException::class => ExceptionHandler\IlluminateValidationExceptionHandler::class, + ValidationException::class => ExceptionHandler\ValidationExceptionHandler::class, + ]; + }); + + $this->app->singleton(Registry::class, function () { + return new Registry( + $this->app->make('flarum.error.statuses'), + $this->app->make('flarum.error.classes'), + $this->app->make('flarum.error.handlers') + ); + }); + + $this->app->singleton(Reporter::class, LogReporter::class); + } +} diff --git a/framework/core/src/Foundation/InstalledSite.php b/framework/core/src/Foundation/InstalledSite.php index 614cb1119..ffdfcc3d7 100644 --- a/framework/core/src/Foundation/InstalledSite.php +++ b/framework/core/src/Foundation/InstalledSite.php @@ -117,6 +117,7 @@ class InstalledSite implements SiteInterface $laravel->register(DatabaseServiceProvider::class); $laravel->register(DiscussionServiceProvider::class); $laravel->register(ExtensionServiceProvider::class); + $laravel->register(ErrorServiceProvider::class); $laravel->register(FilesystemServiceProvider::class); $laravel->register(FormatterServiceProvider::class); $laravel->register(ForumServiceProvider::class); diff --git a/framework/core/src/Foundation/UninstalledSite.php b/framework/core/src/Foundation/UninstalledSite.php index d363959b2..92aca5142 100644 --- a/framework/core/src/Foundation/UninstalledSite.php +++ b/framework/core/src/Foundation/UninstalledSite.php @@ -69,6 +69,7 @@ class UninstalledSite implements SiteInterface $this->registerLogger($laravel); + $laravel->register(ErrorServiceProvider::class); $laravel->register(LocaleServiceProvider::class); $laravel->register(FilesystemServiceProvider::class); $laravel->register(SessionServiceProvider::class); diff --git a/framework/core/tests/unit/Foundation/ErrorHandling/ExceptionHandler/IlluminateValidationExceptionHandlerTest.php b/framework/core/tests/unit/Foundation/ErrorHandling/ExceptionHandler/IlluminateValidationExceptionHandlerTest.php new file mode 100644 index 000000000..028fa5e33 --- /dev/null +++ b/framework/core/tests/unit/Foundation/ErrorHandling/ExceptionHandler/IlluminateValidationExceptionHandlerTest.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Tests\unit\Foundation\ErrorHandling\ExceptionHandler; + +use Flarum\Foundation\ErrorHandling\ExceptionHandler\IlluminateValidationExceptionHandler; +use Illuminate\Translation\ArrayLoader; +use Illuminate\Translation\Translator; +use Illuminate\Validation\Factory; +use Illuminate\Validation\ValidationException; +use PHPUnit\Framework\TestCase; + +class IlluminateValidationExceptionHandlerTest extends TestCase +{ + private $handler; + + public function setUp() + { + $this->handler = new IlluminateValidationExceptionHandler; + } + + public function test_it_creates_the_desired_output() + { + $exception = new ValidationException($this->makeValidator(['foo' => ''], ['foo' => 'required'])); + + $error = $this->handler->handle($exception); + + $this->assertEquals(422, $error->getStatusCode()); + $this->assertEquals('validation_error', $error->getType()); + $this->assertEquals([ + [ + 'detail' => 'validation.required', + 'source' => ['pointer' => '/data/attributes/foo'] + ] + ], $error->getDetails()); + } + + private function makeValidator($data = [], $rules = []) + { + $translator = new Translator(new ArrayLoader(), 'en'); + $factory = new Factory($translator); + + return $factory->make($data, $rules); + } +} diff --git a/framework/core/tests/unit/Foundation/ErrorHandling/ExceptionHandler/ValidationExceptionHandlerTest.php b/framework/core/tests/unit/Foundation/ErrorHandling/ExceptionHandler/ValidationExceptionHandlerTest.php new file mode 100644 index 000000000..32062249d --- /dev/null +++ b/framework/core/tests/unit/Foundation/ErrorHandling/ExceptionHandler/ValidationExceptionHandlerTest.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Tests\unit\Foundation\ErrorHandling\ExceptionHandler; + +use Flarum\Foundation\ErrorHandling\ExceptionHandler\ValidationExceptionHandler; +use Flarum\Foundation\ValidationException; +use PHPUnit\Framework\TestCase; + +class ValidationExceptionHandlerTest extends TestCase +{ + private $handler; + + public function setUp() + { + $this->handler = new ValidationExceptionHandler; + } + + public function test_managing_exceptions() + { + $error = $this->handler->handle(new ValidationException( + ['foo' => 'Attribute error'], + ['bar' => 'Relationship error'] + )); + + $this->assertEquals(422, $error->getStatusCode()); + $this->assertEquals('validation_error', $error->getType()); + $this->assertEquals([ + [ + 'detail' => 'Attribute error', + 'source' => ['pointer' => '/data/attributes/foo'] + ], + [ + 'detail' => 'Relationship error', + 'source' => ['pointer' => '/data/relationships/bar'] + ] + ], $error->getDetails()); + } +}