diff --git a/README.md b/README.md index a8ef248..3d48263 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ Original Slim's error handling is bypassed by the HTTP exception manager forcing Non HttpExceptions (for example an exception thrown by a third party library) will be automatically transformed into a 500 HttpException and alternatively you can use `HttpExceptionFactory` to throw HTTP exceptions yourself. Those exceptions will be handled to their corresponding handler depending on their "status code" or by the default handler in case no handler is defined for the specific status code -In the above example if you `throw HttpExceptionFactory::unauthorized()` during the execution of the application it'll be captured by the manager had handed over to "YourUnauthorizedRequestHandler" so you can format and return a proper custom response +In the above example if you `throw HttpExceptionFactory::unauthorized()` during the execution of the application it'll be captured by the manager hand handed over to "YourUnauthorizedRequestHandler" so you can format and return a proper custom response ### HTTP exceptions @@ -73,9 +73,9 @@ The base of this error handling are the HTTP exceptions. This exceptions carry a ```php use Jgut\Slim\Exception\HttpException; -$exceptionCode = 101; // Internal code +$exceptionCode = 1001; // Internal code $httpStatusCode = 401; // Unauthorized -$exception = new HttpException('You shall not pass!', $exceptionCode, $httpStatusCode); +$exception = new HttpException('You shall not pass!', 'You do not have permission', $exceptionCode, $httpStatusCode); $exception->getHttpStatusCode(); // 401 Unauthorized ``` @@ -91,9 +91,9 @@ $exception->getIdentifier(); In order to simplify HTTP exception creation and assure correct HTTP status code selection there are several shortcut creation methods ```php -throw HttpExceptionFactory::unauthorized('You shall not pass!', 101); -throw HttpExceptionFactory::notAcceptable('Throughput reached', 102); -throw HttpExceptionFactory::unprocessableEntity('Already exists', 103); +throw HttpExceptionFactory::unauthorized('You shall not pass!', 'You do not have permission', 1001); +throw HttpExceptionFactory::notAcceptable('Throughput reached', 'Too much', 1002); +throw HttpExceptionFactory::unprocessableEntity('Already exists', 'Entity already exists', 1030); ``` ### Handlers diff --git a/phpstan.neon b/phpstan.neon index 447e8bc..2b7e4e5 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,3 +1,4 @@ parameters: ignoreErrors: - '/Call to an undefined method Throwable::getIdentifier\(\)/' + - '/Call to an undefined method Throwable::getDescription\(\)/' diff --git a/src/Handler/AbstractHttpExceptionHandler.php b/src/Handler/AbstractHttpExceptionHandler.php index 47c0f07..a001998 100644 --- a/src/Handler/AbstractHttpExceptionHandler.php +++ b/src/Handler/AbstractHttpExceptionHandler.php @@ -73,20 +73,7 @@ function (string $contentType) { * * @return array */ - protected function getContentTypes(): array - { - return [ - 'text/plain', - 'text/json', - 'application/json', - 'application/x-json', - 'text/xml', - 'application/xml', - 'application/x-xml', - 'text/html', - 'application/xhtml+xml', - ]; - } + abstract protected function getContentTypes(): array; /** * Get error content. diff --git a/src/Handler/ExceptionHandler.php b/src/Handler/ExceptionHandler.php index c380679..8280162 100644 --- a/src/Handler/ExceptionHandler.php +++ b/src/Handler/ExceptionHandler.php @@ -21,6 +21,24 @@ */ class ExceptionHandler extends AbstractHttpExceptionHandler { + /** + * {@inheritdoc} + */ + protected function getContentTypes(): array + { + return [ + 'text/plain', + 'text/json', + 'application/json', + 'application/x-json', + 'text/xml', + 'application/xml', + 'application/x-xml', + 'text/html', + 'application/xhtml+xml', + ]; + } + /** * {@inheritdoc} */ @@ -45,6 +63,18 @@ protected function getExceptionOutput( return $this->getTextError($exception); } + /** + * Get simple text formatted error. + * + * @param HttpException $exception + * + * @return string + */ + protected function getTextError(HttpException $exception): string + { + return sprintf('(%s) Application error', $exception->getIdentifier()); + } + /** * Get simple JSON formatted error. * @@ -95,16 +125,4 @@ protected function getHtmlError(HttpException $exception): string $exception->getIdentifier() ); } - - /** - * Get simple text formatted error. - * - * @param HttpException $exception - * - * @return string - */ - protected function getTextError(HttpException $exception): string - { - return sprintf('(%s) Application error', $exception->getIdentifier()); - } } diff --git a/src/Handler/MethodNotAllowedHandler.php b/src/Handler/MethodNotAllowedHandler.php index a1219b1..eb789a3 100644 --- a/src/Handler/MethodNotAllowedHandler.php +++ b/src/Handler/MethodNotAllowedHandler.php @@ -14,35 +14,22 @@ namespace Jgut\Slim\Exception\Handler; use Jgut\Slim\Exception\HttpException; -use Psr\Http\Message\ServerRequestInterface; /** * Method not allowed error handler. */ -class MethodNotAllowedHandler extends AbstractHttpExceptionHandler +class MethodNotAllowedHandler extends ExceptionHandler { /** - * {@inheritdoc} + * Get simple text formatted error. + * + * @param HttpException $exception + * + * @return string */ - protected function getExceptionOutput( - string $contentType, - HttpException $exception, - ServerRequestInterface $request - ): string { - if (in_array($contentType, ['text/json', 'application/json', 'application/x-json'], true)) { - return $this->getJsonError($exception); - } - - if (in_array($contentType, ['text/xml', 'application/xml', 'application/x-xml'], true)) { - return $this->getXmlError($exception); - } - - if (in_array($contentType, ['text/html', 'application/xhtml+xml'], true)) { - return $this->getHtmlError($exception); - } - - // text/plain - return $this->getTextError($exception); + protected function getTextError(HttpException $exception): string + { + return sprintf('(%s) %s', $exception->getIdentifier(), $exception->getMessage()); } /** @@ -92,20 +79,9 @@ protected function getHtmlError(HttpException $exception): string 'Method not allowed

