Skip to content

Commit

Permalink
Added support for Symfony\Component\HttpKernel\Exception\* document…
Browse files Browse the repository at this point in the history
…ation when annotated in exceptions (#674)

* fix: Symfony\Component\HttpKernel\Exception\ types

* added test and removed notfound as is handled seperatly

* fix formatting

* fix formatting and updated logic

* formatting fix

* undo formatting

---------

Co-authored-by: Roman Lytvynenko <[email protected]>
  • Loading branch information
phpsa and romalytvynenko authored Feb 12, 2025
1 parent dfe3bf2 commit 7f0448a
Show file tree
Hide file tree
Showing 2 changed files with 127 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,23 @@
use Dedoc\Scramble\Support\Type\Literal\LiteralStringType;
use Dedoc\Scramble\Support\Type\ObjectType;
use Dedoc\Scramble\Support\Type\Type;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
use Symfony\Component\HttpKernel\Exception\GoneHttpException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\LengthRequiredHttpException;
use Symfony\Component\HttpKernel\Exception\LockedHttpException;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
use Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Exception\PreconditionFailedHttpException;
use Symfony\Component\HttpKernel\Exception\PreconditionRequiredHttpException;
use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;

class HttpExceptionToResponseExtension extends ExceptionToResponseExtension
{
Expand All @@ -34,7 +50,8 @@ public function toResponse(Type $type)
? ($type->templateTypes[7] ?? null)
: ($type->templateTypes[0] ?? null);

if (! $codeType instanceof LiteralIntegerType) {
$responseCode = $this->getResponseCode($codeType, $type);
if ($responseCode === null) {
return null;
}

Expand All @@ -51,11 +68,60 @@ public function toResponse(Type $type)
)
->setRequired(['message']);

return Response::make($codeType->value)
->description('An error')
return Response::make($responseCode)
->description($this->getDescription($type))
->setContent(
'application/json',
Schema::fromType($responseBodyType)
);
}

protected function getResponseCode(?Type $codeType, Type $type): ?int
{
if (! $codeType instanceof LiteralIntegerType) {
return match (true) {
$type->isInstanceOf(AccessDeniedHttpException::class) => 403,
$type->isInstanceOf(BadRequestHttpException::class) => 400,
$type->isInstanceOf(ConflictHttpException::class) => 409,
$type->isInstanceOf(GoneHttpException::class) => 410,
$type->isInstanceOf(LengthRequiredHttpException::class) => 411,
$type->isInstanceOf(LockedHttpException::class) => 423,
$type->isInstanceOf(MethodNotAllowedHttpException::class) => 405,
$type->isInstanceOf(NotAcceptableHttpException::class) => 406,
$type->isInstanceOf(PreconditionFailedHttpException::class) => 412,
$type->isInstanceOf(PreconditionRequiredHttpException::class) => 428,
$type->isInstanceOf(ServiceUnavailableHttpException::class) => 503,
$type->isInstanceOf(TooManyRequestsHttpException::class) => 429,
$type->isInstanceOf(UnauthorizedHttpException::class) => 401,
$type->isInstanceOf(UnprocessableEntityHttpException::class) => 422,
$type->isInstanceOf(UnsupportedMediaTypeHttpException::class) => 415,
default => null,
};
}

return $codeType->value;
}

protected function getDescription(Type $type): string
{
return match (true) {
$type->isInstanceOf(AccessDeniedHttpException::class) => 'Access denied',
$type->isInstanceOf(BadRequestHttpException::class) => 'Bad request',
$type->isInstanceOf(ConflictHttpException::class) => 'Conflict',
$type->isInstanceOf(GoneHttpException::class) => 'Gone',
$type->isInstanceOf(LengthRequiredHttpException::class) => 'Length required',
$type->isInstanceOf(LockedHttpException::class) => 'Locked',
$type->isInstanceOf(MethodNotAllowedHttpException::class) => 'Method not allowed',
$type->isInstanceOf(NotAcceptableHttpException::class) => 'Not acceptable',
$type->isInstanceOf(NotFoundHttpException::class) => 'Not found',
$type->isInstanceOf(PreconditionFailedHttpException::class) => 'Precondition failed',
$type->isInstanceOf(PreconditionRequiredHttpException::class) => 'Precondition required',
$type->isInstanceOf(ServiceUnavailableHttpException::class) => 'Service unavailable',
$type->isInstanceOf(TooManyRequestsHttpException::class) => 'Too many requests',
$type->isInstanceOf(UnauthorizedHttpException::class) => 'Unauthorized',
$type->isInstanceOf(UnprocessableEntityHttpException::class) => 'Unprocessable entity',
$type->isInstanceOf(UnsupportedMediaTypeHttpException::class) => 'Unsupported media type',
default => 'An error',
};
}
}
59 changes: 58 additions & 1 deletion tests/ErrorsResponsesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,21 @@
use Illuminate\Routing\Controller;
use Illuminate\Routing\Route;
use Illuminate\Support\Facades\Route as RouteFacade;

