diff --git a/.circleci/config.yml b/.circleci/config.yml
index f84f63e97e1..e2b41b4698d 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -215,7 +215,7 @@ jobs:
mkdir -p build/logs/tmp build/cov
for f in $(find features -name '*.feature' -not -path 'features/main/exposed_state.feature' -not -path 'features/elasticsearch/*' -not -path 'features/mongodb/*' | circleci tests split --split-by=timings); do
_f=${f//\//_}
- FEATURE="${_f}" phpdbg -qrr vendor/bin/behat --profile=coverage --suite=default --format=progress --out=std --format=junit --out=build/logs/tmp/"${_f}" "$f"
+ FEATURE="${_f}" phpdbg -qrr vendor/bin/behat --profile=coverage --suite=default --format=progress --out=std --format=junit --out=build/logs/tmp/"${_f}" --no-interaction "$f"
done
- run:
name: Merge Behat test reports
diff --git a/.travis.yml b/.travis.yml
index 33df8456376..a498f03d141 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -68,13 +68,13 @@ script:
fi
- tests/Fixtures/app/console cache:clear
- if [[ $APP_ENV = 'postgres' ]]; then
- vendor/bin/behat --suite=postgres --format=progress;
+ vendor/bin/behat --suite=postgres --format=progress --no-interaction;
elif [[ $APP_ENV = 'mongodb' ]]; then
- vendor/bin/behat --suite=mongodb --format=progress;
+ vendor/bin/behat --suite=mongodb --format=progress --no-interaction;
elif [[ $APP_ENV = 'elasticsearch' ]]; then
- vendor/bin/behat --suite=elasticsearch --format=progress;
+ vendor/bin/behat --suite=elasticsearch --format=progress --no-interaction;
else
- vendor/bin/behat --suite=default --format=progress;
+ vendor/bin/behat --suite=default --format=progress --no-interaction;
fi
- tests/Fixtures/app/console api:swagger:export > swagger.json && npx swagger-cli validate swagger.json && rm swagger.json
- tests/Fixtures/app/console api:swagger:export --yaml > swagger.yaml && npx swagger-cli validate swagger.yaml && rm swagger.yaml
diff --git a/appveyor.yml b/appveyor.yml
index 8b3a9cdbde2..cfbced8c7f8 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -33,6 +33,6 @@ services:
test_script:
- cd %APPVEYOR_BUILD_FOLDER%
- - php vendor\behat\behat\bin\behat --format=progress --suite=default
+ - php vendor\behat\behat\bin\behat --format=progress --suite=default --no-interaction
- rmdir tests\Fixtures\app\var\cache /s /q
- php vendor\phpunit\phpunit\phpunit
diff --git a/features/bootstrap/JsonContext.php b/features/bootstrap/JsonContext.php
index b56a60523b1..83d54a9f051 100644
--- a/features/bootstrap/JsonContext.php
+++ b/features/bootstrap/JsonContext.php
@@ -24,27 +24,6 @@ public function __construct(HttpCallResultPool $httpCallResultPool)
parent::__construct($httpCallResultPool);
}
- private function sortArrays($obj)
- {
- $isObject = is_object($obj);
-
- foreach ($obj as $key => $value) {
- if (null === $value || is_scalar($value)) {
- continue;
- }
-
- if (is_array($value)) {
- sort($value);
- }
-
- $value = $this->sortArrays($value);
-
- $isObject ? $obj->{$key} = $value : $obj[$key] = $value;
- }
-
- return $obj;
- }
-
/**
* @Then /^the JSON should be deep equal to:$/
*/
@@ -75,4 +54,25 @@ public function theJsonIsASupersetOf(PyStringNode $content)
$actual = json_decode($this->httpCallResultPool->getResult()->getValue(), true);
Assert::assertArraySubset(json_decode($content->getRaw(), true), $actual);
}
+
+ private function sortArrays($obj)
+ {
+ $isObject = is_object($obj);
+
+ foreach ($obj as $key => $value) {
+ if (null === $value || is_scalar($value)) {
+ continue;
+ }
+
+ if (is_array($value)) {
+ sort($value);
+ }
+
+ $value = $this->sortArrays($value);
+
+ $isObject ? $obj->{$key} = $value : $obj[$key] = $value;
+ }
+
+ return $obj;
+ }
}
diff --git a/features/graphql/input_output.feature b/features/graphql/input_output.feature
new file mode 100644
index 00000000000..c29b9a1e921
--- /dev/null
+++ b/features/graphql/input_output.feature
@@ -0,0 +1,190 @@
+Feature: GraphQL DTO input and output
+ In order to use a hypermedia API
+ As a client software developer
+ I need to be able to use DTOs on my resources as Input or Output objects.
+
+ @createSchema
+ Scenario: Retrieve an Output with GraphQl
+ When I add "Content-Type" header equal to "application/ld+json"
+ And I send a "POST" request to "/dummy_dto_input_outputs" with body:
+ """
+ {
+ "foo": "test",
+ "bar": 1
+ }
+ """
+ Then the response status code should be 201
+ And the JSON should be equal to:
+ """
+ {
+ "@context": {
+ "@vocab": "http://example.com/docs.jsonld#",
+ "hydra": "http://www.w3.org/ns/hydra/core#",
+ "id": "OutputDto/id",
+ "baz": "OutputDto/baz",
+ "bat": "OutputDto/bat"
+ },
+ "@type": "DummyDtoInputOutput",
+ "@id": "/dummy_dto_input_outputs/1",
+ "id": 1,
+ "baz": 1,
+ "bat": "test"
+ }
+ """
+ When I send the following GraphQL request:
+ """
+ {
+ dummyDtoInputOutput(id: "/dummy_dto_input_outputs/1") {
+ _id, id, baz
+ }
+ }
+ """
+ Then the response status code should be 200
+ And the response should be in JSON
+ And the header "Content-Type" should be equal to "application/json"
+ And the JSON should be equal to:
+ """
+ {
+ "data": {
+ "dummyDtoInputOutput": {
+ "_id": 1,
+ "id": "/dummy_dto_input_outputs/1",
+ "baz": 1
+ }
+ }
+ }
+ """
+
+ Scenario: Create an item using custom inputClass & disabled outputClass
+ Given there are 2 dummyDtoNoOutput objects
+ When I send the following GraphQL request:
+ """
+ mutation {
+ createDummyDtoNoOutput(input: {foo: "A new one", bar: 3, clientMutationId: "myId"}) {
+ dummyDtoNoOutput {
+ id
+ }
+ clientMutationId
+ }
+ }
+ """
+ Then the response status code should be 200
+ And the response should be in JSON
+ And the header "Content-Type" should be equal to "application/json"
+ And the JSON should be equal to:
+ """
+ {
+ "errors": [
+ {
+ "message": "Cannot query field \"id\" on type \"DummyDtoNoOutput\".",
+ "extensions": {
+ "category": "graphql"
+ },
+ "locations": [
+ {
+ "line": 4,
+ "column": 7
+ }
+ ]
+ }
+ ]
+ }
+ """
+
+ Scenario: Cannot create an item with input fields using disabled inputClass
+ When I send the following GraphQL request:
+ """
+ mutation {
+ createDummyDtoNoInput(input: {lorem: "A new one", ipsum: 3, clientMutationId: "myId"}) {
+ clientMutationId
+ }
+ }
+ """
+ Then the response status code should be 200
+ And the response should be in JSON
+ And the header "Content-Type" should be equal to "application/json"
+ And the JSON should be equal to:
+ """
+ {
+ "errors": [
+ {
+ "message": "Field \"lorem\" is not defined by type createDummyDtoNoInputInput.",
+ "extensions": {
+ "category": "graphql"
+ },
+ "locations": [
+ {
+ "line": 2,
+ "column": 33
+ }
+ ]
+ },
+ {
+ "message": "Field \"ipsum\" is not defined by type createDummyDtoNoInputInput.",
+ "extensions": {
+ "category": "graphql"
+ },
+ "locations": [
+ {
+ "line": 2,
+ "column": 53
+ }
+ ]
+ }
+ ]
+ }
+ """
+
+ Scenario: Create an item with empty input fields using disabled inputClass (no persist done)
+ When I send the following GraphQL request:
+ """
+ mutation {
+ createDummyDtoNoInput(input: {clientMutationId: "myId"}) {
+ dummyDtoNoInput {
+ id
+ }
+ clientMutationId
+ }
+ }
+ """
+ Then the response status code should be 200
+ And the response should be in JSON
+ And the header "Content-Type" should be equal to "application/json"
+ And the JSON should be equal to:
+ """
+ {
+ "data": {
+ "createDummyDtoNoInput": {
+ "dummyDtoNoInput": null,
+ "clientMutationId": "myId"
+ }
+ }
+ }
+ """
+
+ @!mongodb
+ Scenario: Use messenger with graphql and an input where the handler gives a synchronous result
+ When I send the following GraphQL request:
+ """
+ mutation {
+ createMessengerWithInput(input: {var: "test"}) {
+ messengerWithInput { id, name }
+ }
+ }
+ """
+ Then the response status code should be 200
+ And the response should be in JSON
+ And the header "Content-Type" should be equal to "application/json"
+ And the JSON should be equal to:
+ """
+ {
+ "data": {
+ "createMessengerWithInput": {
+ "messengerWithInput": {
+ "id": "/messenger_with_inputs/1",
+ "name": "test"
+ }
+ }
+ }
+ }
+ """
diff --git a/features/graphql/mutation.feature b/features/graphql/mutation.feature
index 847ca6c0ae2..9e3431a85a6 100644
--- a/features/graphql/mutation.feature
+++ b/features/graphql/mutation.feature
@@ -328,110 +328,3 @@ Feature: GraphQL mutation support
And the response should be in JSON
And the header "Content-Type" should be equal to "application/json"
And the JSON node "errors[0].message" should be equal to "name: This value should not be blank."
-
- Scenario: Create an item using custom inputClass & disabled outputClass
- Given there are 2 dummyDtoNoOutput objects
- When I send the following GraphQL request:
- """
- mutation {
- createDummyDtoNoOutput(input: {foo: "A new one", bar: 3, clientMutationId: "myId"}) {
- dummyDtoNoOutput {
- id
- }
- clientMutationId
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON should be equal to:
- """
- {
- "errors": [
- {
- "message": "Cannot query field \"id\" on type \"DummyDtoNoOutput\".",
- "extensions": {
- "category": "graphql"
- },
- "locations": [
- {
- "line": 4,
- "column": 7
- }
- ]
- }
- ]
- }
- """
-
- Scenario: Cannot create an item with input fields using disabled inputClass
- When I send the following GraphQL request:
- """
- mutation {
- createDummyDtoNoInput(input: {lorem: "A new one", ipsum: 3, clientMutationId: "myId"}) {
- clientMutationId
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON should be equal to:
- """
- {
- "errors": [
- {
- "message": "Field \"lorem\" is not defined by type createDummyDtoNoInputInput.",
- "extensions": {
- "category": "graphql"
- },
- "locations": [
- {
- "line": 2,
- "column": 33
- }
- ]
- },
- {
- "message": "Field \"ipsum\" is not defined by type createDummyDtoNoInputInput.",
- "extensions": {
- "category": "graphql"
- },
- "locations": [
- {
- "line": 2,
- "column": 53
- }
- ]
- }
- ]
- }
- """
-
- Scenario: Create an item with empty input fields using disabled inputClass (no persist done)
- When I send the following GraphQL request:
- """
- mutation {
- createDummyDtoNoInput(input: {clientMutationId: "myId"}) {
- dummyDtoNoInput {
- id
- }
- clientMutationId
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON should be equal to:
- """
- {
- "data": {
- "createDummyDtoNoInput": {
- "dummyDtoNoInput": null,
- "clientMutationId": "myId"
- }
- }
- }
- """
diff --git a/features/graphql/non_resource.feature b/features/graphql/non_resource.feature
new file mode 100644
index 00000000000..909e61390b6
--- /dev/null
+++ b/features/graphql/non_resource.feature
@@ -0,0 +1,92 @@
+# TODO: FIXME: GraphQL support for non-resources is non-existent
+
+Feature: GraphQL non-resource handling
+ In order to use non-resource types
+ As a developer
+ I should be able to serialize types not mapped to an API resource.
+
+ Scenario: Get a resource containing a raw object
+ Then it is not supported
+# When I send the following GraphQL request:
+# """
+# {
+# containNonResource(id: "/contain_non_resources/1") {
+# _id
+# id
+# nested {
+# _id
+# id
+# notAResource {
+# foo
+# bar
+# }
+# }
+# notAResource {
+# foo
+# bar
+# }
+# }
+# }
+# """
+# Then the response status code should be 200
+# And the response should be in JSON
+# And the header "Content-Type" should be equal to "application/json"
+# And the JSON should be equal to:
+# """
+# {
+# "data": {
+# "containNonResource": {
+# "_id": 1,
+# "id": "/contain_non_resources/1",
+# "nested": {
+# "_id": "1-nested",
+# "id": "/contain_non_resources/1-nested",
+# "notAResource": {
+# "foo": "f2",
+# "bar": "b2"
+# }
+# },
+# "notAResource": {
+# "foo": "f1",
+# "bar": "b1"
+# }
+# }
+# }
+# }
+# """
+
+ @!mongodb
+ @createSchema
+ Scenario: Create a resource that has a non-resource relation.
+ Then it is not supported
+# When I send the following GraphQL request:
+# """
+# mutation {
+# createNonRelationResource(input: {relation: {foo: "test"}}) {
+# nonRelationResource {
+# _id
+# id
+# relation {
+# foo
+# }
+# }
+# }
+# }
+# """
+# Then the response status code should be 200
+# And the response should be in JSON
+# And the header "Content-Type" should be equal to "application/json"
+# And the JSON should be equal to:
+# """
+# {
+# "data": {
+# "nonRelationResource": {
+# "_id": 1,
+# "id": "/non_relation_resources/1",
+# "relation": {
+# "foo": "test"
+# }
+# }
+# }
+# }
+# """
diff --git a/features/hal/non_resource.feature b/features/hal/non_resource.feature
new file mode 100644
index 00000000000..320ea122b83
--- /dev/null
+++ b/features/hal/non_resource.feature
@@ -0,0 +1,45 @@
+Feature: HAL non-resource handling
+ In order to use non-resource types
+ As a developer
+ I should be able to serialize types not mapped to an API resource.
+
+ Background:
+ Given I add "Accept" header equal to "application/hal+json"
+
+ Scenario: Get a resource containing a raw object
+ When I send a "GET" request to "/contain_non_resources/1"
+ Then the response status code should be 200
+ And the response should be in JSON
+ And the header "Content-Type" should be equal to "application/hal+json; charset=utf-8"
+ And the JSON should be equal to:
+ """
+ {
+ "_links": {
+ "self": {
+ "href": "/contain_non_resources/1"
+ },
+ "nested": {
+ "href": "/contain_non_resources/1-nested"
+ }
+ },
+ "_embedded": {
+ "nested": {
+ "_links": {
+ "self": {
+ "href": "/contain_non_resources/1-nested"
+ }
+ },
+ "id": "1-nested",
+ "notAResource": {
+ "foo": "f2",
+ "bar": "b2"
+ }
+ }
+ },
+ "id": 1,
+ "notAResource": {
+ "foo": "f1",
+ "bar": "b1"
+ }
+ }
+ """
diff --git a/features/jsonapi/non_resource.feature b/features/jsonapi/non_resource.feature
new file mode 100644
index 00000000000..b4654ac0f0e
--- /dev/null
+++ b/features/jsonapi/non_resource.feature
@@ -0,0 +1,88 @@
+Feature: JSON API non-resource handling
+ In order to use non-resource types
+ As a developer
+ I should be able to serialize types not mapped to an API resource.
+
+ Background:
+ Given I add "Accept" header equal to "application/vnd.api+json"
+ And I add "Content-Type" header equal to "application/vnd.api+json"
+
+ Scenario: Get a resource containing a raw object
+ When I send a "GET" request to "/contain_non_resources/1?include=nested"
+ Then the response status code should be 200
+ And the response should be in JSON
+ And the header "Content-Type" should be equal to "application/vnd.api+json; charset=utf-8"
+ And the JSON should be valid according to the JSON API schema
+ And the JSON should be a superset of:
+ """
+ {
+ "data": {
+ "id": "/contain_non_resources/1",
+ "type": "ContainNonResource",
+ "attributes": {
+ "_id": 1,
+ "notAResource": {
+ "foo": "f1",
+ "bar": "b1"
+ }
+ },
+ "relationships": {
+ "nested": {
+ "data": {
+ "id": "/contain_non_resources/1-nested",
+ "type": "ContainNonResource"
+ }
+ }
+ }
+ },
+ "included": [
+ {
+ "id": "/contain_non_resources/1-nested",
+ "type": "ContainNonResource",
+ "attributes": {
+ "_id": "1-nested",
+ "notAResource": {
+ "foo": "f2",
+ "bar": "b2"
+ }
+ }
+ }
+ ]
+ }
+ """
+
+ @!mongodb
+ @createSchema
+ Scenario: Create a resource that has a non-resource relation.
+ When I send a "POST" request to "/non_relation_resources" with body:
+ """
+ {
+ "data": {
+ "type": "NonRelationResource",
+ "attributes": {
+ "relation": {
+ "foo": "test"
+ }
+ }
+ }
+ }
+ """
+ Then the response status code should be 201
+ And the response should be in JSON
+ And the header "Content-Type" should be equal to "application/vnd.api+json; charset=utf-8"
+ And the JSON should be valid according to the JSON API schema
+ And the JSON should be a superset of:
+ """
+ {
+ "data": {
+ "id": "/non_relation_resources/1",
+ "type": "NonRelationResource",
+ "attributes": {
+ "_id": 1,
+ "relation": {
+ "foo": "test"
+ }
+ }
+ }
+ }
+ """
diff --git a/features/jsonapi/related-resouces-inclusion.feature b/features/jsonapi/related-resouces-inclusion.feature
index ebc03e8d0f5..2f00db1fac5 100644
--- a/features/jsonapi/related-resouces-inclusion.feature
+++ b/features/jsonapi/related-resouces-inclusion.feature
@@ -363,11 +363,11 @@ Feature: JSON API Inclusion of Related Resources
"dummyDate": null,
"dummyBoolean": null,
"embeddedDummy": {
+ "dummyName": null,
"dummyBoolean": null,
"dummyDate": null,
"dummyFloat": null,
"dummyPrice": null,
- "dummyName": null,
"symfony": null
},
"_id": 1,
diff --git a/features/main/input_output.feature b/features/jsonld/input_output.feature
similarity index 66%
rename from features/main/input_output.feature
rename to features/jsonld/input_output.feature
index 7c132ab80da..7d3880aa633 100644
--- a/features/main/input_output.feature
+++ b/features/jsonld/input_output.feature
@@ -1,12 +1,15 @@
-Feature: DTO input and output
+Feature: JSON-LD DTO input and output
In order to use a hypermedia API
As a client software developer
I need to be able to use DTOs on my resources as Input or Output objects.
+ Background:
+ Given I add "Accept" header equal to "application/ld+json"
+ And I add "Content-Type" header equal to "application/ld+json"
+
@createSchema
Scenario: Create a resource with a custom Input
- When I add "Content-Type" header equal to "application/ld+json"
- And I send a "POST" request to "/dummy_dto_customs" with body:
+ When I send a "POST" request to "/dummy_dto_customs" with body:
"""
{
"foo": "test",
@@ -82,8 +85,7 @@ Feature: DTO input and output
@createSchema
Scenario: Create a DummyDtoCustom object without output
- When I add "Content-Type" header equal to "application/ld+json"
- And I send a "POST" request to "/dummy_dto_custom_post_without_output" with body:
+ When I send a "POST" request to "/dummy_dto_custom_post_without_output" with body:
"""
{
"lorem": "test",
@@ -95,8 +97,7 @@ Feature: DTO input and output
@createSchema
Scenario: Create and update a DummyInputOutput
- When I add "Content-Type" header equal to "application/ld+json"
- And I send a "POST" request to "/dummy_dto_input_outputs" with body:
+ When I send a "POST" request to "/dummy_dto_input_outputs" with body:
"""
{
"foo": "test",
@@ -121,7 +122,8 @@ Feature: DTO input and output
"bat": "test"
}
"""
- When I add "Content-Type" header equal to "application/ld+json"
+ When I add "Accept" header equal to "application/ld+json"
+ And I add "Content-Type" header equal to "application/ld+json"
And I send a "PUT" request to "/dummy_dto_input_outputs/1" with body:
"""
{
@@ -151,8 +153,7 @@ Feature: DTO input and output
@!mongodb
@createSchema
Scenario: Use DTO with relations on User
- When I add "Content-Type" header equal to "application/ld+json"
- And I send a "POST" request to "/users" with body:
+ When I send a "POST" request to "/users" with body:
"""
{
"username": "soyuka",
@@ -161,7 +162,8 @@ Feature: DTO input and output
}
"""
Then the response status code should be 201
- When I add "Content-Type" header equal to "application/ld+json"
+ When I add "Accept" header equal to "application/ld+json"
+ And I add "Content-Type" header equal to "application/ld+json"
And I send a "PUT" request to "/users/recover/1" with body:
"""
{
@@ -183,60 +185,9 @@ Feature: DTO input and output
}
"""
- @createSchema
- Scenario: Retrieve an Output with GraphQl
- When I add "Content-Type" header equal to "application/ld+json"
- And I send a "POST" request to "/dummy_dto_input_outputs" with body:
- """
- {
- "foo": "test",
- "bar": 1
- }
- """
- Then the response status code should be 201
- And the JSON should be equal to:
- """
- {
- "@context": {
- "@vocab": "http://example.com/docs.jsonld#",
- "hydra": "http://www.w3.org/ns/hydra/core#",
- "id": "OutputDto/id",
- "baz": "OutputDto/baz",
- "bat": "OutputDto/bat"
- },
- "@type": "DummyDtoInputOutput",
- "@id": "/dummy_dto_input_outputs/1",
- "id": 1,
- "baz": 1,
- "bat": "test"
- }
- """
- When I send the following GraphQL request:
- """
- {
- dummyDtoInputOutput(id: "/dummy_dto_input_outputs/1") {
- _id, id, baz
- }
- }
- """
- Then the response status code should be 200
- And the JSON should be equal to:
- """
- {
- "data": {
- "dummyDtoInputOutput": {
- "_id": 1,
- "id": "/dummy_dto_input_outputs/1",
- "baz": 1
- }
- }
- }
- """
-
@createSchema
Scenario: Create a resource with no input
- When I add "Content-Type" header equal to "application/ld+json"
- And I send a "POST" request to "/dummy_dto_no_inputs" with body:
+ When I send a "POST" request to "/dummy_dto_no_inputs" with body:
"""
{
"foo": "test",
@@ -248,7 +199,6 @@ Feature: DTO input and output
@!mongodb
Scenario: Use messenger with an input where the handler gives a synchronous result
- When I add "Content-Type" header equal to "application/ld+json"
And I send a "POST" request to "/messenger_with_inputs" with body:
"""
{
@@ -271,8 +221,7 @@ Feature: DTO input and output
@!mongodb
Scenario: Use messenger with an input where the handler gives a synchronous Response result
- When I add "Content-Type" header equal to "application/ld+json"
- And I send a "POST" request to "/messenger_with_responses" with body:
+ When I send a "POST" request to "/messenger_with_responses" with body:
"""
{
"var": "test"
@@ -287,29 +236,3 @@ Feature: DTO input and output
"data": 123
}
"""
-
- @!mongodb
- Scenario: Use messenger with graphql and an input where the handler gives a synchronous result
- When I send the following GraphQL request:
- """
- mutation {
- createMessengerWithInput(input: {var: "test"}) {
- messengerWithInput { id, name }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the JSON should be equal to:
- """
- {
- "data": {
- "createMessengerWithInput": {
- "messengerWithInput": {
- "id": "/messenger_with_inputs/1",
- "name": "test"
- }
- }
- }
- }
- """
diff --git a/features/jsonld/non_resource.feature b/features/jsonld/non_resource.feature
new file mode 100644
index 00000000000..e967e44f3e3
--- /dev/null
+++ b/features/jsonld/non_resource.feature
@@ -0,0 +1,64 @@
+Feature: JSON-LD non-resource handling
+ In order to use non-resource types
+ As a developer
+ I should be able to serialize types not mapped to an API resource.
+
+ Background:
+ Given I add "Accept" header equal to "application/ld+json"
+ And I add "Content-Type" header equal to "application/ld+json"
+
+ Scenario: Get a resource containing a raw object
+ When I send a "GET" request to "/contain_non_resources/1"
+ Then the response status code should be 200
+ And the response should be in JSON
+ And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
+ And the JSON should be equal to:
+ """
+ {
+ "@context": "/contexts/ContainNonResource",
+ "@id": "/contain_non_resources/1",
+ "@type": "ContainNonResource",
+ "id": 1,
+ "nested": {
+ "@id": "/contain_non_resources/1-nested",
+ "@type": "ContainNonResource",
+ "id": "1-nested",
+ "nested": null,
+ "notAResource": {
+ "foo": "f2",
+ "bar": "b2"
+ }
+ },
+ "notAResource": {
+ "foo": "f1",
+ "bar": "b1"
+ }
+ }
+ """
+
+ @!mongodb
+ @createSchema
+ Scenario: Create a resource that has a non-resource relation.
+ When I send a "POST" request to "/non_relation_resources" with body:
+ """
+ {
+ "relation": {
+ "foo": "test"
+ }
+ }
+ """
+ Then the response status code should be 201
+ And the response should be in JSON
+ And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
+ And the JSON should be equal to:
+ """
+ {
+ "@context": "/contexts/NonRelationResource",
+ "@id": "/non_relation_resources/1",
+ "@type": "NonRelationResource",
+ "relation": {
+ "foo": "test"
+ },
+ "id": 1
+ }
+ """
diff --git a/features/main/non_resource.feature b/features/main/non_resource.feature
deleted file mode 100644
index 97751d396ac..00000000000
--- a/features/main/non_resource.feature
+++ /dev/null
@@ -1,54 +0,0 @@
-Feature: Non-resources handling
- In order to handle use non-resource types
- As a developer
- I should be able serialize types not mapped to an API resource.
-
- Scenario: Get a resource containing a raw object
- When I send a "GET" request to "/contain_non_resources/1"
- Then the JSON should be equal to:
- """
- {
- "@context": "/contexts/ContainNonResource",
- "@id": "/contain_non_resources/1",
- "@type": "ContainNonResource",
- "id": 1,
- "nested": {
- "@id": "/contain_non_resources/1-nested",
- "@type": "ContainNonResource",
- "id": "1-nested",
- "nested": null,
- "notAResource": {
- "foo": "f2",
- "bar": "b2"
- }
- },
- "notAResource": {
- "foo": "f1",
- "bar": "b1"
- }
- }
- """
-
- @!mongodb
- @createSchema
- Scenario: Create a resource that has a non-resource relation.
- When I add "Content-Type" header equal to "application/ld+json"
- And I send a "POST" request to "/non_relation_resources" with body:
- """
- {"relation": {"foo": "test"}}
- """
- Then the response status code should be 201
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be equal to:
- """
- {
- "@context": "/contexts/NonRelationResource",
- "@id": "/non_relation_resources/1",
- "@type": "NonRelationResource",
- "relation": {
- "foo": "test"
- },
- "id": 1
- }
- """
diff --git a/src/Bridge/Elasticsearch/Serializer/ItemNormalizer.php b/src/Bridge/Elasticsearch/Serializer/ItemNormalizer.php
index 7dd977ffe65..c5bc46ffedf 100644
--- a/src/Bridge/Elasticsearch/Serializer/ItemNormalizer.php
+++ b/src/Bridge/Elasticsearch/Serializer/ItemNormalizer.php
@@ -14,9 +14,9 @@
namespace ApiPlatform\Core\Bridge\Elasticsearch\Serializer;
use ApiPlatform\Core\Bridge\Elasticsearch\Api\IdentifierExtractorInterface;
-use ApiPlatform\Core\Exception\RuntimeException;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
+use Symfony\Component\Serializer\Exception\LogicException;
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorResolverInterface;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
@@ -67,15 +67,18 @@ public function denormalize($data, $class, $format = null, array $context = [])
*/
public function supportsNormalization($data, $format = null): bool
{
- return false;
+ // prevent the use of lower priority normalizers (e.g. serializer.normalizer.object) for this format
+ return self::FORMAT === $format;
}
/**
* {@inheritdoc}
+ *
+ * @throws LogicException
*/
public function normalize($object, $format = null, array $context = [])
{
- throw new RuntimeException(sprintf('%s is a read-only format.', self::FORMAT));
+ throw new LogicException(sprintf('%s is a write-only format.', self::FORMAT));
}
/**
diff --git a/src/Bridge/Symfony/Bundle/Resources/config/api.xml b/src/Bridge/Symfony/Bundle/Resources/config/api.xml
index e9536d7e6ef..458a990747c 100644
--- a/src/Bridge/Symfony/Bundle/Resources/config/api.xml
+++ b/src/Bridge/Symfony/Bundle/Resources/config/api.xml
@@ -103,34 +103,10 @@
false
-
+
-
-
-
-
-
-
-
-
-
- %api_platform.allow_plain_identifiers%
- null
-
-
- true
-
-
-
-
-
-
-
-
-
-
diff --git a/src/Bridge/Symfony/Bundle/Resources/config/elasticsearch.xml b/src/Bridge/Symfony/Bundle/Resources/config/elasticsearch.xml
index 418a1c604d0..f2049f82875 100644
--- a/src/Bridge/Symfony/Bundle/Resources/config/elasticsearch.xml
+++ b/src/Bridge/Symfony/Bundle/Resources/config/elasticsearch.xml
@@ -56,7 +56,7 @@
-
+
diff --git a/src/Bridge/Symfony/Bundle/Resources/config/graphql.xml b/src/Bridge/Symfony/Bundle/Resources/config/graphql.xml
index 19ed3520214..1d156febbeb 100644
--- a/src/Bridge/Symfony/Bundle/Resources/config/graphql.xml
+++ b/src/Bridge/Symfony/Bundle/Resources/config/graphql.xml
@@ -82,26 +82,15 @@
false
-
+
-
-
-
+
+
-
-
-
-
-
- %api_platform.allow_plain_identifiers%
- null
-
-
- true
-
+
diff --git a/src/Bridge/Symfony/Bundle/Resources/config/hal.xml b/src/Bridge/Symfony/Bundle/Resources/config/hal.xml
index f20c624eb3d..ce96df6c777 100644
--- a/src/Bridge/Symfony/Bundle/Resources/config/hal.xml
+++ b/src/Bridge/Symfony/Bundle/Resources/config/hal.xml
@@ -41,26 +41,15 @@
false
-
+
-
-
-
+
+
-
-
-
-
- null
- false
-
-
-
- true
-
+
diff --git a/src/Bridge/Symfony/Bundle/Resources/config/jsonapi.xml b/src/Bridge/Symfony/Bundle/Resources/config/jsonapi.xml
index 5cddfc7a6e3..4efa79dfa59 100644
--- a/src/Bridge/Symfony/Bundle/Resources/config/jsonapi.xml
+++ b/src/Bridge/Symfony/Bundle/Resources/config/jsonapi.xml
@@ -43,23 +43,17 @@
false
-
+
-
-
-
+
+
-
-
-
-
- true
-
+
diff --git a/src/Bridge/Symfony/Bundle/Resources/config/jsonld.xml b/src/Bridge/Symfony/Bundle/Resources/config/jsonld.xml
index 19d6227e158..f1de0a7a23c 100644
--- a/src/Bridge/Symfony/Bundle/Resources/config/jsonld.xml
+++ b/src/Bridge/Symfony/Bundle/Resources/config/jsonld.xml
@@ -29,25 +29,16 @@
false
-
+
-
-
-
-
+
+
-
-
-
-
-
-
- true
-
+
diff --git a/src/GraphQl/Serializer/ItemNormalizer.php b/src/GraphQl/Serializer/ItemNormalizer.php
index 43064b86e4a..cb9e5b829c0 100644
--- a/src/GraphQl/Serializer/ItemNormalizer.php
+++ b/src/GraphQl/Serializer/ItemNormalizer.php
@@ -33,7 +33,7 @@ final class ItemNormalizer extends BaseItemNormalizer
/**
* {@inheritdoc}
*/
- public function supportsNormalization($data, $format = null, array $context = [])
+ public function supportsNormalization($data, $format = null, array $context = []): bool
{
return self::FORMAT === $format && parent::supportsNormalization($data, $format, $context);
}
@@ -45,7 +45,7 @@ public function supportsNormalization($data, $format = null, array $context = []
*/
public function normalize($object, $format = null, array $context = [])
{
- if (!$this->handleNonResource && null !== $outputClass = $this->getOutputClass($this->getObjectClass($object), $context)) {
+ if (null !== $outputClass = $this->getOutputClass($this->getObjectClass($object), $context)) {
return parent::normalize($object, $format, $context);
}
@@ -54,15 +54,6 @@ public function normalize($object, $format = null, array $context = [])
throw new UnexpectedValueException('Expected data to be an array');
}
- if ($this->handleNonResource) {
- // when using an output class, get the IRI from the resource
- if (isset($context['api_resource']) && isset($data['id'])) {
- $data['_id'] = $data['id'];
- $data['id'] = $this->iriConverter->getIriFromItem($context['api_resource']);
- unset($context['api_resource']);
- }
- }
-
$data[self::ITEM_KEY] = serialize($object); // calling serialize prevent weird normalization process done by Webonyx's GraphQL PHP
return $data;
@@ -80,7 +71,7 @@ protected function normalizeCollectionOfRelations(PropertyMetadata $propertyMeta
/**
* {@inheritdoc}
*/
- public function supportsDenormalization($data, $type, $format = null, array $context = [])
+ public function supportsDenormalization($data, $type, $format = null, array $context = []): bool
{
return self::FORMAT === $format && parent::supportsDenormalization($data, $type, $format, $context);
}
@@ -103,7 +94,7 @@ protected function getAllowedAttributes($classOrObject, array $context, $attribu
/**
* {@inheritdoc}
*/
- protected function setAttributeValue($object, $attribute, $value, $format = null, array $context = [])
+ protected function setAttributeValue($object, $attribute, $value, $format = null, array $context = []): void
{
if ('_id' === $attribute) {
$attribute = 'id';
diff --git a/src/GraphQl/Serializer/ObjectNormalizer.php b/src/GraphQl/Serializer/ObjectNormalizer.php
new file mode 100644
index 00000000000..af40b9fcc29
--- /dev/null
+++ b/src/GraphQl/Serializer/ObjectNormalizer.php
@@ -0,0 +1,82 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace ApiPlatform\Core\GraphQl\Serializer;
+
+use ApiPlatform\Core\Api\IriConverterInterface;
+use Symfony\Component\Serializer\Exception\UnexpectedValueException;
+use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface;
+use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
+
+/**
+ * Decorates the output with GraphQL metadata when appropriate, but otherwise just
+ * passes through to the decorated normalizer.
+ */
+final class ObjectNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface
+{
+ public const FORMAT = 'graphql';
+ public const ITEM_KEY = '#item';
+
+ private $decorated;
+ private $iriConverter;
+
+ public function __construct(NormalizerInterface $decorated, IriConverterInterface $iriConverter)
+ {
+ $this->decorated = $decorated;
+ $this->iriConverter = $iriConverter;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supportsNormalization($data, $format = null, array $context = []): bool
+ {
+ return self::FORMAT === $format && $this->decorated->supportsNormalization($data, $format, $context);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hasCacheableSupportsMethod(): bool
+ {
+ return $this->decorated->hasCacheableSupportsMethod();
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @throws UnexpectedValueException
+ */
+ public function normalize($object, $format = null, array $context = [])
+ {
+ if (isset($context['api_resource'])) {
+ $originalResource = $context['api_resource'];
+ unset($context['api_resource']);
+ }
+
+ $data = $this->decorated->normalize($object, $format, $context);
+ if (!\is_array($data)) {
+ throw new UnexpectedValueException('Expected data to be an array');
+ }
+
+ // when using an output class, get the IRI from the resource
+ if (isset($originalResource) && isset($data['id'])) {
+ $data['_id'] = $data['id'];
+ $data['id'] = $this->iriConverter->getIriFromItem($originalResource);
+ }
+
+ $data[self::ITEM_KEY] = serialize($object); // calling serialize prevent weird normalization process done by Webonyx's GraphQL PHP
+
+ return $data;
+ }
+}
diff --git a/src/Hal/Serializer/ItemNormalizer.php b/src/Hal/Serializer/ItemNormalizer.php
index 9e628c7e41e..e75db6785d8 100644
--- a/src/Hal/Serializer/ItemNormalizer.php
+++ b/src/Hal/Serializer/ItemNormalizer.php
@@ -38,7 +38,7 @@ final class ItemNormalizer extends AbstractItemNormalizer
/**
* {@inheritdoc}
*/
- public function supportsNormalization($data, $format = null, array $context = [])
+ public function supportsNormalization($data, $format = null, array $context = []): bool
{
return self::FORMAT === $format && parent::supportsNormalization($data, $format, $context);
}
@@ -48,7 +48,7 @@ public function supportsNormalization($data, $format = null, array $context = []
*/
public function normalize($object, $format = null, array $context = [])
{
- if (!$this->handleNonResource && null !== $outputClass = $this->getOutputClass($this->getObjectClass($object), $context)) {
+ if (null !== $outputClass = $this->getOutputClass($this->getObjectClass($object), $context)) {
return parent::normalize($object, $format, $context);
}
@@ -56,43 +56,17 @@ public function normalize($object, $format = null, array $context = [])
$context['cache_key'] = $this->getHalCacheKey($format, $context);
}
- if ($this->handleNonResource) {
- if (isset($context['api_resource'])) {
- $originalResource = $context['api_resource'];
- unset($context['api_resource']);
- }
-
- $rawData = parent::normalize($object, $format, $context);
- if (!\is_array($rawData)) {
- return $rawData;
- }
-
- if (!isset($originalResource)) {
- return $rawData;
- }
-
- $metadataData = [
- '_links' => [
- 'self' => [
- 'href' => $this->iriConverter->getIriFromItem($originalResource),
- ],
- ],
- ];
-
- return $metadataData + $rawData;
- }
-
$resourceClass = $this->resourceClassResolver->getResourceClass($object, $context['resource_class'] ?? null, true);
$context = $this->initContext($resourceClass, $context);
$context['iri'] = $this->iriConverter->getIriFromItem($object);
$context['api_normalize'] = true;
- $rawData = parent::normalize($object, $format, $context);
- if (!\is_array($rawData)) {
- return $rawData;
+ $data = parent::normalize($object, $format, $context);
+ if (!\is_array($data)) {
+ return $data;
}
- $metadataData = [
+ $metadata = [
'_links' => [
'self' => [
'href' => $context['iri'],
@@ -100,18 +74,19 @@ public function normalize($object, $format = null, array $context = [])
],
];
$components = $this->getComponents($object, $format, $context);
- $metadataData = $this->populateRelation($metadataData, $object, $format, $context, $components, 'links');
- $metadataData = $this->populateRelation($metadataData, $object, $format, $context, $components, 'embedded');
+ $metadata = $this->populateRelation($metadata, $object, $format, $context, $components, 'links');
+ $metadata = $this->populateRelation($metadata, $object, $format, $context, $components, 'embedded');
- return $metadataData + $rawData;
+ return $metadata + $data;
}
/**
* {@inheritdoc}
*/
- public function supportsDenormalization($data, $type, $format = null, array $context = [])
+ public function supportsDenormalization($data, $type, $format = null, array $context = []): bool
{
- return false;
+ // prevent the use of lower priority normalizers (e.g. serializer.normalizer.object) for this format
+ return self::FORMAT === $format;
}
/**
@@ -127,7 +102,7 @@ public function denormalize($data, $class, $format = null, array $context = [])
/**
* {@inheritdoc}
*/
- protected function getAttributes($object, $format = null, array $context)
+ protected function getAttributes($object, $format = null, array $context): array
{
return $this->getComponents($object, $format, $context)['states'];
}
diff --git a/src/Hal/Serializer/ObjectNormalizer.php b/src/Hal/Serializer/ObjectNormalizer.php
new file mode 100644
index 00000000000..2c959e1355b
--- /dev/null
+++ b/src/Hal/Serializer/ObjectNormalizer.php
@@ -0,0 +1,103 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace ApiPlatform\Core\Hal\Serializer;
+
+use ApiPlatform\Core\Api\IriConverterInterface;
+use Symfony\Component\Serializer\Exception\LogicException;
+use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface;
+use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
+use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
+
+/**
+ * Decorates the output with JSON HAL metadata when appropriate, but otherwise
+ * just passes through to the decorated normalizer.
+ */
+final class ObjectNormalizer implements NormalizerInterface, DenormalizerInterface, CacheableSupportsMethodInterface
+{
+ public const FORMAT = 'jsonhal';
+
+ private $decorated;
+ private $iriConverter;
+
+ public function __construct(NormalizerInterface $decorated, IriConverterInterface $iriConverter)
+ {
+ $this->decorated = $decorated;
+ $this->iriConverter = $iriConverter;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supportsNormalization($data, $format = null, array $context = []): bool
+ {
+ return self::FORMAT === $format && $this->decorated->supportsNormalization($data, $format, $context);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hasCacheableSupportsMethod(): bool
+ {
+ return $this->decorated->hasCacheableSupportsMethod();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function normalize($object, $format = null, array $context = [])
+ {
+ if (isset($context['api_resource'])) {
+ $originalResource = $context['api_resource'];
+ unset($context['api_resource']);
+ }
+
+ $data = $this->decorated->normalize($object, $format, $context);
+ if (!\is_array($data)) {
+ return $data;
+ }
+
+ if (!isset($originalResource)) {
+ return $data;
+ }
+
+ $metadata = [
+ '_links' => [
+ 'self' => [
+ 'href' => $this->iriConverter->getIriFromItem($originalResource),
+ ],
+ ],
+ ];
+
+ return $metadata + $data;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supportsDenormalization($data, $type, $format = null, array $context = []): bool
+ {
+ // prevent the use of lower priority normalizers (e.g. serializer.normalizer.object) for this format
+ return self::FORMAT === $format;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @throws LogicException
+ */
+ public function denormalize($data, $class, $format = null, array $context = [])
+ {
+ throw new LogicException(sprintf('%s is a read-only format.', self::FORMAT));
+ }
+}
diff --git a/src/JsonApi/Serializer/ItemNormalizer.php b/src/JsonApi/Serializer/ItemNormalizer.php
index 4018c229df5..c954ed1e35d 100644
--- a/src/JsonApi/Serializer/ItemNormalizer.php
+++ b/src/JsonApi/Serializer/ItemNormalizer.php
@@ -24,6 +24,7 @@
use ApiPlatform\Core\Serializer\AbstractItemNormalizer;
use ApiPlatform\Core\Util\ClassInfoTrait;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
+use Symfony\Component\Serializer\Exception\LogicException;
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
use Symfony\Component\Serializer\Exception\RuntimeException;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
@@ -46,15 +47,15 @@ final class ItemNormalizer extends AbstractItemNormalizer
private $componentsCache = [];
- public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, ?PropertyAccessorInterface $propertyAccessor, ?NameConverterInterface $nameConverter, ResourceMetadataFactoryInterface $resourceMetadataFactory, array $defaultContext = [], iterable $dataTransformers = [], bool $handleNonResource = false)
+ public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, ?PropertyAccessorInterface $propertyAccessor, ?NameConverterInterface $nameConverter, ResourceMetadataFactoryInterface $resourceMetadataFactory, array $defaultContext = [], iterable $dataTransformers = [])
{
- parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, null, null, false, $defaultContext, $dataTransformers, $resourceMetadataFactory, $handleNonResource);
+ parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, null, null, false, $defaultContext, $dataTransformers, $resourceMetadataFactory);
}
/**
* {@inheritdoc}
*/
- public function supportsNormalization($data, $format = null, array $context = [])
+ public function supportsNormalization($data, $format = null, array $context = []): bool
{
return self::FORMAT === $format && parent::supportsNormalization($data, $format, $context);
}
@@ -64,7 +65,7 @@ public function supportsNormalization($data, $format = null, array $context = []
*/
public function normalize($object, $format = null, array $context = [])
{
- if (!$this->handleNonResource && null !== $outputClass = $this->getOutputClass($this->getObjectClass($object), $context)) {
+ if (null !== $outputClass = $this->getOutputClass($this->getObjectClass($object), $context)) {
return parent::normalize($object, $format, $context);
}
@@ -72,46 +73,9 @@ public function normalize($object, $format = null, array $context = [])
$context['cache_key'] = $this->getJsonApiCacheKey($format, $context);
}
- if ($this->handleNonResource) {
- if (isset($context['api_resource'])) {
- $originalResource = $context['api_resource'];
- unset($context['api_resource']);
- }
-
- $attributesData = parent::normalize($object, $format, $context);
- if (!\is_array($attributesData)) {
- return $attributesData;
- }
-
- if (isset($context['api_attribute'])) {
- return $attributesData;
- }
-
- if (isset($originalResource)) {
- $resourceClass = $this->resourceClassResolver->getResourceClass($originalResource, $context['resource_class'] ?? null, true);
- $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
-
- $resourceData = [
- 'id' => $this->iriConverter->getIriFromItem($originalResource),
- 'type' => $resourceMetadata->getShortName(),
- ];
- } else {
- $resourceData = [
- 'id' => \function_exists('spl_object_id') ? spl_object_id($object) : spl_object_hash($object),
- 'type' => (new \ReflectionClass($this->getObjectClass($object)))->getShortName(),
- ];
- }
-
- if ($attributesData) {
- $resourceData['attributes'] = $attributesData;
- }
-
- return ['data' => $resourceData];
- }
-
- $attributesData = parent::normalize($object, $format, $context);
- if (!\is_array($attributesData)) {
- return $attributesData;
+ $data = parent::normalize($object, $format, $context);
+ if (!\is_array($data)) {
+ return $data;
}
$resourceClass = $this->resourceClassResolver->getResourceClass($object, $context['resource_class'] ?? null, true);
@@ -128,8 +92,8 @@ public function normalize($object, $format = null, array $context = [])
'type' => $resourceMetadata->getShortName(),
];
- if ($attributesData) {
- $resourceData['attributes'] = $attributesData;
+ if ($data) {
+ $resourceData['attributes'] = $data;
}
if ($relationshipsData) {
@@ -148,7 +112,7 @@ public function normalize($object, $format = null, array $context = [])
/**
* {@inheritdoc}
*/
- public function supportsDenormalization($data, $type, $format = null, array $context = [])
+ public function supportsDenormalization($data, $type, $format = null, array $context = []): bool
{
return self::FORMAT === $format && parent::supportsDenormalization($data, $type, $format, $context);
}
@@ -162,7 +126,7 @@ public function denormalize($data, $class, $format = null, array $context = [])
{
// Avoid issues with proxies if we populated the object
if (!isset($context[self::OBJECT_TO_POPULATE]) && isset($data['data']['id'])) {
- if (isset($context['api_allow_update']) && true !== $context['api_allow_update']) {
+ if (true !== ($context['api_allow_update'] ?? true)) {
throw new NotNormalizableValueException('Update is not allowed for this operation.');
}
@@ -172,7 +136,7 @@ public function denormalize($data, $class, $format = null, array $context = [])
);
}
- // Merge attributes and relations previous to apply parents denormalizing
+ // Merge attributes and relationships, into format expected by the parent normalizer
$dataToDenormalize = array_merge(
$data['data']['attributes'] ?? [],
$data['data']['relationships'] ?? []
@@ -189,7 +153,7 @@ public function denormalize($data, $class, $format = null, array $context = [])
/**
* {@inheritdoc}
*/
- protected function getAttributes($object, $format = null, array $context)
+ protected function getAttributes($object, $format = null, array $context): array
{
return $this->getComponents($object, $format, $context)['attributes'];
}
@@ -197,7 +161,7 @@ protected function getAttributes($object, $format = null, array $context)
/**
* {@inheritdoc}
*/
- protected function setAttributeValue($object, $attribute, $value, $format = null, array $context = [])
+ protected function setAttributeValue($object, $attribute, $value, $format = null, array $context = []): void
{
parent::setAttributeValue($object, $attribute, \is_array($value) && \array_key_exists('data', $value) ? $value['data'] : $value, $format, $context);
}
@@ -207,6 +171,7 @@ protected function setAttributeValue($object, $attribute, $value, $format = null
*
* @see http://jsonapi.org/format/#document-resource-object-linkage
*
+ * @throws LogicException
* @throws RuntimeException
* @throws NotNormalizableValueException
*/
@@ -219,7 +184,7 @@ protected function denormalizeRelation(string $attributeName, PropertyMetadata $
if ($this->serializer instanceof DenormalizerInterface) {
return $this->serializer->denormalize($value, $className, $format, $context);
}
- throw new RuntimeException(sprintf('The injected serializer must be an instance of "%s".', DenormalizerInterface::class));
+ throw new LogicException(sprintf('The injected serializer must be an instance of "%s".', DenormalizerInterface::class));
}
if (!\is_array($value) || !isset($value['id'], $value['type'])) {
@@ -238,7 +203,7 @@ protected function denormalizeRelation(string $attributeName, PropertyMetadata $
*
* @see http://jsonapi.org/format/#document-resource-object-linkage
*
- * @throws RuntimeException
+ * @throws LogicException
*/
protected function normalizeRelation(PropertyMetadata $propertyMetadata, $relatedObject, string $resourceClass, ?string $format, array $context)
{
@@ -251,7 +216,7 @@ protected function normalizeRelation(PropertyMetadata $propertyMetadata, $relate
if ($this->serializer instanceof NormalizerInterface) {
return $this->serializer->normalize($relatedObject, $format, $context);
}
- throw new RuntimeException(sprintf('The injected serializer must be an instance of "%s".', NormalizerInterface::class));
+ throw new LogicException(sprintf('The injected serializer must be an instance of "%s".', NormalizerInterface::class));
}
} else {
$iri = $this->iriConverter->getIriFromItem($relatedObject);
@@ -260,15 +225,11 @@ protected function normalizeRelation(PropertyMetadata $propertyMetadata, $relate
$context['resources'][$iri] = $iri;
}
if (isset($context['api_included'])) {
- $context['api_sub_level'] = true;
-
if (!$this->serializer instanceof NormalizerInterface) {
- throw new RuntimeException(sprintf('The injected serializer must be an instance of "%s".', NormalizerInterface::class));
+ throw new LogicException(sprintf('The injected serializer must be an instance of "%s".', NormalizerInterface::class));
}
- $data = $this->serializer->normalize($relatedObject, $format, $context);
- unset($context['api_sub_level']);
- return $data;
+ return $this->serializer->normalize($relatedObject, $format, $context);
}
}
@@ -281,7 +242,7 @@ protected function normalizeRelation(PropertyMetadata $propertyMetadata, $relate
/**
* {@inheritdoc}
*/
- protected function isAllowedAttribute($classOrObject, $attribute, $format = null, array $context = [])
+ protected function isAllowedAttribute($classOrObject, $attribute, $format = null, array $context = []): bool
{
return preg_match('/^\\w[-\\w_]*$/', $attribute) && parent::isAllowedAttribute($classOrObject, $attribute, $format, $context);
}
diff --git a/src/JsonApi/Serializer/ObjectNormalizer.php b/src/JsonApi/Serializer/ObjectNormalizer.php
new file mode 100644
index 00000000000..5066b8429fd
--- /dev/null
+++ b/src/JsonApi/Serializer/ObjectNormalizer.php
@@ -0,0 +1,98 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace ApiPlatform\Core\JsonApi\Serializer;
+
+use ApiPlatform\Core\Api\IriConverterInterface;
+use ApiPlatform\Core\Api\ResourceClassResolverInterface;
+use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
+use ApiPlatform\Core\Util\ClassInfoTrait;
+use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface;
+use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
+
+/**
+ * Decorates the output with JSON API metadata when appropriate, but otherwise
+ * just passes through to the decorated normalizer.
+ */
+final class ObjectNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface
+{
+ use ClassInfoTrait;
+
+ public const FORMAT = 'jsonapi';
+
+ private $decorated;
+ private $iriConverter;
+ private $resourceClassResolver;
+ private $resourceMetadataFactory;
+
+ public function __construct(NormalizerInterface $decorated, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, ResourceMetadataFactoryInterface $resourceMetadataFactory)
+ {
+ $this->decorated = $decorated;
+ $this->iriConverter = $iriConverter;
+ $this->resourceClassResolver = $resourceClassResolver;
+ $this->resourceMetadataFactory = $resourceMetadataFactory;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supportsNormalization($data, $format = null, array $context = []): bool
+ {
+ return self::FORMAT === $format && $this->decorated->supportsNormalization($data, $format, $context);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hasCacheableSupportsMethod(): bool
+ {
+ return $this->decorated->hasCacheableSupportsMethod();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function normalize($object, $format = null, array $context = [])
+ {
+ if (isset($context['api_resource'])) {
+ $originalResource = $context['api_resource'];
+ unset($context['api_resource']);
+ }
+
+ $data = $this->decorated->normalize($object, $format, $context);
+ if (!\is_array($data) || isset($context['api_attribute'])) {
+ return $data;
+ }
+
+ if (isset($originalResource)) {
+ $resourceClass = $this->resourceClassResolver->getResourceClass($originalResource, $context['resource_class'] ?? null, true);
+ $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
+
+ $resourceData = [
+ 'id' => $this->iriConverter->getIriFromItem($originalResource),
+ 'type' => $resourceMetadata->getShortName(),
+ ];
+ } else {
+ $resourceData = [
+ 'id' => \function_exists('spl_object_id') ? spl_object_id($object) : spl_object_hash($object),
+ 'type' => (new \ReflectionClass($this->getObjectClass($object)))->getShortName(),
+ ];
+ }
+
+ if ($data) {
+ $resourceData['attributes'] = $data;
+ }
+
+ return ['data' => $resourceData];
+ }
+}
diff --git a/src/JsonLd/Serializer/ItemNormalizer.php b/src/JsonLd/Serializer/ItemNormalizer.php
index ba25688eed9..7d147a42b1e 100644
--- a/src/JsonLd/Serializer/ItemNormalizer.php
+++ b/src/JsonLd/Serializer/ItemNormalizer.php
@@ -43,9 +43,9 @@ final class ItemNormalizer extends AbstractItemNormalizer
private $contextBuilder;
- public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, ContextBuilderInterface $contextBuilder, PropertyAccessorInterface $propertyAccessor = null, NameConverterInterface $nameConverter = null, ClassMetadataFactoryInterface $classMetadataFactory = null, array $defaultContext = [], iterable $dataTransformers = [], bool $handleNonResource = false)
+ public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, ContextBuilderInterface $contextBuilder, PropertyAccessorInterface $propertyAccessor = null, NameConverterInterface $nameConverter = null, ClassMetadataFactoryInterface $classMetadataFactory = null, array $defaultContext = [], iterable $dataTransformers = [])
{
- parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, $classMetadataFactory, null, false, $defaultContext, $dataTransformers, $resourceMetadataFactory, $handleNonResource);
+ parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, $classMetadataFactory, null, false, $defaultContext, $dataTransformers, $resourceMetadataFactory);
$this->contextBuilder = $contextBuilder;
}
@@ -53,7 +53,7 @@ public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFa
/**
* {@inheritdoc}
*/
- public function supportsNormalization($data, $format = null, array $context = [])
+ public function supportsNormalization($data, $format = null, array $context = []): bool
{
return self::FORMAT === $format && parent::supportsNormalization($data, $format, $context);
}
@@ -65,56 +65,33 @@ public function supportsNormalization($data, $format = null, array $context = []
*/
public function normalize($object, $format = null, array $context = [])
{
- if (!$this->handleNonResource && null !== $outputClass = $this->getOutputClass($this->getObjectClass($object), $context)) {
+ if (null !== $outputClass = $this->getOutputClass($this->getObjectClass($object), $context)) {
return parent::normalize($object, $format, $context);
}
- if ($this->handleNonResource) {
- if (!($context['api_normalize'] ?? false)) {
- throw new LogicException('"api_normalize" must be set to true in context to normalize non-resource');
- }
-
- if (isset($context['api_resource'])) {
- $context['output']['iri'] = $this->iriConverter->getIriFromItem($context['api_resource']);
- }
-
- $data = $this->createJsonLdContext($this->contextBuilder, $object, $context);
-
- if (isset($context['api_resource'])) {
- unset($context['api_resource']);
- }
-
- $rawData = parent::normalize($object, $format, $context);
- if (!\is_array($rawData)) {
- return $rawData;
- }
-
- return $data + $rawData;
- }
-
$resourceClass = $this->resourceClassResolver->getResourceClass($object, $context['resource_class'] ?? null, true);
$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
- $data = $this->addJsonLdContext($this->contextBuilder, $resourceClass, $context);
+ $metadata = $this->addJsonLdContext($this->contextBuilder, $resourceClass, $context);
// Use resolved resource class instead of given resource class to support multiple inheritance child types
$context['resource_class'] = $resourceClass;
$context['iri'] = $this->iriConverter->getIriFromItem($object);
- $rawData = parent::normalize($object, $format, $context);
- if (!\is_array($rawData)) {
- return $rawData;
+ $data = parent::normalize($object, $format, $context);
+ if (!\is_array($data)) {
+ return $data;
}
- $data['@id'] = $context['iri'];
- $data['@type'] = $resourceMetadata->getIri() ?: $resourceMetadata->getShortName();
+ $metadata['@id'] = $context['iri'];
+ $metadata['@type'] = $resourceMetadata->getIri() ?: $resourceMetadata->getShortName();
- return $data + $rawData;
+ return $metadata + $data;
}
/**
* {@inheritdoc}
*/
- public function supportsDenormalization($data, $type, $format = null, array $context = [])
+ public function supportsDenormalization($data, $type, $format = null, array $context = []): bool
{
return self::FORMAT === $format && parent::supportsDenormalization($data, $type, $format, $context);
}
@@ -128,7 +105,7 @@ public function denormalize($data, $class, $format = null, array $context = [])
{
// Avoid issues with proxies if we populated the object
if (isset($data['@id']) && !isset($context[self::OBJECT_TO_POPULATE])) {
- if (isset($context['api_allow_update']) && true !== $context['api_allow_update']) {
+ if (true !== ($context['api_allow_update'] ?? true)) {
throw new NotNormalizableValueException('Update is not allowed for this operation.');
}
diff --git a/src/JsonLd/Serializer/ObjectNormalizer.php b/src/JsonLd/Serializer/ObjectNormalizer.php
new file mode 100644
index 00000000000..70f0e47cc56
--- /dev/null
+++ b/src/JsonLd/Serializer/ObjectNormalizer.php
@@ -0,0 +1,95 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace ApiPlatform\Core\JsonLd\Serializer;
+
+use ApiPlatform\Core\Api\IriConverterInterface;
+use ApiPlatform\Core\JsonLd\AnonymousContextBuilderInterface;
+use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
+use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface;
+use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
+
+/**
+ * Decorates the output with JSON-LD metadata when appropriate, but otherwise just
+ * passes through to the decorated normalizer.
+ */
+final class ObjectNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface
+{
+ use JsonLdContextTrait;
+
+ public const FORMAT = 'jsonld';
+
+ private $decorated;
+ private $iriConverter;
+ private $anonymousContextBuilder;
+
+ public function __construct(NormalizerInterface $decorated, IriConverterInterface $iriConverter, AnonymousContextBuilderInterface $anonymousContextBuilder)
+ {
+ $this->decorated = $decorated;
+ $this->iriConverter = $iriConverter;
+ $this->anonymousContextBuilder = $anonymousContextBuilder;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supportsNormalization($data, $format = null, array $context = []): bool
+ {
+ return self::FORMAT === $format && $this->decorated->supportsNormalization($data, $format, $context);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hasCacheableSupportsMethod(): bool
+ {
+ return $this->decorated->hasCacheableSupportsMethod();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function normalize($object, $format = null, array $context = [])
+ {
+ if (isset($context['api_resource'])) {
+ $originalResource = $context['api_resource'];
+ unset($context['api_resource']);
+ }
+
+ /*
+ * Converts the normalized data array of a resource into an IRI, if the
+ * normalized data array is empty.
+ *
+ * This is useful when traversing from a non-resource towards an attribute
+ * which is a resource, as we do not have the benefit of {@see PropertyMetadata::isReadableLink}.
+ *
+ * It must not be propagated to subresources, as {@see PropertyMetadata::isReadableLink}
+ * should take effect.
+ */
+ $context['api_empty_resource_as_iri'] = true;
+
+ $data = $this->decorated->normalize($object, $format, $context);
+ if (!\is_array($data)) {
+ return $data;
+ }
+
+ if (isset($originalResource)) {
+ $context['output']['iri'] = $this->iriConverter->getIriFromItem($originalResource);
+ $context['api_resource'] = $originalResource;
+ }
+
+ $metadata = $this->createJsonLdContext($this->anonymousContextBuilder, $object, $context);
+
+ return $metadata + $data;
+ }
+}
diff --git a/src/Serializer/AbstractItemNormalizer.php b/src/Serializer/AbstractItemNormalizer.php
index a23b95d70c5..6f6e599a50b 100644
--- a/src/Serializer/AbstractItemNormalizer.php
+++ b/src/Serializer/AbstractItemNormalizer.php
@@ -61,10 +61,9 @@ abstract class AbstractItemNormalizer extends AbstractObjectNormalizer implement
protected $itemDataProvider;
protected $allowPlainIdentifiers;
protected $dataTransformers = [];
- protected $handleNonResource;
protected $localCache = [];
- public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, PropertyAccessorInterface $propertyAccessor = null, NameConverterInterface $nameConverter = null, ClassMetadataFactoryInterface $classMetadataFactory = null, ItemDataProviderInterface $itemDataProvider = null, bool $allowPlainIdentifiers = false, array $defaultContext = [], iterable $dataTransformers = [], ResourceMetadataFactoryInterface $resourceMetadataFactory = null, bool $handleNonResource = false)
+ public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, PropertyAccessorInterface $propertyAccessor = null, NameConverterInterface $nameConverter = null, ClassMetadataFactoryInterface $classMetadataFactory = null, ItemDataProviderInterface $itemDataProvider = null, bool $allowPlainIdentifiers = false, array $defaultContext = [], iterable $dataTransformers = [], ResourceMetadataFactoryInterface $resourceMetadataFactory = null)
{
if (!isset($defaultContext['circular_reference_handler'])) {
$defaultContext['circular_reference_handler'] = function ($object) {
@@ -86,7 +85,6 @@ public function __construct(PropertyNameCollectionFactoryInterface $propertyName
$this->allowPlainIdentifiers = $allowPlainIdentifiers;
$this->dataTransformers = $dataTransformers;
$this->resourceMetadataFactory = $resourceMetadataFactory;
- $this->handleNonResource = $handleNonResource;
}
/**
@@ -98,10 +96,6 @@ public function supportsNormalization($data, $format = null, array $context = []
return false;
}
- if ($this->handleNonResource) {
- return $context['api_normalize'] ?? false;
- }
-
return $this->resourceClassResolver->isResourceClass($this->getObjectClass($data));
}
@@ -110,7 +104,7 @@ public function supportsNormalization($data, $format = null, array $context = []
*/
public function hasCacheableSupportsMethod(): bool
{
- return !$this->handleNonResource;
+ return true;
}
/**
@@ -120,37 +114,47 @@ public function hasCacheableSupportsMethod(): bool
*/
public function normalize($object, $format = null, array $context = [])
{
- if (!$this->handleNonResource && $object !== $transformed = $this->transformOutput($object, $context)) {
+ if ($object !== $transformed = $this->transformOutput($object, $context)) {
if (!$this->serializer instanceof NormalizerInterface) {
- throw new RuntimeException('Cannot normalize the transformed value because the injected serializer is not a normalizer');
+ throw new LogicException('Cannot normalize the transformed value because the injected serializer is not a normalizer');
}
$context['api_normalize'] = true;
$context['api_resource'] = $object;
+ unset($context['output']);
return $this->serializer->normalize($transformed, $format, $context);
}
- if ($this->handleNonResource) {
- if (!($context['api_normalize'] ?? false)) {
- throw new LogicException('"api_normalize" must be set to true in context to normalize non-resource');
- }
-
- $context = $this->initContext($this->getObjectClass($object), $context);
-
- return parent::normalize($object, $format, $context);
- }
-
$resourceClass = $this->resourceClassResolver->getResourceClass($object, $context['resource_class'] ?? null, true);
$context = $this->initContext($resourceClass, $context);
$context['api_normalize'] = true;
+ $context['iri'] ?? $context['iri'] = $this->iriConverter->getIriFromItem($object);
+
+ /*
+ * When true, converts the normalized data array of a resource into an
+ * IRI, if the normalized data array is empty.
+ *
+ * This is useful when traversing from a non-resource towards an attribute
+ * which is a resource, as we do not have the benefit of {@see PropertyMetadata::isReadableLink}.
+ *
+ * It must not be propagated to subresources, as {@see PropertyMetadata::isReadableLink}
+ * should take effect.
+ */
+ $emptyResourceAsIri = $context['api_empty_resource_as_iri'] ?? false;
+ unset($context['api_empty_resource_as_iri']);
if (isset($context['resources'])) {
- $resource = $context['iri'] ?? $this->iriConverter->getIriFromItem($object);
+ $resource = $context['iri'];
$context['resources'][$resource] = $resource;
}
- return parent::normalize($object, $format, $context);
+ $data = parent::normalize($object, $format, $context);
+ if ($emptyResourceAsIri && \is_array($data) && 0 === \count($data)) {
+ return $context['iri'];
+ }
+
+ return $data;
}
/**
@@ -158,10 +162,6 @@ public function normalize($object, $format = null, array $context = [])
*/
public function supportsDenormalization($data, $type, $format = null, array $context = [])
{
- if ($this->handleNonResource) {
- return $context['api_denormalize'] ?? false;
- }
-
return $this->localCache[$type] ?? $this->localCache[$type] = $this->resourceClassResolver->isResourceClass($type);
}
@@ -410,7 +410,7 @@ protected function denormalizeCollection(string $attribute, PropertyMetadata $pr
/**
* Denormalizes a relation.
*
- * @throws RuntimeException
+ * @throws LogicException
* @throws UnexpectedValueException
*
* @return object|null
@@ -436,7 +436,7 @@ protected function denormalizeRelation(string $attributeName, PropertyMetadata $
try {
if (!$this->serializer instanceof DenormalizerInterface) {
- throw new RuntimeException(sprintf('The injected serializer must be an instance of "%s".', DenormalizerInterface::class));
+ throw new LogicException(sprintf('The injected serializer must be an instance of "%s".', DenormalizerInterface::class));
}
return $this->serializer->denormalize($value, $className, $format, $context);
@@ -520,7 +520,7 @@ protected function createRelationSerializationContext(string $resourceClass, arr
* {@inheritdoc}
*
* @throws NoSuchPropertyException
- * @throws RuntimeException
+ * @throws LogicException
*/
protected function getAttributeValue($object, $attribute, $format = null, array $context = [])
{
@@ -561,7 +561,7 @@ protected function getAttributeValue($object, $attribute, $format = null, array
unset($context['resource_class']);
if (!$this->serializer instanceof NormalizerInterface) {
- throw new RuntimeException(sprintf('The injected serializer must be an instance of "%s".', NormalizerInterface::class));
+ throw new LogicException(sprintf('The injected serializer must be an instance of "%s".', NormalizerInterface::class));
}
return $this->serializer->normalize($attributeValue, $format, $context);
@@ -585,7 +585,7 @@ protected function normalizeCollectionOfRelations(PropertyMetadata $propertyMeta
/**
* Normalizes a relation as an object if is a Link or as an URI.
*
- * @throws RuntimeException
+ * @throws LogicException
*
* @return string|array
*/
@@ -599,7 +599,7 @@ protected function normalizeRelation(PropertyMetadata $propertyMetadata, $relate
}
if (!$this->serializer instanceof NormalizerInterface) {
- throw new RuntimeException(sprintf('The injected serializer must be an instance of "%s".', NormalizerInterface::class));
+ throw new LogicException(sprintf('The injected serializer must be an instance of "%s".', NormalizerInterface::class));
}
return $this->serializer->normalize($relatedObject, $format, $context);
diff --git a/src/Serializer/ItemNormalizer.php b/src/Serializer/ItemNormalizer.php
index 0e6ab8b2c7f..f1e18b6d24d 100644
--- a/src/Serializer/ItemNormalizer.php
+++ b/src/Serializer/ItemNormalizer.php
@@ -38,9 +38,9 @@ class ItemNormalizer extends AbstractItemNormalizer
{
private $logger;
- public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, PropertyAccessorInterface $propertyAccessor = null, NameConverterInterface $nameConverter = null, ClassMetadataFactoryInterface $classMetadataFactory = null, ItemDataProviderInterface $itemDataProvider = null, bool $allowPlainIdentifiers = false, LoggerInterface $logger = null, iterable $dataTransformers = [], ResourceMetadataFactoryInterface $resourceMetadataFactory = null, bool $handleNonResource = false)
+ public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, PropertyAccessorInterface $propertyAccessor = null, NameConverterInterface $nameConverter = null, ClassMetadataFactoryInterface $classMetadataFactory = null, ItemDataProviderInterface $itemDataProvider = null, bool $allowPlainIdentifiers = false, LoggerInterface $logger = null, iterable $dataTransformers = [], ResourceMetadataFactoryInterface $resourceMetadataFactory = null)
{
- parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, $classMetadataFactory, $itemDataProvider, $allowPlainIdentifiers, [], $dataTransformers, $resourceMetadataFactory, $handleNonResource);
+ parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, $classMetadataFactory, $itemDataProvider, $allowPlainIdentifiers, [], $dataTransformers, $resourceMetadataFactory);
$this->logger = $logger ?: new NullLogger();
}
diff --git a/src/Serializer/NoOpScalarNormalizer.php b/src/Serializer/NoOpScalarNormalizer.php
deleted file mode 100644
index e4144e549d1..00000000000
--- a/src/Serializer/NoOpScalarNormalizer.php
+++ /dev/null
@@ -1,53 +0,0 @@
-
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-declare(strict_types=1);
-
-namespace ApiPlatform\Core\Serializer;
-
-use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface;
-use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
-
-/**
- * A no-op normalizer that passes through scalar values.
- *
- * When there are non-cacheable normalizers in use, and you don't need to normalize
- * scalar values, register this normalizer with a higher priority than the non-cacheable
- * normalizers. This allows caching supportsNormalization calls for scalar values.
- *
- * @internal
- */
-final class NoOpScalarNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface
-{
- /**
- * {@inheritdoc}
- */
- public function supportsNormalization($data, $format = null): bool
- {
- return is_scalar($data);
- }
-
- /**
- * {@inheritdoc}
- */
- public function normalize($object, $format = null, array $context = [])
- {
- return $object;
- }
-
- /**
- * {@inheritdoc}
- */
- public function hasCacheableSupportsMethod(): bool
- {
- return true;
- }
-}
diff --git a/tests/Bridge/Elasticsearch/Serializer/ItemNormalizerTest.php b/tests/Bridge/Elasticsearch/Serializer/ItemNormalizerTest.php
index b753e690d19..1ec70f7ccec 100644
--- a/tests/Bridge/Elasticsearch/Serializer/ItemNormalizerTest.php
+++ b/tests/Bridge/Elasticsearch/Serializer/ItemNormalizerTest.php
@@ -15,9 +15,9 @@
use ApiPlatform\Core\Bridge\Elasticsearch\Api\IdentifierExtractorInterface;
use ApiPlatform\Core\Bridge\Elasticsearch\Serializer\ItemNormalizer;
-use ApiPlatform\Core\Exception\RuntimeException;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Foo;
use PHPUnit\Framework\TestCase;
+use Symfony\Component\Serializer\Exception\LogicException;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
@@ -82,13 +82,13 @@ public function testSupportsNormalization()
{
$itemNormalizer = new ItemNormalizer($this->prophesize(IdentifierExtractorInterface::class)->reveal());
- self::assertFalse($itemNormalizer->supportsNormalization(new Foo(), ItemNormalizer::FORMAT));
+ self::assertTrue($itemNormalizer->supportsNormalization(new Foo(), ItemNormalizer::FORMAT));
}
public function testNormalize()
{
- $this->expectException(RuntimeException::class);
- $this->expectExceptionMessage(sprintf('%s is a read-only format.', ItemNormalizer::FORMAT));
+ $this->expectException(LogicException::class);
+ $this->expectExceptionMessage(sprintf('%s is a write-only format.', ItemNormalizer::FORMAT));
(new ItemNormalizer($this->prophesize(IdentifierExtractorInterface::class)->reveal()))->normalize(new Foo(), ItemNormalizer::FORMAT);
}
diff --git a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php
index f9726d07459..a2d2b5d1186 100644
--- a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php
+++ b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php
@@ -324,7 +324,7 @@ public function testDisableGraphQl()
$containerBuilderProphecy->setDefinition('api_platform.graphql.executor', Argument::type(Definition::class))->shouldNotBeCalled();
$containerBuilderProphecy->setDefinition('api_platform.graphql.schema_builder', Argument::type(Definition::class))->shouldNotBeCalled();
$containerBuilderProphecy->setDefinition('api_platform.graphql.normalizer.item', Argument::type(Definition::class))->shouldNotBeCalled();
- $containerBuilderProphecy->setDefinition('api_platform.graphql.normalizer.item.non_resource', Argument::type(Definition::class))->shouldNotBeCalled();
+ $containerBuilderProphecy->setDefinition('api_platform.graphql.normalizer.object', Argument::type(Definition::class))->shouldNotBeCalled();
$containerBuilderProphecy->setParameter('api_platform.graphql.enabled', true)->shouldNotBeCalled();
$containerBuilderProphecy->setParameter('api_platform.graphql.enabled', false)->shouldBeCalled();
$containerBuilderProphecy->setParameter('api_platform.graphql.graphiql.enabled', true)->shouldNotBeCalled();
@@ -842,8 +842,6 @@ private function getPartialContainerBuilderProphecy()
'api_platform.serializer.context_builder.filter',
'api_platform.serializer.group_filter',
'api_platform.serializer.normalizer.item',
- 'api_platform.serializer.normalizer.item.non_resource',
- 'api_platform.serializer.normalizer.no_op_scalar',
'api_platform.serializer.property_filter',
'api_platform.serializer_locator',
'api_platform.subresource_data_provider',
@@ -1019,12 +1017,12 @@ private function getBaseContainerBuilderProphecy()
'api_platform.graphql.resolver.item',
'api_platform.graphql.resolver.resource_field',
'api_platform.graphql.normalizer.item',
- 'api_platform.graphql.normalizer.item.non_resource',
+ 'api_platform.graphql.normalizer.object',
'api_platform.hal.encoder',
'api_platform.hal.normalizer.collection',
'api_platform.hal.normalizer.entrypoint',
'api_platform.hal.normalizer.item',
- 'api_platform.hal.normalizer.item.non_resource',
+ 'api_platform.hal.normalizer.object',
'api_platform.http_cache.listener.response.add_tags',
'api_platform.http_cache.listener.response.configure',
'api_platform.http_cache.purger.varnish_client',
@@ -1041,7 +1039,7 @@ private function getBaseContainerBuilderProphecy()
'api_platform.jsonld.context_builder',
'api_platform.jsonld.encoder',
'api_platform.jsonld.normalizer.item',
- 'api_platform.jsonld.normalizer.item.non_resource',
+ 'api_platform.jsonld.normalizer.object',
'api_platform.mercure.listener.response.add_link_header',
'api_platform.messenger.data_persister',
'api_platform.messenger.data_transformer',
diff --git a/tests/Fixtures/NotAResource.php b/tests/Fixtures/NotAResource.php
index b7323769792..3d14df2d93c 100644
--- a/tests/Fixtures/NotAResource.php
+++ b/tests/Fixtures/NotAResource.php
@@ -13,6 +13,8 @@
namespace ApiPlatform\Core\Tests\Fixtures;
+use Symfony\Component\Serializer\Annotation\Groups;
+
/**
* This class is not mapped as an API resource.
*
@@ -20,7 +22,14 @@
*/
class NotAResource
{
+ /**
+ * @Groups("contain_non_resource")
+ */
private $foo;
+
+ /**
+ * @Groups("contain_non_resource")
+ */
private $bar;
public function __construct($foo, $bar)
diff --git a/tests/Fixtures/TestBundle/Document/ContainNonResource.php b/tests/Fixtures/TestBundle/Document/ContainNonResource.php
index f9d988a2555..b6fcf3032a1 100644
--- a/tests/Fixtures/TestBundle/Document/ContainNonResource.php
+++ b/tests/Fixtures/TestBundle/Document/ContainNonResource.php
@@ -16,12 +16,18 @@
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Tests\Fixtures\NotAResource;
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
+use Symfony\Component\Serializer\Annotation\Groups;
/**
* Resource linked to a standard object.
*
* @ODM\Document
- * @ApiResource
+ *
+ * @ApiResource(
+ * normalizationContext={
+ * "groups"="contain_non_resource",
+ * },
+ * )
*
* @author Kévin Dunglas
*/
@@ -31,16 +37,22 @@ class ContainNonResource
* @var mixed
*
* @ODM\Id(strategy="INCREMENT", type="integer")
+ *
+ * @Groups("contain_non_resource")
*/
public $id;
/**
- * @var self
+ * @var ContainNonResource
+ *
+ * @Groups("contain_non_resource")
*/
public $nested;
/**
* @var NotAResource
+ *
+ * @Groups("contain_non_resource")
*/
public $notAResource;
}
diff --git a/tests/Fixtures/TestBundle/Document/RelatedDummy.php b/tests/Fixtures/TestBundle/Document/RelatedDummy.php
index 2f62df96cf7..04f0871ea2f 100644
--- a/tests/Fixtures/TestBundle/Document/RelatedDummy.php
+++ b/tests/Fixtures/TestBundle/Document/RelatedDummy.php
@@ -75,12 +75,6 @@ class RelatedDummy extends ParentDummy
*/
public $relatedToDummyFriend;
- public function __construct()
- {
- $this->relatedToDummyFriend = new ArrayCollection();
- $this->embeddedDummy = new EmbeddableDummy();
- }
-
/**
* @var bool A dummy bool
*
@@ -97,6 +91,12 @@ public function __construct()
*/
public $embeddedDummy;
+ public function __construct()
+ {
+ $this->relatedToDummyFriend = new ArrayCollection();
+ $this->embeddedDummy = new EmbeddableDummy();
+ }
+
public function getId()
{
return $this->id;
diff --git a/tests/Fixtures/TestBundle/Entity/ContainNonResource.php b/tests/Fixtures/TestBundle/Entity/ContainNonResource.php
index 2771f1ae87b..34fa14b5f1b 100644
--- a/tests/Fixtures/TestBundle/Entity/ContainNonResource.php
+++ b/tests/Fixtures/TestBundle/Entity/ContainNonResource.php
@@ -16,12 +16,18 @@
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Tests\Fixtures\NotAResource;
use Doctrine\ORM\Mapping as ORM;
+use Symfony\Component\Serializer\Annotation\Groups;
/**
* Resource linked to a standard object.
*
* @ORM\Entity
- * @ApiResource
+ *
+ * @ApiResource(
+ * normalizationContext={
+ * "groups"="contain_non_resource",
+ * },
+ * )
*
* @author Kévin Dunglas
*/
@@ -33,16 +39,22 @@ class ContainNonResource
* @ORM\Column(type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
+ *
+ * @Groups("contain_non_resource")
*/
public $id;
/**
- * @var self
+ * @var ContainNonResource
+ *
+ * @Groups("contain_non_resource")
*/
public $nested;
/**
* @var NotAResource
+ *
+ * @Groups("contain_non_resource")
*/
public $notAResource;
}
diff --git a/tests/Fixtures/TestBundle/Entity/RelatedDummy.php b/tests/Fixtures/TestBundle/Entity/RelatedDummy.php
index b3af4e6c637..8802cdfc054 100644
--- a/tests/Fixtures/TestBundle/Entity/RelatedDummy.php
+++ b/tests/Fixtures/TestBundle/Entity/RelatedDummy.php
@@ -76,11 +76,6 @@ class RelatedDummy extends ParentDummy
*/
public $relatedToDummyFriend;
- public function __construct()
- {
- $this->relatedToDummyFriend = new ArrayCollection();
- }
-
/**
* @var bool A dummy bool
*
@@ -97,6 +92,12 @@ public function __construct()
*/
public $embeddedDummy;
+ public function __construct()
+ {
+ $this->relatedToDummyFriend = new ArrayCollection();
+ $this->embeddedDummy = new EmbeddableDummy();
+ }
+
public function getId()
{
return $this->id;
diff --git a/tests/GraphQl/Serializer/ItemNormalizerTest.php b/tests/GraphQl/Serializer/ItemNormalizerTest.php
index 6069c25aab3..d8d1c9e9293 100644
--- a/tests/GraphQl/Serializer/ItemNormalizerTest.php
+++ b/tests/GraphQl/Serializer/ItemNormalizerTest.php
@@ -100,8 +100,7 @@ public function testNormalize()
false,
null,
[],
- null,
- false
+ null
);
$normalizer->setSerializer($serializerProphecy->reveal());
@@ -139,8 +138,7 @@ public function testDenormalize()
false,
null,
[],
- null,
- false
+ null
);
$normalizer->setSerializer($serializerProphecy->reveal());
diff --git a/tests/Hal/Serializer/ItemNormalizerTest.php b/tests/Hal/Serializer/ItemNormalizerTest.php
index 263af28b748..3ac060292f0 100644
--- a/tests/Hal/Serializer/ItemNormalizerTest.php
+++ b/tests/Hal/Serializer/ItemNormalizerTest.php
@@ -150,8 +150,7 @@ public function testNormalize()
false,
[],
[],
- null,
- false
+ null
);
$normalizer->setSerializer($serializerProphecy->reveal());
@@ -217,8 +216,7 @@ public function testNormalizeWithoutCache()
false,
[],
[],
- null,
- false
+ null
);
$normalizer->setSerializer($serializerProphecy->reveal());
@@ -297,8 +295,7 @@ public function testMaxDepth()
false,
[],
[],
- null,
- false
+ null
);
$serializer = new Serializer([$normalizer]);
$normalizer->setSerializer($serializer);
diff --git a/tests/JsonApi/Serializer/ItemNormalizerTest.php b/tests/JsonApi/Serializer/ItemNormalizerTest.php
index f04ceb2917c..741c6229e7b 100644
--- a/tests/JsonApi/Serializer/ItemNormalizerTest.php
+++ b/tests/JsonApi/Serializer/ItemNormalizerTest.php
@@ -139,8 +139,7 @@ public function testNormalize()
new ReservedAttributeNameConverter(),
$resourceMetadataFactoryProphecy->reveal(),
[],
- [],
- false
+ []
);
$normalizer->setSerializer($serializerProphecy->reveal());
@@ -180,8 +179,7 @@ public function testNormalizeIsNotAnArray()
new ReservedAttributeNameConverter(),
$resourceMetadataFactory->reveal(),
[],
- [],
- false
+ []
);
$normalizer->setSerializer(new Serializer([$normalizer]));
@@ -240,8 +238,7 @@ public function testNormalizeThrowsNoSuchPropertyException()
new ReservedAttributeNameConverter(),
$resourceMetadataFactory->reveal(),
[],
- [],
- false
+ []
);
$normalizer->normalize($foo, ItemNormalizer::FORMAT);
@@ -325,8 +322,7 @@ public function testDenormalize()
new ReservedAttributeNameConverter(),
$resourceMetadataFactory->reveal(),
[],
- [],
- false
+ []
);
$normalizer->setSerializer($serializerProphecy->reveal());
@@ -378,8 +374,7 @@ public function testDenormalizeUpdateOperationNotAllowed()
null,
$this->prophesize(ResourceMetadataFactoryInterface::class)->reveal(),
[],
- [],
- false
+ []
);
$normalizer->denormalize(
@@ -435,8 +430,7 @@ public function testDenormalizeCollectionIsNotArray()
new ReservedAttributeNameConverter(),
$resourceMetadataFactory->reveal(),
[],
- [],
- false
+ []
);
$normalizer->denormalize(
@@ -493,8 +487,7 @@ public function testDenormalizeCollectionWithInvalidKey()
new ReservedAttributeNameConverter(),
$resourceMetadataFactory->reveal(),
[],
- [],
- false
+ []
);
$normalizer->denormalize(
@@ -553,8 +546,7 @@ public function testDenormalizeRelationIsNotResourceLinkage()
new ReservedAttributeNameConverter(),
$resourceMetadataFactory->reveal(),
[],
- [],
- false
+ []
);
$normalizer->denormalize(
diff --git a/tests/Hydra/Serializer/ItemNormalizerTest.php b/tests/JsonLd/Serializer/ItemNormalizerTest.php
similarity index 98%
rename from tests/Hydra/Serializer/ItemNormalizerTest.php
rename to tests/JsonLd/Serializer/ItemNormalizerTest.php
index e8953638811..fb5b1bf984f 100644
--- a/tests/Hydra/Serializer/ItemNormalizerTest.php
+++ b/tests/JsonLd/Serializer/ItemNormalizerTest.php
@@ -11,7 +11,7 @@
declare(strict_types=1);
-namespace ApiPlatform\Core\Tests\Hydra\Serializer;
+namespace ApiPlatform\Core\Tests\JsonLd\Serializer;
use ApiPlatform\Core\Api\IriConverterInterface;
use ApiPlatform\Core\Api\ResourceClassResolverInterface;
@@ -127,8 +127,7 @@ public function testNormalize()
null,
null,
[],
- [],
- false
+ []
);
$normalizer->setSerializer($serializerProphecy->reveal());
diff --git a/tests/Serializer/AbstractItemNormalizerTest.php b/tests/Serializer/AbstractItemNormalizerTest.php
index e38287c9df2..f38d04bdd18 100644
--- a/tests/Serializer/AbstractItemNormalizerTest.php
+++ b/tests/Serializer/AbstractItemNormalizerTest.php
@@ -388,7 +388,7 @@ public function testCanDenormalizeInputClassWithDifferentFieldsThanResourceClass
(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_STRING), '', true, false))->withInitializable(true)
);
- $normalizer = new class($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $this->prophesize(IriConverterInterface::class)->reveal(), $this->prophesize(ResourceClassResolverInterface::class)->reveal(), null, null, null, null, false, [], [], null, false) extends AbstractItemNormalizer {
+ $normalizer = new class($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $this->prophesize(IriConverterInterface::class)->reveal(), $this->prophesize(ResourceClassResolverInterface::class)->reveal(), null, null, null, null, false, [], [], null) extends AbstractItemNormalizer {
};
/** @var DummyForAdditionalFieldsInput $res */
diff --git a/tests/Serializer/ItemNormalizerTest.php b/tests/Serializer/ItemNormalizerTest.php
index d46a0712c56..9aa8b4e3efe 100644
--- a/tests/Serializer/ItemNormalizerTest.php
+++ b/tests/Serializer/ItemNormalizerTest.php
@@ -105,8 +105,7 @@ public function testNormalize()
false,
null,
[],
- null,
- false
+ null
);
$normalizer->setSerializer($serializerProphecy->reveal());
@@ -144,8 +143,7 @@ public function testDenormalize()
false,
null,
[],
- null,
- false
+ null
);
$normalizer->setSerializer($serializerProphecy->reveal());
@@ -184,8 +182,7 @@ public function testDenormalizeWithIri()
false,
null,
[],
- null,
- false
+ null
);
$normalizer->setSerializer($serializerProphecy->reveal());
@@ -222,8 +219,7 @@ public function testDenormalizeWithIdAndUpdateNotAllowed()
false,
null,
[],
- null,
- false
+ null
);
$normalizer->setSerializer($serializerProphecy->reveal());
$normalizer->denormalize(['id' => '12', 'name' => 'hello'], Dummy::class, null, $context);
@@ -261,8 +257,7 @@ public function testDenormalizeWithIdAndNoResourceClass()
false,
null,
[],
- null,
- true
+ null
);
$normalizer->setSerializer($serializerProphecy->reveal());
@@ -307,8 +302,7 @@ public function testNormalizeWithDataTransformers()
false,
null,
[$dataTransformer->reveal()],
- null,
- false
+ null
);
$normalizer->setSerializer($serializerProphecy->reveal());