Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions features/main/exception_to_status.feature
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,3 @@ Feature: Using exception_to_status config
And I send a "GET" request to "/issue5924"
Then the response status code should be 429
Then the header "retry-after" should be equal to 32

Scenario: Show error page
When I add "Accept" header equal to "text/html"
And I send a "GET" request to "/errors/404"
Then the response status code should be 200
4 changes: 2 additions & 2 deletions src/JsonSchema/DefinitionNameFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ public function create(string $className, string $format = 'json', ?string $inpu
}

$definitionName = $serializerContext[SchemaFactory::OPENAPI_DEFINITION_NAME] ?? null;
if ($definitionName) {
$name = \sprintf('%s-%s', $prefix, $definitionName);
if (null !== $definitionName) {
$name = \sprintf('%s%s', $prefix, $definitionName ? '-'.$definitionName : $definitionName);
} else {
$groups = (array) ($serializerContext[AbstractNormalizer::GROUPS] ?? []);
$name = $groups ? \sprintf('%s-%s', $prefix, implode('_', $groups)) : $prefix;
Expand Down
2 changes: 1 addition & 1 deletion src/JsonSchema/ResourceMetadataTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ private function findOperation(string $className, string $type, ?Operation $oper
$operation = new HttpOperation();
}

return $this->findOperationForType($resourceMetadataCollection, $type, $operation, $format);
return $this->findOperationForType($resourceMetadataCollection, $type, $operation, $forceSubschema ? null : $format);
}

// The best here is to use an Operation when calling `buildSchema`, we try to do a smart guess otherwise
Expand Down
7 changes: 5 additions & 2 deletions src/JsonSchema/SchemaFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public function buildSchema(string $className, string $format = 'json', string $
$inputOrOutputClass = $className;
$serializerContext ??= [];
} else {
$operation = $this->findOperation($className, $type, $operation, $serializerContext);
$operation = $this->findOperation($className, $type, $operation, $serializerContext, $format);
$inputOrOutputClass = $this->findOutputClass($className, $type, $operation, $serializerContext);
$serializerContext ??= $this->getSerializerContext($operation, $type);
}
Expand All @@ -74,7 +74,6 @@ public function buildSchema(string $className, string $format = 'json', string $
$validationGroups = $operation ? $this->getValidationGroups($operation) : [];
$version = $schema->getVersion();
$definitionName = $this->definitionNameFactory->create($className, $format, $inputOrOutputClass, $operation, $serializerContext);

$method = $operation instanceof HttpOperation ? $operation->getMethod() : 'GET';
if (!$operation) {
$method = Schema::TYPE_INPUT === $type ? 'POST' : 'GET';
Expand Down Expand Up @@ -291,6 +290,10 @@ private function getFactoryOptions(array $serializerContext, array $validationGr
$options['validation_groups'] = $validationGroups;
}

if ($operation && ($ignoredAttributes = $operation->getNormalizationContext()['ignored_attributes'] ?? null)) {
$options['ignored_attributes'] = $ignoredAttributes;
}

return $options;
}

Expand Down
23 changes: 21 additions & 2 deletions src/Laravel/ApiPlatformProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
use ApiPlatform\JsonSchema\SchemaFactory;
use ApiPlatform\JsonSchema\SchemaFactoryInterface;
use ApiPlatform\Laravel\ApiResource\Error;
use ApiPlatform\Laravel\ApiResource\ValidationError;
use ApiPlatform\Laravel\Controller\ApiPlatformController;
use ApiPlatform\Laravel\Controller\DocumentationController;
use ApiPlatform\Laravel\Controller\EntrypointController;
Expand Down Expand Up @@ -177,6 +178,7 @@
use ApiPlatform\Serializer\SerializerContextBuilder;
use ApiPlatform\State\CallableProcessor;
use ApiPlatform\State\CallableProvider;
use ApiPlatform\State\ErrorProvider;
use ApiPlatform\State\Pagination\Pagination;
use ApiPlatform\State\Pagination\PaginationOptions;
use ApiPlatform\State\ParameterProviderInterface;
Expand Down Expand Up @@ -773,6 +775,9 @@ public function register(): void
licenseUrl: $config->get('api-platform.swagger_ui.license.url', ''),
persistAuthorization: $config->get('api-platform.swagger_ui.persist_authorization', false),
httpAuth: $config->get('api-platform.swagger_ui.http_auth', []),
tags: $config->get('api-platform.openapi.tags', []),
errorResourceClass: Error::class,
validationErrorResourceClass: ValidationError::class
);
});

