Skip to content

Commit

Permalink
fix: use error normalizers (#5931)
Browse files Browse the repository at this point in the history
Use defaults.extra_properties.skip_deprecated_exception_normalizers to
skip these normalizers.

fixes #5921
  • Loading branch information
soyuka authored Oct 27, 2023
1 parent 4f51b51 commit cd6f583
Show file tree
Hide file tree
Showing 16 changed files with 187 additions and 23 deletions.
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 <[email protected]>
* @author Samuel ROZE <[email protected]>
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 <[email protected]>
*/
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 <[email protected]>
*/
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
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,10 @@ public function load(array $configs, ContainerBuilder $container): void
$errorFormats['json'] = ['application/problem+json', 'application/json'];
}

if (!isset($errorFormats['jsonproblem'])) {
$errorFormats['jsonproblem'] = ['application/problem+json'];
}

if ($this->isConfigEnabled($container, $config['graphql']) && !isset($formats['json'])) {
trigger_deprecation('api-platform/core', '3.2', 'Add the "json" format to the configuration to use GraphQL.');
$formats['json'] = ['application/json'];
Expand Down
2 changes: 2 additions & 0 deletions src/Symfony/Bundle/Resources/config/hydra.xml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@
<service id="api_platform.hydra.normalizer.error" class="ApiPlatform\Hydra\Serializer\ErrorNormalizer" public="false">
<argument type="service" id="api_platform.router" />
<argument>%kernel.debug%</argument>
<argument type="collection"></argument>
<argument type="service" id="api_platform.jsonld.normalizer.item" on-invalid="null" />

<tag name="serializer.normalizer" priority="-800" />
</service>
Expand Down
Loading

0 comments on commit cd6f583

Please sign in to comment.