diff --git a/src/Laravel/ApiPlatformDeferredProvider.php b/src/Laravel/ApiPlatformDeferredProvider.php index 247a06a0f4..af3eef2c75 100644 --- a/src/Laravel/ApiPlatformDeferredProvider.php +++ b/src/Laravel/ApiPlatformDeferredProvider.php @@ -22,7 +22,6 @@ use ApiPlatform\GraphQl\Type\TypesContainerInterface; use ApiPlatform\JsonApi\Filter\SparseFieldset; use ApiPlatform\JsonApi\Filter\SparseFieldsetParameterProvider; -use ApiPlatform\Laravel\Controller\ApiPlatformController; use ApiPlatform\Laravel\Eloquent\Extension\FilterQueryExtension; use ApiPlatform\Laravel\Eloquent\Extension\QueryExtensionInterface; use ApiPlatform\Laravel\Eloquent\Filter\BooleanFilter; @@ -41,12 +40,10 @@ use ApiPlatform\Laravel\Eloquent\State\LinksHandlerInterface; use ApiPlatform\Laravel\Eloquent\State\PersistProcessor; use ApiPlatform\Laravel\Eloquent\State\RemoveProcessor; -use ApiPlatform\Laravel\Exception\ErrorHandler; use ApiPlatform\Laravel\Metadata\CacheResourceCollectionMetadataFactory; use ApiPlatform\Laravel\Metadata\ParameterValidationResourceMetadataCollectionFactory; use ApiPlatform\Laravel\State\ParameterValidatorProvider; use ApiPlatform\Laravel\State\SwaggerUiProcessor; -use ApiPlatform\Metadata\IdentifiersExtractorInterface; use ApiPlatform\Metadata\InflectorInterface; use ApiPlatform\Metadata\Operation\PathSegmentNameGeneratorInterface; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; @@ -81,11 +78,9 @@ use ApiPlatform\State\Provider\ParameterProvider; use ApiPlatform\State\Provider\SecurityParameterProvider; use ApiPlatform\State\ProviderInterface; -use Illuminate\Contracts\Debug\ExceptionHandler as ExceptionHandlerInterface; use Illuminate\Contracts\Foundation\Application; use Illuminate\Contracts\Support\DeferrableProvider; use Illuminate\Support\ServiceProvider; -use Negotiation\Negotiator; use Psr\Log\LoggerInterface; use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; @@ -248,25 +243,6 @@ public function register(): void ); }); - $this->app->singleton( - ExceptionHandlerInterface::class, - function (Application $app) { - /** @var ConfigRepository */ - $config = $app['config']; - - return new ErrorHandler( - $app, - $app->make(ResourceMetadataCollectionFactoryInterface::class), - $app->make(ApiPlatformController::class), - $app->make(IdentifiersExtractorInterface::class), - $app->make(ResourceClassResolverInterface::class), - $app->make(Negotiator::class), - $config->get('api-platform.exception_to_status'), - $config->get('app.debug') - ); - } - ); - if (interface_exists(FieldsBuilderEnumInterface::class)) { $this->registerGraphQl(); } diff --git a/src/Laravel/ApiPlatformProvider.php b/src/Laravel/ApiPlatformProvider.php index ce19c35f16..f9a42c9047 100644 --- a/src/Laravel/ApiPlatformProvider.php +++ b/src/Laravel/ApiPlatformProvider.php @@ -75,6 +75,7 @@ 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; use ApiPlatform\Laravel\Eloquent\Filter\JsonApi\SortFilterParameterProvider; @@ -87,6 +88,7 @@ use ApiPlatform\Laravel\Eloquent\PropertyAccess\PropertyAccessor as EloquentPropertyAccessor; use ApiPlatform\Laravel\Eloquent\Serializer\SerializerContextBuilder as EloquentSerializerContextBuilder; use ApiPlatform\Laravel\Eloquent\Serializer\SnakeCaseToCamelCaseNameConverter; +use ApiPlatform\Laravel\Exception\ErrorHandler; use ApiPlatform\Laravel\GraphQl\Controller\EntrypointController as GraphQlEntrypointController; use ApiPlatform\Laravel\GraphQl\Controller\GraphiQlController; use ApiPlatform\Laravel\JsonApi\State\JsonApiProvider; @@ -154,6 +156,7 @@ use ApiPlatform\State\ProviderInterface; use ApiPlatform\State\SerializerContextBuilderInterface; use Illuminate\Config\Repository as ConfigRepository; +use Illuminate\Contracts\Debug\ExceptionHandler as ExceptionHandlerInterface; use Illuminate\Contracts\Foundation\Application; use Illuminate\Routing\Router; use Illuminate\Support\ServiceProvider; @@ -318,6 +321,10 @@ public function register(): void return new HydraPrefixNameConverter(new MetadataAwareNameConverter($app->make(ClassMetadataFactoryInterface::class), $app->make(SnakeCaseToCamelCaseNameConverter::class)), $defaultContext); }); + $this->app->singleton(OperationMetadataFactory::class, function (Application $app) { + return new OperationMetadataFactory($app->make(ResourceNameCollectionFactoryInterface::class), $app->make(ResourceMetadataCollectionFactoryInterface::class)); + }); + $this->app->bind(OperationMetadataFactoryInterface::class, OperationMetadataFactory::class); $this->app->singleton(ReadProvider::class, function (Application $app) { @@ -1121,6 +1128,26 @@ private function registerGraphQl(): void formats: $config->get('api-platform.formats') ); }); + + $this->app->singleton( + ExceptionHandlerInterface::class, + function (Application $app) { + /** @var ConfigRepository */ + $config = $app['config']; + + return new ErrorHandler( + $app, + $app->make(ResourceMetadataCollectionFactoryInterface::class), + $app->make(ApiPlatformController::class), + $app->make(IdentifiersExtractorInterface::class), + $app->make(ResourceClassResolverInterface::class), + $app->make(Negotiator::class), + $config->get('api-platform.exception_to_status'), + $config->get('app.debug'), + $config->get('api-platform.error_formats') + ); + } + ); } /** diff --git a/src/Laravel/Controller/ApiPlatformController.php b/src/Laravel/Controller/ApiPlatformController.php index 261f97bafc..f1017d356e 100644 --- a/src/Laravel/Controller/ApiPlatformController.php +++ b/src/Laravel/Controller/ApiPlatformController.php @@ -14,10 +14,9 @@ namespace ApiPlatform\Laravel\Controller; use ApiPlatform\Metadata\HttpOperation; -use ApiPlatform\Metadata\Operation\Factory\OperationMetadataFactory; +use ApiPlatform\Metadata\Operation\Factory\OperationMetadataFactoryInterface; use ApiPlatform\State\ProcessorInterface; use ApiPlatform\State\ProviderInterface; -use Illuminate\Foundation\Application; use Illuminate\Http\Request; use Illuminate\Routing\Controller; use Symfony\Component\HttpFoundation\Response; @@ -29,10 +28,9 @@ class ApiPlatformController extends Controller * @param ProcessorInterface|object|null, Response> $processor */ public function __construct( - protected OperationMetadataFactory $operationMetadataFactory, + protected OperationMetadataFactoryInterface $operationMetadataFactory, protected ProviderInterface $provider, protected ProcessorInterface $processor, - protected Application $app, ) { } diff --git a/src/Laravel/Exception/ErrorHandler.php b/src/Laravel/Exception/ErrorHandler.php index db79581801..f359068f73 100644 --- a/src/Laravel/Exception/ErrorHandler.php +++ b/src/Laravel/Exception/ErrorHandler.php @@ -27,7 +27,6 @@ use Illuminate\Auth\AuthenticationException; use Illuminate\Contracts\Container\Container; use Illuminate\Foundation\Exceptions\Handler as ExceptionsHandler; -use Illuminate\Http\Request; use Negotiation\Negotiator; use Symfony\Component\HttpFoundation\Exception\RequestExceptionInterface; use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface as SymfonyHttpExceptionInterface; @@ -52,112 +51,114 @@ public function __construct( ?Negotiator $negotiator = null, private readonly ?array $exceptionToStatus = null, private readonly ?bool $debug = false, + private readonly ?array $errorFormats = null, ) { $this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory; $this->negotiator = $negotiator; - // calls register parent::__construct($container); - $this->register(); } - public function register(): void + public function render($request, \Throwable $exception) { - $this->renderable(function (\Throwable $exception, Request $request) { - $apiOperation = $this->initializeOperation($request); - if (!$apiOperation) { - return null; - } + $apiOperation = $this->initializeOperation($request); + + if (!$apiOperation) { + return parent::render($request, $exception); + } - $formats = config('api-platform.error_formats') ?? ['jsonproblem' => ['application/problem+json']]; - $format = $request->getRequestFormat() ?? $this->getRequestFormat($request, $formats, false); + $formats = $this->errorFormats ?? ['jsonproblem' => ['application/problem+json']]; + $format = $request->getRequestFormat() ?? $this->getRequestFormat($request, $formats, false); - if ($this->resourceClassResolver->isResourceClass($exception::class)) { - $resourceCollection = $this->resourceMetadataCollectionFactory->create($exception::class); + if ($this->resourceClassResolver->isResourceClass($exception::class)) { + $resourceCollection = $this->resourceMetadataCollectionFactory->create($exception::class); - $operation = null; - foreach ($resourceCollection as $resource) { - foreach ($resource->getOperations() as $op) { - foreach ($op->getOutputFormats() as $key => $value) { - if ($key === $format) { - $operation = $op; - break 3; - } + $operation = null; + foreach ($resourceCollection as $resource) { + foreach ($resource->getOperations() as $op) { + foreach ($op->getOutputFormats() as $key => $value) { + if ($key === $format) { + $operation = $op; + break 3; } } } + } - // No operation found for the requested format, we take the first available - if (!$operation) { - $operation = $resourceCollection->getOperation(); - } - $errorResource = $exception; - if ($errorResource instanceof ProblemExceptionInterface && $operation instanceof HttpOperation) { - $statusCode = $this->getStatusCode($apiOperation, $operation, $exception); - $operation = $operation->withStatus($statusCode); - if ($errorResource instanceof StatusAwareExceptionInterface) { - $errorResource->setStatus($statusCode); - } - } - } else { - // Create a generic, rfc7807 compatible error according to the wanted format - $operation = $this->resourceMetadataCollectionFactory->create(Error::class)->getOperation($this->getFormatOperation($format)); - // status code may be overridden by the exceptionToStatus option - $statusCode = 500; - if ($operation instanceof HttpOperation) { - $statusCode = $this->getStatusCode($apiOperation, $operation, $exception); - $operation = $operation->withStatus($statusCode); + // No operation found for the requested format, we take the first available + if (!$operation) { + $operation = $resourceCollection->getOperation(); + } + $errorResource = $exception; + if ($errorResource instanceof ProblemExceptionInterface && $operation instanceof HttpOperation) { + $statusCode = $this->getStatusCode($apiOperation, $operation, $exception); + $operation = $operation->withStatus($statusCode); + if ($errorResource instanceof StatusAwareExceptionInterface) { + $errorResource->setStatus($statusCode); } - - $errorResource = Error::createFromException($exception, $statusCode); } - - /** @var HttpOperation $operation */ - if (!$operation->getProvider()) { - static::$error = $errorResource; - $operation = $operation->withProvider([self::class, 'provide']); + } else { + // Create a generic, rfc7807 compatible error according to the wanted format + $operation = $this->resourceMetadataCollectionFactory->create(Error::class)->getOperation($this->getFormatOperation($format)); + // status code may be overridden by the exceptionToStatus option + $statusCode = 500; + if ($operation instanceof HttpOperation) { + $statusCode = $this->getStatusCode($apiOperation, $operation, $exception); + $operation = $operation->withStatus($statusCode); } - // For our swagger Ui errors - if ('html' === $format) { - $operation = $operation->withOutputFormats(['html' => ['text/html']]); - } + $errorResource = Error::createFromException($exception, $statusCode); + } - $identifiers = []; - try { - $identifiers = $this->identifiersExtractor?->getIdentifiersFromItem($errorResource, $operation) ?? []; - } catch (\Exception $e) { - } + /** @var HttpOperation $operation */ + if (!$operation->getProvider()) { + static::$error = $errorResource; + $operation = $operation->withProvider([self::class, 'provide']); + } - $normalizationContext = $operation->getNormalizationContext() ?? []; - if (!($normalizationContext['api_error_resource'] ?? false)) { - $normalizationContext += ['api_error_resource' => true]; - } + // For our swagger Ui errors + if ('html' === $format) { + $operation = $operation->withOutputFormats(['html' => ['text/html']]); + } - if (!isset($normalizationContext[AbstractObjectNormalizer::IGNORED_ATTRIBUTES])) { - $normalizationContext[AbstractObjectNormalizer::IGNORED_ATTRIBUTES] = true === $this->debug ? [] : ['originalTrace']; - } + $identifiers = []; + try { + $identifiers = $this->identifiersExtractor?->getIdentifiersFromItem($errorResource, $operation) ?? []; + } catch (\Exception $e) { + } - $operation = $operation->withNormalizationContext($normalizationContext); - - $dup = $request->duplicate(null, null, []); - $dup->setMethod('GET'); - $dup->attributes->set('_api_resource_class', $operation->getClass()); - $dup->attributes->set('_api_previous_operation', $apiOperation); - $dup->attributes->set('_api_operation', $operation); - $dup->attributes->set('_api_operation_name', $operation->getName()); - $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')); - $dup->attributes->set('_api_original_route_params', $request->attributes->get('_route_params')); - $dup->attributes->set('_api_requested_operation', $request->attributes->get('_api_requested_operation')); - - foreach ($identifiers as $name => $value) { - $dup->attributes->set($name, $value); - } + $normalizationContext = $operation->getNormalizationContext() ?? []; + if (!($normalizationContext['api_error_resource'] ?? false)) { + $normalizationContext += ['api_error_resource' => true]; + } + + if (!isset($normalizationContext[AbstractObjectNormalizer::IGNORED_ATTRIBUTES])) { + $normalizationContext[AbstractObjectNormalizer::IGNORED_ATTRIBUTES] = true === $this->debug ? [] : ['originalTrace']; + } + $operation = $operation->withNormalizationContext($normalizationContext); + + $dup = $request->duplicate(null, null, []); + $dup->setMethod('GET'); + $dup->attributes->set('_api_resource_class', $operation->getClass()); + $dup->attributes->set('_api_previous_operation', $apiOperation); + $dup->attributes->set('_api_operation', $operation); + $dup->attributes->set('_api_operation_name', $operation->getName()); + $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')); + $dup->attributes->set('_api_original_route_params', $request->attributes->get('_route_params')); + $dup->attributes->set('_api_requested_operation', $request->attributes->get('_api_requested_operation')); + + foreach ($identifiers as $name => $value) { + $dup->attributes->set($name, $value); + } + + try { return $this->apiPlatformController->__invoke($dup); - }); + } catch (\Throwable $e) { + return parent::render($dup, $e); + } } private function getStatusCode(?HttpOperation $apiOperation, ?HttpOperation $errorOperation, \Throwable $exception): int