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
7 changes: 7 additions & 0 deletions features/main/exception_to_status.feature
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,10 @@ Feature: Using exception_to_status config
And I send a "DELETE" request to "/error_with_overriden_status/1"
Then the response status code should be 403
And the JSON node "status" should be equal to 403

@!mongodb
Scenario: Get HTTP Exception headers
When I add "Accept" header equal to "application/ld+json"
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
11 changes: 8 additions & 3 deletions src/Hydra/Serializer/ErrorNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
/**
* Converts {@see \Exception} or {@see FlattenException} to a Hydra error representation.
*
* @deprecated
* @deprecated we use ItemNormalizer instead
*
* @author Kévin Dunglas <dunglas@gmail.com>
* @author Samuel ROZE <samuel.roze@gmail.com>
Expand All @@ -37,7 +37,7 @@ final class ErrorNormalizer implements NormalizerInterface, CacheableSupportsMet
public const TITLE = 'title';
private array $defaultContext = [self::TITLE => 'An error occurred'];

public function __construct(private readonly UrlGeneratorInterface|LegacyUrlGeneratorInterface $urlGenerator, private readonly bool $debug = false, array $defaultContext = [])
public function __construct(private readonly UrlGeneratorInterface|LegacyUrlGeneratorInterface $urlGenerator, private readonly bool $debug = false, array $defaultContext = [], private readonly ?NormalizerInterface $itemNormalizer = null)
{
$this->defaultContext = array_merge($this->defaultContext, $defaultContext);
}
Expand All @@ -47,7 +47,12 @@ public function __construct(private readonly UrlGeneratorInterface|LegacyUrlGene
*/
public function normalize(mixed $object, string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null
{
trigger_deprecation('api-platform', '3.2', sprintf('The class "%s" is deprecated in favor of using an Error resource.', __CLASS__));
trigger_deprecation('api-platform', '3.2', sprintf('The class "%s" is deprecated in favor of using an Error resource. We fallback on "api_platform.serializer.normalizer.item".', __CLASS__));

if ($this->itemNormalizer) {
return $this->itemNormalizer->normalize($object, $format, $context);
}

$data = [
'@context' => $this->urlGenerator->generate('api_jsonld_context', ['shortName' => 'Error']),
'@type' => 'hydra:Error',
Expand Down
11 changes: 9 additions & 2 deletions src/JsonApi/Serializer/ErrorNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
/**
* Converts {@see \Exception} or {@see FlattenException} or to a JSON API error representation.
*
* @deprecated we use ItemNormalizer instead
*
* @author Héctor Hurtarte <hectorh30@gmail.com>
*/
final class ErrorNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface
Expand All @@ -34,7 +36,7 @@ final class ErrorNormalizer implements NormalizerInterface, CacheableSupportsMet
self::TITLE => 'An error occurred',
];

public function __construct(private readonly bool $debug = false, array $defaultContext = [])
public function __construct(private readonly bool $debug = false, array $defaultContext = [], private readonly ?NormalizerInterface $itemNormalizer = null)
{
$this->defaultContext = array_merge($this->defaultContext, $defaultContext);
}
Expand All @@ -44,7 +46,12 @@ public function __construct(private readonly bool $debug = false, array $default
*/
public function normalize(mixed $object, string $format = null, array $context = []): array
{
trigger_deprecation('api-platform', '3.2', sprintf('The class "%s" is deprecated in favor of using an Error resource.', __CLASS__));
trigger_deprecation('api-platform', '3.2', sprintf('The class "%s" is deprecated in favor of using an Error resource. We fallback on "api_platform.serializer.normalizer.item".', __CLASS__));

if ($this->itemNormalizer) {
return $this->itemNormalizer->normalize($object, $format, $context);
}

$data = [
'title' => $context[self::TITLE] ?? $this->defaultContext[self::TITLE],
'description' => $this->getErrorMessage($object, $context, $this->debug),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
namespace ApiPlatform\Metadata\Resource\Factory;

use ApiPlatform\Metadata\Operation;
use ApiPlatform\Metadata\Operations;
use ApiPlatform\Metadata\Put;
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;

Expand All @@ -37,12 +38,22 @@ public function create(string $resourceClass): ResourceMetadataCollection
{
$resourceMetadataCollection = $this->decorated->create($resourceClass);

foreach ($resourceMetadataCollection as $resourceMetadata) {
foreach ($resourceMetadata->getOperations() as $operation) {
if ($operation instanceof Put && null === ($operation->getExtraProperties()['standard_put'] ?? null)) {
foreach ($resourceMetadataCollection as $i => $resourceMetadata) {
$newOperations = [];
foreach ($resourceMetadata->getOperations() as $operationName => $operation) {
$extraProperties = $operation->getExtraProperties();
if ($operation instanceof Put && null === ($extraProperties['standard_put'] ?? null)) {
$this->triggerDeprecationOnce($operation, 'extraProperties["standard_put"]', 'In API Platform 4 PUT will always replace the data, use extraProperties["standard_put"] to "true" on every operation to avoid breaking PUT\'s behavior. Use PATCH to use the old behavior.');
}

if (null === ($extraProperties['skip_deprecated_exception_normalizers'] ?? null)) {
$operation = $operation->withExtraProperties(['skip_deprecated_exception_normalizers' => false] + $extraProperties);
}

$newOperations[$operationName] = $operation;
}

$resourceMetadataCollection[$i] = $resourceMetadata->withOperations(new Operations($newOperations));
}

return $resourceMetadataCollection;
Expand Down
14 changes: 10 additions & 4 deletions src/Problem/Serializer/ErrorNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
* Normalizes errors according to the API Problem spec (RFC 7807).
*
* @see https://tools.ietf.org/html/rfc7807
* @deprecated we use ItemNormalizer instead
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
Expand All @@ -36,7 +37,7 @@ final class ErrorNormalizer implements NormalizerInterface, CacheableSupportsMet
self::TITLE => 'An error occurred',
];

public function __construct(private readonly bool $debug = false, array $defaultContext = [])
public function __construct(private readonly bool $debug = false, array $defaultContext = [], private readonly ?NormalizerInterface $itemNormalizer = null)
{
$this->defaultContext = array_merge($this->defaultContext, $defaultContext);
}
Expand All @@ -46,7 +47,12 @@ public function __construct(private readonly bool $debug = false, array $default
*/
public function normalize(mixed $object, string $format = null, array $context = []): array
{
trigger_deprecation('api-platform', '3.2', sprintf('The class "%s" is deprecated in favor of using an Error resource.', __CLASS__));
trigger_deprecation('api-platform', '3.2', sprintf('The class "%s" is deprecated in favor of using an Error resource. We fallback on "api_platform.serializer.normalizer.item".', __CLASS__));

if ($this->itemNormalizer) {
return $this->itemNormalizer->normalize($object, $format, $context);
}

$data = [
'type' => $context[self::TYPE] ?? $this->defaultContext[self::TYPE],
'title' => $context[self::TITLE] ?? $this->defaultContext[self::TITLE],
Expand All @@ -69,12 +75,12 @@ public function supportsNormalization(mixed $data, string $format = null, array
return false;
}

return self::FORMAT === $format && ($data instanceof \Exception || $data instanceof FlattenException);
return (self::FORMAT === $format || 'json' === $format) && ($data instanceof \Exception || $data instanceof FlattenException);
}

public function getSupportedTypes($format): array
{
if (self::FORMAT === $format) {
if (self::FORMAT === $format || 'json' === $format) {
return [
\Exception::class => false,
FlattenException::class => false,
Expand Down
2 changes: 1 addition & 1 deletion src/Serializer/SerializerContextBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public function createFromRequest(Request $request, bool $normalization, array $
$context['uri'] = $request->getUri();
$context['input'] = $operation->getInput();
$context['output'] = $operation->getOutput();
$context['skip_deprecated_exception_normalizers'] = true;
$context['skip_deprecated_exception_normalizers'] = $operation->getExtraProperties()['skip_deprecated_exception_normalizers'] ?? false;

// Special case as this is usually handled by our OperationContextTrait, here we want to force the IRI in the response
if (!$operation instanceof CollectionOperationInterface && method_exists($operation, 'getItemUriTemplate') && $operation->getItemUriTemplate()) {
Expand Down
18 changes: 9 additions & 9 deletions src/Serializer/Tests/SerializerContextBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,42 +67,42 @@ public function testCreateFromRequest(): void
{
$request = Request::create('/foos/1');
$request->attributes->replace(['_api_resource_class' => 'Foo', '_api_operation_name' => 'get', '_api_format' => 'xml', '_api_mime_type' => 'text/xml']);
$expected = ['foo' => 'bar', 'operation_name' => 'get', 'resource_class' => 'Foo', 'request_uri' => '/foos/1', 'uri' => 'http://localhost/foos/1', 'output' => null, 'input' => null, 'iri_only' => false, 'skip_null_values' => true, 'operation' => $this->operation, 'exclude_from_cache_key' => ['root_operation', 'operation'], 'skip_deprecated_exception_normalizers' => true];
$expected = ['foo' => 'bar', 'operation_name' => 'get', 'resource_class' => 'Foo', 'request_uri' => '/foos/1', 'uri' => 'http://localhost/foos/1', 'output' => null, 'input' => null, 'iri_only' => false, 'skip_null_values' => true, 'operation' => $this->operation, 'exclude_from_cache_key' => ['root_operation', 'operation'], 'skip_deprecated_exception_normalizers' => false];
$this->assertEquals($expected, $this->builder->createFromRequest($request, true));

$request = Request::create('/foos');
$request->attributes->replace(['_api_resource_class' => 'Foo', '_api_operation_name' => 'get_collection', '_api_format' => 'xml', '_api_mime_type' => 'text/xml']);
$expected = ['foo' => 'bar', 'operation_name' => 'get_collection', 'resource_class' => 'Foo', 'request_uri' => '/foos', 'uri' => 'http://localhost/foos', 'output' => null, 'input' => null, 'iri_only' => false, 'skip_null_values' => true, 'operation' => $this->operation->withName('get_collection'), 'exclude_from_cache_key' => ['root_operation', 'operation'], 'skip_deprecated_exception_normalizers' => true];
$expected = ['foo' => 'bar', 'operation_name' => 'get_collection', 'resource_class' => 'Foo', 'request_uri' => '/foos', 'uri' => 'http://localhost/foos', 'output' => null, 'input' => null, 'iri_only' => false, 'skip_null_values' => true, 'operation' => $this->operation->withName('get_collection'), 'exclude_from_cache_key' => ['root_operation', 'operation'], 'skip_deprecated_exception_normalizers' => false];
$this->assertEquals($expected, $this->builder->createFromRequest($request, true));

$request = Request::create('/foos/1');
$request->attributes->replace(['_api_resource_class' => 'Foo', '_api_operation_name' => 'get', '_api_format' => 'xml', '_api_mime_type' => 'text/xml']);
$expected = ['bar' => 'baz', 'operation_name' => 'get', 'resource_class' => 'Foo', 'request_uri' => '/foos/1', 'api_allow_update' => false, 'uri' => 'http://localhost/foos/1', 'output' => null, 'input' => null, 'iri_only' => false, 'skip_null_values' => true, 'operation' => $this->operation, 'exclude_from_cache_key' => ['root_operation', 'operation'], 'skip_deprecated_exception_normalizers' => true];
$expected = ['bar' => 'baz', 'operation_name' => 'get', 'resource_class' => 'Foo', 'request_uri' => '/foos/1', 'api_allow_update' => false, 'uri' => 'http://localhost/foos/1', 'output' => null, 'input' => null, 'iri_only' => false, 'skip_null_values' => true, 'operation' => $this->operation, 'exclude_from_cache_key' => ['root_operation', 'operation'], 'skip_deprecated_exception_normalizers' => false];
$this->assertEquals($expected, $this->builder->createFromRequest($request, false));

$request = Request::create('/foos', 'POST');
$request->attributes->replace(['_api_resource_class' => 'Foo', '_api_operation_name' => 'post', '_api_format' => 'xml', '_api_mime_type' => 'text/xml']);
$expected = ['bar' => 'baz', 'operation_name' => 'post', 'resource_class' => 'Foo', 'request_uri' => '/foos', 'api_allow_update' => false, 'uri' => 'http://localhost/foos', 'output' => null, 'input' => null, 'iri_only' => false, 'skip_null_values' => true, 'operation' => $this->operation->withName('post'), 'exclude_from_cache_key' => ['root_operation', 'operation'], 'skip_deprecated_exception_normalizers' => true];
$expected = ['bar' => 'baz', 'operation_name' => 'post', 'resource_class' => 'Foo', 'request_uri' => '/foos', 'api_allow_update' => false, 'uri' => 'http://localhost/foos', 'output' => null, 'input' => null, 'iri_only' => false, 'skip_null_values' => true, 'operation' => $this->operation->withName('post'), 'exclude_from_cache_key' => ['root_operation', 'operation'], 'skip_deprecated_exception_normalizers' => false];
$this->assertEquals($expected, $this->builder->createFromRequest($request, false));

$request = Request::create('/foos', 'PUT');
$request->attributes->replace(['_api_resource_class' => 'Foo', '_api_operation_name' => 'put', '_api_format' => 'xml', '_api_mime_type' => 'text/xml']);
$expected = ['bar' => 'baz', 'operation_name' => 'put', 'resource_class' => 'Foo', 'request_uri' => '/foos', 'api_allow_update' => true, 'uri' => 'http://localhost/foos', 'output' => null, 'input' => null, 'iri_only' => false, 'skip_null_values' => true, 'operation' => (new Put(name: 'put'))->withOperation($this->operation), 'exclude_from_cache_key' => ['root_operation', 'operation'], 'skip_deprecated_exception_normalizers' => true];
$expected = ['bar' => 'baz', 'operation_name' => 'put', 'resource_class' => 'Foo', 'request_uri' => '/foos', 'api_allow_update' => true, 'uri' => 'http://localhost/foos', 'output' => null, 'input' => null, 'iri_only' => false, 'skip_null_values' => true, 'operation' => (new Put(name: 'put'))->withOperation($this->operation), 'exclude_from_cache_key' => ['root_operation', 'operation'], 'skip_deprecated_exception_normalizers' => false];
$this->assertEquals($expected, $this->builder->createFromRequest($request, false));

$request = Request::create('/bars/1/foos');
$request->attributes->replace(['_api_resource_class' => 'Foo', '_api_operation_name' => 'get', '_api_format' => 'xml', '_api_mime_type' => 'text/xml']);
$expected = ['bar' => 'baz', 'operation_name' => 'get', 'resource_class' => 'Foo', 'request_uri' => '/bars/1/foos', 'api_allow_update' => false, 'uri' => 'http://localhost/bars/1/foos', 'output' => null, 'input' => null, 'iri_only' => false, 'skip_null_values' => true, 'operation' => $this->operation, 'exclude_from_cache_key' => ['root_operation', 'operation'], 'skip_deprecated_exception_normalizers' => true];
$expected = ['bar' => 'baz', 'operation_name' => 'get', 'resource_class' => 'Foo', 'request_uri' => '/bars/1/foos', 'api_allow_update' => false, 'uri' => 'http://localhost/bars/1/foos', 'output' => null, 'input' => null, 'iri_only' => false, 'skip_null_values' => true, 'operation' => $this->operation, 'exclude_from_cache_key' => ['root_operation', 'operation'], 'skip_deprecated_exception_normalizers' => false];
$this->assertEquals($expected, $this->builder->createFromRequest($request, false));

$request = Request::create('/foowithpatch/1', 'PATCH');
$request->attributes->replace(['_api_resource_class' => 'FooWithPatch', '_api_operation_name' => 'patch', '_api_format' => 'json', '_api_mime_type' => 'application/json']);
$expected = ['operation_name' => 'patch', 'resource_class' => 'FooWithPatch', 'request_uri' => '/foowithpatch/1', 'api_allow_update' => true, 'uri' => 'http://localhost/foowithpatch/1', 'output' => null, 'input' => null, 'deep_object_to_populate' => true, 'skip_null_values' => true, 'iri_only' => false, 'operation' => $this->patchOperation, 'exclude_from_cache_key' => ['root_operation', 'operation'], 'skip_deprecated_exception_normalizers' => true];
$expected = ['operation_name' => 'patch', 'resource_class' => 'FooWithPatch', 'request_uri' => '/foowithpatch/1', 'api_allow_update' => true, 'uri' => 'http://localhost/foowithpatch/1', 'output' => null, 'input' => null, 'deep_object_to_populate' => true, 'skip_null_values' => true, 'iri_only' => false, 'operation' => $this->patchOperation, 'exclude_from_cache_key' => ['root_operation', 'operation'], 'skip_deprecated_exception_normalizers' => false];
$this->assertEquals($expected, $this->builder->createFromRequest($request, false));

$request = Request::create('/bars/1/foos');
$request->attributes->replace(['_api_resource_class' => 'Foo', '_api_operation_name' => 'get', '_api_format' => 'xml', '_api_mime_type' => 'text/xml', 'id' => '1']);
$expected = ['bar' => 'baz', 'operation_name' => 'get', 'resource_class' => 'Foo', 'request_uri' => '/bars/1/foos', 'api_allow_update' => false, 'uri' => 'http://localhost/bars/1/foos', 'output' => null, 'input' => null, 'iri_only' => false, 'operation' => $this->operation, 'skip_null_values' => true, 'exclude_from_cache_key' => ['root_operation', 'operation'], 'skip_deprecated_exception_normalizers' => true];
$expected = ['bar' => 'baz', 'operation_name' => 'get', 'resource_class' => 'Foo', 'request_uri' => '/bars/1/foos', 'api_allow_update' => false, 'uri' => 'http://localhost/bars/1/foos', 'output' => null, 'input' => null, 'iri_only' => false, 'operation' => $this->operation, 'skip_null_values' => true, 'exclude_from_cache_key' => ['root_operation', 'operation'], 'skip_deprecated_exception_normalizers' => false];
$this->assertEquals($expected, $this->builder->createFromRequest($request, false));
}

Expand All @@ -115,7 +115,7 @@ public function testThrowExceptionOnInvalidRequest(): void

public function testReuseExistingAttributes(): void
{
$expected = ['bar' => 'baz', 'operation_name' => 'get', 'resource_class' => 'Foo', 'request_uri' => '/foos/1', 'api_allow_update' => false, 'uri' => 'http://localhost/foos/1', 'output' => null, 'input' => null, 'iri_only' => false, 'skip_null_values' => true, 'operation' => $this->operation, 'exclude_from_cache_key' => ['root_operation', 'operation'], 'skip_deprecated_exception_normalizers' => true];
$expected = ['bar' => 'baz', 'operation_name' => 'get', 'resource_class' => 'Foo', 'request_uri' => '/foos/1', 'api_allow_update' => false, 'uri' => 'http://localhost/foos/1', 'output' => null, 'input' => null, 'iri_only' => false, 'skip_null_values' => true, 'operation' => $this->operation, 'exclude_from_cache_key' => ['root_operation', 'operation'], 'skip_deprecated_exception_normalizers' => false];
$this->assertEquals($expected, $this->builder->createFromRequest(Request::create('/foos/1'), false, ['resource_class' => 'Foo', 'operation_name' => 'get']));
}

Expand Down
Loading