Skip to content

Commit

Permalink
fix(laravel): jsonapi error serialization
Browse files Browse the repository at this point in the history
  • Loading branch information
soyuka committed Oct 25, 2024
1 parent 5a8ef11 commit abbd308
Show file tree
Hide file tree
Showing 7 changed files with 130 additions and 24 deletions.
4 changes: 3 additions & 1 deletion src/JsonApi/Serializer/ErrorNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ public function normalize(mixed $object, ?string $format = null, array $context
$jsonApiObject = $this->itemNormalizer->normalize($object, $format, $context);
$error = $jsonApiObject['data']['attributes'];
$error['id'] = $jsonApiObject['data']['id'];
$error['type'] = $jsonApiObject['data']['id'];
$error['links'] = ['type' => $error['type']];
$error['code'] = $object->getId();
unset($error['type']);

return ['errors' => [$error]];
}
Expand Down
14 changes: 11 additions & 3 deletions src/Laravel/ApiPlatformProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
use ApiPlatform\JsonApi\JsonSchema\SchemaFactory as JsonApiSchemaFactory;
use ApiPlatform\JsonApi\Serializer\CollectionNormalizer as JsonApiCollectionNormalizer;
use ApiPlatform\JsonApi\Serializer\EntrypointNormalizer as JsonApiEntrypointNormalizer;
use ApiPlatform\JsonApi\Serializer\ErrorNormalizer as JsonApiErrorNormalizer;
use ApiPlatform\JsonApi\Serializer\ItemNormalizer as JsonApiItemNormalizer;
use ApiPlatform\JsonApi\Serializer\ObjectNormalizer as JsonApiObjectNormalizer;
use ApiPlatform\JsonApi\Serializer\ReservedAttributeNameConverter;
Expand Down Expand Up @@ -907,6 +908,10 @@ public function register(): void
return new ReservedAttributeNameConverter($app->make(NameConverterInterface::class));
});

if (interface_exists(FieldsBuilderEnumInterface::class)) {
$this->registerGraphQl($this->app);
}

$this->app->singleton(JsonApiEntrypointNormalizer::class, function (Application $app) {
return new JsonApiEntrypointNormalizer(
$app->make(ResourceMetadataCollectionFactoryInterface::class),
Expand Down Expand Up @@ -946,9 +951,11 @@ public function register(): void
);
});

if (interface_exists(FieldsBuilderEnumInterface::class)) {
$this->registerGraphQl($this->app);
}
$this->app->singleton(JsonApiErrorNormalizer::class, function (Application $app) {
return new JsonApiErrorNormalizer(
$app->make(JsonApiItemNormalizer::class),
);
});

$this->app->singleton(JsonApiObjectNormalizer::class, function (Application $app) {
return new JsonApiObjectNormalizer(
Expand Down Expand Up @@ -985,6 +992,7 @@ public function register(): void
$list->insert($app->make(JsonApiEntrypointNormalizer::class), -800);
$list->insert($app->make(JsonApiCollectionNormalizer::class), -985);
$list->insert($app->make(JsonApiItemNormalizer::class), -890);
$list->insert($app->make(JsonApiErrorNormalizer::class), -790);
$list->insert($app->make(JsonApiObjectNormalizer::class), -995);

if (interface_exists(FieldsBuilderEnumInterface::class)) {
Expand Down
16 changes: 11 additions & 5 deletions src/Laravel/ApiResource/Error.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
name: '_api_errors_jsonapi',
outputFormats: ['jsonapi' => ['application/vnd.api+json']],
normalizationContext: ['groups' => ['jsonapi'], 'skip_null_values' => true],
uriTemplate: '/errros/{status}.jsonapi'
uriTemplate: '/errors/{status}.jsonapi'
),
],
graphQlOperations: []
Expand Down Expand Up @@ -124,6 +124,12 @@ public function getStatusCode(): int
return $this->status;
}

#[Groups(['jsonapi'])]
public function getId()
{
return $this->status;
}

/**
* @param array<string, string> $headers
*/
Expand All @@ -132,7 +138,7 @@ public function setHeaders(array $headers): void
$this->headers = $headers;
}

#[Groups(['jsonld', 'jsonproblem'])]
#[Groups(['jsonld', 'jsonproblem', 'jsonapi'])]
public function getType(): string
{
return $this->type;
Expand All @@ -149,7 +155,7 @@ public function setType(string $type): void
$this->type = $type;
}

#[Groups(['jsonld', 'jsonproblem'])]
#[Groups(['jsonld', 'jsonproblem', 'jsonapi'])]
public function getStatus(): ?int
{
return $this->status;
Expand All @@ -160,13 +166,13 @@ public function setStatus(int $status): void
$this->status = $status;
}

#[Groups(['jsonld', 'jsonproblem'])]
#[Groups(['jsonld', 'jsonproblem', 'jsonapi'])]
public function getDetail(): ?string
{
return $this->detail;
}

#[Groups(['jsonld', 'jsonproblem'])]
#[Groups(['jsonld', 'jsonproblem', 'jsonapi'])]
public function getInstance(): ?string
{
return $this->instance;
Expand Down
10 changes: 5 additions & 5 deletions src/Laravel/ApiResource/ValidationError.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,19 +92,19 @@ public function getDescription(): string
return $this->detail;
}

#[Groups(['jsonld', 'json'])]
#[Groups(['jsonld', 'json', 'jsonapi'])]
public function getType(): string
{
return '/validation_errors/'.$this->id;
}

