Skip to content

Commit cd6f583

Browse files
authored
fix: use error normalizers (#5931)
Use defaults.extra_properties.skip_deprecated_exception_normalizers to skip these normalizers. fixes #5921
1 parent 4f51b51 commit cd6f583

File tree

16 files changed

+187
-23
lines changed

16 files changed

+187
-23
lines changed

src/Hydra/Serializer/ErrorNormalizer.php

+8-3
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
/**
2525
* Converts {@see \Exception} or {@see FlattenException} to a Hydra error representation.
2626
*
27-
* @deprecated
27+
* @deprecated we use ItemNormalizer instead
2828
*
2929
* @author Kévin Dunglas <[email protected]>
3030
* @author Samuel ROZE <[email protected]>
@@ -37,7 +37,7 @@ final class ErrorNormalizer implements NormalizerInterface, CacheableSupportsMet
3737
public const TITLE = 'title';
3838
private array $defaultContext = [self::TITLE => 'An error occurred'];
3939

40-
public function __construct(private readonly UrlGeneratorInterface|LegacyUrlGeneratorInterface $urlGenerator, private readonly bool $debug = false, array $defaultContext = [])
40+
public function __construct(private readonly UrlGeneratorInterface|LegacyUrlGeneratorInterface $urlGenerator, private readonly bool $debug = false, array $defaultContext = [], private readonly ?NormalizerInterface $itemNormalizer = null)
4141
{
4242
$this->defaultContext = array_merge($this->defaultContext, $defaultContext);
4343
}
@@ -47,7 +47,12 @@ public function __construct(private readonly UrlGeneratorInterface|LegacyUrlGene
4747
*/
4848
public function normalize(mixed $object, string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null
4949
{
50-
trigger_deprecation('api-platform', '3.2', sprintf('The class "%s" is deprecated in favor of using an Error resource.', __CLASS__));
50+
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__));
51+
52+
if ($this->itemNormalizer) {
53+
return $this->itemNormalizer->normalize($object, $format, $context);
54+
}
55+
5156
$data = [
5257
'@context' => $this->urlGenerator->generate('api_jsonld_context', ['shortName' => 'Error']),
5358
'@type' => 'hydra:Error',

src/JsonApi/Serializer/ErrorNormalizer.php

+9-2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
/**
2323
* Converts {@see \Exception} or {@see FlattenException} or to a JSON API error representation.
2424
*
25+
* @deprecated we use ItemNormalizer instead
26+
*
2527
* @author Héctor Hurtarte <[email protected]>
2628
*/
2729
final class ErrorNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface
@@ -34,7 +36,7 @@ final class ErrorNormalizer implements NormalizerInterface, CacheableSupportsMet
3436
self::TITLE => 'An error occurred',
3537
];
3638

37-
public function __construct(private readonly bool $debug = false, array $defaultContext = [])
39+
public function __construct(private readonly bool $debug = false, array $defaultContext = [], private readonly ?NormalizerInterface $itemNormalizer = null)
3840
{
3941
$this->defaultContext = array_merge($this->defaultContext, $defaultContext);
4042
}
@@ -44,7 +46,12 @@ public function __construct(private readonly bool $debug = false, array $default
4446
*/
4547
public function normalize(mixed $object, string $format = null, array $context = []): array
4648
{
47-
trigger_deprecation('api-platform', '3.2', sprintf('The class "%s" is deprecated in favor of using an Error resource.', __CLASS__));
49+
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__));
50+
51+
if ($this->itemNormalizer) {
52+
return $this->itemNormalizer->normalize($object, $format, $context);
53+
}
54+
4855
$data = [
4956
'title' => $context[self::TITLE] ?? $this->defaultContext[self::TITLE],
5057
'description' => $this->getErrorMessage($object, $context, $this->debug),

src/Metadata/Resource/Factory/DeprecationResourceMetadataCollectionFactory.php

+14-3
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
namespace ApiPlatform\Metadata\Resource\Factory;
1515

1616
use ApiPlatform\Metadata\Operation;
17+
use ApiPlatform\Metadata\Operations;
1718
use ApiPlatform\Metadata\Put;
1819
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
1920

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

40-
foreach ($resourceMetadataCollection as $resourceMetadata) {
41-
foreach ($resourceMetadata->getOperations() as $operation) {
42-
if ($operation instanceof Put && null === ($operation->getExtraProperties()['standard_put'] ?? null)) {
41+
foreach ($resourceMetadataCollection as $i => $resourceMetadata) {
42+
$newOperations = [];
43+
foreach ($resourceMetadata->getOperations() as $operationName => $operation) {
44+
$extraProperties = $operation->getExtraProperties();
45+
if ($operation instanceof Put && null === ($extraProperties['standard_put'] ?? null)) {
4346
$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.');
4447
}
48+
49+
if (null === ($extraProperties['skip_deprecated_exception_normalizers'] ?? null)) {
50+
$operation = $operation->withExtraProperties(['skip_deprecated_exception_normalizers' => false] + $extraProperties);
51+
}
52+
53+
$newOperations[$operationName] = $operation;
4554
}
55+
56+
$resourceMetadataCollection[$i] = $resourceMetadata->withOperations(new Operations($newOperations));
4657
}
4758

4859
return $resourceMetadataCollection;

src/Problem/Serializer/ErrorNormalizer.php

+10-4
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
* Normalizes errors according to the API Problem spec (RFC 7807).
2323
*
2424
* @see https://tools.ietf.org/html/rfc7807
25+
* @deprecated we use ItemNormalizer instead
2526
*
2627
* @author Kévin Dunglas <[email protected]>
2728
*/
@@ -36,7 +37,7 @@ final class ErrorNormalizer implements NormalizerInterface, CacheableSupportsMet
3637
self::TITLE => 'An error occurred',
3738
];
3839

39-
public function __construct(private readonly bool $debug = false, array $defaultContext = [])
40+
public function __construct(private readonly bool $debug = false, array $defaultContext = [], private readonly ?NormalizerInterface $itemNormalizer = null)
4041
{
4142
$this->defaultContext = array_merge($this->defaultContext, $defaultContext);
4243
}
@@ -46,7 +47,12 @@ public function __construct(private readonly bool $debug = false, array $default
4647
*/
4748
public function normalize(mixed $object, string $format = null, array $context = []): array
4849
{
49-
trigger_deprecation('api-platform', '3.2', sprintf('The class "%s" is deprecated in favor of using an Error resource.', __CLASS__));
50+
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__));
51+
52+
if ($this->itemNormalizer) {
53+
return $this->itemNormalizer->normalize($object, $format, $context);
54+
}
55+
5056
$data = [
5157
'type' => $context[self::TYPE] ?? $this->defaultContext[self::TYPE],
5258
'title' => $context[self::TITLE] ?? $this->defaultContext[self::TITLE],
@@ -69,12 +75,12 @@ public function supportsNormalization(mixed $data, string $format = null, array
6975
return false;
7076
}
7177

72-
return self::FORMAT === $format && ($data instanceof \Exception || $data instanceof FlattenException);
78+
return (self::FORMAT === $format || 'json' === $format) && ($data instanceof \Exception || $data instanceof FlattenException);
7379
}
7480

7581
public function getSupportedTypes($format): array
7682
{
77-
if (self::FORMAT === $format) {
83+
if (self::FORMAT === $format || 'json' === $format) {
7884
return [
7985
\Exception::class => false,
8086
FlattenException::class => false,

src/Serializer/SerializerContextBuilder.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ public function createFromRequest(Request $request, bool $normalization, array $
6262
$context['uri'] = $request->getUri();
6363
$context['input'] = $operation->getInput();
6464
$context['output'] = $operation->getOutput();
65-
$context['skip_deprecated_exception_normalizers'] = true;
65+
$context['skip_deprecated_exception_normalizers'] = $operation->getExtraProperties()['skip_deprecated_exception_normalizers'] ?? false;
6666

6767
// Special case as this is usually handled by our OperationContextTrait, here we want to force the IRI in the response
6868
if (!$operation instanceof CollectionOperationInterface && method_exists($operation, 'getItemUriTemplate') && $operation->getItemUriTemplate()) {

src/Serializer/Tests/SerializerContextBuilderTest.php

+9-9
Original file line numberDiff line numberDiff line change
@@ -67,42 +67,42 @@ public function testCreateFromRequest(): void
6767
{
6868
$request = Request::create('/foos/1');
6969
$request->attributes->replace(['_api_resource_class' => 'Foo', '_api_operation_name' => 'get', '_api_format' => 'xml', '_api_mime_type' => 'text/xml']);
70-
$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];
70+
$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];
7171
$this->assertEquals($expected, $this->builder->createFromRequest($request, true));
7272

7373
$request = Request::create('/foos');
7474
$request->attributes->replace(['_api_resource_class' => 'Foo', '_api_operation_name' => 'get_collection', '_api_format' => 'xml', '_api_mime_type' => 'text/xml']);
75-
$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];
75+
$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];
7676
$this->assertEquals($expected, $this->builder->createFromRequest($request, true));
7777

7878
$request = Request::create('/foos/1');
7979
$request->attributes->replace(['_api_resource_class' => 'Foo', '_api_operation_name' => 'get', '_api_format' => 'xml', '_api_mime_type' => 'text/xml']);
80-
$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];
80+
$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];
8181
$this->assertEquals($expected, $this->builder->createFromRequest($request, false));
8282

8383
$request = Request::create('/foos', 'POST');
8484
$request->attributes->replace(['_api_resource_class' => 'Foo', '_api_operation_name' => 'post', '_api_format' => 'xml', '_api_mime_type' => 'text/xml']);
85-
$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];
85+
$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];
8686
$this->assertEquals($expected, $this->builder->createFromRequest($request, false));
8787

8888
$request = Request::create('/foos', 'PUT');
8989
$request->attributes->replace(['_api_resource_class' => 'Foo', '_api_operation_name' => 'put', '_api_format' => 'xml', '_api_mime_type' => 'text/xml']);
90-
$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];
90+
$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];
9191
$this->assertEquals($expected, $this->builder->createFromRequest($request, false));
9292

9393
$request = Request::create('/bars/1/foos');
9494
$request->attributes->replace(['_api_resource_class' => 'Foo', '_api_operation_name' => 'get', '_api_format' => 'xml', '_api_mime_type' => 'text/xml']);
95-
$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];
95+
$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];
9696
$this->assertEquals($expected, $this->builder->createFromRequest($request, false));
9797

