diff --git a/features/bootstrap/DoctrineContext.php b/features/bootstrap/DoctrineContext.php
index 47a1d773356..43eb85993ea 100644
--- a/features/bootstrap/DoctrineContext.php
+++ b/features/bootstrap/DoctrineContext.php
@@ -21,6 +21,7 @@
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\DummyCar as DummyCarDocument;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\DummyCarColor as DummyCarColorDocument;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\DummyDate as DummyDateDocument;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\DummyDtoCustom as DummyDtoCustomDocument;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\DummyDtoNoInput as DummyDtoNoInputDocument;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\DummyDtoNoOutput as DummyDtoNoOutputDocument;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\DummyFriend as DummyFriendDocument;
@@ -59,6 +60,7 @@
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyCar;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyCarColor;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyDate;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyDtoCustom;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyDtoNoInput;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyDtoNoOutput;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyFriend;
@@ -1182,6 +1184,30 @@ public function thereIsAMaxDepthDummyWithLevelOfDescendants(int $level)
$this->manager->flush();
}
+ /**
+ * @Given there is a DummyCustomDto
+ */
+ public function thereIsADummyCustomDto()
+ {
+ $this->thereAreNbDummyCustomDto(1);
+ }
+
+ /**
+ * @Given there are :nb DummyCustomDto
+ */
+ public function thereAreNbDummyCustomDto($nb)
+ {
+ for ($i = 0; $i < $nb; ++$i) {
+ $dto = $this->isOrm() ? new DummyDtoCustom() : new DummyDtoCustomDocument();
+ $dto->lorem = 'test';
+ $dto->ipsum = (string) $i + 1;
+ $this->manager->persist($dto);
+ }
+
+ $this->manager->flush();
+ $this->manager->clear();
+ }
+
private function isOrm(): bool
{
return null !== $this->schemaTool;
diff --git a/features/graphql/mutation.feature b/features/graphql/mutation.feature
index 074c819bf4a..4af8417a29a 100644
--- a/features/graphql/mutation.feature
+++ b/features/graphql/mutation.feature
@@ -27,7 +27,7 @@ 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 "data.__type.fields[0].name" should contain "delete"
- And the JSON node "data.__type.fields[0].description" should match '/^Deletes a [A-z0-9]+\.$/'
+ And the JSON node "data.__type.fields[0].description" should match '/^Deletes a [A-z0-9]+.$/'
And the JSON node "data.__type.fields[0].type.name" should match "/^delete[A-z0-9]+Payload$/"
And the JSON node "data.__type.fields[0].type.kind" should be equal to "OBJECT"
And the JSON node "data.__type.fields[0].args[0].name" should be equal to "input"
@@ -310,7 +310,7 @@ Feature: GraphQL mutation support
When I send the following GraphQL request:
"""
mutation {
- createDummyDtoNoInput(input: {foo: "A new one", bar: 3, clientMutationId: "myId"}) {
+ createDummyDtoNoInput(input: {lorem: "A new one", ipsum: 3, clientMutationId: "myId"}) {
clientMutationId
}
}
@@ -323,7 +323,19 @@ Feature: GraphQL mutation support
{
"errors": [
{
- "message": "Field \"foo\" is not defined by type createDummyDtoNoInputInput.",
+ "message": "Field createDummyDtoNoInputInput.id of required type ID! was not provided.",
+ "extensions": {
+ "category": "graphql"
+ },
+ "locations": [
+ {
+ "line": 2,
+ "column": 32
+ }
+ ]
+ },
+ {
+ "message": "Field \"lorem\" is not defined by type createDummyDtoNoInputInput.",
"extensions": {
"category": "graphql"
},
@@ -335,14 +347,14 @@ Feature: GraphQL mutation support
]
},
{
- "message": "Field \"bar\" is not defined by type createDummyDtoNoInputInput.",
+ "message": "Field \"ipsum\" is not defined by type createDummyDtoNoInputInput.",
"extensions": {
"category": "graphql"
},
"locations": [
{
"line": 2,
- "column": 51
+ "column": 53
}
]
}
diff --git a/features/jsonapi/related-resouces-inclusion.feature b/features/jsonapi/related-resouces-inclusion.feature
index 2f00db1fac5..ebc03e8d0f5 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/main/input_output.feature
new file mode 100644
index 00000000000..27775b0eccb
--- /dev/null
+++ b/features/main/input_output.feature
@@ -0,0 +1,212 @@
+Feature: 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: 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:
+ """
+ {
+ "foo": "test",
+ "bar": 1
+ }
+ """
+ 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/DummyDtoCustom",
+ "@id": "/dummy_dto_customs/1",
+ "@type": "DummyDtoCustom",
+ "lorem": "test",
+ "ipsum": "1",
+ "id": 1
+ }
+ """
+
+ @createSchema
+ Scenario: Get an item with a custom output
+ Given there is a DummyCustomDto
+ When I send a "GET" request to "/dummy_dto_custom_output/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 a superset of:
+ """
+ {
+ "@context": {
+ "@vocab": "http://example.com/docs.jsonld#",
+ "hydra": "http://www.w3.org/ns/hydra/core#",
+ "foo": {
+ "@type": "@id"
+ },
+ "bar": {
+ "@type": "@id"
+ }
+ },
+ "@type": "CustomOutputDto",
+ "foo": "test",
+ "bar": 1
+ }
+ """
+
+ @createSchema
+ Scenario: Get a collection with a custom output
+ Given there are 2 DummyCustomDto
+ When I send a "GET" request to "/dummy_dto_custom_output"
+ 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 a superset of:
+ """
+ {
+ "@context": "/contexts/DummyDtoCustom",
+ "@id": "/dummy_dto_customs",
+ "@type": "hydra:Collection",
+ "hydra:member": [
+ {
+ "foo": "test",
+ "bar": 1
+ },
+ {
+ "foo": "test",
+ "bar": 2
+ }
+ ],
+ "hydra:totalItems": 2
+ }
+ """
+
+ @createSchema
+ Scenario: Create a DummyCustomDto 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:
+ """
+ {
+ "lorem": "test",
+ "ipsum": "1"
+ }
+ """
+ Then the response status code should be 201
+ And the response should be empty
+
+ @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:
+ """
+ {
+ "foo": "test",
+ "bar": 1
+ }
+ """
+ Then the response status code should be 201
+ And the JSON should be a superset of:
+ """
+ {
+ "@context": {
+ "@vocab": "http://example.com/docs.jsonld#",
+ "hydra": "http://www.w3.org/ns/hydra/core#",
+ "baz": {
+ "@type": "@id"
+ },
+ "bat": {
+ "@type": "@id"
+ }
+ },
+ "@type": "OutputDto",
+ "baz": 1,
+ "bat": "test"
+ }
+ """
+ When I add "Content-Type" header equal to "application/ld+json"
+ And I send a "PUT" request to "/dummy_dto_input_outputs/1" with body:
+ """
+ {
+ "foo": "test",
+ "bar": 2
+ }
+ """
+ Then the response status code should be 200
+ And the JSON should be a superset of:
+ """
+ {
+ "@context": {
+ "@vocab": "http:\/\/example.com\/docs.jsonld#",
+ "hydra": "http:\/\/www.w3.org\/ns\/hydra\/core#",
+ "baz": {
+ "@type": "@id"
+ },
+ "bat": {
+ "@type": "@id"
+ }
+ },
+ "@type": "OutputDto",
+ "baz": 2,
+ "bat": "test"
+ }
+ """
+
+ @!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:
+ """
+ {
+ "username": "soyuka",
+ "plainPassword": "a real password",
+ "email": "soyuka@example.com"
+ }
+ """
+ Then the response status code should be 201
+ When I add "Content-Type" header equal to "application/ld+json"
+ And I send a "PUT" request to "/users/recover/1" with body:
+ """
+ {
+ "user": "/users/1"
+ }
+ """
+ Then the response status code should be 200
+ And the JSON should be a superset of:
+ """
+ {
+ "@context": {
+ "@vocab": "http://example.com/docs.jsonld#",
+ "hydra": "http://www.w3.org/ns/hydra/core#",
+ "dummy": {
+ "@type": "@id"
+ }
+ },
+ "@type": "RecoverPasswordOutput",
+ "dummy": "/dummies/1"
+ }
+ """
+
+ @!mongodb
+ @createSchema
+ Scenario: Create a resource that has a non-resource DTO 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/features/main/table_inheritance.feature b/features/main/table_inheritance.feature
index a4bc49a9dcd..54971c078aa 100644
--- a/features/main/table_inheritance.feature
+++ b/features/main/table_inheritance.feature
@@ -292,34 +292,38 @@ Feature: Table inheritance
}
"""
- Scenario: Get the parent interface collection
- When I send a "GET" request to "/resource_interfaces"
- 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 valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@type": {
- "type": "string",
- "pattern": "^ResourceInterface$"
- },
- "foo": {
- "type": "string",
- "required": "true"
- }
- }
- },
- "minItems": 1
- }
- },
- "required": ["hydra:member"]
- }
- """
+ Scenario: Get the parent interface collection
+ When I send a "GET" request to "/resource_interfaces"
+ 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 valid according to this schema:
+ """
+ {
+ "type": "object",
+ "properties": {
+ "hydra:member": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "@type": {
+ "type": "string",
+ "pattern": "^ResourceInterface$"
+ },
+ "@id": {
+ "type": "string",
+ "pattern": "^_:"
+ },
+ "foo": {
+ "type": "string",
+ "required": "true"
+ }
+ }
+ },
+ "minItems": 1
+ }
+ },
+ "required": ["hydra:member"]
+ }
+ """
diff --git a/src/Annotation/ApiResource.php b/src/Annotation/ApiResource.php
index 35f4f4f7b60..899b841e5eb 100644
--- a/src/Annotation/ApiResource.php
+++ b/src/Annotation/ApiResource.php
@@ -37,7 +37,7 @@
* @Attribute("filters", type="string[]"),
* @Attribute("graphql", type="array"),
* @Attribute("hydraContext", type="array"),
- * @Attribute("inputClass", type="mixed"),
+ * @Attribute("input", type="mixed"),
* @Attribute("iri", type="string"),
* @Attribute("itemOperations", type="array"),
* @Attribute("maximumItemsPerPage", type="int"),
@@ -46,7 +46,7 @@
* @Attribute("normalizationContext", type="array"),
* @Attribute("openapiContext", type="array"),
* @Attribute("order", type="array"),
- * @Attribute("outputClass", type="mixed"),
+ * @Attribute("output", type="mixed"),
* @Attribute("paginationClientEnabled", type="bool"),
* @Attribute("paginationClientItemsPerPage", type="bool"),
* @Attribute("paginationClientPartial", type="bool"),
@@ -288,14 +288,14 @@ final class ApiResource
*
* @var string|false
*/
- private $inputClass;
+ private $input;
/**
* @see https://github.com/Haehnchen/idea-php-annotation-plugin/issues/112
*
* @var string|false
*/
- private $outputClass;
+ private $output;
/**
* @see https://github.com/Haehnchen/idea-php-annotation-plugin/issues/112
diff --git a/src/Api/ResourceClassResolver.php b/src/Api/ResourceClassResolver.php
index 9f360c1de42..f75f58227d1 100644
--- a/src/Api/ResourceClassResolver.php
+++ b/src/Api/ResourceClassResolver.php
@@ -54,6 +54,11 @@ public function getResourceClass($value, string $resourceClass = null, bool $str
return $resourceClass;
}
+ // The Resource is an interface
+ if ($value instanceof $resourceClass && $type !== $resourceClass && interface_exists($resourceClass)) {
+ throw new InvalidArgumentException(sprintf('The given object\'s resource is the interface "%s", finding a class is not possible.', $resourceClass));
+ }
+
if (
($isResourceClass ?? $this->isResourceClass($type))
|| (is_subclass_of($type, $resourceClass) && $this->isResourceClass($resourceClass))
diff --git a/src/Bridge/Symfony/Bundle/Resources/config/api.xml b/src/Bridge/Symfony/Bundle/Resources/config/api.xml
index dc15cb59e81..f5d049cfa11 100644
--- a/src/Bridge/Symfony/Bundle/Resources/config/api.xml
+++ b/src/Bridge/Symfony/Bundle/Resources/config/api.xml
@@ -98,8 +98,13 @@
%api_platform.allow_plain_identifiers%
+ null
+
+
+ true
-
+
+
@@ -156,7 +161,7 @@
-
+
@@ -282,6 +287,12 @@
+
+
+
+
+
+
diff --git a/src/Bridge/Symfony/Bundle/Resources/config/elasticsearch.xml b/src/Bridge/Symfony/Bundle/Resources/config/elasticsearch.xml
index 40949dfd961..c47f1c6ca6c 100644
--- a/src/Bridge/Symfony/Bundle/Resources/config/elasticsearch.xml
+++ b/src/Bridge/Symfony/Bundle/Resources/config/elasticsearch.xml
@@ -56,7 +56,8 @@
-
+
+
diff --git a/src/Bridge/Symfony/Bundle/Resources/config/graphql.xml b/src/Bridge/Symfony/Bundle/Resources/config/graphql.xml
index bdc491d1b38..0d9c87fa6c4 100644
--- a/src/Bridge/Symfony/Bundle/Resources/config/graphql.xml
+++ b/src/Bridge/Symfony/Bundle/Resources/config/graphql.xml
@@ -76,8 +76,13 @@
%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 9e6a3f5d358..c05de3f33a9 100644
--- a/src/Bridge/Symfony/Bundle/Resources/config/hal.xml
+++ b/src/Bridge/Symfony/Bundle/Resources/config/hal.xml
@@ -16,14 +16,14 @@
-
+
%api_platform.collection.pagination.page_parameter_name%
-
+
@@ -34,8 +34,15 @@
-
-
+ null
+ false
+
+
+
+ true
+
+
+
diff --git a/src/Bridge/Symfony/Bundle/Resources/config/hydra.xml b/src/Bridge/Symfony/Bundle/Resources/config/hydra.xml
index b4b4dd3b094..864e2aa118e 100644
--- a/src/Bridge/Symfony/Bundle/Resources/config/hydra.xml
+++ b/src/Bridge/Symfony/Bundle/Resources/config/hydra.xml
@@ -15,7 +15,7 @@
-
+
@@ -34,7 +34,7 @@
%api_platform.validator.serialize_payload_fields%
-
+
@@ -42,14 +42,14 @@
-
+
%kernel.debug%
-
+
@@ -57,7 +57,7 @@
-
+
diff --git a/src/Bridge/Symfony/Bundle/Resources/config/jsonapi.xml b/src/Bridge/Symfony/Bundle/Resources/config/jsonapi.xml
index ccebf5ceb3c..cdf78a92f1a 100644
--- a/src/Bridge/Symfony/Bundle/Resources/config/jsonapi.xml
+++ b/src/Bridge/Symfony/Bundle/Resources/config/jsonapi.xml
@@ -20,7 +20,7 @@
-
+
@@ -28,7 +28,7 @@
%api_platform.collection.pagination.page_parameter_name%
-
+
@@ -39,21 +39,25 @@
+
+
+ true
-
+
+
-
+
%kernel.debug%
-
+
diff --git a/src/Bridge/Symfony/Bundle/Resources/config/jsonld.xml b/src/Bridge/Symfony/Bundle/Resources/config/jsonld.xml
index 999ce4ff92b..c53482a9f4d 100644
--- a/src/Bridge/Symfony/Bundle/Resources/config/jsonld.xml
+++ b/src/Bridge/Symfony/Bundle/Resources/config/jsonld.xml
@@ -25,8 +25,12 @@
+
+
+ true
-
+
+
diff --git a/src/Bridge/Symfony/Bundle/Resources/config/metadata/metadata.xml b/src/Bridge/Symfony/Bundle/Resources/config/metadata/metadata.xml
index 8b8386c6775..02d7c6abdd5 100644
--- a/src/Bridge/Symfony/Bundle/Resources/config/metadata/metadata.xml
+++ b/src/Bridge/Symfony/Bundle/Resources/config/metadata/metadata.xml
@@ -14,6 +14,9 @@
+
+
+
diff --git a/src/Bridge/Symfony/Bundle/Resources/config/problem.xml b/src/Bridge/Symfony/Bundle/Resources/config/problem.xml
index dee6616a9fa..da6c5fd730a 100644
--- a/src/Bridge/Symfony/Bundle/Resources/config/problem.xml
+++ b/src/Bridge/Symfony/Bundle/Resources/config/problem.xml
@@ -15,14 +15,14 @@
%api_platform.validator.serialize_payload_fields%
-
-
+
+
%kernel.debug%
-
+
diff --git a/src/Bridge/Symfony/Bundle/Resources/config/swagger.xml b/src/Bridge/Symfony/Bundle/Resources/config/swagger.xml
index 61244f4ec38..af3ac59bd50 100644
--- a/src/Bridge/Symfony/Bundle/Resources/config/swagger.xml
+++ b/src/Bridge/Symfony/Bundle/Resources/config/swagger.xml
@@ -31,12 +31,12 @@
%api_platform.collection.pagination.client_enabled%
%api_platform.collection.pagination.enabled_parameter_name%
-
+
-
+
diff --git a/src/DataTransformer/ChainDataTransformer.php b/src/DataTransformer/ChainDataTransformer.php
new file mode 100644
index 00000000000..0e81dc88be1
--- /dev/null
+++ b/src/DataTransformer/ChainDataTransformer.php
@@ -0,0 +1,57 @@
+
+ *
+ * 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\DataTransformer;
+
+/**
+ * Transforms an Input to a Resource object.
+ *
+ * @author Antoine Bluchet
+ */
+final class ChainDataTransformer implements DataTransformerInterface
+{
+ private $transformers;
+
+ public function __construct(iterable $transformers)
+ {
+ $this->transformers = $transformers;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function transform($object, string $to, array $context = [])
+ {
+ foreach ($this->transformers as $transformer) {
+ if ($transformer->supportsTransformation($object, $to, $context)) {
+ return $transformer->transform($object, $to, $context);
+ }
+ }
+
+ return $object;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supportsTransformation($object, string $to, array $context = []): bool
+ {
+ foreach ($this->transformers as $transformer) {
+ if ($transformer->supportsTransformation($object, $to, $context)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/src/DataTransformer/DataTransformerInterface.php b/src/DataTransformer/DataTransformerInterface.php
new file mode 100644
index 00000000000..b00ce557da5
--- /dev/null
+++ b/src/DataTransformer/DataTransformerInterface.php
@@ -0,0 +1,39 @@
+
+ *
+ * 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\DataTransformer;
+
+/**
+ * Transforms a DTO or an Anonymous class to a Resource object.
+ *
+ * @author Antoine Bluchet
+ */
+interface DataTransformerInterface
+{
+ /**
+ * Transforms the given object to something else, usually another object.
+ * This must return the original object if no transformation has been done.
+ *
+ * @param object $object
+ *
+ * @return object
+ */
+ public function transform($object, string $to, array $context = []);
+
+ /**
+ * Checks whether the transformation is supported for a given object and context.
+ *
+ * @param object $object
+ */
+ public function supportsTransformation($object, string $to, array $context = []): bool;
+}
diff --git a/src/EventListener/DeserializeListener.php b/src/EventListener/DeserializeListener.php
index d25848beff1..206174fe08d 100644
--- a/src/EventListener/DeserializeListener.php
+++ b/src/EventListener/DeserializeListener.php
@@ -40,7 +40,7 @@ final class DeserializeListener
/**
* @throws InvalidArgumentException
*/
- public function __construct(SerializerInterface $serializer, SerializerContextBuilderInterface $serializerContextBuilder, /* FormatsProviderInterface */ $formatsProvider)
+ public function __construct(SerializerInterface $serializer, SerializerContextBuilderInterface $serializerContextBuilder, /* FormatsProviderInterface */$formatsProvider)
{
$this->serializer = $serializer;
$this->serializerContextBuilder = $serializerContextBuilder;
@@ -78,7 +78,7 @@ public function onKernelRequest(GetResponseEvent $event)
}
$context = $this->serializerContextBuilder->createFromRequest($request, false, $attributes);
- if (false === $context['input_class']) {
+ if (isset($context['input']) && \array_key_exists('class', $context['input']) && null === $context['input']['class']) {
return;
}
@@ -97,7 +97,7 @@ public function onKernelRequest(GetResponseEvent $event)
$request->attributes->set(
'data',
$this->serializer->deserialize(
- $requestContent, $context['input_class'], $format, $context
+ $requestContent, $context['resource_class'], $format, $context
)
);
}
diff --git a/src/EventListener/SerializeListener.php b/src/EventListener/SerializeListener.php
index 803c25296cc..50d6dd45fba 100644
--- a/src/EventListener/SerializeListener.php
+++ b/src/EventListener/SerializeListener.php
@@ -62,15 +62,10 @@ public function onKernelView(GetResponseForControllerResultEvent $event)
$request->attributes->set('_api_respond', true);
$context = $this->serializerContextBuilder->createFromRequest($request, true, $attributes);
- if (isset($context['output_class'])) {
- if (false === $context['output_class']) {
- // If the output class is explicitly set to false, the response must be empty
- $event->setControllerResult('');
+ if (isset($context['output']) && \array_key_exists('class', $context['output']) && null === $context['output']['class']) {
+ $event->setControllerResult('');
- return;
- }
-
- $context['resource_class'] = $context['output_class'];
+ return;
}
if ($included = $request->attributes->get('_api_included')) {
diff --git a/src/EventListener/WriteListener.php b/src/EventListener/WriteListener.php
index 7752f049ccf..2b85fbaef07 100644
--- a/src/EventListener/WriteListener.php
+++ b/src/EventListener/WriteListener.php
@@ -75,7 +75,8 @@ public function onKernelView(GetResponseForControllerResultEvent $event)
$hasOutput = true;
if (null !== $this->resourceMetadataFactory) {
$resourceMetadata = $this->resourceMetadataFactory->create($attributes['resource_class']);
- $hasOutput = false !== $resourceMetadata->getOperationAttribute($attributes, 'output_class', null, true);
+ $outputMetadata = $resourceMetadata->getOperationAttribute($attributes, 'output', ['class' => $attributes['resource_class']], true);
+ $hasOutput = \array_key_exists('class', $outputMetadata) && null !== $outputMetadata['class'];
}
$class = \get_class($controllerResult);
diff --git a/src/GraphQl/Resolver/Factory/ItemMutationResolverFactory.php b/src/GraphQl/Resolver/Factory/ItemMutationResolverFactory.php
index 46f77aa08b5..875b678fd48 100644
--- a/src/GraphQl/Resolver/Factory/ItemMutationResolverFactory.php
+++ b/src/GraphQl/Resolver/Factory/ItemMutationResolverFactory.php
@@ -93,8 +93,10 @@ public function __invoke(string $resourceClass = null, string $rootClass = null,
$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
$this->canAccess($this->resourceAccessChecker, $resourceMetadata, $resourceClass, $info, $item, $operationName);
- if (false === $resourceClass = $resourceMetadata->getAttribute('input_class', $resourceClass)) {
- return null;
+
+ $inputMetadata = $resourceMetadata->getAttribute('input', ['class' => $resourceClass]);
+ if (null === $resourceClass = $inputMetadata['class'] ?? null) {
+ return true;
}
switch ($operationName) {
diff --git a/src/GraphQl/Serializer/ItemNormalizer.php b/src/GraphQl/Serializer/ItemNormalizer.php
index b4e988fc4fc..9b5133a0e46 100644
--- a/src/GraphQl/Serializer/ItemNormalizer.php
+++ b/src/GraphQl/Serializer/ItemNormalizer.php
@@ -15,6 +15,7 @@
use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
use ApiPlatform\Core\Serializer\ItemNormalizer as BaseItemNormalizer;
+use ApiPlatform\Core\Util\ClassInfoTrait;
/**
* GraphQL normalizer.
@@ -23,6 +24,8 @@
*/
final class ItemNormalizer extends BaseItemNormalizer
{
+ use ClassInfoTrait;
+
const FORMAT = 'graphql';
const ITEM_KEY = '#item';
@@ -39,6 +42,11 @@ public function supportsNormalization($data, $format = null)
*/
public function normalize($object, $format = null, array $context = [])
{
+ $outputClass = $this->getOutputClass($this->getObjectClass($object), $context);
+ if (null !== $outputClass && null !== $this->dataTransformer && $this->dataTransformer->supportsTransformation($object, $outputClass, $context)) {
+ $object = $this->dataTransformer->transform($object, $outputClass, $context);
+ }
+
$data = parent::normalize($object, $format, $context);
$data[self::ITEM_KEY] = serialize($object); // calling serialize prevent weird normalization process done by Webonyx's GraphQL PHP
diff --git a/src/GraphQl/Type/SchemaBuilder.php b/src/GraphQl/Type/SchemaBuilder.php
index d92cbdeb41c..971e060802c 100644
--- a/src/GraphQl/Type/SchemaBuilder.php
+++ b/src/GraphQl/Type/SchemaBuilder.php
@@ -381,10 +381,7 @@ private function convertType(Type $type, bool $input = false, string $mutationNa
return null;
}
- if (false !== $dtoClass = $resourceMetadata->getAttribute($input ? 'input_class' : 'output_class', $resourceClass)) {
- $resourceClass = $dtoClass;
- }
- $graphqlType = $this->getResourceObjectType(false === $dtoClass ? null : $resourceClass, $resourceMetadata, $input, $mutationName, $depth);
+ $graphqlType = $this->getResourceObjectType($resourceClass, $resourceMetadata, $input, $mutationName, $depth);
break;
default:
throw new InvalidTypeException(sprintf('The type "%s" is not supported.', $builtinType));
@@ -418,12 +415,18 @@ private function getResourceObjectType(?string $resourceClass, ResourceMetadata
return $this->graphqlTypes[$shortName];
}
+ $ioMetadata = $resourceMetadata->getAttribute($input ? 'input' : 'output');
+
+ if (null !== $ioMetadata && \array_key_exists('class', $ioMetadata) && null !== $ioMetadata['class']) {
+ $resourceClass = $ioMetadata['class'];
+ }
+
$configuration = [
'name' => $shortName,
'description' => $resourceMetadata->getDescription(),
'resolveField' => $this->defaultFieldResolver,
- 'fields' => function () use ($resourceClass, $resourceMetadata, $input, $mutationName, $depth) {
- return $this->getResourceObjectTypeFields($resourceClass, $resourceMetadata, $input, $mutationName, $depth);
+ 'fields' => function () use ($resourceClass, $resourceMetadata, $input, $mutationName, $depth, $ioMetadata) {
+ return $this->getResourceObjectTypeFields($resourceClass, $resourceMetadata, $input, $mutationName, $depth, $ioMetadata);
},
'interfaces' => [$this->getNodeInterface()],
];
@@ -434,13 +437,13 @@ private function getResourceObjectType(?string $resourceClass, ResourceMetadata
/**
* Gets the fields of the type of the given resource.
*/
- private function getResourceObjectTypeFields(?string $resourceClass, ResourceMetadata $resourceMetadata, bool $input = false, string $mutationName = null, int $depth = 0): array
+ private function getResourceObjectTypeFields(?string $resourceClass, ResourceMetadata $resourceMetadata, bool $input = false, string $mutationName = null, int $depth = 0, ?array $ioMetadata = null): array
{
$fields = [];
$idField = ['type' => GraphQLType::nonNull(GraphQLType::id())];
$clientMutationId = GraphQLType::nonNull(GraphQLType::string());
- if ('delete' === $mutationName) {
+ if ('delete' === $mutationName || (null !== $ioMetadata && null === $ioMetadata['class'])) {
return [
'id' => $idField,
'clientMutationId' => $clientMutationId,
diff --git a/src/Hal/Serializer/ItemNormalizer.php b/src/Hal/Serializer/ItemNormalizer.php
index 942d5f84d5c..f05cb65533a 100644
--- a/src/Hal/Serializer/ItemNormalizer.php
+++ b/src/Hal/Serializer/ItemNormalizer.php
@@ -13,6 +13,7 @@
namespace ApiPlatform\Core\Hal\Serializer;
+use ApiPlatform\Core\Exception\InvalidArgumentException;
use ApiPlatform\Core\Exception\RuntimeException;
use ApiPlatform\Core\Serializer\AbstractItemNormalizer;
use ApiPlatform\Core\Serializer\ContextTrait;
@@ -51,7 +52,23 @@ public function normalize($object, $format = null, array $context = [])
$context['cache_key'] = $this->getHalCacheKey($format, $context);
}
- $resourceClass = $this->resourceClassResolver->getResourceClass($object, $context['resource_class'] ?? null, true);
+ $outputClass = $this->getOutputClass($this->getObjectClass($object), $context);
+ if (null !== $outputClass && null !== $this->dataTransformer && $this->dataTransformer->supportsTransformation($object, $outputClass, $context)) {
+ $object = $this->dataTransformer->transform($object, $outputClass, $context);
+ }
+
+ try {
+ $resourceClass = $this->resourceClassResolver->getResourceClass($object, $context['resource_class'] ?? null, true);
+ } catch (InvalidArgumentException $e) {
+ $data = $this->initContext(\get_class($object), $context);
+ $rawData = parent::normalize($object, $format, $context);
+ if (!\is_array($rawData)) {
+ return $rawData;
+ }
+
+ return $data + $rawData;
+ }
+
$context = $this->initContext($resourceClass, $context);
$context['iri'] = $this->iriConverter->getIriFromItem($object);
$context['api_normalize'] = true;
diff --git a/src/Hydra/Serializer/DocumentationNormalizer.php b/src/Hydra/Serializer/DocumentationNormalizer.php
index 10f38bb7762..1bbf93231ce 100644
--- a/src/Hydra/Serializer/DocumentationNormalizer.php
+++ b/src/Hydra/Serializer/DocumentationNormalizer.php
@@ -183,12 +183,14 @@ private function getHydraProperties(string $resourceClass, ResourceMetadata $res
{
$classes = [];
foreach ($resourceMetadata->getCollectionOperations() as $operationName => $operation) {
- if (false !== $class = $resourceMetadata->getCollectionOperationAttribute($operationName, 'input_class', $resourceClass, true)) {
- $classes[$class] = true;
+ $inputMetadata = $resourceMetadata->getTypedOperationAttribute(OperationType::COLLECTION, $operationName, 'input', ['class' => $resourceClass], true);
+ if (null !== $inputClass = $inputMetadata['class'] ?? null) {
+ $classes[$inputClass] = true;
}
- if (false !== $class = $resourceMetadata->getCollectionOperationAttribute($operationName, 'output_class', $resourceClass, true)) {
- $classes[$class] = true;
+ $outputMetadata = $resourceMetadata->getTypedOperationAttribute(OperationType::COLLECTION, $operationName, 'output', ['class' => $resourceClass], true);
+ if (null !== $outputClass = $outputMetadata['class'] ?? null) {
+ $classes[$outputClass] = true;
}
}
@@ -257,8 +259,10 @@ private function getHydraOperation(string $resourceClass, ResourceMetadata $reso
}
$shortName = $resourceMetadata->getShortName();
- $inputClass = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'input_class', null, true);
- $outputClass = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'output_class', null, true);
+ $inputMetadata = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'input', ['class' => false]);
+ $inputClass = \array_key_exists('class', $inputMetadata) ? $inputMetadata['class'] : false;
+ $outputMetadata = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'output', ['class' => false]);
+ $outputClass = \array_key_exists('class', $outputMetadata) ? $outputMetadata['class'] : false;
if ('GET' === $method && OperationType::COLLECTION === $operationType) {
$hydraOperation += [
@@ -270,34 +274,34 @@ private function getHydraOperation(string $resourceClass, ResourceMetadata $reso
$hydraOperation += [
'@type' => ['hydra:Operation', 'schema:FindAction'],
'hydra:title' => $subresourceMetadata && $subresourceMetadata->isCollection() ? "Retrieves the collection of $shortName resources." : "Retrieves a $shortName resource.",
- 'returns' => false === $outputClass ? 'owl:Nothing' : "#$shortName",
+ 'returns' => null === $outputClass ? 'owl:Nothing' : "#$shortName",
];
} elseif ('GET' === $method) {
$hydraOperation += [
'@type' => ['hydra:Operation', 'schema:FindAction'],
'hydra:title' => "Retrieves $shortName resource.",
- 'returns' => false === $outputClass ? 'owl:Nothing' : $prefixedShortName,
+ 'returns' => null === $outputClass ? 'owl:Nothing' : $prefixedShortName,
];
} elseif ('PATCH' === $method) {
$hydraOperation += [
'@type' => 'hydra:Operation',
'hydra:title' => "Updates the $shortName resource.",
- 'returns' => false === $outputClass ? 'owl:Nothing' : $prefixedShortName,
- 'expects' => false === $inputClass ? 'owl:Nothing' : $prefixedShortName,
+ 'returns' => null === $outputClass ? 'owl:Nothing' : $prefixedShortName,
+ 'expects' => null === $inputClass ? 'owl:Nothing' : $prefixedShortName,
];
} elseif ('POST' === $method) {
$hydraOperation += [
'@type' => ['hydra:Operation', 'schema:CreateAction'],
'hydra:title' => "Creates a $shortName resource.",
- 'returns' => false === $outputClass ? 'owl:Nothing' : $prefixedShortName,
- 'expects' => false === $inputClass ? 'owl:Nothing' : $prefixedShortName,
+ 'returns' => null === $outputClass ? 'owl:Nothing' : $prefixedShortName,
+ 'expects' => null === $inputClass ? 'owl:Nothing' : $prefixedShortName,
];
} elseif ('PUT' === $method) {
$hydraOperation += [
'@type' => ['hydra:Operation', 'schema:ReplaceAction'],
'hydra:title' => "Replaces the $shortName resource.",
- 'returns' => false === $outputClass ? 'owl:Nothing' : $prefixedShortName,
- 'expects' => false === $inputClass ? 'owl:Nothing' : $prefixedShortName,
+ 'returns' => null === $outputClass ? 'owl:Nothing' : $prefixedShortName,
+ 'expects' => null === $inputClass ? 'owl:Nothing' : $prefixedShortName,
];
} elseif ('DELETE' === $method) {
$hydraOperation += [
diff --git a/src/Identifier/IdentifierConverter.php b/src/Identifier/IdentifierConverter.php
index d392925afb3..53fc196f9a5 100644
--- a/src/Identifier/IdentifierConverter.php
+++ b/src/Identifier/IdentifierConverter.php
@@ -46,7 +46,7 @@ public function convert(string $data, string $class, array $context = []): array
{
if (null !== $this->resourceMetadataFactory) {
$resourceMetadata = $this->resourceMetadataFactory->create($class);
- $class = $resourceMetadata->getOperationAttribute($context, 'output_class', $class, true);
+ $class = $resourceMetadata->getOperationAttribute($context, 'output', ['class' => $class], true)['class'];
}
$keys = $this->identifiersExtractor->getIdentifiersFromResourceClass($class);
diff --git a/src/JsonApi/Serializer/ItemNormalizer.php b/src/JsonApi/Serializer/ItemNormalizer.php
index 8dbd25bcadb..4d06d04fb2f 100644
--- a/src/JsonApi/Serializer/ItemNormalizer.php
+++ b/src/JsonApi/Serializer/ItemNormalizer.php
@@ -16,6 +16,7 @@
use ApiPlatform\Core\Api\IriConverterInterface;
use ApiPlatform\Core\Api\OperationType;
use ApiPlatform\Core\Api\ResourceClassResolverInterface;
+use ApiPlatform\Core\DataTransformer\DataTransformerInterface;
use ApiPlatform\Core\Exception\InvalidArgumentException;
use ApiPlatform\Core\Exception\ItemNotFoundException;
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
@@ -23,6 +24,7 @@
use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
use ApiPlatform\Core\Serializer\AbstractItemNormalizer;
+use ApiPlatform\Core\Util\ClassInfoTrait;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
@@ -37,16 +39,15 @@
*/
final class ItemNormalizer extends AbstractItemNormalizer
{
+ use ClassInfoTrait;
+
const FORMAT = 'jsonapi';
private $componentsCache = [];
- private $resourceMetadataFactory;
- public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, PropertyAccessorInterface $propertyAccessor = null, NameConverterInterface $nameConverter = null, ResourceMetadataFactoryInterface $resourceMetadataFactory, array $defaultContext = [])
+ public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, PropertyAccessorInterface $propertyAccessor = null, NameConverterInterface $nameConverter = null, ResourceMetadataFactoryInterface $resourceMetadataFactory, array $defaultContext = [], DataTransformerInterface $dataTransformer = null, bool $allowUnmappedClass = false)
{
- parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, null, null, false, $defaultContext);
-
- $this->resourceMetadataFactory = $resourceMetadataFactory;
+ parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, null, null, false, $defaultContext, $dataTransformer, $resourceMetadataFactory, $allowUnmappedClass);
}
/**
@@ -66,6 +67,11 @@ public function normalize($object, $format = null, array $context = [])
$context['cache_key'] = $this->getJsonApiCacheKey($format, $context);
}
+ $outputClass = $this->getOutputClass($this->getObjectClass($object), $context);
+ if (null !== $outputClass && null !== $this->dataTransformer && $this->dataTransformer->supportsTransformation($object, $context)) {
+ $object = $this->dataTransformer->transform($object, $context);
+ }
+
// Get and populate attributes data
$objectAttributesData = parent::normalize($object, $format, $context);
@@ -74,7 +80,12 @@ public function normalize($object, $format = null, array $context = [])
}
// Get and populate item type
- $resourceClass = $this->resourceClassResolver->getResourceClass($object, $context['resource_class'] ?? null, true);
+ try {
+ $resourceClass = $this->resourceClassResolver->getResourceClass($object, $context['resource_class'] ?? null, true);
+ } catch (InvalidArgumentException $e) {
+ return parent::normalize($object, $format, $context);
+ }
+
$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
// Get and populate relations
diff --git a/src/JsonLd/AnonymousContextBuilderInterface.php b/src/JsonLd/AnonymousContextBuilderInterface.php
new file mode 100644
index 00000000000..20fdaf427ed
--- /dev/null
+++ b/src/JsonLd/AnonymousContextBuilderInterface.php
@@ -0,0 +1,30 @@
+
+ *
+ * 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;
+
+use ApiPlatform\Core\Api\UrlGeneratorInterface;
+
+/**
+ * JSON-LD context builder with Input Output DTO support interface.
+ *
+ * @author Antoine Bluchet
+ */
+interface AnonymousContextBuilderInterface extends ContextBuilderInterface
+{
+ /**
+ * Creates a JSON-LD context based on the given object.
+ * Usually this is used with an Input or Output DTO object.
+ */
+ public function getAnonymousResourceContext($object, array $context, int $referenceType = UrlGeneratorInterface::ABS_PATH): array;
+}
diff --git a/src/JsonLd/ContextBuilder.php b/src/JsonLd/ContextBuilder.php
index 9a34baf0c41..94b56d44322 100644
--- a/src/JsonLd/ContextBuilder.php
+++ b/src/JsonLd/ContextBuilder.php
@@ -25,7 +25,7 @@
*
* @author Kévin Dunglas
*/
-final class ContextBuilder implements ContextBuilderInterface
+final class ContextBuilder implements AnonymousContextBuilderInterface
{
const FORMAT = 'jsonld';
@@ -87,9 +87,45 @@ public function getEntrypointContext(int $referenceType = UrlGeneratorInterface:
*/
public function getResourceContext(string $resourceClass, int $referenceType = UrlGeneratorInterface::ABS_PATH): array
{
- $context = $this->getBaseContext($referenceType);
+ $metadata = $this->resourceMetadataFactory->create($resourceClass);
+ if (null === $shortName = $metadata->getShortName()) {
+ return [];
+ }
+
+ return $this->getResourceContextWithShortname($resourceClass, $referenceType, $shortName);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getResourceContextUri(string $resourceClass, int $referenceType = UrlGeneratorInterface::ABS_PATH): string
+ {
$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
- $shortName = $resourceMetadata->getShortName();
+
+ return $this->urlGenerator->generate('api_jsonld_context', ['shortName' => $resourceMetadata->getShortName()], $referenceType);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getAnonymousResourceContext($object, array $context, int $referenceType = UrlGeneratorInterface::ABS_PATH): array
+ {
+ $id = $context['iri'] ?? '_:'.(\function_exists('spl_object_id') ? spl_object_id($object) : spl_object_hash($object));
+ $jsonLdContext = [
+ '@context' => $this->getResourceContextWithShortname(\get_class($object), $referenceType, $id),
+ '@id' => $id,
+ ];
+
+ if ($context['name'] ?? false) {
+ $jsonLdContext['@type'] = $context['name'];
+ }
+
+ return $jsonLdContext;
+ }
+
+ private function getResourceContextWithShortname(string $resourceClass, int $referenceType = UrlGeneratorInterface::ABS_PATH, string $shortName)
+ {
+ $context = $this->getBaseContext($referenceType);
foreach ($this->propertyNameCollectionFactory->create($resourceClass) as $propertyName) {
$propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName);
@@ -123,14 +159,4 @@ public function getResourceContext(string $resourceClass, int $referenceType = U
return $context;
}
-
- /**
- * {@inheritdoc}
- */
- public function getResourceContextUri(string $resourceClass, int $referenceType = UrlGeneratorInterface::ABS_PATH): string
- {
- $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
-
- return $this->urlGenerator->generate('api_jsonld_context', ['shortName' => $resourceMetadata->getShortName()], $referenceType);
- }
}
diff --git a/src/JsonLd/Serializer/ItemNormalizer.php b/src/JsonLd/Serializer/ItemNormalizer.php
index 5506a965b5a..6512119fb25 100644
--- a/src/JsonLd/Serializer/ItemNormalizer.php
+++ b/src/JsonLd/Serializer/ItemNormalizer.php
@@ -15,6 +15,7 @@
use ApiPlatform\Core\Api\IriConverterInterface;
use ApiPlatform\Core\Api\ResourceClassResolverInterface;
+use ApiPlatform\Core\DataTransformer\DataTransformerInterface;
use ApiPlatform\Core\Exception\InvalidArgumentException;
use ApiPlatform\Core\JsonLd\ContextBuilderInterface;
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
@@ -22,6 +23,7 @@
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
use ApiPlatform\Core\Serializer\AbstractItemNormalizer;
use ApiPlatform\Core\Serializer\ContextTrait;
+use ApiPlatform\Core\Util\ClassInfoTrait;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
@@ -33,19 +35,18 @@
*/
final class ItemNormalizer extends AbstractItemNormalizer
{
+ use ClassInfoTrait;
use ContextTrait;
use JsonLdContextTrait;
const FORMAT = 'jsonld';
- private $resourceMetadataFactory;
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 = [])
+ 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 = [], DataTransformerInterface $dataTransformer = null, bool $allowUnmappedClass = false)
{
- parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, $classMetadataFactory, null, false, $defaultContext);
+ parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, $classMetadataFactory, null, false, $defaultContext, $dataTransformer, $resourceMetadataFactory, $allowUnmappedClass);
- $this->resourceMetadataFactory = $resourceMetadataFactory;
$this->contextBuilder = $contextBuilder;
}
@@ -62,7 +63,23 @@ public function supportsNormalization($data, $format = null)
*/
public function normalize($object, $format = null, array $context = [])
{
- $resourceClass = $this->resourceClassResolver->getResourceClass($object, $context['resource_class'] ?? null, true);
+ $outputClass = $this->getOutputClass($this->getObjectClass($object), $context);
+ if (null !== $outputClass && null !== $this->dataTransformer && $this->dataTransformer->supportsTransformation($object, $outputClass, $context)) {
+ $object = $this->dataTransformer->transform($object, $outputClass, $context);
+ }
+
+ try {
+ $resourceClass = $this->resourceClassResolver->getResourceClass($object, $context['resource_class'] ?? null, true);
+ } catch (InvalidArgumentException $e) {
+ $data = $this->createJsonLdContext($this->contextBuilder, $object, $context);
+ $rawData = parent::normalize($object, $format, $context);
+ if (!\is_array($rawData)) {
+ return $rawData;
+ }
+
+ return $data + $rawData;
+ }
+
$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
$data = $this->addJsonLdContext($this->contextBuilder, $resourceClass, $context);
diff --git a/src/JsonLd/Serializer/JsonLdContextTrait.php b/src/JsonLd/Serializer/JsonLdContextTrait.php
index 3ade372f4c8..cb138258ab8 100644
--- a/src/JsonLd/Serializer/JsonLdContextTrait.php
+++ b/src/JsonLd/Serializer/JsonLdContextTrait.php
@@ -13,6 +13,7 @@
namespace ApiPlatform\Core\JsonLd\Serializer;
+use ApiPlatform\Core\JsonLd\AnonymousContextBuilderInterface;
use ApiPlatform\Core\JsonLd\ContextBuilderInterface;
/**
@@ -45,4 +46,15 @@ private function addJsonLdContext(ContextBuilderInterface $contextBuilder, strin
return $data;
}
+
+ private function createJsonLdContext(AnonymousContextBuilderInterface $contextBuilder, $object, array &$context, array $data = []): array
+ {
+ if (isset($context['jsonld_has_context'])) {
+ return $data;
+ }
+
+ $context['jsonld_has_context'] = true;
+
+ return $contextBuilder->getAnonymousResourceContext($object, $context['output']);
+ }
}
diff --git a/src/Metadata/Resource/Factory/InputOutputResourceMetadataFactory.php b/src/Metadata/Resource/Factory/InputOutputResourceMetadataFactory.php
new file mode 100644
index 00000000000..6a56332652c
--- /dev/null
+++ b/src/Metadata/Resource/Factory/InputOutputResourceMetadataFactory.php
@@ -0,0 +1,88 @@
+
+ *
+ * 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\Metadata\Resource\Factory;
+
+use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
+
+/**
+ * Transforms the given input/output metadata to a normalized one.
+ *
+ * @author Antoine Bluchet
+ */
+final class InputOutputResourceMetadataFactory implements ResourceMetadataFactoryInterface
+{
+ private $decorated;
+
+ public function __construct(ResourceMetadataFactoryInterface $decorated)
+ {
+ $this->decorated = $decorated;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function create(string $resourceClass): ResourceMetadata
+ {
+ $resourceMetadata = $this->decorated->create($resourceClass);
+
+ $attributes = $resourceMetadata->getAttributes() ?: [];
+ $attributes['input'] = isset($attributes['input']) ? $this->transformInputOutput($attributes['input']) : null;
+ $attributes['output'] = isset($attributes['output']) ? $this->transformInputOutput($attributes['output']) : null;
+
+ if (null !== $collectionOperations = $resourceMetadata->getCollectionOperations()) {
+ $resourceMetadata = $resourceMetadata->withCollectionOperations($this->getTransformedOperations($collectionOperations, $attributes));
+ }
+
+ if (null !== $itemOperations = $resourceMetadata->getItemOperations()) {
+ $resourceMetadata = $resourceMetadata->withItemOperations($this->getTransformedOperations($itemOperations, $attributes));
+ }
+
+ return $resourceMetadata->withAttributes($attributes);
+ }
+
+ private function getTransformedOperations(array $operations, array $resourceAttributes): array
+ {
+ foreach ($operations as $key => &$operation) {
+ if (!\is_array($operation)) {
+ continue;
+ }
+
+ $operation['input'] = isset($operation['input']) ? $this->transformInputOutput($operation['input']) : $resourceAttributes['input'];
+ $operation['output'] = isset($operation['output']) ? $this->transformInputOutput($operation['output']) : $resourceAttributes['output'];
+ }
+
+ return $operations;
+ }
+
+ private function transformInputOutput($attribute): ?array
+ {
+ if (null === $attribute) {
+ return null;
+ }
+
+ if (false === $attribute) {
+ return ['class' => null];
+ }
+
+ if (\is_string($attribute)) {
+ $attribute = ['class' => $attribute];
+ }
+
+ if (!isset($attribute['name'])) {
+ $attribute['name'] = (new \ReflectionClass($attribute['class']))->getShortName();
+ }
+
+ return $attribute;
+ }
+}
diff --git a/src/Serializer/AbstractItemNormalizer.php b/src/Serializer/AbstractItemNormalizer.php
index 9f5c62b46ff..62285fda664 100644
--- a/src/Serializer/AbstractItemNormalizer.php
+++ b/src/Serializer/AbstractItemNormalizer.php
@@ -15,13 +15,16 @@
use ApiPlatform\Core\Api\IriConverterInterface;
use ApiPlatform\Core\Api\ResourceClassResolverInterface;
+use ApiPlatform\Core\Bridge\Elasticsearch\Serializer\ItemNormalizer;
use ApiPlatform\Core\DataProvider\ItemDataProviderInterface;
+use ApiPlatform\Core\DataTransformer\DataTransformerInterface;
use ApiPlatform\Core\Exception\InvalidArgumentException;
use ApiPlatform\Core\Exception\InvalidValueException;
use ApiPlatform\Core\Exception\ItemNotFoundException;
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
+use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
use ApiPlatform\Core\Util\ClassInfoTrait;
use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
use Symfony\Component\PropertyAccess\PropertyAccess;
@@ -45,6 +48,7 @@ abstract class AbstractItemNormalizer extends AbstractObjectNormalizer
{
use ClassInfoTrait;
use ContextTrait;
+ use InputOutputMetadataTrait;
protected $propertyNameCollectionFactory;
protected $propertyMetadataFactory;
@@ -54,8 +58,10 @@ abstract class AbstractItemNormalizer extends AbstractObjectNormalizer
protected $localCache = [];
protected $itemDataProvider;
protected $allowPlainIdentifiers;
+ protected $allowUnmappedClass;
+ protected $dataTransformer;
- 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 = [])
+ 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 = [], DataTransformerInterface $dataTransformer = null, ResourceMetadataFactoryInterface $resourceMetadataFactory = null, bool $allowUnmappedClass = false)
{
if (!isset($defaultContext['circular_reference_handler'])) {
$defaultContext['circular_reference_handler'] = function ($object) {
@@ -66,6 +72,10 @@ public function __construct(PropertyNameCollectionFactoryInterface $propertyName
$this->setCircularReferenceHandler($defaultContext['circular_reference_handler']);
}
+ if (false === $allowUnmappedClass) {
+ @trigger_error(sprintf('Passing a falsy $allowUnmappedClass flag in %s is deprecated since version 2.4 and will default to true in 3.0.', self::class), E_USER_DEPRECATED);
+ }
+
parent::__construct($classMetadataFactory, $nameConverter, null, null, \Closure::fromCallable([$this, 'getObjectClass']), $defaultContext);
$this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
@@ -75,6 +85,9 @@ public function __construct(PropertyNameCollectionFactoryInterface $propertyName
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
$this->itemDataProvider = $itemDataProvider;
$this->allowPlainIdentifiers = $allowPlainIdentifiers;
+ $this->dataTransformer = $dataTransformer;
+ $this->resourceMetadataFactory = $resourceMetadataFactory;
+ $this->allowUnmappedClass = $allowUnmappedClass;
}
/**
@@ -82,11 +95,15 @@ public function __construct(PropertyNameCollectionFactoryInterface $propertyName
*/
public function supportsNormalization($data, $format = null)
{
- if (!\is_object($data)) {
+ if (!\is_object($data) || $data instanceof \Traversable) {
return false;
}
- return $this->resourceClassResolver->isResourceClass($this->getObjectClass($data));
+ if (false === $this->allowUnmappedClass) {
+ return $this->resourceClassResolver->isResourceClass($this->getObjectClass($data));
+ }
+
+ return true;
}
/**
@@ -102,7 +119,15 @@ public function hasCacheableSupportsMethod(): bool
*/
public function normalize($object, $format = null, array $context = [])
{
- $resourceClass = $this->resourceClassResolver->getResourceClass($object, $context['resource_class'] ?? null, true);
+ try {
+ $resourceClass = $this->resourceClassResolver->getResourceClass($object, $context['resource_class'] ?? null, true);
+ } catch (InvalidArgumentException $e) {
+ $context = $this->initContext(\get_class($object), $context);
+ $context['api_normalize'] = true;
+
+ return parent::normalize($object, $format, $context);
+ }
+
$context = $this->initContext($resourceClass, $context);
$context['api_normalize'] = true;
@@ -119,7 +144,15 @@ public function normalize($object, $format = null, array $context = [])
*/
public function supportsDenormalization($data, $type, $format = null)
{
- return $this->localCache[$type] ?? $this->localCache[$type] = $this->resourceClassResolver->isResourceClass($type);
+ if (ItemNormalizer::FORMAT === $format) {
+ return false;
+ }
+
+ if (false === $this->allowUnmappedClass) {
+ return $this->localCache[$type] ?? $this->localCache[$type] = $this->resourceClassResolver->isResourceClass($type);
+ }
+
+ return true;
}
/**
@@ -129,6 +162,17 @@ public function denormalize($data, $class, $format = null, array $context = [])
{
$context['api_denormalize'] = true;
$context['resource_class'] = $class;
+ $inputClass = $this->getInputClass($class, $context);
+
+ if (null !== $inputClass) {
+ $data = parent::denormalize($data, $inputClass, $format, ['resource_class' => $inputClass] + $context);
+
+ if (null !== $this->dataTransformer && $this->dataTransformer->supportsTransformation($data, $class, $context)) {
+ $data = $this->dataTransformer->transform($data, $class, $context);
+ }
+
+ return $data;
+ }
return parent::denormalize($data, $class, $format, $context);
}
diff --git a/src/Serializer/InputOutputMetadataTrait.php b/src/Serializer/InputOutputMetadataTrait.php
new file mode 100644
index 00000000000..788058b0cb9
--- /dev/null
+++ b/src/Serializer/InputOutputMetadataTrait.php
@@ -0,0 +1,49 @@
+
+ *
+ * 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 ApiPlatform\Core\Exception\ResourceClassNotFoundException;
+
+trait InputOutputMetadataTrait
+{
+ /**
+ * @var \ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface
+ */
+ protected $resourceMetadataFactory;
+
+ private function getInputOutputMetadata(string $class, string $inputOrOutput, array $context)
+ {
+ if (null === $this->resourceMetadataFactory || null !== ($context[$inputOrOutput]['class'] ?? null)) {
+ return $context[$inputOrOutput]['class'] ?? null;
+ }
+
+ try {
+ $metadata = $this->resourceMetadataFactory->create($class);
+ } catch (ResourceClassNotFoundException $e) {
+ return null;
+ }
+
+ return $metadata->getAttribute($inputOrOutput)['class'] ?? null;
+ }
+
+ protected function getInputClass(string $class, array $context = []): ?string
+ {
+ return $this->getInputOutputMetadata($class, 'input', $context);
+ }
+
+ protected function getOutputClass(string $class, array $context = []): ?string
+ {
+ return $this->getInputOutputMetadata($class, 'output', $context);
+ }
+}
diff --git a/src/Serializer/ItemNormalizer.php b/src/Serializer/ItemNormalizer.php
index f34540440d8..0169b0dcc88 100644
--- a/src/Serializer/ItemNormalizer.php
+++ b/src/Serializer/ItemNormalizer.php
@@ -16,9 +16,11 @@
use ApiPlatform\Core\Api\IriConverterInterface;
use ApiPlatform\Core\Api\ResourceClassResolverInterface;
use ApiPlatform\Core\DataProvider\ItemDataProviderInterface;
+use ApiPlatform\Core\DataTransformer\DataTransformerInterface;
use ApiPlatform\Core\Exception\InvalidArgumentException;
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
+use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
@@ -36,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)
+ 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, DataTransformerInterface $dataTransformer = null, ResourceMetadataFactoryInterface $resourceMetadataFactory = null, $allowUnmappedClass = false)
{
- parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, $classMetadataFactory, $itemDataProvider, $allowPlainIdentifiers);
+ parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, $classMetadataFactory, $itemDataProvider, $allowPlainIdentifiers, [], $dataTransformer, $resourceMetadataFactory, $allowUnmappedClass);
$this->logger = $logger ?: new NullLogger();
}
diff --git a/src/Serializer/SerializerContextBuilder.php b/src/Serializer/SerializerContextBuilder.php
index b14d8a7ca2d..6d02e1956dd 100644
--- a/src/Serializer/SerializerContextBuilder.php
+++ b/src/Serializer/SerializerContextBuilder.php
@@ -65,8 +65,8 @@ public function createFromRequest(Request $request, bool $normalization, array $
}
$context['resource_class'] = $attributes['resource_class'];
- $context['input_class'] = $resourceMetadata->getTypedOperationAttribute($operationType, $attributes[$operationKey], 'input_class', $attributes['resource_class'], true);
- $context['output_class'] = $resourceMetadata->getTypedOperationAttribute($operationType, $attributes[$operationKey], 'output_class', $attributes['resource_class'], true);
+ $context['input'] = $resourceMetadata->getTypedOperationAttribute($operationType, $attributes[$operationKey], 'input', $resourceMetadata->getAttribute('input'));
+ $context['output'] = $resourceMetadata->getTypedOperationAttribute($operationType, $attributes[$operationKey], 'output', $resourceMetadata->getAttribute('output'));
$context['request_uri'] = $request->getRequestUri();
$context['uri'] = $request->getUri();
diff --git a/src/Swagger/Serializer/DocumentationNormalizer.php b/src/Swagger/Serializer/DocumentationNormalizer.php
index 411adbf98b4..e03ffc4e5ef 100644
--- a/src/Swagger/Serializer/DocumentationNormalizer.php
+++ b/src/Swagger/Serializer/DocumentationNormalizer.php
@@ -155,7 +155,8 @@ public function normalize($object, $format = null, array $context = [])
$serializerContext = $this->getSerializerContext(OperationType::SUBRESOURCE, false, $subResourceMetadata, $operationName);
$responseDefinitionKey = false;
- if (false !== $outputClass = $subResourceMetadata->getSubresourceOperationAttribute($operationName, 'output_class')) {
+ $outputMetadata = $resourceMetadata->getTypedOperationAttribute(OperationType::SUBRESOURCE, $operationName, 'output', ['class' => $subresourceOperation['resource_class']], true);
+ if (null !== $outputClass = $outputMetadata['class'] ?? null) {
$responseDefinitionKey = $this->getDefinition($v3, $definitions, $subResourceMetadata, $subresourceOperation['resource_class'], $outputClass, $serializerContext);
}
@@ -315,7 +316,8 @@ private function updateGetOperation(bool $v3, \ArrayObject $pathOperation, array
$serializerContext = $this->getSerializerContext($operationType, false, $resourceMetadata, $operationName);
$responseDefinitionKey = false;
- if (false !== $outputClass = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'output_class', null, true)) {
+ $outputMetadata = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'output', ['class' => $resourceClass], true);
+ if (null !== $outputClass = $outputMetadata['class'] ?? null) {
$responseDefinitionKey = $this->getDefinition($v3, $definitions, $resourceMetadata, $resourceClass, $outputClass, $serializerContext);
}
@@ -423,7 +425,8 @@ private function updatePostOperation(bool $v3, \ArrayObject $pathOperation, arra
$pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Creates a %s resource.', $resourceShortName);
$responseDefinitionKey = false;
- if (false !== $outputClass = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'output_class', null, true)) {
+ $outputMetadata = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'output', ['class' => $resourceClass], true);
+ if (null !== $outputClass = $outputMetadata['class'] ?? null) {
$responseDefinitionKey = $this->getDefinition($v3, $definitions, $resourceMetadata, $resourceClass, $outputClass, $this->getSerializerContext($operationType, false, $resourceMetadata, $operationName));
}
@@ -445,7 +448,8 @@ private function updatePostOperation(bool $v3, \ArrayObject $pathOperation, arra
'404' => ['description' => 'Resource not found'],
];
- if (false === $inputClass = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'input_class', null, true)) {
+ $inputMetadata = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'input', ['class' => $resourceClass], true);
+ if (null === $inputClass = $inputMetadata['class'] ?? null) {
return $pathOperation;
}
@@ -485,7 +489,8 @@ private function updatePutOperation(bool $v3, \ArrayObject $pathOperation, array
$pathOperation['parameters'] ?? $pathOperation['parameters'] = [$parameter];
$responseDefinitionKey = false;
- if (false !== $outputClass = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'output_class', null, true)) {
+ $outputMetadata = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'output', ['class' => $resourceClass], true);
+ if (null !== $outputClass = $outputMetadata['class'] ?? null) {
$responseDefinitionKey = $this->getDefinition($v3, $definitions, $resourceMetadata, $resourceClass, $outputClass, $this->getSerializerContext($operationType, false, $resourceMetadata, $operationName));
}
@@ -504,7 +509,8 @@ private function updatePutOperation(bool $v3, \ArrayObject $pathOperation, array
'404' => ['description' => 'Resource not found'],
];
- if (false === $inputClass = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'input_class', null, true)) {
+ $inputMetadata = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'input', ['class' => $resourceClass], true);
+ if (null === $inputClass = $inputMetadata['class'] ?? null) {
return $pathOperation;
}
@@ -549,7 +555,7 @@ private function updateDeleteOperation(bool $v3, \ArrayObject $pathOperation, st
private function getDefinition(bool $v3, \ArrayObject $definitions, ResourceMetadata $resourceMetadata, string $resourceClass, ?string $publicClass, array $serializerContext = null): string
{
$keyPrefix = $resourceMetadata->getShortName();
- if (null !== $publicClass) {
+ if (null !== $publicClass && $resourceClass !== $publicClass) {
$keyPrefix .= ':'.md5($publicClass);
}
@@ -680,7 +686,7 @@ private function getType(bool $v3, string $type, bool $isCollection, string $cla
return [
'$ref' => sprintf(
$v3 ? '#/components/schemas/%s' : '#/definitions/%s',
- $this->getDefinition($v3, $definitions, $resourceMetadata = $this->resourceMetadataFactory->create($className), $className, $resourceMetadata->getAttribute('output_class'), $serializerContext)
+ $this->getDefinition($v3, $definitions, $resourceMetadata = $this->resourceMetadataFactory->create($className), $className, $resourceMetadata->getAttribute('output')['class'] ?? $className, $serializerContext)
),
];
}
diff --git a/tests/Annotation/ApiResourceTest.php b/tests/Annotation/ApiResourceTest.php
index 51f2088609c..ce0f86e7cdf 100644
--- a/tests/Annotation/ApiResourceTest.php
+++ b/tests/Annotation/ApiResourceTest.php
@@ -38,7 +38,7 @@ public function testConstruct()
'formats' => ['foo', 'bar' => ['application/bar']],
'filters' => ['foo', 'bar'],
'graphql' => ['query' => ['normalization_context' => ['groups' => ['foo', 'bar']]]],
- 'inputClass' => 'Foo',
+ 'input' => 'Foo',
'iri' => 'http://example.com/res',
'itemOperations' => ['foo' => ['bar']],
'maximumItemsPerPage' => 42,
@@ -47,7 +47,7 @@ public function testConstruct()
'normalizationContext' => ['groups' => ['bar']],
'order' => ['foo', 'bar' => 'ASC'],
'openapiContext' => ['description' => 'foo'],
- 'outputClass' => 'Bar',
+ 'output' => 'Bar',
'paginationClientEnabled' => true,
'paginationClientItemsPerPage' => true,
'paginationClientPartial' => true,
@@ -79,14 +79,14 @@ public function testConstruct()
'force_eager' => false,
'formats' => ['foo', 'bar' => ['application/bar']],
'filters' => ['foo', 'bar'],
- 'input_class' => 'Foo',
+ 'input' => 'Foo',
'maximum_items_per_page' => 42,
'mercure' => '[\'foo\', object.owner]',
'messenger' => true,
'normalization_context' => ['groups' => ['bar']],
'order' => ['foo', 'bar' => 'ASC'],
'openapi_context' => ['description' => 'foo'],
- 'output_class' => 'Bar',
+ 'output' => 'Bar',
'pagination_client_enabled' => true,
'pagination_client_items_per_page' => true,
'pagination_client_partial' => true,
diff --git a/tests/Api/ResourceClassResolverTest.php b/tests/Api/ResourceClassResolverTest.php
index 31dc028432c..50579549ab7 100644
--- a/tests/Api/ResourceClassResolverTest.php
+++ b/tests/Api/ResourceClassResolverTest.php
@@ -175,12 +175,12 @@ public function testGetResourceClassWithChildResource()
public function testGetResourceClassWithInterfaceResource()
{
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage("The given object's resource is the interface \"ApiPlatform\Core\Tests\Fixtures\DummyResourceInterface\", finding a class is not possible.");
$dummy = new DummyResourceImplementation();
$resourceNameCollectionFactoryProphecy = $this->prophesize(ResourceNameCollectionFactoryInterface::class);
- $resourceNameCollectionFactoryProphecy->create()->willReturn(new ResourceNameCollection([DummyResourceInterface::class]))->shouldBeCalled();
$resourceClassResolver = new ResourceClassResolver($resourceNameCollectionFactoryProphecy->reveal());
$resourceClass = $resourceClassResolver->getResourceClass($dummy, DummyResourceInterface::class, true);
- $this->assertEquals(DummyResourceImplementation::class, $resourceClass);
}
}
diff --git a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php
index 0dc3c51934f..ca9583dc910 100644
--- a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php
+++ b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php
@@ -735,6 +735,7 @@ private function getPartialContainerBuilderProphecy()
'api_platform.cache.route_name_resolver',
'api_platform.cache.subresource_operation_factory',
'api_platform.collection_data_provider',
+ 'api_platform.data_transformer.chain_transformer',
'api_platform.formats_provider',
'api_platform.filter_locator',
'api_platform.filter_collection_factory',
@@ -772,6 +773,7 @@ private function getPartialContainerBuilderProphecy()
'api_platform.metadata.property.name_collection_factory.xml',
'api_platform.metadata.resource.metadata_factory.cached',
'api_platform.metadata.resource.metadata_factory.operation',
+ 'api_platform.metadata.resource.metadata_factory.input_output',
'api_platform.metadata.resource.metadata_factory.short_name',
'api_platform.metadata.resource.metadata_factory.xml',
'api_platform.metadata.resource.name_collection_factory.cached',
@@ -815,6 +817,7 @@ private function getPartialContainerBuilderProphecy()
'api_platform.action.post_collection' => 'api_platform.action.placeholder',
'api_platform.action.put_item' => 'api_platform.action.placeholder',
'api_platform.action.patch_item' => 'api_platform.action.placeholder',
+ 'api_platform.data_transformer' => 'api_platform.data_transformer.chain_transformer',
'api_platform.metadata.property.metadata_factory' => 'api_platform.metadata.property.metadata_factory.xml',
'api_platform.metadata.property.name_collection_factory' => 'api_platform.metadata.property.name_collection_factory.property_info',
'api_platform.metadata.resource.metadata_factory' => 'api_platform.metadata.resource.metadata_factory.xml',
diff --git a/tests/EventListener/DeserializeListenerTest.php b/tests/EventListener/DeserializeListenerTest.php
index dec97154af0..20345460d3f 100644
--- a/tests/EventListener/DeserializeListenerTest.php
+++ b/tests/EventListener/DeserializeListenerTest.php
@@ -136,7 +136,7 @@ public function testDoNotCallWhenInputClassDisabled()
$serializerProphecy->deserialize()->shouldNotBeCalled();
$serializerContextBuilderProphecy = $this->prophesize(SerializerContextBuilderInterface::class);
- $serializerContextBuilderProphecy->createFromRequest(Argument::type(Request::class), false, Argument::type('array'))->willReturn(['input_class' => false, 'output_class' => false]);
+ $serializerContextBuilderProphecy->createFromRequest(Argument::type(Request::class), false, Argument::type('array'))->willReturn(['input' => ['class' => null], 'output' => ['class' => null]]);
$formatsProviderProphecy = $this->prophesize(FormatsProviderInterface::class);
$formatsProviderProphecy->getFormatsFromAttributes(Argument::type('array'))->shouldNotBeCalled();
@@ -159,14 +159,17 @@ public function testDeserialize(string $method, bool $populateObject)
$eventProphecy->getRequest()->willReturn($request)->shouldBeCalled();
$serializerProphecy = $this->prophesize(SerializerInterface::class);
- $context = $populateObject ? [AbstractNormalizer::OBJECT_TO_POPULATE => $populateObject, 'input_class' => 'Foo', 'output_class' => 'Foo'] : ['input_class' => 'Foo', 'output_class' => 'Foo'];
+ $context = $populateObject ? [AbstractNormalizer::OBJECT_TO_POPULATE => $populateObject] : [];
+ $context['input'] = ['class' => 'Foo'];
+ $context['output'] = ['class' => 'Foo'];
+ $context['resource_class'] = 'Foo';
$serializerProphecy->deserialize('{}', 'Foo', 'json', $context)->willReturn($result)->shouldBeCalled();
$formatsProviderProphecy = $this->prophesize(FormatsProviderInterface::class);
$formatsProviderProphecy->getFormatsFromAttributes(Argument::type('array'))->willReturn(self::FORMATS)->shouldBeCalled();
$serializerContextBuilderProphecy = $this->prophesize(SerializerContextBuilderInterface::class);
- $serializerContextBuilderProphecy->createFromRequest(Argument::type(Request::class), false, Argument::type('array'))->willReturn(['input_class' => 'Foo', 'output_class' => 'Foo'])->shouldBeCalled();
+ $serializerContextBuilderProphecy->createFromRequest(Argument::type(Request::class), false, Argument::type('array'))->willReturn(['input' => ['class' => 'Foo'], 'output' => ['class' => 'Foo'], 'resource_class' => 'Foo'])->shouldBeCalled();
$listener = new DeserializeListener($serializerProphecy->reveal(), $serializerContextBuilderProphecy->reveal(), $formatsProviderProphecy->reveal());
$listener->onKernelRequest($eventProphecy->reveal());
@@ -186,11 +189,14 @@ public function testDeserializeResourceClassSupportedFormat(string $method, bool
$eventProphecy->getRequest()->willReturn($request)->shouldBeCalled();
$serializerProphecy = $this->prophesize(SerializerInterface::class);
- $context = $populateObject ? [AbstractNormalizer::OBJECT_TO_POPULATE => $populateObject, 'input_class' => 'Foo', 'output_class' => 'Foo'] : ['input_class' => 'Foo', 'output_class' => 'Foo'];
+ $context = $populateObject ? [AbstractNormalizer::OBJECT_TO_POPULATE => $populateObject] : [];
+ $context['input'] = ['class' => 'Foo'];
+ $context['output'] = ['class' => 'Foo'];
+ $context['resource_class'] = 'Foo';
$serializerProphecy->deserialize('{}', 'Foo', 'json', $context)->willReturn($result)->shouldBeCalled();
$serializerContextBuilderProphecy = $this->prophesize(SerializerContextBuilderInterface::class);
- $serializerContextBuilderProphecy->createFromRequest(Argument::type(Request::class), false, Argument::type('array'))->willReturn(['input_class' => 'Foo', 'output_class' => 'Foo'])->shouldBeCalled();
+ $serializerContextBuilderProphecy->createFromRequest(Argument::type(Request::class), false, Argument::type('array'))->willReturn(['input' => ['class' => 'Foo'], 'output' => ['class' => 'Foo'], 'resource_class' => 'Foo'])->shouldBeCalled();
$formatsProviderProphecy = $this->prophesize(FormatsProviderInterface::class);
$formatsProviderProphecy->getFormatsFromAttributes(['resource_class' => 'Foo', 'collection_operation_name' => 'post', 'receive' => true, 'persist' => true])->willReturn(self::FORMATS)->shouldBeCalled();
@@ -215,11 +221,13 @@ public function testContentNegotiation()
$request->setFormat('xml', 'text/xml'); // Workaround to avoid weird behaviors
$eventProphecy->getRequest()->willReturn($request)->shouldBeCalled();
+ $context = ['input' => ['class' => 'Foo'], 'output' => ['class' => 'Foo'], 'resource_class' => 'Foo'];
+
$serializerProphecy = $this->prophesize(SerializerInterface::class);
- $serializerProphecy->deserialize('{}', 'Foo', 'xml', ['input_class' => 'Foo', 'output_class' => 'Foo'])->willReturn(new \stdClass())->shouldBeCalled();
+ $serializerProphecy->deserialize('{}', 'Foo', 'xml', $context)->willReturn(new \stdClass())->shouldBeCalled();
$serializerContextBuilderProphecy = $this->prophesize(SerializerContextBuilderInterface::class);
- $serializerContextBuilderProphecy->createFromRequest(Argument::type(Request::class), false, Argument::type('array'))->willReturn(['input_class' => 'Foo', 'output_class' => 'Foo'])->shouldBeCalled();
+ $serializerContextBuilderProphecy->createFromRequest(Argument::type(Request::class), false, Argument::type('array'))->willReturn($context)->shouldBeCalled();
$formatsProviderProphecy = $this->prophesize(FormatsProviderInterface::class);
$formatsProviderProphecy->getFormatsFromAttributes(Argument::type('array'))->willReturn(['jsonld' => ['application/ld+json'], 'xml' => ['text/xml']])->shouldBeCalled();
@@ -249,7 +257,7 @@ public function testNotSupportedContentType()
$serializerProphecy->deserialize()->shouldNotBeCalled();
$serializerContextBuilderProphecy = $this->prophesize(SerializerContextBuilderInterface::class);
- $serializerContextBuilderProphecy->createFromRequest(Argument::type(Request::class), false, Argument::type('array'))->willReturn(['input_class' => 'Foo', 'output_class' => 'Foo']);
+ $serializerContextBuilderProphecy->createFromRequest(Argument::type(Request::class), false, Argument::type('array'))->willReturn(['input' => ['class' => 'Foo'], 'output' => ['class' => 'Foo']]);
$formatsProviderProphecy = $this->prophesize(FormatsProviderInterface::class);
$formatsProviderProphecy->getFormatsFromAttributes(Argument::type('array'))->willReturn(['jsonld' => ['application/ld+json'], 'xml' => ['text/xml']])->shouldBeCalled();
@@ -278,7 +286,7 @@ public function testNoContentType()
$serializerProphecy->deserialize()->shouldNotBeCalled();
$serializerContextBuilderProphecy = $this->prophesize(SerializerContextBuilderInterface::class);
- $serializerContextBuilderProphecy->createFromRequest(Argument::type(Request::class), false, Argument::type('array'))->willReturn(['input_class' => 'Foo', 'output_class' => 'Foo']);
+ $serializerContextBuilderProphecy->createFromRequest(Argument::type(Request::class), false, Argument::type('array'))->willReturn(['input' => ['class' => 'Foo'], 'output' => ['class' => 'Foo']]);
$formatsProviderProphecy = $this->prophesize(FormatsProviderInterface::class);
$formatsProviderProphecy->getFormatsFromAttributes(Argument::type('array'))->willReturn(['jsonld' => ['application/ld+json'], 'xml' => ['text/xml']])->shouldBeCalled();
diff --git a/tests/EventListener/SerializeListenerTest.php b/tests/EventListener/SerializeListenerTest.php
index b6f43acadd7..48414dc9fbc 100644
--- a/tests/EventListener/SerializeListenerTest.php
+++ b/tests/EventListener/SerializeListenerTest.php
@@ -104,7 +104,7 @@ public function testSerializeCollectionOperation()
public function testSerializeCollectionOperationWithOutputClassDisabled()
{
- $expectedContext = ['request_uri' => '', 'resource_class' => 'Foo', 'collection_operation_name' => 'post', 'output_class' => false];
+ $expectedContext = ['request_uri' => '', 'resource_class' => 'Foo', 'collection_operation_name' => 'post', 'output' => ['class' => null]];
$serializerProphecy = $this->prophesize(SerializerInterface::class);
$request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_collection_operation_name' => 'get', '_api_output_class' => false]);
diff --git a/tests/EventListener/WriteListenerTest.php b/tests/EventListener/WriteListenerTest.php
index 0ed15abd39e..794f15ee2fc 100644
--- a/tests/EventListener/WriteListenerTest.php
+++ b/tests/EventListener/WriteListenerTest.php
@@ -148,11 +148,11 @@ public function testOnKernelViewDoNotCallIriConverterWhenOutputClassDisabled()
$iriConverterProphecy->getIriFromItem($dummy)->shouldNotBeCalled();
$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
- $resourceMetadataFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadata(null, null, null, null, null, ['output_class' => false]));
+ $resourceMetadataFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadata(null, null, null, null, null, ['output' => ['class' => null]]));
$dataPersisterProphecy->persist($dummy)->willReturn($dummy)->shouldBeCalled();
- $request = new Request([], [], ['_api_resource_class' => Dummy::class, '_api_collection_operation_name' => 'post', '_api_output_class' => false]);
+ $request = new Request([], [], ['_api_resource_class' => Dummy::class, '_api_collection_operation_name' => 'post']);
$request->setMethod('POST');
$event = new GetResponseForControllerResultEvent(
diff --git a/tests/Fixtures/TestBundle/DataPersister/DummyInputDataPersister.php b/tests/Fixtures/TestBundle/DataPersister/DummyInputDataPersister.php
deleted file mode 100644
index 18a35d0f567..00000000000
--- a/tests/Fixtures/TestBundle/DataPersister/DummyInputDataPersister.php
+++ /dev/null
@@ -1,46 +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\Tests\Fixtures\TestBundle\DataPersister;
-
-use ApiPlatform\Core\DataPersister\DataPersisterInterface;
-use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyInput;
-use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyOutput;
-
-class DummyInputDataPersister implements DataPersisterInterface
-{
- public function supports($data): bool
- {
- return $data instanceof DummyInput;
- }
-
- /**
- * {@inheritdoc}
- */
- public function persist($data)
- {
- $output = new DummyOutput();
- $output->name = $data->name;
- $output->id = 1;
-
- return $output;
- }
-
- /**
- * {@inheritdoc}
- */
- public function remove($data)
- {
- return null;
- }
-}
diff --git a/tests/Fixtures/TestBundle/DataTransformer/CustomInputDtoDataTransformer.php b/tests/Fixtures/TestBundle/DataTransformer/CustomInputDtoDataTransformer.php
new file mode 100644
index 00000000000..885ecff4743
--- /dev/null
+++ b/tests/Fixtures/TestBundle/DataTransformer/CustomInputDtoDataTransformer.php
@@ -0,0 +1,50 @@
+
+ *
+ * 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\Tests\Fixtures\TestBundle\DataTransformer;
+
+use ApiPlatform\Core\DataTransformer\DataTransformerInterface;
+use ApiPlatform\Core\Serializer\AbstractItemNormalizer;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\DummyDtoCustom as DummyDtoCustomDocument;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\CustomInputDto;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyDtoCustom;
+
+final class CustomInputDtoDataTransformer implements DataTransformerInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function transform($object, string $to, array $context = [])
+ {
+ if (!$object instanceof CustomInputDto) {
+ throw new \InvalidArgumentException();
+ }
+
+ /**
+ * @var \ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyDtoCustom
+ */
+ $resourceObject = $context[AbstractItemNormalizer::OBJECT_TO_POPULATE] ?? new $context['resource_class']();
+ $resourceObject->lorem = $object->foo;
+ $resourceObject->ipsum = (string) $object->bar;
+
+ return $resourceObject;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supportsTransformation($object, string $to, array $context = []): bool
+ {
+ return (DummyDtoCustom::class === $to || DummyDtoCustomDocument::class === $to) && $object instanceof CustomInputDto;
+ }
+}
diff --git a/tests/Fixtures/TestBundle/DataTransformer/CustomOutputDtoDataTransformer.php b/tests/Fixtures/TestBundle/DataTransformer/CustomOutputDtoDataTransformer.php
new file mode 100644
index 00000000000..66936c0cfa4
--- /dev/null
+++ b/tests/Fixtures/TestBundle/DataTransformer/CustomOutputDtoDataTransformer.php
@@ -0,0 +1,62 @@
+
+ *
+ * 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\Tests\Fixtures\TestBundle\DataTransformer;
+
+use ApiPlatform\Core\DataTransformer\DataTransformerInterface;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\DummyDtoCustom as DummyDtoCustomDocument;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\CustomOutputDto;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyDtoCustom;
+
+final class CustomOutputDtoDataTransformer implements DataTransformerInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function transform($object, string $to, array $context = [])
+ {
+ if ($object instanceof \Traversable) {
+ foreach ($object as &$value) {
+ $value = $this->doTransformation($value);
+ }
+
+ return $object;
+ }
+
+ return $this->doTransformation($object);
+ }
+
+ private function doTransformation($object): CustomOutputDto
+ {
+ $output = new CustomOutputDto();
+ $output->foo = $object->lorem;
+ $output->bar = (int) $object->ipsum;
+
+ return $output;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supportsTransformation($object, string $to, array $context = []): bool
+ {
+ if ($object instanceof \IteratorAggregate) {
+ $iterator = $object->getIterator();
+ if ($iterator instanceof \Iterator) {
+ $object = $iterator->current();
+ }
+ }
+
+ return ($object instanceof DummyDtoCustom || $object instanceof DummyDtoCustomDocument) && CustomOutputDto::class === $to;
+ }
+}
diff --git a/tests/Fixtures/TestBundle/DataTransformer/InputDtoDataTransformer.php b/tests/Fixtures/TestBundle/DataTransformer/InputDtoDataTransformer.php
new file mode 100644
index 00000000000..0e51b12346e
--- /dev/null
+++ b/tests/Fixtures/TestBundle/DataTransformer/InputDtoDataTransformer.php
@@ -0,0 +1,50 @@
+
+ *
+ * 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\Tests\Fixtures\TestBundle\DataTransformer;
+
+use ApiPlatform\Core\DataTransformer\DataTransformerInterface;
+use ApiPlatform\Core\Serializer\AbstractItemNormalizer;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\DummyDtoInputOutput as DummyDtoInputOutputDocument;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\InputDto;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyDtoInputOutput;
+
+final class InputDtoDataTransformer implements DataTransformerInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function transform($object, string $to, array $context = [])
+ {
+ if (!$object instanceof InputDto) {
+ throw new \InvalidArgumentException();
+ }
+
+ /**
+ * @var \ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyDtoInputOutput
+ */
+ $resourceObject = $context[AbstractItemNormalizer::OBJECT_TO_POPULATE] ?? new $context['resource_class']();
+ $resourceObject->str = $object->foo;
+ $resourceObject->num = $object->bar;
+
+ return $resourceObject;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supportsTransformation($object, string $to, array $context = []): bool
+ {
+ return (DummyDtoInputOutput::class === $to || DummyDtoInputOutputDocument::class === $to) && $object instanceof InputDto;
+ }
+}
diff --git a/tests/Fixtures/TestBundle/DataTransformer/OutputDtoDataTransformer.php b/tests/Fixtures/TestBundle/DataTransformer/OutputDtoDataTransformer.php
new file mode 100644
index 00000000000..fc45aa05425
--- /dev/null
+++ b/tests/Fixtures/TestBundle/DataTransformer/OutputDtoDataTransformer.php
@@ -0,0 +1,46 @@
+
+ *
+ * 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\Tests\Fixtures\TestBundle\DataTransformer;
+
+use ApiPlatform\Core\DataTransformer\DataTransformerInterface;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\DummyDtoInputOutput as DummyDtoInputOutputDocument;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\OutputDto;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyDtoInputOutput;
+
+final class OutputDtoDataTransformer implements DataTransformerInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function transform($object, string $to, array $context = [])
+ {
+ if (!$object instanceof DummyDtoInputOutput && !$object instanceof DummyDtoInputOutputDocument) {
+ throw new \InvalidArgumentException();
+ }
+
+ $output = new OutputDto();
+ $output->bat = (string) $object->str;
+ $output->baz = (float) $object->num;
+
+ return $output;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supportsTransformation($data, string $to, array $context = []): bool
+ {
+ return ($data instanceof DummyDtoInputOutput || $data instanceof DummyDtoInputOutputDocument) && OutputDto::class === $to;
+ }
+}
diff --git a/tests/Fixtures/TestBundle/DataTransformer/RecoverPasswordInputDataTransformer.php b/tests/Fixtures/TestBundle/DataTransformer/RecoverPasswordInputDataTransformer.php
new file mode 100644
index 00000000000..ce03f105a19
--- /dev/null
+++ b/tests/Fixtures/TestBundle/DataTransformer/RecoverPasswordInputDataTransformer.php
@@ -0,0 +1,44 @@
+
+ *
+ * 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\Tests\Fixtures\TestBundle\DataTransformer;
+
+use ApiPlatform\Core\DataTransformer\DataTransformerInterface;
+use ApiPlatform\Core\Serializer\AbstractItemNormalizer;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\User as UserDocument;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\RecoverPasswordInput;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\User;
+
+final class RecoverPasswordInputDataTransformer implements DataTransformerInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function transform($data, string $to, array $context = [])
+ {
+ // Because we're in a PUT operation, we will use the retrieved object...
+ $resourceObject = $context[AbstractItemNormalizer::OBJECT_TO_POPULATE] ?? new $context['resource_class']();
+ // ...where we remove the credentials
+ $resourceObject->eraseCredentials();
+
+ return $resourceObject;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supportsTransformation($data, string $to, array $context = []): bool
+ {
+ return (User::class === $to || UserDocument::class === $to) && $data instanceof RecoverPasswordInput;
+ }
+}
diff --git a/tests/Fixtures/TestBundle/DataTransformer/RecoverPasswordOutputDataTransformer.php b/tests/Fixtures/TestBundle/DataTransformer/RecoverPasswordOutputDataTransformer.php
new file mode 100644
index 00000000000..9b9ef386ef4
--- /dev/null
+++ b/tests/Fixtures/TestBundle/DataTransformer/RecoverPasswordOutputDataTransformer.php
@@ -0,0 +1,46 @@
+
+ *
+ * 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\Tests\Fixtures\TestBundle\DataTransformer;
+
+use ApiPlatform\Core\DataTransformer\DataTransformerInterface;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\RecoverPasswordOutput;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\User;
+
+final class RecoverPasswordOutputDataTransformer implements DataTransformerInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function transform($object, string $to, array $context = [])
+ {
+ if (!$object instanceof User) {
+ throw new \InvalidArgumentException();
+ }
+
+ $output = new RecoverPasswordOutput();
+ $output->dummy = new Dummy();
+ $output->dummy->setId(1);
+
+ return $output;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supportsTransformation($object, string $to, array $context = []): bool
+ {
+ return $object instanceof User && RecoverPasswordOutput::class === $to;
+ }
+}
diff --git a/tests/Fixtures/TestBundle/Document/DummyDtoCustom.php b/tests/Fixtures/TestBundle/Document/DummyDtoCustom.php
new file mode 100644
index 00000000000..af10f191639
--- /dev/null
+++ b/tests/Fixtures/TestBundle/Document/DummyDtoCustom.php
@@ -0,0 +1,58 @@
+
+ *
+ * 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\Tests\Fixtures\TestBundle\Document;
+
+use ApiPlatform\Core\Annotation\ApiResource;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\CustomInputDto;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\CustomOutputDto;
+use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
+
+/**
+ * DummyDtoCustom.
+ *
+ * @ODM\Document
+ *
+ * @ApiResource(
+ * collectionOperations={"post"={"input"=CustomInputDto::class}, "get", "custom_output"={"output"=CustomOutputDto::class, "path"="dummy_dto_custom_output", "method"="GET"}, "post_without_output"={"output"=false, "method"="POST", "path"="dummy_dto_custom_post_without_output"}},
+ * itemOperations={"get", "custom_output"={"output"=CustomOutputDto::class, "method"="GET", "path"="dummy_dto_custom_output/{id}"}, "put", "delete"}
+ * )
+ */
+class DummyDtoCustom
+{
+ /**
+ * @var int The id
+ *
+ * @ODM\Id(strategy="INCREMENT", type="integer")
+ */
+ private $id;
+
+ /**
+ * @var string
+ *
+ * @ODM\Field
+ */
+ public $lorem;
+
+ /**
+ * @var string
+ *
+ * @ODM\Field
+ */
+ public $ipsum;
+
+ public function getId()
+ {
+ return $this->id;
+ }
+}
diff --git a/tests/Fixtures/TestBundle/Document/DummyDtoInputOutput.php b/tests/Fixtures/TestBundle/Document/DummyDtoInputOutput.php
new file mode 100644
index 00000000000..12a5642ff59
--- /dev/null
+++ b/tests/Fixtures/TestBundle/Document/DummyDtoInputOutput.php
@@ -0,0 +1,50 @@
+
+ *
+ * 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\Tests\Fixtures\TestBundle\Document;
+
+use ApiPlatform\Core\Annotation\ApiProperty;
+use ApiPlatform\Core\Annotation\ApiResource;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\InputDto;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\OutputDto;
+use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
+
+/**
+ * Dummy InputOutput.
+ *
+ * @author Kévin Dunglas
+ *
+ * @ApiResource(attributes={"input"=InputDto::class, "output"=OutputDto::class})
+ * @ODM\Document
+ */
+class DummyDtoInputOutput
+{
+ /**
+ * @var int The id
+ * @ApiProperty(identifier=true)
+ * @ODM\Id(strategy="INCREMENT", type="integer")
+ */
+ public $id;
+
+ /**
+ * @var int The id
+ * @ODM\Field
+ */
+ public $str;
+
+ /**
+ * @var int The id
+ * @ODM\Field(type="float")
+ */
+ public $num;
+}
diff --git a/tests/Fixtures/TestBundle/Document/DummyDtoNoInput.php b/tests/Fixtures/TestBundle/Document/DummyDtoNoInput.php
index e3d87618bbc..05431be7d7e 100644
--- a/tests/Fixtures/TestBundle/Document/DummyDtoNoInput.php
+++ b/tests/Fixtures/TestBundle/Document/DummyDtoNoInput.php
@@ -26,8 +26,8 @@
*
* @ApiResource(
* attributes={
- * "input_class"=false,
- * "output_class"=OutputDto::class
+ * "input"=false,
+ * "output"=OutputDto::class
* }
* )
*/
diff --git a/tests/Fixtures/TestBundle/Document/DummyDtoNoOutput.php b/tests/Fixtures/TestBundle/Document/DummyDtoNoOutput.php
index c4c66eb5694..89b0940c620 100644
--- a/tests/Fixtures/TestBundle/Document/DummyDtoNoOutput.php
+++ b/tests/Fixtures/TestBundle/Document/DummyDtoNoOutput.php
@@ -26,8 +26,8 @@
*
* @ApiResource(
* attributes={
- * "input_class"=InputDto::class,
- * "output_class"=false
+ * "input"=InputDto::class,
+ * "output"=false
* }
* )
*/
diff --git a/tests/Fixtures/TestBundle/Document/User.php b/tests/Fixtures/TestBundle/Document/User.php
index 5891f2e1344..9d559b87218 100644
--- a/tests/Fixtures/TestBundle/Document/User.php
+++ b/tests/Fixtures/TestBundle/Document/User.php
@@ -14,6 +14,8 @@
namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Document;
use ApiPlatform\Core\Annotation\ApiResource;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\RecoverPasswordInput;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\RecoverPasswordOutput;
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
use FOS\UserBundle\Model\User as BaseUser;
use FOS\UserBundle\Model\UserInterface;
@@ -23,10 +25,17 @@
* A User.
*
* @ODM\Document(collection="user_test")
- * @ApiResource(attributes={
- * "normalization_context"={"groups"={"user", "user-read"}},
- * "denormalization_context"={"groups"={"user", "user-write"}}
- * })
+ * @ApiResource(
+ * attributes={
+ * "normalization_context"={"groups"={"user", "user-read"}},
+ * "denormalization_context"={"groups"={"user", "user-write"}}
+ * },
+ * itemOperations={"get", "put", "delete",
+ * "recover_password"={
+ * "input"=RecoverPasswordInput::class, "output"=RecoverPasswordOutput::class, "method"="PUT", "path"="users/recover/{id}"
+ * }
+ * }
+ * )
*
* @author Théo FIDRY
* @author Kévin Dunglas
diff --git a/tests/Fixtures/TestBundle/Dto/CustomInputDto.php b/tests/Fixtures/TestBundle/Dto/CustomInputDto.php
new file mode 100644
index 00000000000..6873d9bf7d7
--- /dev/null
+++ b/tests/Fixtures/TestBundle/Dto/CustomInputDto.php
@@ -0,0 +1,27 @@
+
+ *
+ * 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\Tests\Fixtures\TestBundle\Dto;
+
+class CustomInputDto
+{
+ /**
+ * @var string
+ */
+ public $foo;
+
+ /**
+ * @var int
+ */
+ public $bar;
+}
diff --git a/tests/Fixtures/TestBundle/Dto/CustomOutputDto.php b/tests/Fixtures/TestBundle/Dto/CustomOutputDto.php
new file mode 100644
index 00000000000..df07ac289aa
--- /dev/null
+++ b/tests/Fixtures/TestBundle/Dto/CustomOutputDto.php
@@ -0,0 +1,27 @@
+
+ *
+ * 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\Tests\Fixtures\TestBundle\Dto;
+
+class CustomOutputDto
+{
+ /**
+ * @var string
+ */
+ public $foo;
+
+ /**
+ * @var int
+ */
+ public $bar;
+}
diff --git a/tests/Fixtures/TestBundle/Dto/NonResourceClass.php b/tests/Fixtures/TestBundle/Dto/NonResourceClass.php
new file mode 100644
index 00000000000..787229ad02a
--- /dev/null
+++ b/tests/Fixtures/TestBundle/Dto/NonResourceClass.php
@@ -0,0 +1,27 @@
+
+ *
+ * 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\Tests\Fixtures\TestBundle\Dto;
+
+class NonResourceClass
+{
+ /**
+ * @var string
+ */
+ public $foo;
+
+ public function __toString()
+ {
+ return $this->foo;
+ }
+}
diff --git a/tests/Fixtures/TestBundle/Dto/RecoverPasswordInput.php b/tests/Fixtures/TestBundle/Dto/RecoverPasswordInput.php
new file mode 100644
index 00000000000..f0f109bf4ad
--- /dev/null
+++ b/tests/Fixtures/TestBundle/Dto/RecoverPasswordInput.php
@@ -0,0 +1,25 @@
+
+ *
+ * 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\Tests\Fixtures\TestBundle\Dto;
+
+use Symfony\Component\Serializer\Annotation\Groups;
+
+class RecoverPasswordInput
+{
+ /**
+ * @var \ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\User
+ * @Groups({"user"})
+ */
+ public $user;
+}
diff --git a/tests/Fixtures/TestBundle/Dto/RecoverPasswordOutput.php b/tests/Fixtures/TestBundle/Dto/RecoverPasswordOutput.php
new file mode 100644
index 00000000000..26da73e4c34
--- /dev/null
+++ b/tests/Fixtures/TestBundle/Dto/RecoverPasswordOutput.php
@@ -0,0 +1,25 @@
+
+ *
+ * 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\Tests\Fixtures\TestBundle\Dto;
+
+use Symfony\Component\Serializer\Annotation\Groups;
+
+class RecoverPasswordOutput
+{
+ /**
+ * @var \ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy
+ * @Groups({"user"})
+ */
+ public $dummy;
+}
diff --git a/tests/Fixtures/TestBundle/Entity/DummyDtoCustom.php b/tests/Fixtures/TestBundle/Entity/DummyDtoCustom.php
new file mode 100644
index 00000000000..6d3d0e06536
--- /dev/null
+++ b/tests/Fixtures/TestBundle/Entity/DummyDtoCustom.php
@@ -0,0 +1,60 @@
+
+ *
+ * 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\Tests\Fixtures\TestBundle\Entity;
+
+use ApiPlatform\Core\Annotation\ApiResource;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\CustomInputDto;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\CustomOutputDto;
+use Doctrine\ORM\Mapping as ORM;
+
+/**
+ * DummyDtoCustom.
+ *
+ * @ORM\Entity
+ *
+ * @ApiResource(
+ * collectionOperations={"post"={"input"=CustomInputDto::class}, "get", "custom_output"={"output"=CustomOutputDto::class, "path"="dummy_dto_custom_output", "method"="GET"}, "post_without_output"={"output"=false, "method"="POST", "path"="dummy_dto_custom_post_without_output"}},
+ * itemOperations={"get", "custom_output"={"output"=CustomOutputDto::class, "method"="GET", "path"="dummy_dto_custom_output/{id}"}, "put", "delete"}
+ * )
+ */
+class DummyDtoCustom
+{
+ /**
+ * @var int The id
+ *
+ * @ORM\Column(type="integer")
+ * @ORM\Id
+ * @ORM\GeneratedValue(strategy="AUTO")
+ */
+ private $id;
+
+ /**
+ * @var string
+ *
+ * @ORM\Column
+ */
+ public $lorem;
+
+ /**
+ * @var string
+ *
+ * @ORM\Column
+ */
+ public $ipsum;
+
+ public function getId()
+ {
+ return $this->id;
+ }
+}
diff --git a/tests/Fixtures/TestBundle/Entity/DummyInputOutput.php b/tests/Fixtures/TestBundle/Entity/DummyDtoInputOutput.php
similarity index 52%
rename from tests/Fixtures/TestBundle/Entity/DummyInputOutput.php
rename to tests/Fixtures/TestBundle/Entity/DummyDtoInputOutput.php
index 442a83c1121..28f0e064424 100644
--- a/tests/Fixtures/TestBundle/Entity/DummyInputOutput.php
+++ b/tests/Fixtures/TestBundle/Entity/DummyDtoInputOutput.php
@@ -15,19 +15,38 @@
use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiResource;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\InputDto;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\OutputDto;
+use Doctrine\ORM\Mapping as ORM;
/**
* Dummy InputOutput.
*
* @author Kévin Dunglas
*
- * @ApiResource(attributes={"input_class"=DummyInput::class, "output_class"=DummyOutput::class})
+ * @ApiResource(attributes={"input"=InputDto::class, "output"=OutputDto::class})
+ * @ORM\Entity
*/
-class DummyInputOutput
+class DummyDtoInputOutput
{
/**
* @var int The id
* @ApiProperty(identifier=true)
+ * @ORM\Column(type="integer")
+ * @ORM\Id
+ * @ORM\GeneratedValue(strategy="AUTO")
*/
public $id;
+
+ /**
+ * @var string
+ * @ORM\Column(type="string")
+ */
+ public $str;
+
+ /**
+ * @var int
+ * @ORM\Column(type="decimal")
+ */
+ public $num;
}
diff --git a/tests/Fixtures/TestBundle/Entity/DummyDtoNoInput.php b/tests/Fixtures/TestBundle/Entity/DummyDtoNoInput.php
index a8c35081e6e..b29654ad92e 100644
--- a/tests/Fixtures/TestBundle/Entity/DummyDtoNoInput.php
+++ b/tests/Fixtures/TestBundle/Entity/DummyDtoNoInput.php
@@ -26,8 +26,8 @@
*
* @ApiResource(
* attributes={
- * "input_class"=false,
- * "output_class"=OutputDto::class
+ * "input"=false,
+ * "output"=OutputDto::class
* }
* )
*/
diff --git a/tests/Fixtures/TestBundle/Entity/DummyDtoNoOutput.php b/tests/Fixtures/TestBundle/Entity/DummyDtoNoOutput.php
index 315afdeb820..232364a94e2 100644
--- a/tests/Fixtures/TestBundle/Entity/DummyDtoNoOutput.php
+++ b/tests/Fixtures/TestBundle/Entity/DummyDtoNoOutput.php
@@ -26,8 +26,8 @@
*
* @ApiResource(
* attributes={
- * "input_class"=InputDto::class,
- * "output_class"=false
+ * "input"=InputDto::class,
+ * "output"=false
* }
* )
*/
diff --git a/tests/Fixtures/TestBundle/Entity/DummyInput.php b/tests/Fixtures/TestBundle/Entity/DummyInput.php
deleted file mode 100644
index 24ffc48b405..00000000000
--- a/tests/Fixtures/TestBundle/Entity/DummyInput.php
+++ /dev/null
@@ -1,40 +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\Tests\Fixtures\TestBundle\Entity;
-
-use ApiPlatform\Core\Annotation\ApiProperty;
-use ApiPlatform\Core\Annotation\ApiResource;
-
-/**
- * Dummy Input.
- *
- * @author Kévin Dunglas
- *
- * @ApiResource
- */
-class DummyInput
-{
- /**
- * @var int The id
- * @ApiProperty(identifier=true)
- */
- public $id;
-
- /**
- * @var string The dummy name
- *
- * @ApiProperty
- */
- public $name;
-}
diff --git a/tests/Fixtures/TestBundle/Entity/DummyOutput.php b/tests/Fixtures/TestBundle/Entity/NonRelationResource.php
similarity index 65%
rename from tests/Fixtures/TestBundle/Entity/DummyOutput.php
rename to tests/Fixtures/TestBundle/Entity/NonRelationResource.php
index b7cbb15de90..be05f3364de 100644
--- a/tests/Fixtures/TestBundle/Entity/DummyOutput.php
+++ b/tests/Fixtures/TestBundle/Entity/NonRelationResource.php
@@ -13,34 +13,30 @@
namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity;
-use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiResource;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\NonResourceClass;
use Doctrine\ORM\Mapping as ORM;
/**
- * Dummy Output.
- *
- * @author Kévin Dunglas
- *
* @ApiResource
* @ORM\Entity
*/
-class DummyOutput
+class NonRelationResource
{
/**
- * @var int The id
- *
* @ORM\Column(type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
- public $id;
+ private $id;
/**
- * @var string The dummy name
- *
- * @ORM\Column
- * @ApiProperty(iri="http://schema.org/name")
+ * @var NonResourceClass
*/
- public $name;
+ public $relation;
+
+ public function getId()
+ {
+ return $this->id;
+ }
}
diff --git a/tests/Fixtures/TestBundle/Entity/User.php b/tests/Fixtures/TestBundle/Entity/User.php
index ac5ecc11ab4..89ca2aa3b07 100644
--- a/tests/Fixtures/TestBundle/Entity/User.php
+++ b/tests/Fixtures/TestBundle/Entity/User.php
@@ -14,6 +14,8 @@
namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\RecoverPasswordInput;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\RecoverPasswordOutput;
use Doctrine\ORM\Mapping as ORM;
use FOS\UserBundle\Model\User as BaseUser;
use FOS\UserBundle\Model\UserInterface;
@@ -24,10 +26,17 @@
*
* @ORM\Entity
* @ORM\Table(name="user_test")
- * @ApiResource(attributes={
- * "normalization_context"={"groups"={"user", "user-read"}},
- * "denormalization_context"={"groups"={"user", "user-write"}}
- * })
+ * @ApiResource(
+ * attributes={
+ * "normalization_context"={"groups"={"user", "user-read"}},
+ * "denormalization_context"={"groups"={"user", "user-write"}}
+ * },
+ * itemOperations={"get", "put", "delete",
+ * "recover_password"={
+ * "input"=RecoverPasswordInput::class, "output"=RecoverPasswordOutput::class, "method"="PUT", "path"="users/recover/{id}"
+ * }
+ * }
+ * )
*
* @author Théo FIDRY
* @author Kévin Dunglas
diff --git a/tests/Fixtures/app/config/config_common.yml b/tests/Fixtures/app/config/config_common.yml
index 456baa29f73..cf677ad4153 100644
--- a/tests/Fixtures/app/config/config_common.yml
+++ b/tests/Fixtures/app/config/config_common.yml
@@ -189,12 +189,6 @@ services:
class: ApiPlatform\Core\Tests\Fixtures\TestBundle\Validator\DummyValidationGroupsGenerator
public: true
- app.dummy_input_data_persister:
- class: ApiPlatform\Core\Tests\Fixtures\TestBundle\DataPersister\DummyInputDataPersister
- public: false
- tags:
- - { name: 'api_platform.data_persister' }
-
mercure.hub.default.publisher:
class: ApiPlatform\Core\Tests\Fixtures\DummyMercurePublisher
@@ -203,3 +197,39 @@ services:
decorates: api_platform.swagger.normalizer.documentation
public: false
arguments: ['@app.serializer.normalizer.override_documentation.inner']
+
+ app.data_transformer.custom_input_dto:
+ class: 'ApiPlatform\Core\Tests\Fixtures\TestBundle\DataTransformer\CustomInputDtoDataTransformer'
+ public: false
+ tags:
+ - { name: 'api_platform.data_transformer' }
+
+ app.data_transformer.custom_output_dto:
+ class: 'ApiPlatform\Core\Tests\Fixtures\TestBundle\DataTransformer\CustomOutputDtoDataTransformer'
+ public: false
+ tags:
+ - { name: 'api_platform.data_transformer' }
+
+ app.data_transformer.input_dto:
+ class: 'ApiPlatform\Core\Tests\Fixtures\TestBundle\DataTransformer\InputDtoDataTransformer'
+ public: false
+ tags:
+ - { name: 'api_platform.data_transformer' }
+
+ app.data_transformer.output_dto:
+ class: 'ApiPlatform\Core\Tests\Fixtures\TestBundle\DataTransformer\OutputDtoDataTransformer'
+ public: false
+ tags:
+ - { name: 'api_platform.data_transformer' }
+
+ app.data_transformer.recover_password_input:
+ class: 'ApiPlatform\Core\Tests\Fixtures\TestBundle\DataTransformer\RecoverPasswordInputDataTransformer'
+ public: false
+ tags:
+ - { name: 'api_platform.data_transformer' }
+
+ app.data_transformer.recover_password_output:
+ class: 'ApiPlatform\Core\Tests\Fixtures\TestBundle\DataTransformer\RecoverPasswordOutputDataTransformer'
+ public: false
+ tags:
+ - { name: 'api_platform.data_transformer' }
diff --git a/tests/GraphQl/Serializer/ItemNormalizerTest.php b/tests/GraphQl/Serializer/ItemNormalizerTest.php
index 60a1b4ec566..fa7f7fe52d1 100644
--- a/tests/GraphQl/Serializer/ItemNormalizerTest.php
+++ b/tests/GraphQl/Serializer/ItemNormalizerTest.php
@@ -32,6 +32,10 @@
*/
class ItemNormalizerTest extends TestCase
{
+ /**
+ * @group legacy
+ * @expectedDeprecation Passing a falsy $allowUnmappedClass flag in ApiPlatform\Core\Serializer\AbstractItemNormalizer is deprecated since version 2.4 and will default to true in 3.0.
+ */
public function testSupportNormalization()
{
$std = new \stdClass();
@@ -89,7 +93,16 @@ public function testNormalize()
$propertyNameCollectionFactoryProphecy->reveal(),
$propertyMetadataFactoryProphecy->reveal(),
$iriConverterProphecy->reveal(),
- $resourceClassResolverProphecy->reveal()
+ $resourceClassResolverProphecy->reveal(),
+ null,
+ null,
+ null,
+ null,
+ false,
+ null,
+ null,
+ null,
+ true
);
$normalizer->setSerializer($serializerProphecy->reveal());
@@ -119,7 +132,16 @@ public function testDenormalize()
$propertyNameCollectionFactoryProphecy->reveal(),
$propertyMetadataFactoryProphecy->reveal(),
$iriConverterProphecy->reveal(),
- $resourceClassResolverProphecy->reveal()
+ $resourceClassResolverProphecy->reveal(),
+ null,
+ null,
+ null,
+ null,
+ false,
+ null,
+ null,
+ null,
+ true
);
$normalizer->setSerializer($serializerProphecy->reveal());
diff --git a/tests/Hal/Serializer/ItemNormalizerTest.php b/tests/Hal/Serializer/ItemNormalizerTest.php
index 95b06fd4b61..714a1ba9839 100644
--- a/tests/Hal/Serializer/ItemNormalizerTest.php
+++ b/tests/Hal/Serializer/ItemNormalizerTest.php
@@ -41,6 +41,10 @@
*/
class ItemNormalizerTest extends TestCase
{
+ /**
+ * @group legacy
+ * @expectedDeprecation Passing a falsy $allowUnmappedClass flag in ApiPlatform\Core\Serializer\AbstractItemNormalizer is deprecated since version 2.4 and will default to true in 3.0.
+ */
public function testDoesNotSupportDenormalization()
{
$this->expectException(RuntimeException::class);
@@ -64,6 +68,10 @@ public function testDoesNotSupportDenormalization()
$normalizer->denormalize(['foo'], 'Foo');
}
+ /**
+ * @group legacy
+ * @expectedDeprecation Passing a falsy $allowUnmappedClass flag in ApiPlatform\Core\Serializer\AbstractItemNormalizer is deprecated since version 2.4 and will default to true in 3.0.
+ */
public function testSupportsNormalization()
{
$std = new \stdClass();
@@ -137,7 +145,14 @@ public function testNormalize()
$iriConverterProphecy->reveal(),
$resourceClassResolverProphecy->reveal(),
null,
- $nameConverter->reveal()
+ $nameConverter->reveal(),
+ null,
+ null,
+ false,
+ [],
+ null,
+ null,
+ true
);
$normalizer->setSerializer($serializerProphecy->reveal());
@@ -197,7 +212,14 @@ public function testNormalizeWithoutCache()
$iriConverterProphecy->reveal(),
$resourceClassResolverProphecy->reveal(),
null,
- $nameConverter->reveal()
+ $nameConverter->reveal(),
+ null,
+ null,
+ false,
+ [],
+ null,
+ null,
+ true
);
$normalizer->setSerializer($serializerProphecy->reveal());
@@ -271,7 +293,13 @@ public function testMaxDepth()
$resourceClassResolverProphecy->reveal(),
null,
null,
- $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()))
+ $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())),
+ null,
+ false,
+ [],
+ null,
+ null,
+ true
);
$serializer = new Serializer([$normalizer]);
$normalizer->setSerializer($serializer);
diff --git a/tests/Hydra/Serializer/DocumentationNormalizerTest.php b/tests/Hydra/Serializer/DocumentationNormalizerTest.php
index b6755366491..a69d4c84080 100644
--- a/tests/Hydra/Serializer/DocumentationNormalizerTest.php
+++ b/tests/Hydra/Serializer/DocumentationNormalizerTest.php
@@ -364,7 +364,7 @@ public function testNormalizeInputOutputClass()
$propertyNameCollectionFactoryProphecy->create('inputClass', [])->shouldBeCalled()->willReturn(new PropertyNameCollection(['a', 'b']));
$propertyNameCollectionFactoryProphecy->create('outputClass', [])->shouldBeCalled()->willReturn(new PropertyNameCollection(['c', 'd']));
- $dummyMetadata = new ResourceMetadata('dummy', 'dummy', '#dummy', ['get' => [], 'put' => ['input_class' => false]], ['get' => [], 'post' => ['output_class' => false]], ['input_class' => 'inputClass', 'output_class' => 'outputClass']);
+ $dummyMetadata = new ResourceMetadata('dummy', 'dummy', '#dummy', ['get' => [], 'put' => ['input' => ['class' => null]]], ['get' => [], 'post' => ['output' => ['class' => null]]], ['input' => ['class' => 'inputClass'], 'output' => ['class' => 'outputClass']]);
$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
$resourceMetadataFactoryProphecy->create('dummy')->shouldBeCalled()->willReturn($dummyMetadata);
diff --git a/tests/Hydra/Serializer/ItemNormalizerTest.php b/tests/Hydra/Serializer/ItemNormalizerTest.php
index 9cbf0c9a92a..01dd49c42ee 100644
--- a/tests/Hydra/Serializer/ItemNormalizerTest.php
+++ b/tests/Hydra/Serializer/ItemNormalizerTest.php
@@ -34,6 +34,10 @@
*/
class ItemNormalizerTest extends TestCase
{
+ /**
+ * @group legacy
+ * @expectedDeprecation Passing a falsy $allowUnmappedClass flag in ApiPlatform\Core\Serializer\AbstractItemNormalizer is deprecated since version 2.4 and will default to true in 3.0.
+ */
public function testDontSupportDenormalization()
{
$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
@@ -51,6 +55,10 @@ public function testDontSupportDenormalization()
$this->assertTrue($normalizer->hasCacheableSupportsMethod());
}
+ /**
+ * @group legacy
+ * @expectedDeprecation Passing a falsy $allowUnmappedClass flag in ApiPlatform\Core\Serializer\AbstractItemNormalizer is deprecated since version 2.4 and will default to true in 3.0.
+ */
public function testSupportNormalization()
{
$std = new \stdClass();
@@ -116,7 +124,13 @@ public function testNormalize()
$propertyMetadataFactoryProphecy->reveal(),
$iriConverterProphecy->reveal(),
$resourceClassResolverProphecy->reveal(),
- $contextBuilderProphecy->reveal()
+ $contextBuilderProphecy->reveal(),
+ null,
+ null,
+ null,
+ [],
+ null,
+ true
);
$normalizer->setSerializer($serializerProphecy->reveal());
diff --git a/tests/JsonApi/Serializer/ItemNormalizerTest.php b/tests/JsonApi/Serializer/ItemNormalizerTest.php
index 2e5eb92008c..6230b369b66 100644
--- a/tests/JsonApi/Serializer/ItemNormalizerTest.php
+++ b/tests/JsonApi/Serializer/ItemNormalizerTest.php
@@ -16,6 +16,7 @@
use ApiPlatform\Core\Api\IriConverterInterface;
use ApiPlatform\Core\Api\ResourceClassResolverInterface;
use ApiPlatform\Core\Exception\InvalidArgumentException;
+use ApiPlatform\Core\Exception\ResourceClassNotFoundException;
use ApiPlatform\Core\JsonApi\Serializer\ItemNormalizer;
use ApiPlatform\Core\JsonApi\Serializer\ReservedAttributeNameConverter;
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
@@ -42,6 +43,10 @@
*/
class ItemNormalizerTest extends TestCase
{
+ /**
+ * @group legacy
+ * @expectedDeprecation Passing a falsy $allowUnmappedClass flag in ApiPlatform\Core\Serializer\AbstractItemNormalizer is deprecated since version 2.4 and will default to true in 3.0.
+ */
public function testSupportDenormalization()
{
$resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
@@ -63,6 +68,10 @@ public function testSupportDenormalization()
$this->assertTrue($normalizer->hasCacheableSupportsMethod());
}
+ /**
+ * @group legacy
+ * @expectedDeprecation Passing a falsy $allowUnmappedClass flag in ApiPlatform\Core\Serializer\AbstractItemNormalizer is deprecated since version 2.4 and will default to true in 3.0.
+ */
public function testSupportNormalization()
{
$std = new \stdClass();
@@ -129,7 +138,10 @@ public function testNormalize()
$resourceClassResolverProphecy->reveal(),
$propertyAccessorProphecy->reveal(),
new ReservedAttributeNameConverter(),
- $resourceMetadataFactoryProphecy->reveal()
+ $resourceMetadataFactoryProphecy->reveal(),
+ [],
+ null,
+ true
);
$normalizer->setSerializer($serializerProphecy->reveal());
@@ -157,6 +169,8 @@ public function testNormalizeIsNotAnArray()
$resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
$resourceClassResolverProphecy->getResourceClass($object, null, true)->willReturn(\stdClass::class)->shouldBeCalled();
+ $resourceMetadataFactory = $this->prophesize(ResourceMetadataFactoryInterface::class);
+ $resourceMetadataFactory->create(\stdClass::class)->willThrow(ResourceClassNotFoundException::class);
$normalizer = new ItemNormalizer(
$this->prophesize(PropertyNameCollectionFactoryInterface::class)->reveal(),
@@ -165,7 +179,10 @@ public function testNormalizeIsNotAnArray()
$resourceClassResolverProphecy->reveal(),
$this->prophesize(PropertyAccessorInterface::class)->reveal(),
new ReservedAttributeNameConverter(),
- $this->prophesize(ResourceMetadataFactoryInterface::class)->reveal()
+ $resourceMetadataFactory->reveal(),
+ [],
+ null,
+ true
);
$normalizer->setSerializer(new Serializer([$normalizer]));
@@ -212,6 +229,9 @@ public function testNormalizeThrowsNoSuchPropertyException()
$propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class);
$propertyAccessorProphecy->getValue($foo, 'bar')->willThrow(new NoSuchPropertyException());
+ $resourceMetadataFactory = $this->prophesize(ResourceMetadataFactoryInterface::class);
+ $resourceMetadataFactory->create(\stdClass::class)->willThrow(ResourceClassNotFoundException::class);
+
$normalizer = new ItemNormalizer(
$propertyNameCollectionFactoryProphecy->reveal(),
$propertyMetadataFactoryProphecy->reveal(),
@@ -219,7 +239,10 @@ public function testNormalizeThrowsNoSuchPropertyException()
$resourceClassResolverProphecy->reveal(),
$propertyAccessorProphecy->reveal(),
new ReservedAttributeNameConverter(),
- $this->prophesize(ResourceMetadataFactoryInterface::class)->reveal()
+ $resourceMetadataFactory->reveal(),
+ [],
+ null,
+ true
);
$normalizer->normalize($foo, ItemNormalizer::FORMAT);
@@ -291,6 +314,9 @@ public function testDenormalize()
$serializerProphecy = $this->prophesize(SerializerInterface::class);
$serializerProphecy->willImplement(NormalizerInterface::class);
+ $resourceMetadataFactory = $this->prophesize(ResourceMetadataFactoryInterface::class);
+ $resourceMetadataFactory->create(Dummy::class)->willThrow(ResourceClassNotFoundException::class);
+
$normalizer = new ItemNormalizer(
$propertyNameCollectionFactoryProphecy->reveal(),
$propertyMetadataFactoryProphecy->reveal(),
@@ -298,7 +324,10 @@ public function testDenormalize()
$resourceClassResolverProphecy->reveal(),
$propertyAccessorProphecy->reveal(),
new ReservedAttributeNameConverter(),
- $this->prophesize(ResourceMetadataFactoryInterface::class)->reveal()
+ $resourceMetadataFactory->reveal(),
+ [],
+ null,
+ true
);
$normalizer->setSerializer($serializerProphecy->reveal());
@@ -348,7 +377,10 @@ public function testDenormalizeUpdateOperationNotAllowed()
$this->prophesize(ResourceClassResolverInterface::class)->reveal(),
null,
null,
- $this->prophesize(ResourceMetadataFactoryInterface::class)->reveal()
+ $this->prophesize(ResourceMetadataFactoryInterface::class)->reveal(),
+ [],
+ null,
+ true
);
$normalizer->denormalize(
@@ -392,6 +424,9 @@ public function testDenormalizeCollectionIsNotArray()
)
)->shouldBeCalled();
+ $resourceMetadataFactory = $this->prophesize(ResourceMetadataFactoryInterface::class);
+ $resourceMetadataFactory->create(Dummy::class)->willThrow(ResourceClassNotFoundException::class);
+
$normalizer = new ItemNormalizer(
$propertyNameCollectionFactoryProphecy->reveal(),
$propertyMetadataFactoryProphecy->reveal(),
@@ -399,7 +434,10 @@ public function testDenormalizeCollectionIsNotArray()
$this->prophesize(ResourceClassResolverInterface::class)->reveal(),
$this->prophesize(PropertyAccessorInterface::class)->reveal(),
new ReservedAttributeNameConverter(),
- $this->prophesize(ResourceMetadataFactoryInterface::class)->reveal()
+ $resourceMetadataFactory->reveal(),
+ [],
+ null,
+ true
);
$normalizer->denormalize(
@@ -444,6 +482,9 @@ public function testDenormalizeCollectionWithInvalidKey()
)
)->shouldBeCalled();
+ $resourceMetadataFactory = $this->prophesize(ResourceMetadataFactoryInterface::class);
+ $resourceMetadataFactory->create(Dummy::class)->willThrow(ResourceClassNotFoundException::class);
+
$normalizer = new ItemNormalizer(
$propertyNameCollectionFactoryProphecy->reveal(),
$propertyMetadataFactoryProphecy->reveal(),
@@ -451,7 +492,10 @@ public function testDenormalizeCollectionWithInvalidKey()
$this->prophesize(ResourceClassResolverInterface::class)->reveal(),
$this->prophesize(PropertyAccessorInterface::class)->reveal(),
new ReservedAttributeNameConverter(),
- $this->prophesize(ResourceMetadataFactoryInterface::class)->reveal()
+ $resourceMetadataFactory->reveal(),
+ [],
+ null,
+ true
);
$normalizer->denormalize(
@@ -498,6 +542,9 @@ public function testDenormalizeRelationIsNotResourceLinkage()
$resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
$resourceClassResolverProphecy->isResourceClass(RelatedDummy::class)->willReturn(true)->shouldBeCalled();
+ $resourceMetadataFactory = $this->prophesize(ResourceMetadataFactoryInterface::class);
+ $resourceMetadataFactory->create(Dummy::class)->willThrow(ResourceClassNotFoundException::class);
+
$normalizer = new ItemNormalizer(
$propertyNameCollectionFactoryProphecy->reveal(),
$propertyMetadataFactoryProphecy->reveal(),
@@ -505,7 +552,10 @@ public function testDenormalizeRelationIsNotResourceLinkage()
$resourceClassResolverProphecy->reveal(),
$this->prophesize(PropertyAccessorInterface::class)->reveal(),
new ReservedAttributeNameConverter(),
- $this->prophesize(ResourceMetadataFactoryInterface::class)->reveal()
+ $resourceMetadataFactory->reveal(),
+ [],
+ null,
+ true
);
$normalizer->denormalize(
diff --git a/tests/Metadata/Resource/Factory/InputOutputResourceMetadataFactoryTest.php b/tests/Metadata/Resource/Factory/InputOutputResourceMetadataFactoryTest.php
new file mode 100644
index 00000000000..0e481704e17
--- /dev/null
+++ b/tests/Metadata/Resource/Factory/InputOutputResourceMetadataFactoryTest.php
@@ -0,0 +1,51 @@
+
+ *
+ * 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\Tests\Metadata\Resource\Factory;
+
+use ApiPlatform\Core\Metadata\Resource\Factory\InputOutputResourceMetadataFactory;
+use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
+use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
+use ApiPlatform\Core\Tests\Fixtures\DummyEntity;
+use PHPUnit\Framework\TestCase;
+
+class InputOutputResourceMetadataFactoryTest extends TestCase
+{
+ /**
+ * @dataProvider getAttributes
+ */
+ public function testExistingDescription($attributes, $expected)
+ {
+ $resourceMetadata = (new ResourceMetadata(null, 'My desc'))->withAttributes($attributes);
+ $decoratedProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
+ $decoratedProphecy->create('Foo')->willReturn($resourceMetadata)->shouldBeCalled();
+ $decorated = $decoratedProphecy->reveal();
+
+ $factory = new InputOutputResourceMetadataFactory($decorated);
+ $this->assertSame($expected, $factory->create('Foo')->getAttributes()['input']);
+ }
+
+ public function getAttributes(): array
+ {
+ return [
+ // no input class defined
+ [[], null],
+ // input is a string
+ [['input' => DummyEntity::class], ['class' => DummyEntity::class, 'name' => 'DummyEntity']],
+ // input is false
+ [['input' => false], ['class' => null]],
+ // input is an array
+ [['input' => ['class' => DummyEntity::class, 'type' => 'Foo']], ['class' => DummyEntity::class, 'type' => 'Foo', 'name' => 'DummyEntity']],
+ ];
+ }
+}
diff --git a/tests/Serializer/AbstractItemNormalizerTest.php b/tests/Serializer/AbstractItemNormalizerTest.php
index 611087bc127..b218865d9ed 100644
--- a/tests/Serializer/AbstractItemNormalizerTest.php
+++ b/tests/Serializer/AbstractItemNormalizerTest.php
@@ -16,11 +16,14 @@
use ApiPlatform\Core\Api\IriConverterInterface;
use ApiPlatform\Core\Api\ResourceClassResolverInterface;
use ApiPlatform\Core\DataProvider\ItemDataProviderInterface;
+use ApiPlatform\Core\DataTransformer\DataTransformerInterface;
use ApiPlatform\Core\Exception\InvalidArgumentException;
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
use ApiPlatform\Core\Metadata\Property\PropertyNameCollection;
+use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
+use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
use ApiPlatform\Core\Serializer\AbstractItemNormalizer;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyForAdditionalFields;
@@ -45,7 +48,11 @@
*/
class AbstractItemNormalizerTest extends TestCase
{
- public function testSupportNormalizationAndSupportDenormalization()
+ /**
+ * @group legacy
+ * @expectedDeprecation Passing a falsy $allowUnmappedClass flag in ApiPlatform\Core\Serializer\AbstractItemNormalizer is deprecated since version 2.4 and will default to true in 3.0.
+ */
+ public function testLegacySupportNormalizationAndSupportDenormalization()
{
$std = new \stdClass();
$dummy = new Dummy();
@@ -74,6 +81,41 @@ public function testSupportNormalizationAndSupportDenormalization()
$this->assertTrue($normalizer->hasCacheableSupportsMethod());
}
+ public function testSupportNormalizationAndSupportDenormalization()
+ {
+ $std = new \stdClass();
+ $dummy = new Dummy();
+
+ $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class);
+ $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
+ $iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
+ $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class);
+
+ $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
+
+ $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [
+ $propertyNameCollectionFactoryProphecy->reveal(),
+ $propertyMetadataFactoryProphecy->reveal(),
+ $iriConverterProphecy->reveal(),
+ $resourceClassResolverProphecy->reveal(),
+ $propertyAccessorProphecy->reveal(),
+ null,
+ null,
+ null,
+ false,
+ [],
+ null,
+ null,
+ true,
+ ]);
+
+ $this->assertTrue($normalizer->supportsNormalization($dummy));
+ $this->assertTrue($normalizer->supportsNormalization($std));
+ $this->assertTrue($normalizer->supportsDenormalization($dummy, Dummy::class));
+ $this->assertTrue($normalizer->supportsDenormalization($std, \stdClass::class));
+ $this->assertTrue($normalizer->hasCacheableSupportsMethod());
+ }
+
public function testNormalize()
{
$relatedDummy = new RelatedDummy();
@@ -141,6 +183,14 @@ public function testNormalize()
$iriConverterProphecy->reveal(),
$resourceClassResolverProphecy->reveal(),
$propertyAccessorProphecy->reveal(),
+ null,
+ null,
+ null,
+ false,
+ [],
+ null,
+ null,
+ true,
]);
$normalizer->setSerializer($serializerProphecy->reveal());
@@ -212,6 +262,14 @@ public function testNormalizeReadableLinks()
$iriConverterProphecy->reveal(),
$resourceClassResolverProphecy->reveal(),
$propertyAccessorProphecy->reveal(),
+ null,
+ null,
+ null,
+ false,
+ [],
+ null,
+ null,
+ true,
]);
$normalizer->setSerializer($serializerProphecy->reveal());
@@ -282,6 +340,14 @@ public function testDenormalize()
$iriConverterProphecy->reveal(),
$resourceClassResolverProphecy->reveal(),
$propertyAccessorProphecy->reveal(),
+ null,
+ null,
+ null,
+ false,
+ [],
+ null,
+ null,
+ true,
]);
$normalizer->setSerializer($serializerProphecy->reveal());
@@ -318,7 +384,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()) 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, null, true) extends AbstractItemNormalizer {
};
/** @var DummyForAdditionalFieldsInput $res */
@@ -326,8 +392,8 @@ public function testCanDenormalizeInputClassWithDifferentFieldsThanResourceClass
'dummyName' => 'Dummy Name',
], DummyForAdditionalFieldsInput::class, 'json', [
'resource_class' => DummyForAdditionalFields::class,
- 'input_class' => DummyForAdditionalFieldsInput::class,
- 'output_class' => DummyForAdditionalFields::class,
+ 'input' => ['class' => DummyForAdditionalFieldsInput::class],
+ 'output' => ['class' => DummyForAdditionalFields::class],
]);
$this->assertInstanceOf(DummyForAdditionalFieldsInput::class, $res);
@@ -396,6 +462,14 @@ public function testDenormalizeWritableLinks()
$iriConverterProphecy->reveal(),
$resourceClassResolverProphecy->reveal(),
$propertyAccessorProphecy->reveal(),
+ null,
+ null,
+ null,
+ false,
+ [],
+ null,
+ null,
+ true,
]);
$normalizer->setSerializer($serializerProphecy->reveal());
@@ -443,6 +517,14 @@ public function testBadRelationType()
$iriConverterProphecy->reveal(),
$resourceClassResolverProphecy->reveal(),
$propertyAccessorProphecy->reveal(),
+ null,
+ null,
+ null,
+ false,
+ [],
+ null,
+ null,
+ true,
]);
$normalizer->setSerializer($serializerProphecy->reveal());
@@ -486,6 +568,14 @@ public function testInnerDocumentNotAllowed()
$iriConverterProphecy->reveal(),
$resourceClassResolverProphecy->reveal(),
$propertyAccessorProphecy->reveal(),
+ null,
+ null,
+ null,
+ false,
+ [],
+ null,
+ null,
+ true,
]);
$normalizer->setSerializer($serializerProphecy->reveal());
@@ -520,6 +610,14 @@ public function testBadType()
$iriConverterProphecy->reveal(),
$resourceClassResolverProphecy->reveal(),
$propertyAccessorProphecy->reveal(),
+ null,
+ null,
+ null,
+ false,
+ [],
+ null,
+ null,
+ true,
]);
$normalizer->setSerializer($serializerProphecy->reveal());
@@ -552,6 +650,14 @@ public function testJsonAllowIntAsFloat()
$iriConverterProphecy->reveal(),
$resourceClassResolverProphecy->reveal(),
$propertyAccessorProphecy->reveal(),
+ null,
+ null,
+ null,
+ false,
+ [],
+ null,
+ null,
+ true,
]);
$normalizer->setSerializer($serializerProphecy->reveal());
@@ -601,6 +707,14 @@ public function testDenormalizeBadKeyType()
$iriConverterProphecy->reveal(),
$resourceClassResolverProphecy->reveal(),
$propertyAccessorProphecy->reveal(),
+ null,
+ null,
+ null,
+ false,
+ [],
+ null,
+ null,
+ true,
]);
$normalizer->setSerializer($serializerProphecy->reveal());
@@ -636,6 +750,14 @@ public function testNullable()
$iriConverterProphecy->reveal(),
$resourceClassResolverProphecy->reveal(),
$propertyAccessorProphecy->reveal(),
+ null,
+ null,
+ null,
+ false,
+ [],
+ null,
+ null,
+ true,
]);
$normalizer->setSerializer($serializerProphecy->reveal());
@@ -681,6 +803,14 @@ public function testChildInheritedProperty()
$iriConverterProphecy->reveal(),
$resourceClassResolverProphecy->reveal(),
$propertyAccessorProphecy->reveal(),
+ null,
+ null,
+ null,
+ false,
+ [],
+ null,
+ null,
+ true,
]);
$normalizer->setSerializer($serializerProphecy->reveal());
@@ -731,6 +861,10 @@ public function testDenormalizeRelationWithPlainId()
null,
$itemDataProviderProphecy->reveal(),
true,
+ [],
+ null,
+ null,
+ true,
]);
$normalizer->setSerializer($serializerProphecy->reveal());
@@ -781,9 +915,83 @@ public function testDoNotDenormalizeRelationWithPlainIdWhenPlainIdentifiersAreNo
null,
$itemDataProviderProphecy->reveal(),
false,
+ [],
+ null,
+ null,
+ true,
]);
$normalizer->setSerializer($serializerProphecy->reveal());
$normalizer->denormalize(['relatedDummy' => 1], Dummy::class, 'jsonld');
}
+
+ /**
+ * @group legacy
+ * @expectedDeprecation Passing a falsy $allowUnmappedClass flag in ApiPlatform\Core\Serializer\AbstractItemNormalizer is deprecated since version 2.4 and will default to true in 3.0.
+ */
+ public function testDisallowUnmappedClass()
+ {
+ $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class);
+ $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
+ $iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
+ $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class);
+ $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
+
+ $this->getMockForAbstractClass(AbstractItemNormalizer::class, [
+ $propertyNameCollectionFactoryProphecy->reveal(),
+ $propertyMetadataFactoryProphecy->reveal(),
+ $iriConverterProphecy->reveal(),
+ $resourceClassResolverProphecy->reveal(),
+ $propertyAccessorProphecy->reveal(),
+ ]);
+ }
+
+ public function testNormalizationWithDataTransformer()
+ {
+ $transformed = new \stdClass();
+ $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class);
+ $propertyNameCollectionFactoryProphecy->create(RelatedDummy::class, [])->willReturn(
+ new PropertyNameCollection()
+ )->shouldBeCalled();
+
+ $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
+
+ $iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
+ $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class);
+
+ $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
+
+ $serializerProphecy = $this->prophesize(SerializerInterface::class);
+ $serializerProphecy->willImplement(DenormalizerInterface::class);
+
+ $itemDataProviderProphecy = $this->prophesize(ItemDataProviderInterface::class);
+
+ $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
+ $resourceMetadataFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadata(
+ 'dummy', '', '', null, null, ['input' => ['class' => RelatedDummy::class]]
+ ));
+
+ $dataTransformerProphecy = $this->prophesize(DataTransformerInterface::class);
+ $dataTransformerProphecy->supportsTransformation(Argument::any(), Dummy::class, Argument::any())->shouldBeCalled()->willReturn(true);
+ $dataTransformerProphecy->transform(Argument::any(), Dummy::class, Argument::any())->shouldBeCalled()->willReturn($transformed);
+
+ $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [
+ $propertyNameCollectionFactoryProphecy->reveal(),
+ $propertyMetadataFactoryProphecy->reveal(),
+ $iriConverterProphecy->reveal(),
+ $resourceClassResolverProphecy->reveal(),
+ $propertyAccessorProphecy->reveal(),
+ null,
+ null,
+ $itemDataProviderProphecy->reveal(),
+ false,
+ [],
+ $dataTransformerProphecy->reveal(),
+ $resourceMetadataFactoryProphecy->reveal(),
+ true,
+ ]);
+ $normalizer->setSerializer($serializerProphecy->reveal());
+
+ $this->assertEquals($transformed, $normalizer->denormalize(['foo' => 't'], Dummy::class, 'jsonld'));
+ }
}
diff --git a/tests/Serializer/ItemNormalizerTest.php b/tests/Serializer/ItemNormalizerTest.php
index d7930b1404c..8688a91acb6 100644
--- a/tests/Serializer/ItemNormalizerTest.php
+++ b/tests/Serializer/ItemNormalizerTest.php
@@ -33,6 +33,10 @@
*/
class ItemNormalizerTest extends TestCase
{
+ /**
+ * @group legacy
+ * @expectedDeprecation Passing a falsy $allowUnmappedClass flag in ApiPlatform\Core\Serializer\AbstractItemNormalizer is deprecated since version 2.4 and will default to true in 3.0.
+ */
public function testSupportNormalization()
{
$std = new \stdClass();
@@ -91,7 +95,16 @@ public function testNormalize()
$propertyNameCollectionFactoryProphecy->reveal(),
$propertyMetadataFactoryProphecy->reveal(),
$iriConverterProphecy->reveal(),
- $resourceClassResolverProphecy->reveal()
+ $resourceClassResolverProphecy->reveal(),
+ null,
+ null,
+ null,
+ null,
+ false,
+ null,
+ null,
+ null,
+ true
);
$normalizer->setSerializer($serializerProphecy->reveal());
@@ -121,7 +134,16 @@ public function testDenormalize()
$propertyNameCollectionFactoryProphecy->reveal(),
$propertyMetadataFactoryProphecy->reveal(),
$iriConverterProphecy->reveal(),
- $resourceClassResolverProphecy->reveal()
+ $resourceClassResolverProphecy->reveal(),
+ null,
+ null,
+ null,
+ null,
+ false,
+ null,
+ null,
+ null,
+ true
);
$normalizer->setSerializer($serializerProphecy->reveal());
@@ -152,7 +174,16 @@ public function testDenormalizeWithIri()
$propertyNameCollectionFactoryProphecy->reveal(),
$propertyMetadataFactoryProphecy->reveal(),
$iriConverterProphecy->reveal(),
- $resourceClassResolverProphecy->reveal()
+ $resourceClassResolverProphecy->reveal(),
+ null,
+ null,
+ null,
+ null,
+ false,
+ null,
+ null,
+ null,
+ true
);
$normalizer->setSerializer($serializerProphecy->reveal());
@@ -181,7 +212,16 @@ public function testDenormalizeWithIdAndUpdateNotAllowed()
$propertyNameCollectionFactoryProphecy->reveal(),
$propertyMetadataFactoryProphecy->reveal(),
$iriConverterProphecy->reveal(),
- $resourceClassResolverProphecy->reveal()
+ $resourceClassResolverProphecy->reveal(),
+ null,
+ null,
+ null,
+ null,
+ false,
+ null,
+ null,
+ null,
+ true
);
$normalizer->setSerializer($serializerProphecy->reveal());
$normalizer->denormalize(['id' => '12', 'name' => 'hello'], Dummy::class, null, $context);
@@ -211,7 +251,16 @@ public function testDenormalizeWithIdAndNoResourceClass()
$propertyNameCollectionFactoryProphecy->reveal(),
$propertyMetadataFactoryProphecy->reveal(),
$iriConverterProphecy->reveal(),
- $resourceClassResolverProphecy->reveal()
+ $resourceClassResolverProphecy->reveal(),
+ null,
+ null,
+ null,
+ null,
+ false,
+ null,
+ null,
+ null,
+ true
);
$normalizer->setSerializer($serializerProphecy->reveal());
diff --git a/tests/Serializer/SerializerContextBuilderTest.php b/tests/Serializer/SerializerContextBuilderTest.php
index 08f539522c7..72c68c8534e 100644
--- a/tests/Serializer/SerializerContextBuilderTest.php
+++ b/tests/Serializer/SerializerContextBuilderTest.php
@@ -55,32 +55,32 @@ public function testCreateFromRequest()
{
$request = Request::create('/foos/1');
$request->attributes->replace(['_api_resource_class' => 'Foo', '_api_item_operation_name' => 'get', '_api_format' => 'xml', '_api_mime_type' => 'text/xml']);
- $expected = ['foo' => 'bar', 'item_operation_name' => 'get', 'resource_class' => 'Foo', 'request_uri' => '/foos/1', 'operation_type' => 'item', 'uri' => 'http://localhost/foos/1', 'output_class' => 'Foo', 'input_class' => 'Foo'];
+ $expected = ['foo' => 'bar', 'item_operation_name' => 'get', 'resource_class' => 'Foo', 'request_uri' => '/foos/1', 'operation_type' => 'item', 'uri' => 'http://localhost/foos/1', 'output' => null, 'input' => null];
$this->assertEquals($expected, $this->builder->createFromRequest($request, true));
$request = Request::create('/foos');
$request->attributes->replace(['_api_resource_class' => 'Foo', '_api_collection_operation_name' => 'pot', '_api_format' => 'xml', '_api_mime_type' => 'text/xml']);
- $expected = ['foo' => 'bar', 'collection_operation_name' => 'pot', 'resource_class' => 'Foo', 'request_uri' => '/foos', 'operation_type' => 'collection', 'uri' => 'http://localhost/foos', 'output_class' => 'Foo', 'input_class' => 'Foo'];
+ $expected = ['foo' => 'bar', 'collection_operation_name' => 'pot', 'resource_class' => 'Foo', 'request_uri' => '/foos', 'operation_type' => 'collection', 'uri' => 'http://localhost/foos', 'output' => null, 'input' => null];
$this->assertEquals($expected, $this->builder->createFromRequest($request, true));
$request = Request::create('/foos/1');
$request->attributes->replace(['_api_resource_class' => 'Foo', '_api_item_operation_name' => 'get', '_api_format' => 'xml', '_api_mime_type' => 'text/xml']);
- $expected = ['bar' => 'baz', 'item_operation_name' => 'get', 'resource_class' => 'Foo', 'request_uri' => '/foos/1', 'api_allow_update' => false, 'operation_type' => 'item', 'uri' => 'http://localhost/foos/1', 'output_class' => 'Foo', 'input_class' => 'Foo'];
+ $expected = ['bar' => 'baz', 'item_operation_name' => 'get', 'resource_class' => 'Foo', 'request_uri' => '/foos/1', 'api_allow_update' => false, 'operation_type' => 'item', 'uri' => 'http://localhost/foos/1', 'output' => null, 'input' => null];
$this->assertEquals($expected, $this->builder->createFromRequest($request, false));
$request = Request::create('/foos', 'POST');
$request->attributes->replace(['_api_resource_class' => 'Foo', '_api_collection_operation_name' => 'post', '_api_format' => 'xml', '_api_mime_type' => 'text/xml']);
- $expected = ['bar' => 'baz', 'collection_operation_name' => 'post', 'resource_class' => 'Foo', 'request_uri' => '/foos', 'api_allow_update' => false, 'operation_type' => 'collection', 'uri' => 'http://localhost/foos', 'output_class' => 'Foo', 'input_class' => 'Foo'];
+ $expected = ['bar' => 'baz', 'collection_operation_name' => 'post', 'resource_class' => 'Foo', 'request_uri' => '/foos', 'api_allow_update' => false, 'operation_type' => 'collection', 'uri' => 'http://localhost/foos', 'output' => null, 'input' => null];
$this->assertEquals($expected, $this->builder->createFromRequest($request, false));
$request = Request::create('/foos', 'PUT');
$request->attributes->replace(['_api_resource_class' => 'Foo', '_api_collection_operation_name' => 'put', '_api_format' => 'xml', '_api_mime_type' => 'text/xml']);
- $expected = ['bar' => 'baz', 'collection_operation_name' => 'put', 'resource_class' => 'Foo', 'request_uri' => '/foos', 'api_allow_update' => true, 'operation_type' => 'collection', 'uri' => 'http://localhost/foos', 'output_class' => 'Foo', 'input_class' => 'Foo'];
+ $expected = ['bar' => 'baz', 'collection_operation_name' => 'put', 'resource_class' => 'Foo', 'request_uri' => '/foos', 'api_allow_update' => true, 'operation_type' => 'collection', 'uri' => 'http://localhost/foos', 'output' => null, 'input' => null];
$this->assertEquals($expected, $this->builder->createFromRequest($request, false));
$request = Request::create('/bars/1/foos');
$request->attributes->replace(['_api_resource_class' => 'Foo', '_api_subresource_operation_name' => 'get', '_api_format' => 'xml', '_api_mime_type' => 'text/xml']);
- $expected = ['bar' => 'baz', 'subresource_operation_name' => 'get', 'resource_class' => 'Foo', 'request_uri' => '/bars/1/foos', 'operation_type' => 'subresource', 'api_allow_update' => false, 'uri' => 'http://localhost/bars/1/foos', 'output_class' => 'Foo', 'input_class' => 'Foo'];
+ $expected = ['bar' => 'baz', 'subresource_operation_name' => 'get', 'resource_class' => 'Foo', 'request_uri' => '/bars/1/foos', 'operation_type' => 'subresource', 'api_allow_update' => false, 'uri' => 'http://localhost/bars/1/foos', 'output' => null, 'input' => null];
$this->assertEquals($expected, $this->builder->createFromRequest($request, false));
}
@@ -93,7 +93,7 @@ public function testThrowExceptionOnInvalidRequest()
public function testReuseExistingAttributes()
{
- $expected = ['bar' => 'baz', 'item_operation_name' => 'get', 'resource_class' => 'Foo', 'request_uri' => '/foos/1', 'api_allow_update' => false, 'operation_type' => 'item', 'uri' => 'http://localhost/foos/1', 'output_class' => 'Foo', 'input_class' => 'Foo'];
+ $expected = ['bar' => 'baz', 'item_operation_name' => 'get', 'resource_class' => 'Foo', 'request_uri' => '/foos/1', 'api_allow_update' => false, 'operation_type' => 'item', 'uri' => 'http://localhost/foos/1', 'output' => null, 'input' => null];
$this->assertEquals($expected, $this->builder->createFromRequest(Request::create('/foos/1'), false, ['resource_class' => 'Foo', 'item_operation_name' => 'get']));
}
}
diff --git a/tests/Swagger/Serializer/DocumentationNormalizerV2Test.php b/tests/Swagger/Serializer/DocumentationNormalizerV2Test.php
index cb5756e69ce..320d1aec282 100644
--- a/tests/Swagger/Serializer/DocumentationNormalizerV2Test.php
+++ b/tests/Swagger/Serializer/DocumentationNormalizerV2Test.php
@@ -2318,7 +2318,7 @@ public function testNormalizeWithInputAndOutpusClass()
$propertyNameCollectionFactoryProphecy->create(InputDto::class, [])->shouldBeCalled()->willReturn(new PropertyNameCollection(['foo', 'bar']));
$propertyNameCollectionFactoryProphecy->create(OutputDto::class, [])->shouldBeCalled()->willReturn(new PropertyNameCollection(['baz', 'bat']));
- $dummyMetadata = new ResourceMetadata('Dummy', 'This is a dummy.', 'http://schema.example.com/Dummy', ['get' => [], 'put' => []], ['get' => [], 'post' => []], ['input_class' => InputDto::class, 'output_class' => OutputDto::class]);
+ $dummyMetadata = new ResourceMetadata('Dummy', 'This is a dummy.', 'http://schema.example.com/Dummy', ['get' => [], 'put' => []], ['get' => [], 'post' => []], ['input' => ['class' => InputDto::class], 'output' => ['class' => OutputDto::class]]);
$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
$resourceMetadataFactoryProphecy->create(Dummy::class)->shouldBeCalled()->willReturn($dummyMetadata);