Expand Down Expand Up @@ -846,7 +851,9 @@ public function register(): void
null,
$config->get('api-platform.formats'),
$app->make(Options::class),
$app->make(PaginationOptions::class), // ?PaginationOptions $paginationOptions = null,
$app->make(PaginationOptions::class),
null,
$config->get('api-platform.error_formats'),
// ?RouterInterface $router = null
);
});
Expand Down Expand Up @@ -1216,6 +1223,18 @@ private function registerGraphQl(Application $app): void
});
$app->alias(GraphQlReadProvider::class, 'api_platform.graphql.state_provider.read');

$app->singleton(ErrorProvider::class, function (Application $app) {
/** @var ConfigRepository */
$config = $app['config'];

return new ErrorProvider(
$config->get('app.debug'),
$app->make(ResourceClassResolver::class),
$app->make(ResourceMetadataCollectionFactoryInterface::class),
);
});
$app->tag([ErrorProvider::class], ProviderInterface::class);

$app->singleton(ResolverProvider::class, function (Application $app) {
$resolvers = iterator_to_array($app->tagged('api_platform.graphql.resolver'));
$taggedItemResolvers = iterator_to_array($app->tagged(QueryItemResolverInterface::class));
Expand Down Expand Up @@ -1314,7 +1333,7 @@ private function registerGraphQl(Application $app): void
/** @var ConfigRepository */
$config = $app['config'];

return new Executor($config->get('api-platform.graphql.introspection.enabled') ?? false, $config->get('api-platform.graphql.max_query_complexity'), $config->get('api-platform.graphql.max_query_depth'));
return new Executor($config->get('api-platform.graphql.introspection.enabled') ?? false, $config->get('api-platform.graphql.max_query_complexity') ?? 500, $config->get('api-platform.graphql.max_query_depth') ?? 200);
});