use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
use Symfony\Component\HttpKernel\Exception\GoneHttpException;
use Symfony\Component\HttpKernel\Exception\LengthRequiredHttpException;
use Symfony\Component\HttpKernel\Exception\LockedHttpException;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
use Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException;
use Symfony\Component\HttpKernel\Exception\PreconditionFailedHttpException;
use Symfony\Component\HttpKernel\Exception\PreconditionRequiredHttpException;
use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
use function Spatie\Snapshots\assertMatchesSnapshot;

it('adds validation error response', function () {
Expand Down Expand Up @@ -92,6 +106,42 @@

expect($openApiDocument['paths']['/test']['get']['responses'][409])->toHaveKey('content.application/json.schema.type', 'object');
});


it('adds http error response exception extending sympony HTTP exception is thrown', function () {
$openApiDocument = generateForRoute(fn() => RouteFacade::get('api/test', [ErrorsResponsesTest_Controller::class, 'symfony_http_exception_response']));

// AccessDeniedHttpException
expect($openApiDocument['paths']['/test']['get']['responses'][403])->toHaveKey('content.application/json.schema.type', 'object');
// BadRequestHttpException
expect($openApiDocument['paths']['/test']['get']['responses'][400])->toHaveKey('content.application/json.schema.type', 'object');
// ConflictHttpException
expect($openApiDocument['paths']['/test']['get']['responses'][409])->toHaveKey('content.application/json.schema.type', 'object');
// GoneHttpException
expect($openApiDocument['paths']['/test']['get']['responses'][410])->toHaveKey('content.application/json.schema.type', 'object');
// LengthRequiredHttpException
expect($openApiDocument['paths']['/test']['get']['responses'][411])->toHaveKey('content.application/json.schema.type', 'object');
// LockedHttpException
expect($openApiDocument['paths']['/test']['get']['responses'][423])->toHaveKey('content.application/json.schema.type', 'object');
// MethodNotAllowedHttpException
expect($openApiDocument['paths']['/test']['get']['responses'][405])->toHaveKey('content.application/json.schema.type', 'object');
// NotAcceptableHttpException
expect($openApiDocument['paths']['/test']['get']['responses'][406])->toHaveKey('content.application/json.schema.type', 'object');
// PreconditionFailedHttpException
expect($openApiDocument['paths']['/test']['get']['responses'][412])->toHaveKey('content.application/json.schema.type', 'object');
// PreconditionRequiredHttpException
expect($openApiDocument['paths']['/test']['get']['responses'][428])->toHaveKey('content.application/json.schema.type', 'object');
// ServiceUnavailableHttpException
expect($openApiDocument['paths']['/test']['get']['responses'][503])->toHaveKey('content.application/json.schema.type', 'object');
// TooManyRequestsHttpException
expect($openApiDocument['paths']['/test']['get']['responses'][429])->toHaveKey('content.application/json.schema.type', 'object');
// UnauthorizedHttpException
expect($openApiDocument['paths']['/test']['get']['responses'][401])->toHaveKey('content.application/json.schema.type', 'object');
// UnprocessableEntityHttpException
expect($openApiDocument['paths']['/test']['get']['responses'][422])->toHaveKey('content.application/json.schema.type', 'object');
// UnsupportedMediaTypeHttpException
expect($openApiDocument['paths']['/test']['get']['responses'][415])->toHaveKey('content.application/json.schema.type', 'object');
});
class ErrorsResponsesTest_Controller extends Controller
{
use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
Expand Down Expand Up @@ -129,6 +179,13 @@ public function custom_exception_response(Illuminate\Http\Request $request)
{
throw new BusinessException('The business error');
}

/**
* @throws AccessDeniedHttpException|BadRequestHttpException|ConflictHttpException|GoneHttpException|LengthRequiredHttpException|LockedHttpException|MethodNotAllowedHttpException|NotAcceptableHttpException|PreconditionFailedHttpException|PreconditionRequiredHttpException|ServiceUnavailableHttpException|TooManyRequestsHttpException|UnauthorizedHttpException|UnprocessableEntityHttpException|UnsupportedMediaTypeHttpException
*/
public function symfony_http_exception_response(Illuminate\Http\Request $request)
{
}
}

class BusinessException extends \Symfony\Component\HttpKernel\Exception\HttpException
Expand Down

0 comments on commit 7f0448a

Please sign in to comment.