9898
$request = Request::create('/foowithpatch/1', 'PATCH');
9999
$request->attributes->replace(['_api_resource_class' => 'FooWithPatch', '_api_operation_name' => 'patch', '_api_format' => 'json', '_api_mime_type' => 'application/json']);
100-
$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];
100+
$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];
101101
$this->assertEquals($expected, $this->builder->createFromRequest($request, false));
102102

103103
$request = Request::create('/bars/1/foos');
104104
$request->attributes->replace(['_api_resource_class' => 'Foo', '_api_operation_name' => 'get', '_api_format' => 'xml', '_api_mime_type' => 'text/xml', 'id' => '1']);
105-
$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];
105+
$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];
106106
$this->assertEquals($expected, $this->builder->createFromRequest($request, false));
107107
}
108108

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

116116
public function testReuseExistingAttributes(): void
117117
{
118-
$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];
118+
$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];
119119
$this->assertEquals($expected, $this->builder->createFromRequest(Request::create('/foos/1'), false, ['resource_class' => 'Foo', 'operation_name' => 'get']));
120120
}
121121

src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php

+4
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,10 @@ public function load(array $configs, ContainerBuilder $container): void
127127
$errorFormats['json'] = ['application/problem+json', 'application/json'];
128128
}
129129

130+
if (!isset($errorFormats['jsonproblem'])) {
131+
$errorFormats['jsonproblem'] = ['application/problem+json'];
132+
}
133+
130134
if ($this->isConfigEnabled($container, $config['graphql']) && !isset($formats['json'])) {
131135
trigger_deprecation('api-platform/core', '3.2', 'Add the "json" format to the configuration to use GraphQL.');
132136
$formats['json'] = ['application/json'];

src/Symfony/Bundle/Resources/config/hydra.xml

+2
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@
4949
<service id="api_platform.hydra.normalizer.error" class="ApiPlatform\Hydra\Serializer\ErrorNormalizer" public="false">
5050
<argument type="service" id="api_platform.router" />
5151
<argument>%kernel.debug%</argument>
52+
<argument type="collection"></argument>
53+
<argument type="service" id="api_platform.jsonld.normalizer.item" on-invalid="null" />
5254

5355
<tag name="serializer.normalizer" priority="-800" />
5456
</service>

0 commit comments

Comments
 (0)