$app->singleton(GraphiQlController::class, function (Application $app) {
Expand Down
53 changes: 41 additions & 12 deletions src/Laravel/ApiResource/Error.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,52 +13,81 @@

namespace ApiPlatform\Laravel\ApiResource;

use ApiPlatform\JsonLd\ContextBuilderInterface;
use ApiPlatform\JsonSchema\SchemaFactory;
use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\Error as Operation;
use ApiPlatform\Metadata\ErrorResource;
use ApiPlatform\Metadata\ErrorResourceInterface;
use ApiPlatform\Metadata\Exception\HttpExceptionInterface;
use ApiPlatform\Metadata\Exception\ProblemExceptionInterface;
use ApiPlatform\State\ErrorProvider;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface as SymfonyHttpExceptionInterface;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Annotation\Ignore;
use Symfony\Component\Serializer\Annotation\SerializedName;
use Symfony\Component\WebLink\Link;

#[ErrorResource(
types: ['hydra:Error'],
uriTemplate: '/errors/{status}{._format}',
openapi: false,
uriVariables: ['status'],
operations: [
new Operation(
errors: [],
name: '_api_errors_problem',
outputFormats: ['json' => ['application/problem+json']],
routeName: '_api_errors',
outputFormats: ['json' => ['application/problem+json', 'application/json']],
hideHydraOperation: true,
normalizationContext: [
SchemaFactory::OPENAPI_DEFINITION_NAME => '',
'groups' => ['jsonproblem'],
'skip_null_values' => true,
'ignored_attributes' => ['trace', 'file', 'line', 'code', 'message', 'traceAsString', 'previous'],
],
uriTemplate: '/errors/{status}'
),
new Operation(
errors: [],
name: '_api_errors_hydra',
outputFormats: ['jsonld' => ['application/problem+json']],
routeName: '_api_errors',
outputFormats: ['jsonld' => ['application/problem+json', 'application/ld+json']],
normalizationContext: [
SchemaFactory::OPENAPI_DEFINITION_NAME => '',
'groups' => ['jsonld'],
'skip_null_values' => true,
'ignored_attributes' => ['trace', 'file', 'line', 'code', 'message', 'traceAsString', 'previous'],
],
links: [new Link(rel: ContextBuilderInterface::JSONLD_NS.'error', href: 'http://www.w3.org/ns/hydra/error')],
uriTemplate: '/errors/{status}.jsonld'
links: [new Link(rel: 'http://www.w3.org/ns/json-ld#error', href: 'http://www.w3.org/ns/hydra/error')],
),
new Operation(
errors: [],
name: '_api_errors_jsonapi',
routeName: '_api_errors',
hideHydraOperation: true,
outputFormats: ['jsonapi' => ['application/vnd.api+json']],
normalizationContext: ['groups' => ['jsonapi'], 'skip_null_values' => true],
uriTemplate: '/errors/{status}.jsonapi'
normalizationContext: [
SchemaFactory::OPENAPI_DEFINITION_NAME => '',
'disable_json_schema_serializer_groups' => false,
'groups' => ['jsonapi'],
'skip_null_values' => true,
'ignored_attributes' => ['trace', 'file', 'line', 'code', 'message', 'traceAsString', 'previous'],
],
),
new Operation(
name: '_api_errors',
hideHydraOperation: true,
extraProperties: ['_api_disable_swagger_provider' => true],
outputFormats: ['html' => ['text/html'], 'jsonapi' => ['application/vnd.api+json'], 'jsonld' => ['application/ld+json'], 'json' => ['application/problem+json', 'application/json']],
),
],
graphQlOperations: []
outputFormats: ['jsonapi' => ['application/vnd.api+json'], 'jsonld' => ['application/ld+json'], 'json' => ['application/problem+json', 'application/json']],
provider: ErrorProvider::class,
graphQlOperations: [],
description: 'A representation of common errors.',
)]
class Error extends \Exception implements ProblemExceptionInterface, HttpExceptionInterface
#[ApiProperty(property: 'previous', hydra: false, readable: false)]
#[ApiProperty(property: 'traceAsString', hydra: false, readable: false)]
#[ApiProperty(property: 'string', hydra: false, readable: false)]
class Error extends \Exception implements ProblemExceptionInterface, HttpExceptionInterface, ErrorResourceInterface
{
/**
* @var array<int, mixed>
Expand All @@ -73,7 +102,7 @@ public function __construct(
private readonly string $title,
private readonly string $detail,
#[ApiProperty(identifier: true)] private int $status,
array $originalTrace,
array $originalTrace = [],
private readonly ?string $instance = null,
private string $type = 'about:blank',
private array $headers = [],
Expand Down
27 changes: 23 additions & 4 deletions src/Laravel/ApiResource/ValidationError.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

namespace ApiPlatform\Laravel\ApiResource;

use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\Error as ErrorOperation;
use ApiPlatform\Metadata\ErrorResource;
use ApiPlatform\Metadata\Exception\HttpExceptionInterface;
Expand All @@ -32,35 +33,40 @@
uriTemplate: '/validation_errors/{id}',
status: 422,
openapi: false,
outputFormats: ['jsonapi' => ['application/vnd.api+json'], 'jsonld' => ['application/ld+json'], 'json' => ['application/problem+json', 'application/json']],
uriVariables: ['id'],
shortName: 'ValidationError',
operations: [
new ErrorOperation(
routeName: 'api_validation_errors',
name: '_api_validation_errors_problem',
outputFormats: ['json' => ['application/problem+json']],
normalizationContext: ['groups' => ['json'],
normalizationContext: [
'groups' => ['json'],
'skip_null_values' => true,
'ignored_attributes' => ['trace', 'file', 'line', 'code', 'message', 'traceAsString', 'previous'],
],
uriTemplate: '/validation_errors/{id}'
),
new ErrorOperation(
name: '_api_validation_errors_hydra',
routeName: 'api_validation_errors',
outputFormats: ['jsonld' => ['application/problem+json']],
links: [new Link(rel: 'http://www.w3.org/ns/json-ld#error', href: 'http://www.w3.org/ns/hydra/error')],
normalizationContext: [
'groups' => ['jsonld'],
'skip_null_values' => true,
'ignored_attributes' => ['trace', 'file', 'line', 'code', 'message', 'traceAsString', 'previous'],
],
uriTemplate: '/validation_errors/{id}.jsonld'
),
new ErrorOperation(
name: '_api_validation_errors_jsonapi',
routeName: 'api_validation_errors',
outputFormats: ['jsonapi' => ['application/vnd.api+json']],
normalizationContext: [
'groups' => ['jsonapi'],
'skip_null_values' => true,
'ignored_attributes' => ['trace', 'file', 'line', 'code', 'message', 'traceAsString', 'previous'],
],
uriTemplate: '/validation_errors/{id}.jsonapi'
),
],
graphQlOperations: []
Expand Down Expand Up @@ -139,6 +145,19 @@ public function getInstance(): ?string
*/
#[SerializedName('violations')]
#[Groups(['json', 'jsonld', 'jsonapi'])]
#[ApiProperty(
jsonldContext: ['@type' => 'ConstraintViolationList'],
schema: [
'type' => 'array',
'items' => [
'type' => 'object',
'properties' => [
'propertyPath' => ['type' => 'string', 'description' => 'The property path of the violation'],
'message' => ['type' => 'string', 'description' => 'The message associated with the violation'],
],
],
]
)]
public function getViolations(): array
{
return $this->violations;
Expand Down
4 changes: 1 addition & 3 deletions src/Laravel/Exception/ErrorHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,6 @@ public function register(): void

/** @var HttpOperation $operation */
if (!$operation->getProvider()) {
// TODO: validation
// static::$error = 'jsonapi' === $format && $errorResource instanceof ConstraintViolationListAwareExceptionInterface ? $errorResource->getConstraintViolationList() : $errorResource;
static::$error = $errorResource;
$operation = $operation->withProvider([self::class, 'provide']);
}
Expand Down Expand Up @@ -147,7 +145,7 @@ public function register(): void
$dup->attributes->set('_api_previous_operation', $apiOperation);
$dup->attributes->set('_api_operation', $operation);
$dup->attributes->set('_api_operation_name', $operation->getName());
$dup->attributes->remove('exception');
$dup->attributes->set('exception', $exception);
// These are for swagger
$dup->attributes->set('_api_original_route', $request->attributes->get('_route'));
$dup->attributes->set('_api_original_uri_variables', $request->attributes->get('_api_uri_variables'));
Expand Down
4 changes: 3 additions & 1 deletion src/Laravel/Routing/IriConverter.php
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,9 @@ private function generateRoute(object|string $resource, int $referenceType = Url
}

try {
return $this->router->generate($operation->getName(), $identifiers, $operation->getUrlGenerationStrategy() ?? $referenceType);
$routeName = $operation instanceof HttpOperation ? ($operation->getRouteName() ?? $operation->getName()) : $operation->getName();

return $this->router->generate($routeName, $identifiers, $operation->getUrlGenerationStrategy() ?? $referenceType);
} catch (RoutingExceptionInterface $e) {
throw new InvalidArgumentException(\sprintf('Unable to generate an IRI for the item of type "%s"', $operation->getClass()), $e->getCode(), $e);
}
Expand Down
3 changes: 2 additions & 1 deletion src/Laravel/State/SwaggerUiProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
|| !($request = $context['request'] ?? null)
|| 'html' !== $request->getRequestFormat()
|| !$this->swaggerUiEnabled
|| true === ($operation->getExtraProperties()['_api_disable_swagger_provider'] ?? false)
) {
return $this->decorated->provide($operation, $uriVariables, $context);
}
Expand All @@ -64,7 +65,7 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
// We need to call our operation provider just in case it fails
// when it fails we'll get an Error, and we'll fix the status accordingly
// @see features/main/content_negotiation.feature:119
// DocumentationAction has no content negotiation as well we want HTML so render swagger ui
// When requesting DocumentationAction or EntrypointAction with Accept: text/html we render SwaggerUi
if (!$operation instanceof Error && Documentation::class !== $operation->getClass()) {
$this->decorated->provide($operation, $uriVariables, $context);
}
Expand Down
Loading
Loading