#[Groups(['jsonld', 'json'])]
#[Groups(['jsonld', 'json', 'jsonapi'])]
public function getTitle(): ?string
{
return 'Validation Error';
}

#[Groups(['jsonld', 'json'])]
#[Groups(['jsonld', 'json', 'jsonapi'])]
private string $detail;

public function getDetail(): ?string
Expand All @@ -117,7 +117,7 @@ public function setDetail(string $detail): void
$this->detail = $detail;
}

#[Groups(['jsonld', 'json'])]
#[Groups(['jsonld', 'json', 'jsonapi'])]
public function getStatus(): ?int
{
return $this->status;
Expand All @@ -128,7 +128,7 @@ public function setStatus(int $status): void
$this->status = $status;
}

#[Groups(['jsonld', 'json'])]
#[Groups(['jsonld', 'json', 'jsonapi'])]
public function getInstance(): ?string
{
return null;
Expand Down
9 changes: 5 additions & 4 deletions src/Laravel/Tests/EloquentTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -386,18 +386,19 @@ public function testRangeGreaterThanEqualFilter(): void
'Content-Type' => ['application/merge-patch+json'],
]
);

$json = $response->json();
$response = $this->get('api/books?isbn_range[gte]='.$updated['isbn'], ['Accept' => ['application/ld+json']]);
$this->assertSame($response->json()['member'][0]['@id'], $bookBefore['@id']);
$this->assertSame($response->json()['member'][1]['@id'], $bookAfter['@id']);
$this->assertSame($response->json()['totalItems'], 2);
$this->assertSame($json['member'][0]['@id'], $bookBefore['@id']);
$this->assertSame($json['member'][1]['@id'], $bookAfter['@id']);
$this->assertSame($json['totalItems'], 2);
}

public function testWrongOrderFilter(): void
{
BookFactory::new()->has(AuthorFactory::new())->count(10)->create();
$res = $this->get('/api/authors?order[name]=something', ['Accept' => ['application/ld+json']]);
$this->assertEquals($res->getStatusCode(), 422);
dump($res->json());
}

public function testWithAccessor(): void
Expand Down
67 changes: 61 additions & 6 deletions src/Laravel/Tests/JsonApiTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ protected function defineEnvironment($app): void
tap($app['config'], function (Repository $config): void {
$config->set('api-platform.formats', ['jsonapi' => ['application/vnd.api+json']]);
$config->set('api-platform.docs_formats', ['jsonapi' => ['application/vnd.api+json']]);
$config->set('api-platform.resources', [app_path('Models'), app_path('ApiResource')]);
$config->set('app.debug', true);
});
}
Expand All @@ -48,13 +49,15 @@ public function testGetEntrypoint(): void
$response = $this->get('/api/', ['accept' => ['application/vnd.api+json']]);
$response->assertStatus(200);
$response->assertHeader('content-type', 'application/vnd.api+json; charset=utf-8');
$this->assertJsonContains([
'links' => [
'self' => 'http://localhost/api',
'book' => 'http://localhost/api/books',
$this->assertJsonContains(
[
'links' => [
'self' => 'http://localhost/api',
'book' => 'http://localhost/api/books',
],
],
],
$response->json());
$response->json()
);
}

public function testGetCollection(): void
Expand Down Expand Up @@ -209,4 +212,56 @@ public function testRelationWithGroups(): void
$this->assertArrayHasKey('relation', $content['data']['relationships']);
$this->assertArrayHasKey('data', $content['data']['relationships']['relation']);
}

public function testValidateJsonApi(): void
{
$response = $this->postJson(
'/api/issue6745/rule_validations',
[
'data' => [
'type' => 'string',
'attributes' => [
'prop' => 1,
],
],
],
[
'accept' => 'application/vnd.api+json',
'content_type' => 'application/vnd.api+json',
]
);

$response->assertStatus(422);
$response->assertHeader('content-type', 'application/vnd.api+json; charset=utf-8');
$json = $response->json();
$this->assertJsonContains([
'errors' => [
[
'description' => 'The prop field is required.',
'title' => 'Validation Error',
'detail' => 'The prop field is required.',
'status' => 422,
'code' => 'ac88443768512709',
],
],
], $json);

$this->assertArrayHasKey('id', $json['errors'][0]);
$this->assertArrayHasKey('links', $json['errors'][0]);
$this->assertArrayHasKey('type', $json['errors'][0]['links']);
}

public function testNotFound(): void
{
$response = $this->get('/api/books/notfound', headers: ['accept' => 'application/vnd.api+json']);
$response->assertStatus(404);
$response->assertHeader('content-type', 'application/vnd.api+json; charset=utf-8');

$this->assertJsonContains([
'links' => ['type' => '/errors/404'],
'title' => 'An error occurred',
'status' => 404,
'detail' => 'Not Found',
], $response->json()['errors'][0]);
}
}
34 changes: 34 additions & 0 deletions src/Laravel/workbench/app/ApiResource/RuleValidation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Workbench\App\ApiResource;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Post;

#[ApiResource(
uriTemplate: '/issue6745/rule_validations',
operations: [new Post()],
rules: ['prop' => 'required']
)]
class RuleValidation
{
public function __construct(private int $prop)
{
}

public function getProp(): int
{
return $this->prop;
}
}

0 comments on commit abbd308

Please sign in to comment.