diff --git a/features/bootstrap/FeatureContext.php b/features/bootstrap/FeatureContext.php
index 40b9e8be797..54bb7015b03 100644
--- a/features/bootstrap/FeatureContext.php
+++ b/features/bootstrap/FeatureContext.php
@@ -21,8 +21,10 @@
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyCar;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyCarColor;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyFriend;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyGroup;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyOffer;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyProduct;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyProperty;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\FileConfigDummy;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Node;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Question;
@@ -35,6 +37,7 @@
use Behat\Behat\Context\SnippetAcceptingContext;
use Behatch\HttpCall\Request;
use Doctrine\Common\Persistence\ManagerRegistry;
+use Doctrine\Common\Persistence\ObjectManager;
use Doctrine\ORM\Tools\SchemaTool;
/**
@@ -45,7 +48,7 @@ class FeatureContext implements Context, SnippetAcceptingContext
private $doctrine;
/**
- * @var \Doctrine\Common\Persistence\ObjectManager
+ * @var ObjectManager
*/
private $manager;
@@ -127,6 +130,46 @@ public function thereIsDummyObjects($nb)
$this->manager->flush();
}
+ /**
+ * @Given there is :nb dummy group objects
+ */
+ public function thereIsDummyGroupObjects($nb)
+ {
+ for ($i = 1; $i <= $nb; ++$i) {
+ $dummyGroup = new DummyGroup();
+
+ foreach (['foo', 'bar', 'baz', 'qux'] as $property) {
+ $dummyGroup->$property = ucfirst($property).' #'.$i;
+ }
+
+ $this->manager->persist($dummyGroup);
+ }
+
+ $this->manager->flush();
+ }
+
+ /**
+ * @Given there is :nb dummy property objects
+ */
+ public function thereIsDummyPropertyObjects($nb)
+ {
+ for ($i = 1; $i <= $nb; ++$i) {
+ $dummyProperty = new DummyProperty();
+ $dummyGroup = new DummyGroup();
+
+ foreach (['foo', 'bar', 'baz'] as $property) {
+ $dummyProperty->$property = $dummyGroup->$property = ucfirst($property).' #'.$i;
+ }
+
+ $dummyProperty->group = $dummyGroup;
+
+ $this->manager->persist($dummyGroup);
+ $this->manager->persist($dummyProperty);
+ }
+
+ $this->manager->flush();
+ }
+
/**
* @Given there is :nb dummy objects with relatedDummy
*/
diff --git a/features/serializer/group_filter.feature b/features/serializer/group_filter.feature
new file mode 100644
index 00000000000..23e024e08df
--- /dev/null
+++ b/features/serializer/group_filter.feature
@@ -0,0 +1,750 @@
+Feature: Filter with serialization groups on items and collections
+ In order to retrieve, create and update resources or large collections of resources
+ As a client software developer
+ I need to retrieve, create and update resources or collections of resources with serialization groups
+
+ @createSchema
+ Scenario: Get a collection of resources by group dummy_foo without overriding
+ Given there is "10" dummy group objects
+ When I send a "GET" request to "/dummy_groups?groups[]=dummy_foo"
+ Then the response status code should be 200
+ And the response should be in JSON
+ And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
+ And the JSON should be valid according to this schema:
+ """
+ {
+ "type": "object",
+ "properties": {
+ "@context": {"pattern": "^/contexts/DummyGroup$"},
+ "@id": {"pattern": "^/dummy_groups$"},
+ "@type": {"pattern": "^hydra:Collection$"},
+ "hydra:member": {
+ "type": "array",
+ "items": [
+ {
+ "type": "object",
+ "properties": {
+ "@id": {},
+ "@type": {},
+ "id": {},
+ "foo": {},
+ "bar": {},
+ "baz": {}
+ },
+ "additionalProperties": false,
+ "required": ["@id", "@type", "id", "foo", "bar", "baz"]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "@id": {},
+ "@type": {},
+ "id": {},
+ "foo": {},
+ "bar": {},
+ "baz": {}
+ },
+ "additionalProperties": false,
+ "required": ["@id", "@type", "id", "foo", "bar", "baz"]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "@id": {},
+ "@type": {},
+ "id": {},
+ "foo": {},
+ "bar": {},
+ "baz": {}
+ },
+ "additionalProperties": false,
+ "required": ["@id", "@type", "id", "foo", "bar", "baz"]
+ }
+ ],
+ "additionalItems": false,
+ "maxItems": 3,
+ "minItems": 3
+ },
+ "hydra:view": {
+ "type": "object",
+ "properties": {
+ "@id": {"pattern": "^/dummy_groups\\?groups%5B%5D=dummy_foo&page=1$"},
+ "@type": {"pattern": "^hydra:PartialCollectionView$"}
+ }
+ }
+ }
+ }
+ """
+
+ Scenario: Get a collection of resources by group dummy_foo with overriding
+ When I send a "GET" request to "/dummy_groups?override_groups[]=dummy_foo"
+ Then the response status code should be 200
+ And the response should be in JSON
+ And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
+ And the JSON should be valid according to this schema:
+ """
+ {
+ "type": "object",
+ "properties": {
+ "@context": {"pattern": "^/contexts/DummyGroup$"},
+ "@id": {"pattern": "^/dummy_groups$"},
+ "@type": {"pattern": "^hydra:Collection$"},
+ "hydra:member": {
+ "type": "array",
+ "items": [
+ {
+ "type": "object",
+ "properties": {
+ "@id": {},
+ "@type": {},
+ "foo": {}
+ },
+ "additionalProperties": false,
+ "required": ["@id", "@type", "foo"]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "@id": {},
+ "@type": {},
+ "foo": {}
+ },
+ "additionalProperties": false,
+ "required": ["@id", "@type", "foo"]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "@id": {},
+ "@type": {},
+ "foo": {}
+ },
+ "additionalProperties": false,
+ "required": ["@id", "@type", "foo"]
+ }
+ ],
+ "additionalItems": false,
+ "maxItems": 3,
+ "minItems": 3
+ },
+ "hydra:view": {
+ "type": "object",
+ "properties": {
+ "@id": {"pattern": "^/dummy_groups\\?override_groups%5B%5D=dummy_foo&page=1$"},
+ "@type": {"pattern": "^hydra:PartialCollectionView$"}
+ }
+ }
+ }
+ }
+ """
+
+ Scenario: Get a collection of resources by groups dummy_foo, dummy_qux and without overriding
+ When I send a "GET" request to "/dummy_groups?groups[]=dummy_foo&groups[]=dummy_qux"
+ 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": {
+ "@context": {"pattern": "^/contexts/DummyGroup$"},
+ "@id": {"pattern": "^/dummy_groups$"},
+ "@type": {"pattern": "^hydra:Collection$"},
+ "hydra:member": {
+ "type": "array",
+ "items": [
+ {
+ "type": "object",
+ "properties": {
+ "@id": {},
+ "@type": {},
+ "id": {},
+ "foo": {},
+ "bar": {},
+ "baz": {},
+ "qux": {}
+ },
+ "additionalProperties": false,
+ "required": ["@id", "@type", "id", "foo", "bar", "baz", "qux"]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "@id": {},
+ "@type": {},
+ "id": {},
+ "foo": {},
+ "bar": {},
+ "baz": {},
+ "qux": {}
+ },
+ "additionalProperties": false,
+ "required": ["@id", "@type", "id", "foo", "bar", "baz", "qux"]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "@id": {},
+ "@type": {},
+ "id": {},
+ "foo": {},
+ "bar": {},
+ "baz": {},
+ "qux": {}
+ },
+ "additionalProperties": false,
+ "required": ["@id", "@type", "id", "foo", "bar", "baz", "qux"]
+ }
+ ],
+ "additionalItems": false,
+ "maxItems": 3,
+ "minItems": 3
+ },
+ "hydra:view": {
+ "type": "object",
+ "properties": {
+ "@id": {"pattern": "^/dummy_groups\\?groups%5B%5D=dummy_foo&groups%5B%5D=dummy_qux&page=1$"},
+ "@type": {"pattern": "^hydra:PartialCollectionView$"}
+ }
+ }
+ }
+ }
+ """
+
+ Scenario: Get a collection of resources by groups dummy_foo, dummy_qux and with overriding
+ When I send a "GET" request to "/dummy_groups?override_groups[]=dummy_foo&override_groups[]=dummy_qux"
+ 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": {
+ "@context": {"pattern": "^/contexts/DummyGroup$"},
+ "@id": {"pattern": "^/dummy_groups$"},
+ "@type": {"pattern": "^hydra:Collection$"},
+ "hydra:member": {
+ "type": "array",
+ "items": [
+ {
+ "type": "object",
+ "properties": {
+ "@id": {},
+ "@type": {},
+ "foo": {},
+ "qux": {}
+ },
+ "additionalProperties": false,
+ "required": ["@id", "@type", "foo", "qux"]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "@id": {},
+ "@type": {},
+ "foo": {},
+ "qux": {}
+ },
+ "additionalProperties": false,
+ "required": ["@id", "@type", "foo", "qux"]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "@id": {},
+ "@type": {},
+ "foo": {},
+ "qux": {}
+ },
+ "additionalProperties": false,
+ "required": ["@id", "@type", "foo", "qux"]
+ }
+ ],
+ "additionalItems": false,
+ "maxItems": 3,
+ "minItems": 3
+ },
+ "hydra:view": {
+ "type": "object",
+ "properties": {
+ "@id": {"pattern": "^/dummy_groups\\?override_groups%5B%5D=dummy_foo&override_groups%5B%5D=dummy_qux&page=1$"},
+ "@type": {"pattern": "^hydra:PartialCollectionView$"}
+ }
+ }
+ }
+ }
+ """
+
+ Scenario: Get a collection of resources by group empty and without overriding
+ When I send a "GET" request to "/dummy_groups?groups[]="
+ 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": {
+ "@context": {"pattern": "^/contexts/DummyGroup$"},
+ "@id": {"pattern": "^/dummy_groups$"},
+ "@type": {"pattern": "^hydra:Collection$"},
+ "hydra:member": {
+ "type": "array",
+ "items": [
+ {
+ "type": "object",
+ "properties": {
+ "@id": {},
+ "@type": {},
+ "id": {},
+ "foo": {},
+ "bar": {},
+ "baz": {}
+ },
+ "additionalProperties": false,
+ "required": ["@id", "@type", "id", "foo", "bar", "baz"]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "@id": {},
+ "@type": {},
+ "id": {},
+ "foo": {},
+ "bar": {},
+ "baz": {}
+ },
+ "additionalProperties": false,
+ "required": ["@id", "@type", "id", "foo", "bar", "baz"]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "@id": {},
+ "@type": {},
+ "id": {},
+ "foo": {},
+ "bar": {},
+ "baz": {}
+ },
+ "additionalProperties": false,
+ "required": ["@id", "@type", "id", "foo", "bar", "baz"]
+ }
+ ],
+ "additionalItems": false,
+ "maxItems": 3,
+ "minItems": 3
+ },
+ "hydra:view": {
+ "type": "object",
+ "properties": {
+ "@id": {"pattern": "^/dummy_groups\\?groups%5B%5D=&page=1$"},
+ "@type": {"pattern": "^hydra:PartialCollectionView$"}
+ }
+ }
+ }
+ }
+ """
+
+ Scenario: Get a collection of resources by group empty and with overriding
+ When I send a "GET" request to "/dummy_groups?override_groups[]="
+ 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": {
+ "@context": {"pattern": "^/contexts/DummyGroup$"},
+ "@id": {"pattern": "^/dummy_groups$"},
+ "@type": {"pattern": "^hydra:Collection$"},
+ "hydra:member": {
+ "type": "array",
+ "items": [
+ {
+ "type": "object",
+ "properties": {
+ "@id": {},
+ "@type": {}
+ },
+ "additionalProperties": false,
+ "required": ["@id", "@type"]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "@id": {},
+ "@type": {}
+ },
+ "additionalProperties": false,
+ "required": ["@id", "@type"]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "@id": {},
+ "@type": {}
+ },
+ "additionalProperties": false,
+ "required": ["@id", "@type"]
+ }
+ ],
+ "additionalItems": false,
+ "maxItems": 3,
+ "minItems": 3
+ },
+ "hydra:view": {
+ "type": "object",
+ "properties": {
+ "@id": {"pattern": "^/dummy_groups\\?override_groups%5B%5D=&page=1$"},
+ "@type": {"pattern": "^hydra:PartialCollectionView$"}
+ }
+ }
+ }
+ }
+ """
+
+ Scenario: Get a resource by group dummy_foo without overriding
+ When I send a "GET" request to "/dummy_groups/1?groups[]=dummy_foo"
+ Then the response status code should be 200
+ And the response should be in JSON
+ And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
+ And the JSON should be valid according to this schema:
+ """
+ {
+ "type": "object",
+ "properties": {
+ "@context": {"pattern": "^/contexts/DummyGroup$"},
+ "@id": {"pattern": "^/dummy_groups/1$"},
+ "@type": {"pattern": "^DummyGroup$"},
+ "id": {},
+ "foo": {},
+ "bar": {},
+ "baz": {}
+ },
+ "additionalProperties": false,
+ "required": ["@context", "@id", "@type", "id", "foo", "bar", "baz"]
+ }
+ """
+
+ Scenario: Get a resource by group dummy_foo with overriding
+ When I send a "GET" request to "/dummy_groups/1?override_groups[]=dummy_foo"
+ Then the response status code should be 200
+ And the response should be in JSON
+ And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
+ And the JSON should be valid according to this schema:
+ """
+ {
+ "type": "object",
+ "properties": {
+ "@context": {"pattern": "^/contexts/DummyGroup$"},
+ "@id": {"pattern": "^/dummy_groups/1$"},
+ "@type": {"pattern": "^DummyGroup$"},
+ "foo": {}
+ },
+ "additionalProperties": false,
+ "required": ["@context", "@id", "@type", "foo"]
+ }
+ """
+
+ Scenario: Get a resource by groups dummy_foo, dummy_qux and without overriding
+ When I send a "GET" request to "/dummy_groups/1?groups[]=dummy_foo&groups[]=dummy_qux"
+ 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": {
+ "@context": {"pattern": "^/contexts/DummyGroup$"},
+ "@id": {"pattern": "^/dummy_groups/1$"},
+ "@type": {"pattern": "^DummyGroup$"},
+ "id": {},
+ "foo": {},
+ "bar": {},
+ "baz": {},
+ "qux": {}
+ },
+ "additionalProperties": false,
+ "required": ["@context", "@id", "@type", "id", "foo", "bar", "baz", "qux"]
+ }
+ """
+
+ Scenario: Get a resource by groups dummy_foo, dummy_qux and with overriding
+ When I send a "GET" request to "/dummy_groups/1?override_groups[]=dummy_foo&override_groups[]=dummy_qux"
+ 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": {
+ "@context": {"pattern": "^/contexts/DummyGroup$"},
+ "@id": {"pattern": "^/dummy_groups/1$"},
+ "@type": {"pattern": "^DummyGroup$"},
+ "foo": {},
+ "qux": {}
+ },
+ "additionalProperties": false,
+ "required": ["@context", "@id", "@type", "foo", "qux"]
+ }
+ """
+
+ Scenario: Get a resource by group empty and without overriding
+ When I send a "GET" request to "/dummy_groups/1?groups[]="
+ 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": {
+ "@context": {"pattern": "^/contexts/DummyGroup$"},
+ "@id": {"pattern": "^/dummy_groups/1$"},
+ "@type": {"pattern": "^DummyGroup$"},
+ "id": {},
+ "foo": {},
+ "bar": {},
+ "baz": {}
+ },
+ "additionalProperties": false,
+ "required": ["@context", "@id", "@type", "id", "foo", "bar", "baz"]
+ }
+ """
+
+ Scenario: Get a resource by group empty and with overriding
+ When I send a "GET" request to "/dummy_groups/1?override_groups[]="
+ 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": {
+ "@context": {"pattern": "^/contexts/DummyGroup$"},
+ "@id": {"pattern": "^/dummy_groups/1$"},
+ "@type": {"pattern": "^DummyGroup$"}
+ },
+ "additionalProperties": false,
+ "required": ["@context", "@id", "@type"]
+ }
+ """
+
+ Scenario: Create a resource by group dummy_foo and without overriding
+ When I add "Content-Type" header equal to "application/ld+json"
+ And I send a "POST" request to "/dummy_groups?groups[]=dummy_foo" with body:
+ """
+ {
+ "foo": "Foo",
+ "bar": "Bar",
+ "baz": "Baz",
+ "qux": "Qux"
+ }
+ """
+ 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\/DummyGroup",
+ "@id": "\/dummy_groups\/11",
+ "@type": "DummyGroup",
+ "id": 11,
+ "foo": "Foo",
+ "bar": "Bar",
+ "baz": null
+ }
+ """
+
+ Scenario: Create a resource by group dummy_foo and with overriding
+ When I add "Content-Type" header equal to "application/ld+json"
+ And I send a "POST" request to "/dummy_groups?override_groups[]=dummy_foo" with body:
+ """
+ {
+ "foo": "Foo",
+ "bar": "Bar",
+ "baz": "Baz",
+ "qux": "Qux"
+ }
+ """
+ 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\/DummyGroup",
+ "@id": "\/dummy_groups\/12",
+ "@type": "DummyGroup",
+ "foo": "Foo"
+ }
+ """
+
+ Scenario: Create a resource by groups dummy_foo, dummy_baz, dummy_qux and without overriding
+ When I add "Content-Type" header equal to "application/ld+json"
+ And I send a "POST" request to "/dummy_groups?groups[]=dummy_foo&groups[]=dummy_baz&groups[]=dummy_qux" with body:
+ """
+ {
+ "foo": "Foo",
+ "bar": "Bar",
+ "baz": "Baz",
+ "qux": "Qux"
+ }
+ """
+ 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\/DummyGroup",
+ "@id": "\/dummy_groups\/13",
+ "@type": "DummyGroup",
+ "id": 13,
+ "foo": "Foo",
+ "bar": "Bar",
+ "baz": "Baz",
+ "qux": "Qux"
+ }
+ """
+
+ Scenario: Create a resource by groups dummy_foo, dummy_baz, dummy_qux and with overriding
+ When I add "Content-Type" header equal to "application/ld+json"
+ And I send a "POST" request to "/dummy_groups?override_groups[]=dummy_foo&override_groups[]=dummy_baz&override_groups[]=dummy_qux" with body:
+ """
+ {
+ "foo": "Foo",
+ "bar": "Bar",
+ "baz": "Baz",
+ "qux": "Qux"
+ }
+ """
+ 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\/DummyGroup",
+ "@id": "\/dummy_groups\/14",
+ "@type": "DummyGroup",
+ "foo": "Foo",
+ "baz": "Baz",
+ "qux": "Qux"
+ }
+ """
+
+ Scenario: Create a resource by groups dummy, dummy_baz, without overriding
+ When I add "Content-Type" header equal to "application/ld+json"
+ And I send a "POST" request to "/dummy_groups?groups[]=dummy&groups[]=dummy_baz" with body:
+ """
+ {
+ "foo": "Foo",
+ "bar": "Bar",
+ "baz": "Baz",
+ "qux": "Qux"
+ }
+ """
+ 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\/DummyGroup",
+ "@id": "\/dummy_groups\/15",
+ "@type": "DummyGroup",
+ "id": 15,
+ "foo": "Foo",
+ "bar": "Bar",
+ "baz": "Baz",
+ "qux": "Qux"
+ }
+ """
+
+ Scenario: Create a resource by groups dummy, dummy_baz and with overriding
+ When I add "Content-Type" header equal to "application/ld+json"
+ And I send a "POST" request to "/dummy_groups?override_groups[]=dummy&override_groups[]=dummy_baz" with body:
+ """
+ {
+ "foo": "Foo",
+ "bar": "Bar",
+ "baz": "Baz",
+ "qux": "Qux"
+ }
+ """
+ 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\/DummyGroup",
+ "@id": "\/dummy_groups\/16",
+ "@type": "DummyGroup",
+ "id": 16,
+ "foo": "Foo",
+ "bar": "Bar",
+ "baz": "Baz",
+ "qux": "Qux"
+ }
+ """
+
+ Scenario: Create a resource by group empty and without overriding
+ When I add "Content-Type" header equal to "application/ld+json"
+ And I send a "POST" request to "/dummy_groups?groups[]=" with body:
+ """
+ {
+ "foo": "Foo",
+ "bar": "Bar",
+ "baz": "Baz",
+ "qux": "Qux"
+ }
+ """
+ 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\/DummyGroup",
+ "@id": "\/dummy_groups\/17",
+ "@type": "DummyGroup",
+ "id": 17,
+ "foo": "Foo",
+ "bar": "Bar",
+ "baz": null
+ }
+ """
+
+ @dropSchema
+ Scenario: Create a resource by group empty and with overriding
+ When I add "Content-Type" header equal to "application/ld+json"
+ And I send a "POST" request to "/dummy_groups?override_groups[]=" with body:
+ """
+ {
+ "foo": "Foo",
+ "bar": "Bar",
+ "baz": "Baz",
+ "qux": "Qux"
+ }
+ """
+ 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\/DummyGroup",
+ "@id": "\/dummy_groups\/18",
+ "@type": "DummyGroup"
+ }
+ """
diff --git a/features/serializer/property_filter.feature b/features/serializer/property_filter.feature
new file mode 100644
index 00000000000..9602ea4b002
--- /dev/null
+++ b/features/serializer/property_filter.feature
@@ -0,0 +1,394 @@
+Feature: Filter with serialization attributes on items and collections
+ In order to retrieve, create and update resources or large collection of resources
+ As a client software developer
+ I need to retrieve, create and update resources or collections of resources with serialization attributes
+
+ @createSchema
+ Scenario: Get a collection of resources by attributes id, foo and bar
+ Given there is "10" dummy property objects
+ When I send a "GET" request to "/dummy_properties?properties[]=id&properties[]=foo&properties[]=bar"
+ Then the response status code should be 200
+ And the response should be in JSON
+ And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
+ And the JSON should be valid according to this schema:
+ """
+ {
+ "type": "object",
+ "properties": {
+ "@context": {"pattern": "^/contexts/DummyProperty$"},
+ "@id": {"pattern": "^/dummy_properties$"},
+ "@type": {"pattern": "^hydra:Collection$"},
+ "hydra:member": {
+ "type": "array",
+ "items": [
+ {
+ "type": "object",
+ "properties": {
+ "@id": {},
+ "@type": {},
+ "id": {},
+ "foo": {},
+ "bar": {}
+ },
+ "additionalProperties": false,
+ "required": ["@id", "@type", "id", "foo", "bar"]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "@id": {},
+ "@type": {},
+ "id": {},
+ "foo": {},
+ "bar": {}
+ },
+ "additionalProperties": false,
+ "required": ["@id", "@type", "id", "foo", "bar"]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "@id": {},
+ "@type": {},
+ "id": {},
+ "foo": {},
+ "bar": {}
+ },
+ "additionalProperties": false,
+ "required": ["@id", "@type", "id", "foo", "bar"]
+ }
+ ],
+ "additionalItems": false,
+ "maxItems": 3,
+ "minItems": 3
+ },
+ "hydra:view": {
+ "type": "object",
+ "properties": {
+ "@id": {"pattern": "^/dummy_properties\\?properties%5B%5D=id&properties%5B%5D=foo&properties%5B%5D=bar&page=1$"},
+ "@type": {"pattern": "^hydra:PartialCollectionView$"}
+ }
+ }
+ }
+ }
+ """
+
+ Scenario: Get a collection of resources by attributes foo, bar, group.baz and group.qux
+ When I send a "GET" request to "/dummy_properties?properties[]=foo&properties[]=bar&properties[group][]=baz&properties[group][]=qux"
+ 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": {
+ "@context": {"pattern": "^/contexts/DummyProperty$"},
+ "@id": {"pattern": "^/dummy_properties$"},
+ "@type": {"pattern": "^hydra:Collection$"},
+ "hydra:member": {
+ "type": "array",
+ "items": [
+ {
+ "type": "object",
+ "properties": {
+ "@id": {},
+ "@type": {},
+ "foo": {},
+ "bar": {},
+ "group": {
+ "type": "object",
+ "properties": {
+ "@id": {},
+ "@type": {},
+ "baz": {}
+ },
+ "additionalProperties": false,
+ "required": ["@id", "@type", "baz"]
+ }
+ },
+ "additionalProperties": false,
+ "required": ["@id", "@type", "foo", "bar"]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "@id": {},
+ "@type": {},
+ "foo": {},
+ "bar": {},
+ "group": {
+ "type": "object",
+ "properties": {
+ "@id": {},
+ "@type": {},
+ "baz": {}
+ },
+ "additionalProperties": false,
+ "required": ["@id", "@type", "baz"]
+ }
+ },
+ "additionalProperties": false,
+ "required": ["@id", "@type", "foo", "bar"]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "@id": {},
+ "@type": {},
+ "foo": {},
+ "bar": {},
+ "group": {
+ "type": "object",
+ "properties": {
+ "@id": {},
+ "@type": {},
+ "baz": {}
+ },
+ "additionalProperties": false,
+ "required": ["@id", "@type", "baz"]
+ }
+ },
+ "additionalProperties": false,
+ "required": ["@id", "@type", "foo", "bar"]
+ }
+ ],
+ "additionalItems": false,
+ "maxItems": 3,
+ "minItems": 3
+ },
+ "hydra:view": {
+ "type": "object",
+ "properties": {
+ "@id": {"pattern": "^/dummy_properties\\?properties%5B%5D=foo&properties%5B%5D=bar&properties%5Bgroup%5D%5B%5D=baz&properties%5Bgroup%5D%5B%5D=qux&page=1$"},
+ "@type": {"pattern": "^hydra:PartialCollectionView$"}
+ }
+ }
+ }
+ }
+ """
+
+ Scenario: Get a collection of resources by attributes empty
+ When I send a "GET" request to "/dummy_properties?properties[]=&properties[group][]="
+ 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": {
+ "@context": {"pattern": "^/contexts/DummyProperty$"},
+ "@id": {"pattern": "^/dummy_properties$"},
+ "@type": {"pattern": "^hydra:Collection$"},
+ "hydra:member": {
+ "type": "array",
+ "items": [
+ {
+ "type": "object",
+ "properties": {
+ "@id": {},
+ "@type": {},
+ "group": {
+ "type": "object",
+ "properties": {
+ "@id": {},
+ "@type": {}
+ },
+ "additionalProperties": false,
+ "required": ["@id", "@type"]
+ }
+ },
+ "additionalProperties": false,
+ "required": ["@id", "@type", "group"]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "@id": {},
+ "@type": {},
+ "group": {
+ "type": "object",
+ "properties": {
+ "@id": {},
+ "@type": {}
+ },
+ "additionalProperties": false,
+ "required": ["@id", "@type"]
+ }
+ },
+ "additionalProperties": false,
+ "required": ["@id", "@type", "group"]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "@id": {},
+ "@type": {},
+ "group": {
+ "type": "object",
+ "properties": {
+ "@id": {},
+ "@type": {}
+ },
+ "additionalProperties": false,
+ "required": ["@id", "@type"]
+ }
+ },
+ "additionalProperties": false,
+ "required": ["@id", "@type", "group"]
+ }
+ ],
+ "additionalItems": false,
+ "maxItems": 3,
+ "minItems": 3
+ },
+ "hydra:view": {
+ "type": "object",
+ "properties": {
+ "@id": {"pattern": "^/dummy_properties\\?properties%5B%5D=&properties%5Bgroup%5D%5B%5D=&page=1$"},
+ "@type": {"pattern": "^hydra:PartialCollectionView$"}
+ }
+ }
+ }
+ }
+ """
+
+ Scenario: Get a resource by attributes id, foo and bar
+ When I send a "GET" request to "/dummy_properties/1?properties[]=id&properties[]=foo&properties[]=bar"
+ Then the response status code should be 200
+ And the response should be in JSON
+ And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
+ And the JSON should be valid according to this schema:
+ """
+ {
+ "type": "object",
+ "properties": {
+ "@context": {"pattern": "^/contexts/DummyProperty$"},
+ "@id": {"pattern": "^/dummy_properties/1$"},
+ "@type": {"pattern": "^DummyProperty$"},
+ "id": {},
+ "foo": {},
+ "bar": {}
+ },
+ "additionalProperties": false,
+ "required": ["@context", "@id", "@type", "id", "foo", "bar"]
+ }
+ """
+
+ Scenario: Get a resource by attributes foo, bar, group.baz and group.qux
+ When I send a "GET" request to "/dummy_properties/1?properties[]=foo&properties[]=bar&properties[group][]=baz&properties[group][]=qux"
+ 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": {
+ "@context": {"pattern": "^/contexts/DummyProperty$"},
+ "@id": {"pattern": "^/dummy_properties/1$"},
+ "@type": {"pattern": "^DummyProperty$"},
+ "foo": {},
+ "bar": {},
+ "group": {
+ "type": "object",
+ "properties": {
+ "@id": {},
+ "@type": {},
+ "baz": {}
+ },
+ "additionalProperties": false,
+ "required": ["@id", "@type", "baz"]
+ }
+ },
+ "additionalProperties": false,
+ "required": ["@context", "@id", "@type", "foo", "bar", "group"]
+ }
+ """
+
+ Scenario: Get a resource by attributes empty
+ When I send a "GET" request to "/dummy_properties/1?properties[]=&properties[group][]="
+ 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": {
+ "@context": {"pattern": "^/contexts/DummyProperty$"},
+ "@id": {"pattern": "^/dummy_properties/1$"},
+ "@type": {"pattern": "^DummyProperty$"},
+ "group": {
+ "type": "object",
+ "properties": {
+ "@id": {},
+ "@type": {}
+ },
+ "additionalProperties": false,
+ "required": ["@id", "@type"]
+ }
+ },
+ "additionalProperties": false,
+ "required": ["@context", "@id", "@type", "group"]
+ }
+ """
+
+ Scenario: Create a resource by attributes foo and bar
+ When I add "Content-Type" header equal to "application/ld+json"
+ And I send a "POST" request to "/dummy_properties?properties[]=foo&properties[]=bar" with body:
+ """
+ {
+ "foo": "Foo",
+ "bar": "Bar"
+ }
+ """
+ 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\/DummyProperty",
+ "@id": "\/dummy_properties\/11",
+ "@type": "DummyProperty",
+ "foo": "Foo",
+ "bar": "Bar"
+ }
+ """
+
+ @dropSchema
+ Scenario: Create a resource by attributes foo, bar, group.foo, group.baz and group.qux
+ When I add "Content-Type" header equal to "application/ld+json"
+ And I send a "POST" request to "/dummy_properties?properties[]=foo&properties[]=bar&properties[group][]=foo&properties[group][]=baz&properties[group][]=qux" with body:
+ """
+ {
+ "foo": "Foo",
+ "bar": "Bar",
+ "group": {
+ "foo": "Foo",
+ "baz": "Baz",
+ "qux": "Qux"
+ }
+ }
+ """
+ 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\/DummyProperty",
+ "@id": "\/dummy_properties\/12",
+ "@type": "DummyProperty",
+ "foo": "Foo",
+ "bar": "Bar",
+ "group": {
+ "@id": "\/dummy_groups\/11",
+ "@type": "DummyGroup",
+ "foo": "Foo",
+ "baz": null
+ }
+ }
+ """
diff --git a/src/Bridge/Symfony/Bundle/Resources/config/api.xml b/src/Bridge/Symfony/Bundle/Resources/config/api.xml
index 938ee90901b..43fde27e12d 100644
--- a/src/Bridge/Symfony/Bundle/Resources/config/api.xml
+++ b/src/Bridge/Symfony/Bundle/Resources/config/api.xml
@@ -62,6 +62,16 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Serializer/AbstractItemNormalizer.php b/src/Serializer/AbstractItemNormalizer.php
index 1d9a58bb327..bdf9e49bbfd 100644
--- a/src/Serializer/AbstractItemNormalizer.php
+++ b/src/Serializer/AbstractItemNormalizer.php
@@ -183,7 +183,7 @@ protected function setAttributeValue($object, $attribute, $value, $format = null
$this->setValue(
$object,
$attribute,
- $this->denormalizeRelation($attribute, $propertyMetadata, $className, $value, $format, $context)
+ $this->denormalizeRelation($attribute, $propertyMetadata, $className, $value, $format, $this->createChildContext($context, $attribute))
);
return;
@@ -254,7 +254,7 @@ private function denormalizeCollection(string $attribute, PropertyMetadata $prop
);
}
- $values[$index] = $this->denormalizeRelation($attribute, $propertyMetadata, $className, $obj, $format, $context);
+ $values[$index] = $this->denormalizeRelation($attribute, $propertyMetadata, $className, $obj, $format, $this->createChildContext($context, $attribute));
}
return $values;
@@ -390,7 +390,7 @@ protected function getAttributeValue($object, $attribute, $format = null, array
) {
$value = [];
foreach ($attributeValue as $index => $obj) {
- $value[$index] = $this->normalizeRelation($propertyMetadata, $obj, $className, $format, $context);
+ $value[$index] = $this->normalizeRelation($propertyMetadata, $obj, $className, $format, $this->createChildContext($context, $attribute));
}
return $value;
@@ -408,7 +408,7 @@ protected function getAttributeValue($object, $attribute, $format = null, array
if (null === $attributeValue && $context['operation_type'] === OperationType::SUBRESOURCE && isset($context['subresource_resources'][$className])) {
return $this->iriConverter->getItemIriFromResourceClass($className, $context['subresource_resources'][$className]);
} elseif ($attributeValue) {
- return $this->normalizeRelation($propertyMetadata, $attributeValue, $className, $format, $context);
+ return $this->normalizeRelation($propertyMetadata, $attributeValue, $className, $format, $this->createChildContext($context, $attribute));
}
}
diff --git a/src/Serializer/Filter/FilterInterface.php b/src/Serializer/Filter/FilterInterface.php
new file mode 100644
index 00000000000..7bd5a56d94f
--- /dev/null
+++ b/src/Serializer/Filter/FilterInterface.php
@@ -0,0 +1,35 @@
+
+ *
+ * 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\Filter;
+
+use ApiPlatform\Core\Api\FilterInterface as BaseFilterInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Symfony serializer context builder filter interface.
+ *
+ * @author Baptiste Meyer
+ */
+interface FilterInterface extends BaseFilterInterface
+{
+ /**
+ * Apply a filter to the serializer context.
+ *
+ * @param Request $request
+ * @param bool $normalization
+ * @param array $attributes
+ * @param array $context
+ */
+ public function apply(Request $request, bool $normalization, array $attributes, array &$context);
+}
diff --git a/src/Serializer/Filter/GroupFilter.php b/src/Serializer/Filter/GroupFilter.php
new file mode 100644
index 00000000000..2063b0dd75e
--- /dev/null
+++ b/src/Serializer/Filter/GroupFilter.php
@@ -0,0 +1,63 @@
+
+ *
+ * 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\Filter;
+
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Group filter.
+ *
+ * @author Baptiste Meyer
+ */
+final class GroupFilter implements FilterInterface
+{
+ private $overrideDefaultGroups;
+ private $parameterName;
+
+ public function __construct(string $parameterName = 'groups', bool $overrideDefaultGroups = false)
+ {
+ $this->overrideDefaultGroups = $overrideDefaultGroups;
+ $this->parameterName = $parameterName;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function apply(Request $request, bool $normalization, array $attributes, array &$context)
+ {
+ if (!is_array($groups = $request->query->get($this->parameterName))) {
+ return;
+ }
+
+ if (!$this->overrideDefaultGroups && isset($context['groups'])) {
+ $groups = array_merge((array) $context['groups'], $groups);
+ }
+
+ $context['groups'] = $groups;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDescription(string $resourceClass): array
+ {
+ return [
+ $this->parameterName.'[]' => [
+ 'property' => null,
+ 'type' => 'string',
+ 'required' => false,
+ ],
+ ];
+ }
+}
diff --git a/src/Serializer/Filter/PropertyFilter.php b/src/Serializer/Filter/PropertyFilter.php
new file mode 100644
index 00000000000..282f815822c
--- /dev/null
+++ b/src/Serializer/Filter/PropertyFilter.php
@@ -0,0 +1,63 @@
+
+ *
+ * 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\Filter;
+
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Property filter.
+ *
+ * @author Baptiste Meyer
+ */
+final class PropertyFilter implements FilterInterface
+{
+ private $overrideDefaultProperties;
+ private $parameterName;
+
+ public function __construct(string $parameterName = 'properties', bool $overrideDefaultProperties = false)
+ {
+ $this->overrideDefaultProperties = $overrideDefaultProperties;
+ $this->parameterName = $parameterName;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function apply(Request $request, bool $normalization, array $attributes, array &$context)
+ {
+ if (!is_array($properties = $request->query->get($this->parameterName))) {
+ return;
+ }
+
+ if (!$this->overrideDefaultProperties && isset($context['attributes'])) {
+ $properties = array_merge_recursive((array) $context['attributes'], $properties);
+ }
+
+ $context['attributes'] = $properties;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDescription(string $resourceClass): array
+ {
+ return [
+ $this->parameterName.'[]' => [
+ 'property' => null,
+ 'type' => 'string',
+ 'required' => false,
+ ],
+ ];
+ }
+}
diff --git a/src/Serializer/SerializerFilterContextBuilder.php b/src/Serializer/SerializerFilterContextBuilder.php
new file mode 100644
index 00000000000..c74d8ff1ced
--- /dev/null
+++ b/src/Serializer/SerializerFilterContextBuilder.php
@@ -0,0 +1,76 @@
+
+ *
+ * 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\RuntimeException;
+use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
+use ApiPlatform\Core\Serializer\Filter\FilterInterface;
+use ApiPlatform\Core\Util\RequestAttributesExtractor;
+use Psr\Container\ContainerInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * {@inheritdoc}
+ *
+ * @author Baptiste Meyer
+ */
+final class SerializerFilterContextBuilder implements SerializerContextBuilderInterface
+{
+ private $decorated;
+ private $filterLocator;
+ private $resourceMetadataFactory;
+
+ public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, ContainerInterface $filterLocator, SerializerContextBuilderInterface $decorated)
+ {
+ $this->decorated = $decorated;
+ $this->filterLocator = $filterLocator;
+ $this->resourceMetadataFactory = $resourceMetadataFactory;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createFromRequest(Request $request, bool $normalization, array $attributes = null): array
+ {
+ if (null === $attributes && !$attributes = RequestAttributesExtractor::extractAttributes($request)) {
+ throw new RuntimeException('Request attributes are not valid.');
+ }
+
+ $context = $this->decorated->createFromRequest($request, $normalization, $attributes);
+ $resourceMetadata = $this->resourceMetadataFactory->create($attributes['resource_class']);
+
+ if (isset($attributes['collection_operation_name']) || isset($attributes['subresource_operation_name'])) {
+ $resourceFilters = $resourceMetadata->getCollectionOperationAttribute(
+ $attributes['collection_operation_name'] ?? $attributes['subresource_operation_name'],
+ 'filters',
+ [],
+ true
+ );
+ } else {
+ $resourceFilters = $resourceMetadata->getItemOperationAttribute($attributes['item_operation_name'], 'filters', [], true);
+ }
+
+ if (!$resourceFilters) {
+ return $context;
+ }
+
+ foreach ($resourceFilters as $filterId) {
+ if ($this->filterLocator->has($filterId) && ($filter = $this->filterLocator->get($filterId)) instanceof FilterInterface) {
+ $filter->apply($request, $normalization, $attributes, $context);
+ }
+ }
+
+ return $context;
+ }
+}
diff --git a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php
index b753881fc3f..e86616a8845 100644
--- a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php
+++ b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php
@@ -385,6 +385,9 @@ private function getContainerBuilderProphecy()
'api_platform.route_name_resolver.cached',
'api_platform.router',
'api_platform.serializer.context_builder',
+ 'api_platform.serializer.context_builder.filter',
+ 'api_platform.serializer.property_filter',
+ 'api_platform.serializer.group_filter',
'api_platform.serializer.normalizer.item',
'api_platform.subresource_data_provider',
'api_platform.swagger.action.ui',
diff --git a/tests/Fixtures/TestBundle/Entity/DummyGroup.php b/tests/Fixtures/TestBundle/Entity/DummyGroup.php
new file mode 100644
index 00000000000..ce195da656a
--- /dev/null
+++ b/tests/Fixtures/TestBundle/Entity/DummyGroup.php
@@ -0,0 +1,92 @@
+
+ *
+ * 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 Doctrine\ORM\Mapping as ORM;
+use Symfony\Component\Serializer\Annotation\Groups;
+
+/**
+ * DummyGroup.
+ *
+ * @author Baptiste Meyer
+ *
+ * @ORM\Entity
+ *
+ * @ApiResource(attributes={
+ * "normalization_context"={"groups"={"dummy_read"}},
+ * "denormalization_context"={"groups"={"dummy_write"}},
+ * "filters"={
+ * "dummy_group.group",
+ * "dummy_group.override_group"
+ * }
+ * })
+ */
+class DummyGroup
+{
+ /**
+ * @var int
+ *
+ * @ORM\Id
+ * @ORM\Column(type="integer")
+ * @ORM\GeneratedValue(strategy="AUTO")
+ *
+ * @Groups({"dummy", "dummy_read", "dummy_id"})
+ */
+ private $id;
+
+ /**
+ * @var string
+ *
+ * @ORM\Column(nullable=true)
+ *
+ * @Groups({"dummy", "dummy_read", "dummy_write", "dummy_foo"})
+ */
+ public $foo;
+
+ /**
+ * @var string
+ *
+ * @ORM\Column(nullable=true)
+ *
+ * @Groups({"dummy", "dummy_read", "dummy_write", "dummy_bar"})
+ */
+ public $bar;
+
+ /**
+ * @var string
+ *
+ * @ORM\Column(nullable=true)
+ *
+ * @Groups({"dummy", "dummy_read", "dummy_baz"})
+ */
+ public $baz;
+
+ /**
+ * @var string
+ *
+ * @ORM\Column(nullable=true)
+ *
+ * @Groups({"dummy", "dummy_write", "dummy_qux"})
+ */
+ public $qux;
+
+ /**
+ * @return int
+ */
+ public function getId()
+ {
+ return $this->id;
+ }
+}
diff --git a/tests/Fixtures/TestBundle/Entity/DummyProperty.php b/tests/Fixtures/TestBundle/Entity/DummyProperty.php
new file mode 100644
index 00000000000..09edcf79cd7
--- /dev/null
+++ b/tests/Fixtures/TestBundle/Entity/DummyProperty.php
@@ -0,0 +1,91 @@
+
+ *
+ * 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 Doctrine\ORM\Mapping as ORM;
+use Symfony\Component\Serializer\Annotation\Groups;
+
+/**
+ * DummyProperty.
+ *
+ * @author Baptiste Meyer
+ *
+ * @ORM\Entity
+ *
+ * @ApiResource(attributes={
+ * "normalization_context"={"groups"={"dummy_read"}},
+ * "denormalization_context"={"groups"={"dummy_write"}},
+ * "filters"={
+ * "dummy_property.property"
+ * }
+ * })
+ */
+class DummyProperty
+{
+ /**
+ * @var int
+ *
+ * @ORM\Id
+ * @ORM\Column(type="integer")
+ * @ORM\GeneratedValue(strategy="AUTO")
+ *
+ * @Groups("dummy_read")
+ */
+ private $id;
+
+ /**
+ * @var string
+ *
+ * @ORM\Column(nullable=true)
+ *
+ * @Groups({"dummy_read", "dummy_write"})
+ */
+ public $foo;
+
+ /**
+ * @var string
+ *
+ * @ORM\Column(nullable=true)
+ *
+ * @Groups({"dummy_read", "dummy_write"})
+ */
+ public $bar;
+
+ /**
+ * @var string
+ *
+ * @ORM\Column(nullable=true)
+ *
+ * @Groups({"dummy_read", "dummy_write"})
+ */
+ public $baz;
+
+ /**
+ * @var DummyGroup
+ *
+ * @ORM\ManyToOne(targetEntity=DummyGroup::class, cascade={"persist"})
+ *
+ * @Groups({"dummy_read", "dummy_write"})
+ */
+ public $group;
+
+ /**
+ * @return int
+ */
+ public function getId()
+ {
+ return $this->id;
+ }
+}
diff --git a/tests/Fixtures/app/config/config.yml b/tests/Fixtures/app/config/config.yml
index 6bfe622d14e..af4cabe0174 100644
--- a/tests/Fixtures/app/config/config.yml
+++ b/tests/Fixtures/app/config/config.yml
@@ -136,6 +136,19 @@ services:
arguments: [ { 'colors.prop': 'ipartial' } ]
tags: [ { name: 'api_platform.filter', id: 'dummy_car_colors.search_filter' } ]
+ app.entity.filter.dummy_property.property:
+ parent: 'api_platform.serializer.property_filter'
+ tags: [ { name: 'api_platform.filter', id: 'dummy_property.property' } ]
+
+ app.entity.filter.dummy_group.group:
+ parent: 'api_platform.serializer.group_filter'
+ tags: [ { name: 'api_platform.filter', id: 'dummy_group.group' } ]
+
+ app.entity.filter.dummy_group.override_group:
+ parent: 'api_platform.serializer.group_filter'
+ arguments: [ 'override_groups', true ]
+ tags: [ { name: 'api_platform.filter', id: 'dummy_group.override_group' } ]
+
app.related_dummy_resource.search_filter:
parent: 'api_platform.doctrine.orm.search_filter'
arguments: [ { 'relatedToDummyFriend.dummyFriend': 'exact', 'name': 'partial' } ]
diff --git a/tests/Serializer/Filter/GroupFilterTest.php b/tests/Serializer/Filter/GroupFilterTest.php
new file mode 100644
index 00000000000..6855123a2cf
--- /dev/null
+++ b/tests/Serializer/Filter/GroupFilterTest.php
@@ -0,0 +1,92 @@
+
+ *
+ * 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\Serializer\Filter;
+
+use ApiPlatform\Core\Serializer\Filter\GroupFilter;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyGroup;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * @author Baptiste Meyer
+ */
+class GroupFilterTest extends \PHPUnit_Framework_TestCase
+{
+ public function testApply()
+ {
+ $request = new Request(['groups' => ['foo', 'bar', 'baz']]);
+ $context = ['groups' => ['foo', 'qux']];
+
+ $groupFilter = new GroupFilter();
+ $groupFilter->apply($request, true, [], $context);
+
+ $this->assertEquals(['groups' => ['foo', 'qux', 'foo', 'bar', 'baz']], $context);
+ }
+
+ public function testApplyWithOverriding()
+ {
+ $request = new Request(['custom_groups' => ['foo', 'bar', 'baz']]);
+ $context = ['groups' => ['foo', 'qux']];
+
+ $groupFilter = new GroupFilter('custom_groups', true);
+ $groupFilter->apply($request, false, [], $context);
+
+ $this->assertEquals(['groups' => ['foo', 'bar', 'baz']], $context);
+ }
+
+ public function testApplyWithoutGroupsInRequest()
+ {
+ $context = ['groups' => ['foo', 'bar']];
+
+ $groupFilter = new GroupFilter();
+ $groupFilter->apply(new Request(), false, [], $context);
+
+ $this->assertEquals(['groups' => ['foo', 'bar']], $context);
+ }
+
+ public function testApplyWithInvalidGroupsInRequest()
+ {
+ $request = new Request(['groups' => 'foo,bar,baz']);
+ $context = ['groups' => ['foo', 'bar']];
+
+ $groupFilter = new GroupFilter();
+ $groupFilter->apply($request, true, [], $context);
+
+ $this->assertEquals(['groups' => ['foo', 'bar']], $context);
+ }
+
+ public function testApplyWithInvalidGroupsInContext()
+ {
+ $request = new Request(['custom_groups' => ['foo', 'bar', 'baz']]);
+ $context = ['groups' => 'qux'];
+
+ $groupFilter = new GroupFilter('custom_groups');
+ $groupFilter->apply($request, true, [], $context);
+
+ $this->assertEquals(['groups' => ['qux', 'foo', 'bar', 'baz']], $context);
+ }
+
+ public function testGetDescription()
+ {
+ $groupFilter = new GroupFilter('custom_groups');
+ $expectedDescription = [
+ 'custom_groups[]' => [
+ 'property' => null,
+ 'type' => 'string',
+ 'required' => false,
+ ],
+ ];
+
+ $this->assertEquals($expectedDescription, $groupFilter->getDescription(DummyGroup::class));
+ }
+}
diff --git a/tests/Serializer/Filter/PropertyFilterTest.php b/tests/Serializer/Filter/PropertyFilterTest.php
new file mode 100644
index 00000000000..abcc6875590
--- /dev/null
+++ b/tests/Serializer/Filter/PropertyFilterTest.php
@@ -0,0 +1,81 @@
+
+ *
+ * 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\Serializer\Filter;
+
+use ApiPlatform\Core\Serializer\Filter\PropertyFilter;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyProperty;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * @author Baptiste Meyer
+ */
+class PropertyFilterTest extends \PHPUnit_Framework_TestCase
+{
+ public function testApply()
+ {
+ $request = new Request(['properties' => ['foo', 'bar', 'baz']]);
+ $context = ['attributes' => ['foo', 'qux']];
+
+ $propertyFilter = new PropertyFilter();
+ $propertyFilter->apply($request, true, [], $context);
+
+ $this->assertEquals(['attributes' => ['foo', 'qux', 'foo', 'bar', 'baz']], $context);
+ }
+
+ public function testApplyWithOverriding()
+ {
+ $request = new Request(['custom_properties' => ['foo', 'bar', 'baz']]);
+ $context = ['attributes' => ['foo', 'qux']];
+
+ $propertyFilter = new PropertyFilter('custom_properties', true);
+ $propertyFilter->apply($request, false, [], $context);
+
+ $this->assertEquals(['attributes' => ['foo', 'bar', 'baz']], $context);
+ }
+
+ public function testApplyWithoutPropertiesInRequest()
+ {
+ $context = ['attributes' => ['foo', 'bar']];
+
+ $propertyFilter = new PropertyFilter();
+ $propertyFilter->apply(new Request(), false, [], $context);
+
+ $this->assertEquals(['attributes' => ['foo', 'bar']], $context);
+ }
+
+ public function testApplyWithInvalidPropertiesInRequest()
+ {
+ $request = new Request(['properties' => 'foo,bar,baz']);
+ $context = ['attributes' => ['foo', 'bar']];
+
+ $propertyFilter = new PropertyFilter();
+ $propertyFilter->apply($request, true, [], $context);
+
+ $this->assertEquals(['attributes' => ['foo', 'bar']], $context);
+ }
+
+ public function testGetDescription()
+ {
+ $propertyFilter = new PropertyFilter('custom_properties');
+ $expectedDescription = [
+ 'custom_properties[]' => [
+ 'property' => null,
+ 'type' => 'string',
+ 'required' => false,
+ ],
+ ];
+
+ $this->assertEquals($expectedDescription, $propertyFilter->getDescription(DummyProperty::class));
+ }
+}
diff --git a/tests/Serializer/SerializerFilterContextBuilderTest.php b/tests/Serializer/SerializerFilterContextBuilderTest.php
new file mode 100644
index 00000000000..0fd6efd9252
--- /dev/null
+++ b/tests/Serializer/SerializerFilterContextBuilderTest.php
@@ -0,0 +1,204 @@
+
+ *
+ * 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\Serializer;
+
+use ApiPlatform\Core\Api\FilterInterface;
+use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
+use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
+use ApiPlatform\Core\Serializer\Filter\FilterInterface as SerializerFilterInterface;
+use ApiPlatform\Core\Serializer\SerializerContextBuilderInterface;
+use ApiPlatform\Core\Serializer\SerializerFilterContextBuilder;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyGroup;
+use Psr\Container\ContainerInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * @author Baptiste Meyer
+ */
+class SerializerFilterContextBuilderTest extends \PHPUnit_Framework_TestCase
+{
+ public function testCreateFromRequestWithCollectionOperation()
+ {
+ $request = new Request();
+
+ $attributes = [
+ 'resource_class' => DummyGroup::class,
+ 'collection_operation_name' => 'get',
+ ];
+
+ $resourceMetadata = new ResourceMetadata(
+ null,
+ null,
+ null,
+ null,
+ ['get' => ['filters' => ['dummy_group.group', 'dummy_group.search', 'dummy_group.nonexistent']]]
+ );
+
+ $decoratedProphecy = $this->prophesize(SerializerContextBuilderInterface::class);
+ $decoratedProphecy->createFromRequest($request, true, $attributes)->willReturn([])->shouldBeCalled();
+
+ $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
+ $resourceMetadataFactoryProphecy->create(DummyGroup::class)->willReturn($resourceMetadata)->shouldBeCalled();
+
+ $dummyGroupGroupFilterProphecy = $this->prophesize(SerializerFilterInterface::class);
+ $dummyGroupGroupFilterProphecy->apply($request, true, $attributes, [])->shouldBeCalled();
+
+ $dummyGroupSearchFilterProphecy = $this->prophesize(FilterInterface::class);
+
+ $filterLocatorProphecy = $this->prophesize(ContainerInterface::class);
+ $filterLocatorProphecy->has('dummy_group.group')->willReturn(true)->shouldBeCalled();
+ $filterLocatorProphecy->get('dummy_group.group')->willReturn($dummyGroupGroupFilterProphecy->reveal())->shouldBeCalled();
+ $filterLocatorProphecy->has('dummy_group.search')->willReturn(true)->shouldBeCalled();
+ $filterLocatorProphecy->get('dummy_group.search')->willReturn($dummyGroupSearchFilterProphecy->reveal())->shouldBeCalled();
+ $filterLocatorProphecy->has('dummy_group.nonexistent')->willReturn(false)->shouldBeCalled();
+
+ $serializerContextBuilderFilter = new SerializerFilterContextBuilder($resourceMetadataFactoryProphecy->reveal(), $filterLocatorProphecy->reveal(), $decoratedProphecy->reveal());
+ $context = $serializerContextBuilderFilter->createFromRequest($request, true, $attributes);
+
+ $this->assertInternalType('array', $context);
+ }
+
+ public function testCreateFromRequestWithItemOperation()
+ {
+ $request = new Request();
+
+ $attributes = [
+ 'resource_class' => DummyGroup::class,
+ 'item_operation_name' => 'put',
+ ];
+
+ $resourceMetadata = new ResourceMetadata(
+ null,
+ null,
+ null,
+ ['put' => ['filters' => ['dummy_group.group', 'dummy_group.search', 'dummy_group.nonexistent']]],
+ null
+ );
+
+ $decoratedProphecy = $this->prophesize(SerializerContextBuilderInterface::class);
+ $decoratedProphecy->createFromRequest($request, true, $attributes)->willReturn([])->shouldBeCalled();
+
+ $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
+ $resourceMetadataFactoryProphecy->create(DummyGroup::class)->willReturn($resourceMetadata)->shouldBeCalled();
+
+ $dummyGroupGroupFilterProphecy = $this->prophesize(SerializerFilterInterface::class);
+ $dummyGroupGroupFilterProphecy->apply($request, true, $attributes, [])->shouldBeCalled();
+
+ $dummyGroupSearchFilterProphecy = $this->prophesize(FilterInterface::class);
+
+ $filterLocatorProphecy = $this->prophesize(ContainerInterface::class);
+ $filterLocatorProphecy->has('dummy_group.group')->willReturn(true)->shouldBeCalled();
+ $filterLocatorProphecy->get('dummy_group.group')->willReturn($dummyGroupGroupFilterProphecy->reveal())->shouldBeCalled();
+ $filterLocatorProphecy->has('dummy_group.search')->willReturn(true)->shouldBeCalled();
+ $filterLocatorProphecy->get('dummy_group.search')->willReturn($dummyGroupSearchFilterProphecy->reveal())->shouldBeCalled();
+ $filterLocatorProphecy->has('dummy_group.nonexistent')->willReturn(false)->shouldBeCalled();
+
+ $serializerContextBuilderFilter = new SerializerFilterContextBuilder($resourceMetadataFactoryProphecy->reveal(), $filterLocatorProphecy->reveal(), $decoratedProphecy->reveal());
+ $context = $serializerContextBuilderFilter->createFromRequest($request, true, $attributes);
+
+ $this->assertInternalType('array', $context);
+ }
+
+ public function testCreateFromRequestWithoutFilters()
+ {
+ $request = new Request();
+
+ $attributes = [
+ 'resource_class' => DummyGroup::class,
+ 'collection_operation_name' => 'get',
+ ];
+
+ $resourceMetadata = new ResourceMetadata(
+ null,
+ null,
+ null,
+ null,
+ ['get' => []]
+ );
+
+ $decoratedProphecy = $this->prophesize(SerializerContextBuilderInterface::class);
+ $decoratedProphecy->createFromRequest($request, false, $attributes)->willReturn([])->shouldBeCalled();
+
+ $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
+ $resourceMetadataFactoryProphecy->create(DummyGroup::class)->willReturn($resourceMetadata)->shouldBeCalled();
+
+ $filterLocatorProphecy = $this->prophesize(ContainerInterface::class);
+
+ $serializerContextBuilderFilter = new SerializerFilterContextBuilder($resourceMetadataFactoryProphecy->reveal(), $filterLocatorProphecy->reveal(), $decoratedProphecy->reveal());
+ $context = $serializerContextBuilderFilter->createFromRequest($request, false, $attributes);
+
+ $this->assertInternalType('array', $context);
+ }
+
+ public function testCreateFromRequestWithoutAttributes()
+ {
+ $request = new Request([], [], [
+ '_api_resource_class' => DummyGroup::class,
+ '_api_collection_operation_name' => 'get',
+ ]);
+
+ $attributes = [
+ 'resource_class' => DummyGroup::class,
+ 'collection_operation_name' => 'get',
+ 'receive' => true,
+ ];
+
+ $resourceMetadata = new ResourceMetadata(
+ null,
+ null,
+ null,
+ null,
+ ['get' => ['filters' => ['dummy_group.group', 'dummy_group.search', 'dummy_group.nonexistent']]]
+ );
+
+ $decoratedProphecy = $this->prophesize(SerializerContextBuilderInterface::class);
+ $decoratedProphecy->createFromRequest($request, true, $attributes)->willReturn([])->shouldBeCalled();
+
+ $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
+ $resourceMetadataFactoryProphecy->create(DummyGroup::class)->willReturn($resourceMetadata)->shouldBeCalled();
+
+ $dummyGroupGroupFilterProphecy = $this->prophesize(SerializerFilterInterface::class);
+ $dummyGroupGroupFilterProphecy->apply($request, true, $attributes, [])->shouldBeCalled();
+
+ $dummyGroupSearchFilterProphecy = $this->prophesize(FilterInterface::class);
+
+ $filterLocatorProphecy = $this->prophesize(ContainerInterface::class);
+ $filterLocatorProphecy->has('dummy_group.group')->willReturn(true)->shouldBeCalled();
+ $filterLocatorProphecy->get('dummy_group.group')->willReturn($dummyGroupGroupFilterProphecy->reveal())->shouldBeCalled();
+ $filterLocatorProphecy->has('dummy_group.search')->willReturn(true)->shouldBeCalled();
+ $filterLocatorProphecy->get('dummy_group.search')->willReturn($dummyGroupSearchFilterProphecy->reveal())->shouldBeCalled();
+ $filterLocatorProphecy->has('dummy_group.nonexistent')->willReturn(false)->shouldBeCalled();
+
+ $serializerContextBuilderFilter = new SerializerFilterContextBuilder($resourceMetadataFactoryProphecy->reveal(), $filterLocatorProphecy->reveal(), $decoratedProphecy->reveal());
+ $context = $serializerContextBuilderFilter->createFromRequest($request, true, null);
+
+ $this->assertInternalType('array', $context);
+ }
+
+ /**
+ * @expectedException \ApiPlatform\Core\Exception\RuntimeException
+ * @expectedExceptionMessage Request attributes are not valid.
+ */
+ public function testCreateFromRequestThrowsExceptionWithoutAttributesAndRequestAttributes()
+ {
+ $request = new Request();
+
+ $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
+ $filterLocatorProphecy = $this->prophesize(ContainerInterface::class);
+ $decoratedProphecy = $this->prophesize(SerializerContextBuilderInterface::class);
+
+ $serializerContextBuilderFilter = new SerializerFilterContextBuilder($resourceMetadataFactoryProphecy->reveal(), $filterLocatorProphecy->reveal(), $decoratedProphecy->reveal());
+ $serializerContextBuilderFilter->createFromRequest($request, true, null);
+ }
+}