Skip to content

Commit 4ef0ef8

Browse files
feat: error as resources, jsonld errors are now problem-compliant (#5433)
1 parent 039ba86 commit 4ef0ef8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+1026
-144
lines changed

features/hal/problem.feature

+3-2
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@ Feature: Error handling valid according to RFC 7807 (application/problem+json)
1616
And the JSON should be equal to:
1717
"""
1818
{
19-
"type": "https://tools.ietf.org/html/rfc2616#section-10",
19+
"type": "/validation_errors/c1051bb4-d103-4f74-8988-acbcafc7fdc3",
2020
"title": "An error occurred",
2121
"detail": "name: This value should not be blank.",
22+
"status": "422",
2223
"violations": [
2324
{
2425
"propertyPath": "name",
@@ -44,7 +45,7 @@ Feature: Error handling valid according to RFC 7807 (application/problem+json)
4445
Then the response status code should be 400
4546
And the response should be in JSON
4647
And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8"
47-
And the JSON node "type" should be equal to "https://tools.ietf.org/html/rfc2616#section-10"
48+
And the JSON node "type" should be equal to "/errors/400"
4849
And the JSON node "title" should be equal to "An error occurred"
4950
And the JSON node "detail" should be equal to 'Nested documents for attribute "relatedDummy" are not allowed. Use IRIs instead.'
5051
And the JSON node "trace" should exist

src/ApiResource/Error.php

+130
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\ApiResource;
15+
16+
use ApiPlatform\Exception\ProblemExceptionInterface;
17+
use ApiPlatform\Metadata\ApiProperty;
18+
use ApiPlatform\Metadata\ErrorResource;
19+
use ApiPlatform\Metadata\Exception\HttpExceptionInterface;
20+
use ApiPlatform\Metadata\Get;
21+
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface as SymfonyHttpExceptionInterface;
22+
use Symfony\Component\Serializer\Annotation\Groups;
23+
use Symfony\Component\Serializer\Annotation\Ignore;
24+
use Symfony\Component\Serializer\Annotation\SerializedName;
25+
26+
#[ErrorResource(
27+
uriTemplate: '/errors/{status}',
28+
provider: 'api_platform.state_provider.default_error',
29+
types: ['hydra:Error'],
30+
operations: [
31+
new Get(name: '_api_errors_hydra', outputFormats: ['jsonld' => ['application/ld+json']], normalizationContext: ['groups' => ['jsonld'], 'skip_null_values' => true]),
32+
new Get(name: '_api_errors_problem', outputFormats: ['json' => ['application/problem+json']], normalizationContext: ['groups' => ['jsonproblem'], 'skip_null_values' => true]),
33+
new Get(name: '_api_errors_jsonapi', outputFormats: ['jsonapi' => ['application/vnd.api+json']], normalizationContext: ['groups' => ['jsonapi'], 'skip_null_values' => true], provider: 'api_platform.json_api.state_provider.default_error'),
34+
]
35+
)]
36+
class Error extends \Exception implements ProblemExceptionInterface, HttpExceptionInterface
37+
{
38+
public function __construct(
39+
private readonly string $title,
40+
private readonly string $detail,
41+
#[ApiProperty(identifier: true)] private readonly int $status,
42+
#[Groups(['trace'])]
43+
public readonly array $trace,
44+
private ?string $instance = null,
45+
private string $type = 'about:blank',
46+
private array $headers = []
47+
) {
48+
parent::__construct();
49+
}
50+
51+
#[SerializedName('hydra:title')]
52+
#[Groups(['jsonld', 'legacy_jsonld'])]
53+
public function getHydraTitle(): string
54+
{
55+
return $this->title;
56+
}
57+
58+
#[SerializedName('hydra:description')]
59+
#[Groups(['jsonld', 'legacy_jsonld'])]
60+
public function getHydraDescription(): string
61+
{
62+
return $this->detail;
63+
}
64+
65+
#[SerializedName('description')]
66+
#[Groups(['jsonapi', 'legacy_jsonapi'])]
67+
public function getDescription(): string
68+
{
69+
return $this->detail;
70+
}
71+
72+
public static function createFromException(\Exception|\Throwable $exception, int $status): self
73+
{
74+
$headers = ($exception instanceof SymfonyHttpExceptionInterface || $exception instanceof HttpExceptionInterface) ? $exception->getHeaders() : [];
75+
76+
return new self('An error occurred', $exception->getMessage(), $status, $exception->getTrace(), type: '/errors/'.$status, headers: $headers);
77+
}
78+
79+
#[Ignore]
80+
public function getHeaders(): array
81+
{
82+
return $this->headers;
83+
}
84+
85+
#[Ignore]
86+
public function getStatusCode(): int
87+
{
88+
return $this->status;
89+
}
90+
91+
public function setHeaders(array $headers): void
92+
{
93+
$this->headers = $headers;
94+
}
95+
96+
#[Groups(['jsonld', 'jsonproblem'])]
97+
public function getType(): string
98+
{
99+
return $this->type;
100+
}
101+
102+
#[Groups(['jsonld', 'legacy_jsonproblem', 'jsonproblem', 'jsonapi', 'legacy_jsonapi'])]
103+
public function getTitle(): ?string
104+
{
105+
return $this->title;
106+
}
107+
108+
public function setType(string $type): void
109+
{
110+
$this->type = $type;
111+
}
112+
113+
#[Groups(['jsonld', 'jsonproblem', 'legacy_jsonproblem'])]
114+
public function getStatus(): ?int
115+
{
116+
return $this->status;
117+
}
118+
119+
#[Groups(['jsonld', 'jsonproblem', 'legacy_jsonproblem'])]
120+
public function getDetail(): ?string
121+
{
122+
return $this->detail;
123+
}
124+
125+
#[Groups(['jsonld', 'jsonproblem', 'legacy_jsonproblem'])]
126+
public function getInstance(): ?string
127+
{
128+
return $this->instance;
129+
}
130+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Exception;
15+
16+
interface ProblemExceptionInterface
17+
{
18+
public function getType(): string;
19+
20+
/**
21+
* Note from RFC rfc7807: "title" (string) - A short, human-readable summary of the problem type.
22+
* It SHOULD NOT change from occurrence to occurrence of the problem, except for purposes of localization.
23+
*/
24+
public function getTitle(): ?string;
25+
26+
public function getStatus(): ?int;
27+
28+
public function getDetail(): ?string;
29+
30+
public function getInstance(): ?string;
31+
}

src/Hydra/Serializer/ErrorNormalizer.php

+6-1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ public function __construct(private readonly UrlGeneratorInterface $urlGenerator
4343
*/
4444
public function normalize(mixed $object, string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null
4545
{
46+
trigger_deprecation('api-platform', '3.2', sprintf('The class "%s" is deprecated in favor of using an Error resource.', __CLASS__));
4647
$data = [
4748
'@context' => $this->urlGenerator->generate('api_jsonld_context', ['shortName' => 'Error']),
4849
'@type' => 'hydra:Error',
@@ -62,11 +63,15 @@ public function normalize(mixed $object, string $format = null, array $context =
6263
*/
6364
public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool
6465
{
66+
if ($context['skip_deprecated_exception_normalizers'] ?? false) {
67+
return false;
68+
}
69+
6570
return self::FORMAT === $format && ($data instanceof \Exception || $data instanceof FlattenException);
6671
}
6772

6873
public function hasCacheableSupportsMethod(): bool
6974
{
70-
return true;
75+
return false;
7176
}
7277
}

src/JsonApi/Serializer/ErrorNormalizer.php

+6-1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ public function __construct(private readonly bool $debug = false, array $default
4343
*/
4444
public function normalize(mixed $object, string $format = null, array $context = []): array
4545
{
46+
trigger_deprecation('api-platform', '3.2', sprintf('The class "%s" is deprecated in favor of using an Error resource.', __CLASS__));
4647
$data = [
4748
'title' => $context[self::TITLE] ?? $this->defaultContext[self::TITLE],
4849
'description' => $this->getErrorMessage($object, $context, $this->debug),
@@ -64,11 +65,15 @@ public function normalize(mixed $object, string $format = null, array $context =
6465
*/
6566
public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool
6667
{
68+
if ($context['skip_deprecated_exception_normalizers'] ?? false) {
69+
return false;
70+
}
71+
6772
return self::FORMAT === $format && ($data instanceof \Exception || $data instanceof FlattenException);
6873
}
6974

7075
public function hasCacheableSupportsMethod(): bool
7176
{
72-
return true;
77+
return false;
7378
}
7479
}

src/JsonApi/Serializer/ItemNormalizer.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
use ApiPlatform\Serializer\CacheKeyTrait;
2727
use ApiPlatform\Serializer\ContextTrait;
2828
use ApiPlatform\Symfony\Security\ResourceAccessCheckerInterface;
29+
use Symfony\Component\ErrorHandler\Exception\FlattenException;
2930
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
3031
use Symfony\Component\Serializer\Exception\LogicException;
3132
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
@@ -62,7 +63,7 @@ public function __construct(PropertyNameCollectionFactoryInterface $propertyName
6263
*/
6364
public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool
6465
{
65-
return self::FORMAT === $format && parent::supportsNormalization($data, $format, $context);
66+
return self::FORMAT === $format && parent::supportsNormalization($data, $format, $context) && !($data instanceof \Exception || $data instanceof FlattenException);
6667
}
6768

6869
/**
+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\JsonApi\State;
15+
16+
use ApiPlatform\Metadata\Operation;
17+
use ApiPlatform\State\ProviderInterface;
18+
use ApiPlatform\Symfony\Validator\Exception\ConstraintViolationListAwareExceptionInterface;
19+
20+
/**
21+
* @internal
22+
*/
23+
final class DefaultErrorProvider implements ProviderInterface
24+
{
25+
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object
26+
{
27+
$exception = $context['previous_data'];
28+
29+
if ($exception instanceof ConstraintViolationListAwareExceptionInterface) {
30+
return $exception->getConstraintViolationList();
31+
}
32+
33+
return $exception;
34+
}
35+
}

src/JsonLd/Action/ContextAction.php

+1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ public function __invoke(string $shortName): array
4646
return ['@context' => $this->contextBuilder->getEntrypointContext()];
4747
}
4848

49+
// TODO: remove this, exceptions are resources since 3.2
4950
if (isset(self::RESERVED_SHORT_NAMES[$shortName])) {
5051
return ['@context' => $this->contextBuilder->getBaseContext()];
5152
}

src/JsonLd/Serializer/ItemNormalizer.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ public function normalize(mixed $object, string $format = null, array $context =
9898
$context['item_uri_template'] = $itemUriTemplate;
9999
}
100100

101-
if ($iri = $this->iriConverter->getIriFromResource($object, UrlGeneratorInterface::ABS_PATH, $context['operation'] ?? null, $context)) {
101+
if (true === ($context['force_iri_generation'] ?? true) && $iri = $this->iriConverter->getIriFromResource($object, UrlGeneratorInterface::ABS_PATH, $context['operation'] ?? null, $context)) {
102102
$context['iri'] = $iri;
103103
$metadata['@id'] = $iri;
104104
}

0 commit comments

Comments
 (0)