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 47360a5
Show file tree
Hide file tree
Showing 9 changed files with 139 additions and 24 deletions.
1 change: 1 addition & 0 deletions src/JsonApi/.php-cs-fixer.cache
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"php":"8.3.10","version":"3.64.0:v3.64.0#58dd9c931c785a79739310aef5178928305ffa67","indent":" ","lineEnding":"\n","rules":{"binary_operator_spaces":{"default":"at_least_single_space"},"blank_line_after_opening_tag":true,"blank_line_between_import_groups":true,"blank_lines_before_namespace":true,"braces_position":{"allow_single_line_empty_anonymous_classes":true},"class_definition":{"inline_constructor_arguments":false,"space_before_parenthesis":true},"compact_nullable_type_declaration":true,"declare_equal_normalize":true,"lowercase_cast":true,"lowercase_static_reference":true,"new_with_parentheses":true,"no_blank_lines_after_class_opening":true,"no_extra_blank_lines":{"tokens":["use"]},"no_leading_import_slash":true,"no_whitespace_in_blank_line":true,"ordered_class_elements":{"order":["use_trait"]},"ordered_imports":{"imports_order":["class","function","const"],"sort_algorithm":"none"},"return_type_declaration":true,"short_scalar_cast":true,"single_import_per_statement":{"group_to_single_imports":false},"single_space_around_construct":{"constructs_followed_by_a_single_space":["abstract","as","case","catch","class","const_import","do","else","elseif","final","finally","for","foreach","function","function_import","if","insteadof","interface","namespace","new","private","protected","public","static","switch","trait","try","use","use_lambda","while"],"constructs_preceded_by_a_single_space":["as","else","elseif","use_lambda"]},"single_trait_insert_per_statement":true,"ternary_operator_spaces":true,"unary_operator_spaces":{"only_dec_inc":true},"visibility_required":true,"blank_line_after_namespace":true,"constant_case":true,"control_structure_braces":true,"control_structure_continuation_position":true,"elseif":true,"function_declaration":true,"indentation_type":true,"line_ending":true,"lowercase_keywords":true,"method_argument_space":{"attribute_placement":"ignore","on_multiline":"ensure_fully_multiline"},"no_break_comment":true,"no_closing_tag":true,"no_multiple_statements_per_line":true,"no_space_around_double_colon":true,"no_spaces_after_function_name":true,"no_trailing_whitespace":true,"no_trailing_whitespace_in_comment":true,"single_blank_line_at_eof":true,"single_class_element_per_statement":{"elements":["property"]},"single_line_after_imports":true,"spaces_inside_parentheses":true,"statement_indentation":true,"switch_case_semicolon_to_colon":true,"switch_case_space":true,"encoding":true,"full_opening_tag":true},"hashes":{"stdin.php":"5e32b2953a271b5ac0f072f4ef6004a0"}}
6 changes: 5 additions & 1 deletion src/JsonApi/Serializer/ErrorNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@ 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']];
if (!isset($error['code']) && method_exists($object, 'getId')) {
$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'],
]
);

$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);
$json = $response->json();
$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;
}
}
6 changes: 6 additions & 0 deletions src/State/ApiResource/Error.php
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,12 @@ public function __construct(
}
}

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

#[SerializedName('trace')]
#[Groups(['trace'])]
public ?array $originalTrace = null;
Expand Down

0 comments on commit 47360a5

Please sign in to comment.