Method not allowed (Ref. %s)

', - $exception->getIdentifier() + '}

Method not allowed (Ref. %s)

%s

', + $exception->getIdentifier(), + $exception->getMessage() ); } - - /** - * Get simple text formatted error. - * - * @param HttpException $exception - * - * @return string - */ - protected function getTextError(HttpException $exception): string - { - return sprintf('(%s) %s', $exception->getIdentifier(), $exception->getMessage()); - } } diff --git a/src/Handler/NotFoundHandler.php b/src/Handler/NotFoundHandler.php index 7930de3..4475d79 100644 --- a/src/Handler/NotFoundHandler.php +++ b/src/Handler/NotFoundHandler.php @@ -14,35 +14,22 @@ namespace Jgut\Slim\Exception\Handler; use Jgut\Slim\Exception\HttpException; -use Psr\Http\Message\ServerRequestInterface; /** * Route not found error handler. */ -class NotFoundHandler extends AbstractHttpExceptionHandler +class NotFoundHandler extends ExceptionHandler { /** - * {@inheritdoc} + * Get simple text formatted error. + * + * @param HttpException $exception + * + * @return string */ - protected function getExceptionOutput( - string $contentType, - HttpException $exception, - ServerRequestInterface $request - ): string { - if (in_array($contentType, ['text/json', 'application/json', 'application/x-json'], true)) { - return $this->getJsonError($exception); - } - - if (in_array($contentType, ['text/xml', 'application/xml', 'application/x-xml'], true)) { - return $this->getXmlError($exception); - } - - if (in_array($contentType, ['text/html', 'application/xhtml+xml'], true)) { - return $this->getHtmlError($exception); - } - - // text/plain - return $this->getTextError($exception); + protected function getTextError(HttpException $exception): string + { + return sprintf('(%s) %s', $exception->getIdentifier(), $exception->getMessage()); } /** @@ -54,10 +41,7 @@ protected function getExceptionOutput( */ protected function getJsonError(HttpException $exception): string { - return sprintf( - '{"error":{"ref":"%s","message":"Not found"}}', - $exception->getIdentifier() - ); + return sprintf('{"error":{"ref":"%s","message":"%s"}}', $exception->getIdentifier(), $exception->getMessage()); } /** @@ -71,9 +55,10 @@ protected function getXmlError(HttpException $exception): string { return sprintf( '' . - '%sNot found' . + '%s%s' . '', - $exception->getIdentifier() + $exception->getIdentifier(), + $exception->getMessage() ); } @@ -95,16 +80,4 @@ protected function getHtmlError(HttpException $exception): string $exception->getIdentifier() ); } - - /** - * Get simple text formatted error. - * - * @param HttpException $exception - * - * @return string - */ - protected function getTextError(HttpException $exception): string - { - return sprintf('(%s) Not found', $exception->getIdentifier()); - } } diff --git a/src/Handler/Whoops/DumperTrait.php b/src/Handler/Whoops/DumperTrait.php index 52af7d3..cf6d51e 100644 --- a/src/Handler/Whoops/DumperTrait.php +++ b/src/Handler/Whoops/DumperTrait.php @@ -40,6 +40,7 @@ protected function getExceptionData(Inspector $inspector, bool $addTrace = false 'id' => $exception->getIdentifier(), 'type' => get_class($exception), 'message' => $exception->getMessage(), + 'description' => $exception->getDescription(), ]; if ($addTrace) { diff --git a/src/HttpException.php b/src/HttpException.php index 98cb253..9af3032 100644 --- a/src/HttpException.php +++ b/src/HttpException.php @@ -27,6 +27,13 @@ class HttpException extends \RuntimeException */ protected $identifier; + /** + * Exception description. + * + * @var string + */ + protected $description; + /** * HTTP status code. * @@ -38,20 +45,27 @@ class HttpException extends \RuntimeException * Exception constructor. * * @param string $message + * @param string $description * @param int $code * @param int $httpStatusCode * @param \Throwable|null $previous */ - public function __construct(string $message, int $code, int $httpStatusCode, \Throwable $previous = null) - { + public function __construct( + string $message, + string $description, + int $code, + int $httpStatusCode, + \Throwable $previous = null + ) { parent::__construct($message, $code, $previous); $this->identifier = ShortUuid::uuid4(); + $this->description = $description; $this->httpStatusCode = $httpStatusCode; } /** - * Get error unique identifier. + * Get exception unique identifier. * * @return string */ @@ -60,6 +74,16 @@ public function getIdentifier(): string return $this->identifier; } + /** + * Get exception description. + * + * @return string + */ + public function getDescription(): string + { + return $this->description; + } + /** * Get HTTP status code. * diff --git a/src/HttpExceptionFactory.php b/src/HttpExceptionFactory.php index 64dc0f0..aa2f7ac 100644 --- a/src/HttpExceptionFactory.php +++ b/src/HttpExceptionFactory.php @@ -26,6 +26,7 @@ class HttpExceptionFactory * (400) Generic bad request error exception. * * @param string|null $message + * @param string|null $description * @param int|null $code * @param \Throwable|null $previous * @@ -33,11 +34,13 @@ class HttpExceptionFactory */ public static function badRequest( string $message = null, + string $description = null, int $code = null, \Throwable $previous = null ): HttpException { return static::create( $message ?: 'Bad request', + $description ?: '', $code ?? StatusCodeInterface::STATUS_BAD_REQUEST, StatusCodeInterface::STATUS_BAD_REQUEST, $previous @@ -48,6 +51,7 @@ public static function badRequest( * (401) Generic unauthorized error exception. * * @param string|null $message + * @param string|null $description * @param int|null $code * @param \Throwable|null $previous * @@ -55,11 +59,13 @@ public static function badRequest( */ public static function unauthorized( string $message = null, + string $description = null, int $code = null, \Throwable $previous = null ): HttpException { return static::create( $message ?: 'Unauthorized', + $description ?: '', $code ?? StatusCodeInterface::STATUS_UNAUTHORIZED, StatusCodeInterface::STATUS_UNAUTHORIZED, $previous @@ -70,6 +76,7 @@ public static function unauthorized( * (403) Generic forbidden error exception. * * @param string|null $message + * @param string|null $description * @param int|null $code * @param \Throwable|null $previous * @@ -77,11 +84,13 @@ public static function unauthorized( */ public static function forbidden( string $message = null, + string $description = null, int $code = null, \Throwable $previous = null ): HttpException { return static::create( $message ?: 'Forbidden', + $description ?: '', $code ?? StatusCodeInterface::STATUS_FORBIDDEN, StatusCodeInterface::STATUS_FORBIDDEN, $previous @@ -92,6 +101,7 @@ public static function forbidden( * (404) Generic not found error exception. * * @param string|null $message + * @param string|null $description * @param int|null $code * @param \Throwable|null $previous * @@ -99,11 +109,13 @@ public static function forbidden( */ public static function notFound( string $message = null, + string $description = null, int $code = null, \Throwable $previous = null ): HttpException { return static::create( $message ?: 'Not found', + $description ?: '', $code ?? StatusCodeInterface::STATUS_NOT_FOUND, StatusCodeInterface::STATUS_NOT_FOUND, $previous @@ -114,6 +126,7 @@ public static function notFound( * (405) Generic method not allowed error exception. * * @param string|null $message + * @param string|null $description * @param int|null $code * @param \Throwable|null $previous * @@ -121,11 +134,13 @@ public static function notFound( */ public static function methodNotAllowed( string $message = null, + string $description = null, int $code = null, \Throwable $previous = null ): HttpException { return static::create( $message ?: 'Method not allowed', + $description ?: '', $code ?? StatusCodeInterface::STATUS_METHOD_NOT_ALLOWED, StatusCodeInterface::STATUS_METHOD_NOT_ALLOWED, $previous @@ -136,6 +151,7 @@ public static function methodNotAllowed( * (406) Generic not acceptable error exception. * * @param string|null $message + * @param string|null $description * @param int|null $code * @param \Throwable|null $previous * @@ -143,11 +159,13 @@ public static function methodNotAllowed( */ public static function notAcceptable( string $message = null, + string $description = null, int $code = null, \Throwable $previous = null ): HttpException { return static::create( $message ?: 'Not acceptable', + $description ?: '', $code ?? StatusCodeInterface::STATUS_NOT_ACCEPTABLE, StatusCodeInterface::STATUS_NOT_ACCEPTABLE, $previous @@ -158,6 +176,7 @@ public static function notAcceptable( * (409) Generic conflict error exception. * * @param string|null $message + * @param string|null $description * @param int|null $code * @param \Throwable|null $previous * @@ -165,11 +184,13 @@ public static function notAcceptable( */ public static function conflict( string $message = null, + string $description = null, int $code = null, \Throwable $previous = null ): HttpException { return static::create( $message ?: 'Conflict', + $description ?: '', $code ?? StatusCodeInterface::STATUS_CONFLICT, StatusCodeInterface::STATUS_CONFLICT, $previous @@ -180,6 +201,7 @@ public static function conflict( * (410) Generic gone error exception. * * @param string|null $message + * @param string|null $description * @param int|null $code * @param \Throwable|null $previous * @@ -187,11 +209,13 @@ public static function conflict( */ public static function gone( string $message = null, + string $description = null, int $code = null, \Throwable $previous = null ): HttpException { return static::create( $message ?: 'Gone', + $description ?: '', $code ?? StatusCodeInterface::STATUS_GONE, StatusCodeInterface::STATUS_GONE, $previous @@ -202,6 +226,7 @@ public static function gone( * (415) Generic unsupported media type error exception. * * @param string|null $message + * @param string|null $description * @param int|null $code * @param \Throwable|null $previous * @@ -209,11 +234,13 @@ public static function gone( */ public static function unsupportedMediaType( string $message = null, + string $description = null, int $code = null, \Throwable $previous = null ): HttpException { return static::create( $message ?: 'Unsupported media type', + $description ?: '', $code ?? StatusCodeInterface::STATUS_UNSUPPORTED_MEDIA_TYPE, StatusCodeInterface::STATUS_UNSUPPORTED_MEDIA_TYPE, $previous @@ -224,6 +251,7 @@ public static function unsupportedMediaType( * (422) Generic unprocessable entity error exception. * * @param string|null $message + * @param string|null $description * @param int|null $code * @param \Throwable|null $previous * @@ -231,11 +259,13 @@ public static function unsupportedMediaType( */ public static function unprocessableEntity( string $message = null, + string $description = null, int $code = null, \Throwable $previous = null ): HttpException { return static::create( $message ?: 'Unprocessable entity', + $description ?: '', $code ?? StatusCodeInterface::STATUS_UNPROCESSABLE_ENTITY, StatusCodeInterface::STATUS_UNPROCESSABLE_ENTITY, $previous @@ -246,6 +276,7 @@ public static function unprocessableEntity( * (429) Generic too many requests error exception. * * @param string|null $message + * @param string|null $description * @param int|null $code * @param \Throwable|null $previous * @@ -253,11 +284,13 @@ public static function unprocessableEntity( */ public static function tooManyRequests( string $message = null, + string $description = null, int $code = null, \Throwable $previous = null ): HttpException { return static::create( $message ?: 'Too many requests', + $description ?: '', $code ?? StatusCodeInterface::STATUS_TOO_MANY_REQUESTS, StatusCodeInterface::STATUS_TOO_MANY_REQUESTS, $previous @@ -268,6 +301,7 @@ public static function tooManyRequests( * (500) Generic internal server error exception. * * @param string|null $message + * @param string|null $description * @param int|null $code * @param \Throwable|null $previous * @@ -275,11 +309,13 @@ public static function tooManyRequests( */ public static function internalServerError( string $message = null, + string $description = null, int $code = null, \Throwable $previous = null ): HttpException { return static::create( $message ?: 'Internal server error', + $description ?: '', $code ?? StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR, StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR, $previous @@ -290,6 +326,7 @@ public static function internalServerError( * (501) Generic not implemented error exception. * * @param string|null $message + * @param string|null $description * @param int|null $code * @param \Throwable|null $previous * @@ -297,11 +334,13 @@ public static function internalServerError( */ public static function notImplemented( string $message = null, + string $description = null, int $code = null, \Throwable $previous = null ): HttpException { return static::create( $message ?: 'Not implemented', + $description ?: '', $code ?? StatusCodeInterface::STATUS_NOT_IMPLEMENTED, StatusCodeInterface::STATUS_NOT_IMPLEMENTED, $previous @@ -312,6 +351,7 @@ public static function notImplemented( * Get new HTTP exception. * * @param string $message + * @param string $description * @param int $code * @param int $httpStatusCode * @param \Throwable|null $previous @@ -320,10 +360,11 @@ public static function notImplemented( */ public static function create( string $message, + string $description, int $code, int $httpStatusCode, \Throwable $previous = null ): HttpException { - return new HttpException($message, $code, $httpStatusCode, $previous); + return new HttpException($message, $description, $code, $httpStatusCode, $previous); } } diff --git a/src/HttpExceptionManager.php b/src/HttpExceptionManager.php index 1342069..b9404d5 100644 --- a/src/HttpExceptionManager.php +++ b/src/HttpExceptionManager.php @@ -124,7 +124,7 @@ public function errorHandler( \Throwable $exception ): ResponseInterface { if (!$exception instanceof HttpException) { - $exception = HttpExceptionFactory::internalServerError(null, null, $exception); + $exception = HttpExceptionFactory::internalServerError(null, null, null, $exception); } return $this->handleHttpException($request, $response, $exception); diff --git a/tests/Exception/Handler/Whoops/HtmlHandlerTest.php b/tests/Exception/Handler/Whoops/HtmlHandlerTest.php index 459a16c..68000ce 100644 --- a/tests/Exception/Handler/Whoops/HtmlHandlerTest.php +++ b/tests/Exception/Handler/Whoops/HtmlHandlerTest.php @@ -26,7 +26,7 @@ class HtmlHandlerTest extends TestCase { public function testHtmlOutput() { - $exception = HttpExceptionFactory::internalServerError('Impossible error', null, new \ErrorException()); + $exception = HttpExceptionFactory::internalServerError('Impossible error', null, null, new \ErrorException()); $inspector = new Inspector($exception); $whoops = new Whoops(); diff --git a/tests/Exception/Handler/Whoops/TextHandlerTest.php b/tests/Exception/Handler/Whoops/TextHandlerTest.php index c70d5bc..ad67edb 100644 --- a/tests/Exception/Handler/Whoops/TextHandlerTest.php +++ b/tests/Exception/Handler/Whoops/TextHandlerTest.php @@ -43,7 +43,7 @@ public function testNoTraceOutput() public function testTextOutput() { $originalException = new \ErrorException('Original exception'); - $exception = HttpExceptionFactory::tooManyRequests(null, null, $originalException); + $exception = HttpExceptionFactory::tooManyRequests(null, null, null, $originalException); $inspector = new Inspector($exception); $handler = new TextHandler(); diff --git a/tests/Exception/HttpExceptionFactoryTest.php b/tests/Exception/HttpExceptionFactoryTest.php index 2d504e9..d2af23e 100644 --- a/tests/Exception/HttpExceptionFactoryTest.php +++ b/tests/Exception/HttpExceptionFactoryTest.php @@ -24,7 +24,7 @@ class HttpExceptionFactoryTest extends TestCase public function testCustomException() { $previous = new \Exception(); - $exception = HttpExceptionFactory::create('Message', 10, 400, $previous); + $exception = HttpExceptionFactory::create('Message', 'description', 10, 400, $previous); self::assertEquals('Message', $exception->getMessage()); self::assertEquals(10, $exception->getCode()); diff --git a/tests/Exception/HttpExceptionManagerTest.php b/tests/Exception/HttpExceptionManagerTest.php index db39b7b..a6a832d 100644 --- a/tests/Exception/HttpExceptionManagerTest.php +++ b/tests/Exception/HttpExceptionManagerTest.php @@ -79,7 +79,7 @@ public function testLogErrorException() $manager->handleHttpException( $request, new Response(), - HttpExceptionFactory::internalServerError($exceptionMessage, null, $originalException) + HttpExceptionFactory::internalServerError($exceptionMessage, null, null, $originalException) ); } @@ -101,6 +101,9 @@ public function testErrorHandlerByStatusCode() $request = Request::createFromEnvironment(Environment::mock()); $customHandler = $this->getMockForAbstractClass(AbstractHttpExceptionHandler::class); + $customHandler->expects($this->once()) + ->method('getContentTypes') + ->will($this->returnValue(['text/plain'])); $customHandler->expects($this->once()) ->method('getExceptionOutput') ->will($this->returnValue('Captured exception')); diff --git a/tests/Exception/HttpExceptionTest.php b/tests/Exception/HttpExceptionTest.php index b4fe941..23c3213 100644 --- a/tests/Exception/HttpExceptionTest.php +++ b/tests/Exception/HttpExceptionTest.php @@ -23,9 +23,10 @@ class HttpExceptionTest extends TestCase { public function testException() { - $exception = new HttpException('message', 0, 400); + $exception = new HttpException('message', 'description', 0, 400); self::assertNotNull($exception->getIdentifier()); + self::assertEquals('description', $exception->getDescription()); self::assertEquals(400, $exception->getHttpStatusCode()); } } diff --git a/tests/Exception/Stubs/HandlerStub.php b/tests/Exception/Stubs/HandlerStub.php index f311fa7..a5f1462 100644 --- a/tests/Exception/Stubs/HandlerStub.php +++ b/tests/Exception/Stubs/HandlerStub.php @@ -19,6 +19,24 @@ class HandlerStub extends AbstractHttpExceptionHandler { + /** + * {@inheritdoc} + */ + protected function getContentTypes(): array + { + return [ + 'text/plain', + 'text/json', + 'application/json', + 'application/x-json', + 'text/xml', + 'application/xml', + 'application/x-xml', + 'text/html', + 'application/xhtml+xml', + ]; + } + /** * {@inheritdoc} */