diff --git a/features/bootstrap/DoctrineContext.php b/features/bootstrap/DoctrineContext.php index 46e7cb10fd2..a86ef14d69b 100644 --- a/features/bootstrap/DoctrineContext.php +++ b/features/bootstrap/DoctrineContext.php @@ -17,6 +17,10 @@ use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\CompositeLabel as CompositeLabelDocument; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\CompositePrimitiveItem as CompositePrimitiveItemDocument; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\CompositeRelation as CompositeRelationDocument; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\ConvertedBoolean as ConvertedBoolDocument; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\ConvertedDate as ConvertedDateDocument; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\ConvertedInteger as ConvertedIntegerDocument; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\ConvertedString as ConvertedStringDocument; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\Customer as CustomerDocument; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\Dummy as DummyDocument; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\DummyAggregateOffer as DummyAggregateOfferDocument; @@ -62,6 +66,10 @@ use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\CompositePrimitiveItem; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\CompositeRelation; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Container; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\ConvertedBoolean; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\ConvertedDate; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\ConvertedInteger; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\ConvertedString; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Customer; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyAggregateOffer; @@ -164,6 +172,7 @@ public function thereAreDummyObjects(int $nb) $dummy->setAlias('Alias #'.($nb - $i)); $dummy->setDummy('SomeDummyTest'.$i); $dummy->setDescription($descriptions[($i - 1) % 2]); + $dummy->nameConverted = 'Converted '.$i; $this->manager->persist($dummy); } @@ -257,6 +266,7 @@ public function thereAreDummyPropertyObjects(int $nb) foreach (['foo', 'bar', 'baz'] as $property) { $dummyProperty->{$property} = $dummyGroup->{$property} = ucfirst($property).' #'.$i; } + $dummyProperty->nameConverted = "NameConverted #$i"; $dummyProperty->group = $dummyGroup; @@ -657,6 +667,66 @@ public function thereAreDummyObjectsWithDummyDateAndEmbeddedDummy(int $nb) $this->manager->flush(); } + /** + * @Given there are :nb convertedDate objects + */ + public function thereAreconvertedDateObjectsWith(int $nb) + { + for ($i = 1; $i <= $nb; ++$i) { + $convertedDate = $this->buildConvertedDate(); + $convertedDate->nameConverted = new \DateTime(sprintf('2015-04-%d', $i), new \DateTimeZone('UTC')); + + $this->manager->persist($convertedDate); + } + + $this->manager->flush(); + } + + /** + * @Given there are :nb convertedString objects + */ + public function thereAreconvertedStringObjectsWith(int $nb) + { + for ($i = 1; $i <= $nb; ++$i) { + $convertedString = $this->buildConvertedString(); + $convertedString->nameConverted = ($i % 2) ? "name#$i" : null; + + $this->manager->persist($convertedString); + } + + $this->manager->flush(); + } + + /** + * @Given there are :nb convertedBoolean objects + */ + public function thereAreconvertedBooleanObjectsWith(int $nb) + { + for ($i = 1; $i <= $nb; ++$i) { + $convertedBoolean = $this->buildConvertedBoolean(); + $convertedBoolean->nameConverted = (bool) ($i % 2); + + $this->manager->persist($convertedBoolean); + } + + $this->manager->flush(); + } + + /** + * @Given there are :nb convertedInteger objects + */ + public function thereAreconvertedIntegerObjectsWith(int $nb) + { + for ($i = 1; $i <= $nb; ++$i) { + $convertedInteger = $this->buildConvertedInteger(); + $convertedInteger->nameConverted = $i; + + $this->manager->persist($convertedInteger); + } + + $this->manager->flush(); + } + /** * @Given there are :nb dummy objects with dummyPrice */ @@ -1264,6 +1334,35 @@ public function thereAreNbDummyDtoCustom($nb) $this->manager->clear(); } + /** + * @Given there is a order with same customer and receiver + */ + public function testEagerLoadingNotDuplicateRelation() + { + $customer = $this->isOrm() ? new Customer() : new CustomerDocument(); + $customer->name = 'customer_name'; + + $address1 = $this->isOrm() ? new Address() : new AddressDocument(); + $address1->name = 'foo'; + $address2 = $this->isOrm() ? new Address() : new AddressDocument(); + $address2->name = 'bar'; + + $order = $this->isOrm() ? new Order() : new OrderDocument(); + $order->recipient = $customer; + $order->customer = $customer; + + $customer->addresses->add($address1); + $customer->addresses->add($address2); + + $this->manager->persist($address1); + $this->manager->persist($address2); + $this->manager->persist($customer); + $this->manager->persist($order); + + $this->manager->flush(); + $this->manager->clear(); + } + private function isOrm(): bool { return null !== $this->schemaTool; @@ -1579,31 +1678,34 @@ private function buildThirdLevel() } /** - * @Given there is a order with same customer and receiver + * @return ConvertedDate|ConvertedDateDocument */ - public function testEagerLoadingNotDuplicateRelation() + private function buildConvertedDate() { - $customer = $this->isOrm() ? new Customer() : new CustomerDocument(); - $customer->name = 'customer_name'; - - $address1 = $this->isOrm() ? new Address() : new AddressDocument(); - $address1->name = 'foo'; - $address2 = $this->isOrm() ? new Address() : new AddressDocument(); - $address2->name = 'bar'; - - $order = $this->isOrm() ? new Order() : new OrderDocument(); - $order->recipient = $customer; - $order->customer = $customer; + return $this->isOrm() ? new ConvertedDate() : new ConvertedDateDocument(); + } - $customer->addresses->add($address1); - $customer->addresses->add($address2); + /** + * @return ConvertedBoolean|ConvertedBoolDocument + */ + private function buildConvertedBoolean() + { + return $this->isOrm() ? new ConvertedBoolean() : new ConvertedBoolDocument(); + } - $this->manager->persist($address1); - $this->manager->persist($address2); - $this->manager->persist($customer); - $this->manager->persist($order); + /** + * @return ConvertedInteger|ConvertedIntegerDocument + */ + private function buildConvertedInteger() + { + return $this->isOrm() ? new ConvertedInteger() : new ConvertedIntegerDocument(); + } - $this->manager->flush(); - $this->manager->clear(); + /** + * @return ConvertedString|ConvertedStringDocument + */ + private function buildConvertedString() + { + return $this->isOrm() ? new ConvertedString() : new ConvertedStringDocument(); } } diff --git a/features/doctrine/boolean_filter.feature b/features/doctrine/boolean_filter.feature index 5ee947172b0..27b09201d31 100644 --- a/features/doctrine/boolean_filter.feature +++ b/features/doctrine/boolean_filter.feature @@ -448,3 +448,118 @@ Feature: Boolean filter on collections } """ And the JSON node "hydra:totalItems" should be equal to 25 + + @createSchema + Scenario: Get collection filtered using a name converter + Given there are 5 convertedBoolean objects + When I send a "GET" request to "/converted_booleans?name_converted=false" + Then the response status code should be 200 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the JSON should be equal to: + """ + { + "@context": "/contexts/ConvertedBoolean", + "@id": "/converted_booleans", + "@type": "hydra:Collection", + "hydra:member": [ + { + "@id": "/converted_booleans/2", + "@type": "ConvertedBoolean", + "name_converted": false, + "id": 2 + }, + { + "@id": "/converted_booleans/4", + "@type": "ConvertedBoolean", + "name_converted": false, + "id": 4 + } + ], + "hydra:totalItems": 2, + "hydra:view": { + "@id": "/converted_booleans?name_converted=false", + "@type": "hydra:PartialCollectionView" + }, + "hydra:search": { + "@type": "hydra:IriTemplate", + "hydra:template": "/converted_booleans{?name_converted}", + "hydra:variableRepresentation": "BasicRepresentation", + "hydra:mapping": [ + { + "@type": "IriTemplateMapping", + "variable": "name_converted", + "property": "name_converted", + "required": false + } + ] + } + } + """ + And the JSON should be valid according to this schema: + """ + { + "type": "object", + "properties": { + "@context": {"pattern": "^/contexts/ConvertedBoolean"}, + "@id": {"pattern": "^/converted_booleans"}, + "@type": {"pattern": "^hydra:Collection$"}, + "hydra:member": { + "type": "array", + "items": { + "type": "object", + "properties": { + "@id": {"pattern": "^/converted_booleans/(2|4)$"}, + "@type": {"pattern": "^ConvertedBoolean"}, + "name_converted": {"type": "boolean"}, + "id": {"type": "integer", "minimum":2, "maximum": 4} + }, + "required": ["@id", "@type", "name_converted", "id"], + "additionalProperties": false + }, + "minItems": 2, + "maxItems": 2, + "uniqueItems": true + }, + "hydra:totalItems": {"type": "integer", "minimum": 2, "maximum": 2}, + "hydra:view": { + "type": "object", + "properties": { + "@id": {"pattern": "^/converted_booleans\\?name_converted=false"}, + "@type": {"pattern": "^hydra:PartialCollectionView$"} + }, + "required": ["@id", "@type"], + "additionalProperties": false + }, + "hydra:search": { + "type": "object", + "properties": { + "@type": {"pattern": "^hydra:IriTemplate$"}, + "hydra:template": {"pattern": "^/converted_booleans\\{\\?name_converted\\}$"}, + "hydra:variableRepresentation": {"pattern": "^BasicRepresentation$"}, + "hydra:mapping": { + "type": "array", + "items": { + "type": "object", + "properties": { + "@type": {"pattern": "^IriTemplateMapping$"}, + "variable": {"pattern": "^name_converted$"}, + "property": {"pattern": "^name_converted$"}, + "required": {"type": "boolean"} + }, + "required": ["@type", "variable", "property", "required"], + "additionalProperties": false + }, + "minItems": 1, + "maxItems": 1, + "uniqueItems": true + } + }, + "additionalProperties": false, + "required": ["@type", "hydra:template", "hydra:variableRepresentation", "hydra:mapping"] + }, + "additionalProperties": false, + "required": ["@context", "@id", "@type", "hydra:member", "hydra:totalItems", "hydra:view", "hydra:search"] + } + } + """ diff --git a/features/doctrine/date_filter.feature b/features/doctrine/date_filter.feature index f92b240242b..a12c0a5478e 100644 --- a/features/doctrine/date_filter.feature +++ b/features/doctrine/date_filter.feature @@ -413,7 +413,7 @@ Feature: Date filter on collections }, "hydra:search": { "@type": "hydra:IriTemplate", - "hydra:template": "/dummies{?dummyBoolean,relatedDummy.embeddedDummy.dummyBoolean,dummyDate[before],dummyDate[strictly_before],dummyDate[after],dummyDate[strictly_after],relatedDummy.dummyDate[before],relatedDummy.dummyDate[strictly_before],relatedDummy.dummyDate[after],relatedDummy.dummyDate[strictly_after],exists[alias],exists[description],exists[relatedDummy.name],exists[dummyBoolean],exists[relatedDummy],dummyFloat,dummyFloat[],dummyPrice,dummyPrice[],order[id],order[name],order[description],order[relatedDummy.name],order[relatedDummy.symfony],order[dummyDate],dummyFloat[between],dummyFloat[gt],dummyFloat[gte],dummyFloat[lt],dummyFloat[lte],dummyPrice[between],dummyPrice[gt],dummyPrice[gte],dummyPrice[lt],dummyPrice[lte],id,id[],name,alias,description,relatedDummy.name,relatedDummy.name[],relatedDummies,relatedDummies[],dummy,relatedDummies.name,relatedDummy.thirdLevel.level,relatedDummy.thirdLevel.level[],relatedDummy.thirdLevel.fourthLevel.level,relatedDummy.thirdLevel.fourthLevel.level[],relatedDummy.thirdLevel.badFourthLevel.level,relatedDummy.thirdLevel.badFourthLevel.level[],relatedDummy.thirdLevel.fourthLevel.badThirdLevel.level,relatedDummy.thirdLevel.fourthLevel.badThirdLevel.level[],properties[]}", + "hydra:template": "/dummies{?dummyBoolean,relatedDummy.embeddedDummy.dummyBoolean,dummyDate[before],dummyDate[strictly_before],dummyDate[after],dummyDate[strictly_after],relatedDummy.dummyDate[before],relatedDummy.dummyDate[strictly_before],relatedDummy.dummyDate[after],relatedDummy.dummyDate[strictly_after],exists[alias],exists[description],exists[relatedDummy.name],exists[dummyBoolean],exists[relatedDummy],dummyFloat,dummyFloat[],dummyPrice,dummyPrice[],order[id],order[name],order[description],order[relatedDummy.name],order[relatedDummy.symfony],order[dummyDate],dummyFloat[between],dummyFloat[gt],dummyFloat[gte],dummyFloat[lt],dummyFloat[lte],dummyPrice[between],dummyPrice[gt],dummyPrice[gte],dummyPrice[lt],dummyPrice[lte],id,id[],name,alias,description,relatedDummy.name,relatedDummy.name[],relatedDummies,relatedDummies[],dummy,relatedDummies.name,relatedDummy.thirdLevel.level,relatedDummy.thirdLevel.level[],relatedDummy.thirdLevel.fourthLevel.level,relatedDummy.thirdLevel.fourthLevel.level[],relatedDummy.thirdLevel.badFourthLevel.level,relatedDummy.thirdLevel.badFourthLevel.level[],relatedDummy.thirdLevel.fourthLevel.badThirdLevel.level,relatedDummy.thirdLevel.fourthLevel.badThirdLevel.level[],name_converted,properties[]}", "hydra:variableRepresentation": "BasicRepresentation", "hydra:mapping": [ { @@ -740,6 +740,12 @@ Feature: Date filter on collections "property": "relatedDummy.thirdLevel.fourthLevel.badThirdLevel.level", "required": false }, + { + "@type": "IriTemplateMapping", + "variable": "name_converted", + "property": "name_converted", + "required": false + }, { "@type": "IriTemplateMapping", "variable": "properties[]", @@ -1102,3 +1108,136 @@ Feature: Date filter on collections } } """ + + @createSchema + Scenario: Get collection filtered using a name converter + Given there are 30 convertedDate objects + When I send a "GET" request to "/converted_dates?name_converted[strictly_after]=2015-04-28" + Then the response status code should be 200 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the JSON should be equal to: + """ + { + "@context": "/contexts/ConvertedDate", + "@id": "/converted_dates", + "@type": "hydra:Collection", + "hydra:member": [ + { + "@id": "/converted_dates/29", + "@type": "ConvertedDate", + "name_converted": "2015-04-29T00:00:00+00:00", + "id": 29 + }, + { + "@id": "/converted_dates/30", + "@type": "ConvertedDate", + "name_converted": "2015-04-30T00:00:00+00:00", + "id": 30 + } + ], + "hydra:totalItems": 2, + "hydra:view": { + "@id": "/converted_dates?name_converted%5Bstrictly_after%5D=2015-04-28", + "@type": "hydra:PartialCollectionView" + }, + "hydra:search": { + "@type": "hydra:IriTemplate", + "hydra:template": "/converted_dates{?name_converted[before],name_converted[strictly_before],name_converted[after],name_converted[strictly_after]}", + "hydra:variableRepresentation": "BasicRepresentation", + "hydra:mapping": [ + { + "@type": "IriTemplateMapping", + "variable": "name_converted[before]", + "property": "name_converted", + "required": false + }, + { + "@type": "IriTemplateMapping", + "variable": "name_converted[strictly_before]", + "property": "name_converted", + "required": false + }, + { + "@type": "IriTemplateMapping", + "variable": "name_converted[after]", + "property": "name_converted", + "required": false + }, + { + "@type": "IriTemplateMapping", + "variable": "name_converted[strictly_after]", + "property": "name_converted", + "required": false + } + ] + } + } + """ + And the JSON should be valid according to this schema: + """ + { + "type": "object", + "properties": { + "@context": {"pattern": "^/contexts/ConvertedDate"}, + "@id": {"pattern": "^/converted_dates"}, + "@type": {"pattern": "^hydra:Collection$"}, + "hydra:member": { + "type": "array", + "items": { + "type": "object", + "properties": { + "@id": {"pattern": "^/converted_dates/(29|30)$"}, + "@type": {"pattern": "^ConvertedDate"}, + "name_converted": {"type": "string"}, + "id": {"type": "integer", "minimum":29, "maximum": 30} + }, + "required": ["@id", "@type", "name_converted", "id"], + "additionalProperties": false + }, + "minItems": 2, + "maxItems": 2, + "uniqueItems": true + }, + "hydra:totalItems": {"type": "integer", "minimum": 2, "maximum": 2}, + "hydra:view": { + "type": "object", + "properties": { + "@id": {"pattern": "^/converted_dates\\?name_converted%5Bstrictly_after%5D=2015\\-04\\-28$"}, + "@type": {"pattern": "^hydra:PartialCollectionView$"} + }, + "required": ["@id", "@type"], + "additionalProperties": false + }, + "hydra:search": { + "type": "object", + "properties": { + "@type": {"pattern": "^hydra:IriTemplate$"}, + "hydra:template": {"pattern": "^/converted_dates\\{\\?.*name_converted\\[before\\],name_converted\\[strictly_before\\],name_converted\\[after\\],name_converted\\[strictly_after\\].*\\}$"}, + "hydra:variableRepresentation": {"pattern": "^BasicRepresentation$"}, + "hydra:mapping": { + "type": "array", + "items": { + "type": "object", + "properties": { + "@type": {"pattern": "^IriTemplateMapping$"}, + "variable": {"pattern": "^name_converted(\\[(strictly_)?(before|after)\\])$"}, + "property": {"pattern": "^name_converted$"}, + "required": {"type": "boolean"} + }, + "required": ["@type", "variable", "property", "required"], + "additionalProperties": false + }, + "minItems": 4, + "maxItems": 4, + "uniqueItems": true + } + }, + "additionalProperties": false, + "required": ["@type", "hydra:template", "hydra:variableRepresentation", "hydra:mapping"] + }, + "additionalProperties": false, + "required": ["@context", "@id", "@type", "hydra:member", "hydra:totalItems", "hydra:view", "hydra:search"] + } + } + """ diff --git a/features/doctrine/exists_filter.feature b/features/doctrine/exists_filter.feature index d6df544d5fd..5616dfda56f 100644 --- a/features/doctrine/exists_filter.feature +++ b/features/doctrine/exists_filter.feature @@ -62,3 +62,118 @@ Feature: Exists filter on collections } } """ + + @createSchema + Scenario: Get collection filtered using a name converter + Given there are 4 convertedString objects + When I send a "GET" request to "/converted_strings?exists[name_converted]=true" + Then the response status code should be 200 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the JSON should be equal to: + """ + { + "@context": "/contexts/ConvertedString", + "@id": "/converted_strings", + "@type": "hydra:Collection", + "hydra:member": [ + { + "@id": "/converted_strings/1", + "@type": "ConvertedString", + "name_converted": "name#1", + "id": 1 + }, + { + "@id": "/converted_strings/3", + "@type": "ConvertedString", + "name_converted": "name#3", + "id": 3 + } + ], + "hydra:totalItems": 2, + "hydra:view": { + "@id": "/converted_strings?exists%5Bname_converted%5D=true", + "@type": "hydra:PartialCollectionView" + }, + "hydra:search": { + "@type": "hydra:IriTemplate", + "hydra:template": "/converted_strings{?exists[name_converted]}", + "hydra:variableRepresentation": "BasicRepresentation", + "hydra:mapping": [ + { + "@type": "IriTemplateMapping", + "variable": "exists[name_converted]", + "property": "name_converted", + "required": false + } + ] + } + } + """ + And the JSON should be valid according to this schema: + """ + { + "type": "object", + "properties": { + "@context": {"pattern": "^/contexts/ConvertedString"}, + "@id": {"pattern": "^/converted_strings"}, + "@type": {"pattern": "^hydra:Collection$"}, + "hydra:member": { + "type": "array", + "items": { + "type": "object", + "properties": { + "@id": {"pattern": "^/converted_strings/(1|3)$"}, + "@type": {"pattern": "^ConvertedString"}, + "name_converted": {"pattern": "^name#(1|3)$"}, + "id": {"type": "integer", "minimum":1, "maximum": 3} + }, + "required": ["@id", "@type", "name_converted", "id"], + "additionalProperties": false + }, + "minItems": 2, + "maxItems": 2, + "uniqueItems": true + }, + "hydra:totalItems": {"type": "integer", "minimum": 2, "maximum": 2}, + "hydra:view": { + "type": "object", + "properties": { + "@id": {"pattern": "^/converted_strings\\?exists%5Bname_converted%5D=true"}, + "@type": {"pattern": "^hydra:PartialCollectionView$"} + }, + "required": ["@id", "@type"], + "additionalProperties": false + }, + "hydra:search": { + "type": "object", + "properties": { + "@type": {"pattern": "^hydra:IriTemplate$"}, + "hydra:template": {"pattern": "^/converted_strings\\{\\?exists\\[name_converted\\]\\}$"}, + "hydra:variableRepresentation": {"pattern": "^BasicRepresentation$"}, + "hydra:mapping": { + "type": "array", + "items": { + "type": "object", + "properties": { + "@type": {"pattern": "^IriTemplateMapping$"}, + "variable": {"pattern": "^exists\\[name_converted\\]$"}, + "property": {"pattern": "^name_converted$"}, + "required": {"type": "boolean"} + }, + "required": ["@type", "variable", "property", "required"], + "additionalProperties": false + }, + "minItems": 1, + "maxItems": 1, + "uniqueItems": true + } + }, + "additionalProperties": false, + "required": ["@type", "hydra:template", "hydra:variableRepresentation", "hydra:mapping"] + }, + "additionalProperties": false, + "required": ["@context", "@id", "@type", "hydra:member", "hydra:totalItems", "hydra:view", "hydra:search"] + } + } + """ diff --git a/features/doctrine/numeric_filter.feature b/features/doctrine/numeric_filter.feature index 7670089a4b2..0b6455daeda 100644 --- a/features/doctrine/numeric_filter.feature +++ b/features/doctrine/numeric_filter.feature @@ -136,3 +136,166 @@ Feature: Numeric filter on collections } } """ + + @createSchema + Scenario: Get collection filtered using a name converter + Given there are 5 convertedInteger objects + When I send a "GET" request to "/converted_integers?name_converted[]=2&name_converted[]=3" + Then the response status code should be 200 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the JSON should be equal to: + """ + { + "@context": "/contexts/ConvertedInteger", + "@id": "/converted_integers", + "@type": "hydra:Collection", + "hydra:member": [ + { + "@id": "/converted_integers/2", + "@type": "ConvertedInteger", + "name_converted": 2, + "id": 2 + }, + { + "@id": "/converted_integers/3", + "@type": "ConvertedInteger", + "name_converted": 3, + "id": 3 + } + ], + "hydra:totalItems": 2, + "hydra:view": { + "@id": "/converted_integers?name_converted%5B%5D=2&name_converted%5B%5D=3", + "@type": "hydra:PartialCollectionView" + }, + "hydra:search": { + "@type": "hydra:IriTemplate", + "hydra:template": "/converted_integers{?name_converted,name_converted[],name_converted[between],name_converted[gt],name_converted[gte],name_converted[lt],name_converted[lte],order[name_converted]}", + "hydra:variableRepresentation": "BasicRepresentation", + "hydra:mapping": [ + { + "@type": "IriTemplateMapping", + "variable": "name_converted", + "property": "name_converted", + "required": false + }, + { + "@type": "IriTemplateMapping", + "variable": "name_converted[]", + "property": "name_converted", + "required": false + }, + { + "@type": "IriTemplateMapping", + "variable": "name_converted[between]", + "property": "name_converted", + "required": false + }, + { + "@type": "IriTemplateMapping", + "variable": "name_converted[gt]", + "property": "name_converted", + "required": false + }, + { + "@type": "IriTemplateMapping", + "variable": "name_converted[gte]", + "property": "name_converted", + "required": false + }, + { + "@type": "IriTemplateMapping", + "variable": "name_converted[lt]", + "property": "name_converted", + "required": false + }, + { + "@type": "IriTemplateMapping", + "variable": "name_converted[lte]", + "property": "name_converted", + "required": false + }, + { + "@type": "IriTemplateMapping", + "variable": "order[name_converted]", + "property": "name_converted", + "required": false + } + ] + } + } + """ + And the JSON should be valid according to this schema: + """ + { + "type": "object", + "properties": { + "@context": {"pattern": "^/contexts/ConvertedInteger$"}, + "@id": {"pattern": "^/converted_integers$"}, + "@type": {"pattern": "^hydra:Collection$"}, + "hydra:member": { + "type": "array", + "items": { + "type": "object", + "properties": { + "@id": {"pattern": "^/converted_integers/(2|3)$"}, + "@type": {"pattern": "^ConvertedInteger$"}, + "name_converted": {"type": "integer"}, + "id": {"type": "integer", "minimum":2, "maximum": 3} + }, + "required": ["@id", "@type", "name_converted", "id"], + "additionalProperties": false + }, + "minItems": 2, + "maxItems": 2, + "uniqueItems": true + }, + "hydra:totalItems": {"type": "integer", "minimum": 2, "maximum": 2}, + "hydra:view": { + "type": "object", + "properties": { + "@id": {"pattern": "^/converted_integers\\?name_converted%5B%5D=2&name_converted%5B%5D=3$"}, + "@type": {"pattern": "^hydra:PartialCollectionView$"} + }, + "required": ["@id", "@type"], + "additionalProperties": false + }, + "hydra:search": { + "type": "object", + "properties": { + "@type": {"pattern": "^hydra:IriTemplate$"}, + "hydra:template": {"pattern": "^/converted_integers\\{\\?.*name_converted,name_converted\\[\\].*\\}$"}, + "hydra:variableRepresentation": {"pattern": "^BasicRepresentation$"}, + "hydra:mapping": { + "type": "array", + "items": { + "type": "object", + "properties": { + "@type": {"pattern": "^IriTemplateMapping$"}, + "variable": { + "oneOf": [ + {"pattern": "^name_converted(\\[(between|gt|gte|lt|lte)?\\])?$"}, + {"pattern": "^order\\[name_converted\\]$"} + ] + }, + "property": {"pattern": "^name_converted$"}, + "required": {"type": "boolean"} + }, + "required": ["@type", "variable", "property", "required"], + "additionalProperties": false + }, + "minItems": 8, + "maxItems": 8, + "uniqueItems": true + } + }, + "additionalProperties": false, + "required": ["@type", "hydra:template", "hydra:variableRepresentation", "hydra:mapping"] + }, + "additionalProperties": false, + "required": ["@context", "@id", "@type", "hydra:member", "hydra:totalItems", "hydra:view", "hydra:search"] + } + } + """ + diff --git a/features/doctrine/order_filter.feature b/features/doctrine/order_filter.feature index f235c65f30e..29bb1c45cd5 100644 --- a/features/doctrine/order_filter.feature +++ b/features/doctrine/order_filter.feature @@ -882,3 +882,195 @@ Feature: Order filter on collections } } """ + + @createSchema + Scenario: Get collection filtered using a name converter + Given there are 3 convertedInteger objects + When I send a "GET" request to "/converted_integers?order[name_converted]=desc" + Then the response status code should be 200 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the JSON should be equal to: + """ + { + "@context": "/contexts/ConvertedInteger", + "@id": "/converted_integers", + "@type": "hydra:Collection", + "hydra:member": [ + { + "@id": "/converted_integers/3", + "@type": "ConvertedInteger", + "name_converted": 3, + "id": 3 + }, + { + "@id": "/converted_integers/2", + "@type": "ConvertedInteger", + "name_converted": 2, + "id": 2 + }, + { + "@id": "/converted_integers/1", + "@type": "ConvertedInteger", + "name_converted": 1, + "id": 1 + } + ], + "hydra:totalItems": 3, + "hydra:view": { + "@id": "/converted_integers?order%5Bname_converted%5D=desc", + "@type": "hydra:PartialCollectionView" + }, + "hydra:search": { + "@type": "hydra:IriTemplate", + "hydra:template": "/converted_integers{?name_converted,name_converted[],name_converted[between],name_converted[gt],name_converted[gte],name_converted[lt],name_converted[lte],order[name_converted]}", + "hydra:variableRepresentation": "BasicRepresentation", + "hydra:mapping": [ + { + "@type": "IriTemplateMapping", + "variable": "name_converted", + "property": "name_converted", + "required": false + }, + { + "@type": "IriTemplateMapping", + "variable": "name_converted[]", + "property": "name_converted", + "required": false + }, + { + "@type": "IriTemplateMapping", + "variable": "name_converted[between]", + "property": "name_converted", + "required": false + }, + { + "@type": "IriTemplateMapping", + "variable": "name_converted[gt]", + "property": "name_converted", + "required": false + }, + { + "@type": "IriTemplateMapping", + "variable": "name_converted[gte]", + "property": "name_converted", + "required": false + }, + { + "@type": "IriTemplateMapping", + "variable": "name_converted[lt]", + "property": "name_converted", + "required": false + }, + { + "@type": "IriTemplateMapping", + "variable": "name_converted[lte]", + "property": "name_converted", + "required": false + }, + { + "@type": "IriTemplateMapping", + "variable": "order[name_converted]", + "property": "name_converted", + "required": false + } + ] + } + } + """ + And the JSON should be valid according to this schema: + """ + { + "type": "object", + "properties": { + "@context": {"pattern": "^/contexts/ConvertedInteger$"}, + "@id": {"pattern": "^/converted_integers$"}, + "@type": {"pattern": "^hydra:Collection$"}, + "hydra:member": { + "type": "array", + "items": [ + { + "type": "object", + "properties": { + "@id": {"pattern": "^/converted_integers/3$"}, + "@type": {"pattern": "^ConvertedInteger$"}, + "name_converted": {"type": "integer"}, + "id": {"type": "integer", "minimum":3, "maximum": 3} + }, + "required": ["@id", "@type", "name_converted", "id"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "@id": {"pattern": "^/converted_integers/2$"}, + "@type": {"pattern": "^ConvertedInteger$"}, + "name_converted": {"type": "integer"}, + "id": {"type": "integer", "minimum":2, "maximum": 2} + }, + "required": ["@id", "@type", "name_converted", "id"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "@id": {"pattern": "^/converted_integers/1$"}, + "@type": {"pattern": "^ConvertedInteger$"}, + "name_converted": {"type": "integer"}, + "id": {"type": "integer", "minimum":1, "maximum": 1} + }, + "required": ["@id", "@type", "name_converted", "id"], + "additionalProperties": false + } + ], + "minItems": 3, + "maxItems": 3, + "uniqueItems": true + }, + "hydra:totalItems": {"type": "integer", "minimum": 3, "maximum": 3}, + "hydra:view": { + "type": "object", + "properties": { + "@id": {"pattern": "^/converted_integers\\?order%5Bname_converted%5D=desc$"}, + "@type": {"pattern": "^hydra:PartialCollectionView$"} + }, + "required": ["@id", "@type"], + "additionalProperties": false + }, + "hydra:search": { + "type": "object", + "properties": { + "@type": {"pattern": "^hydra:IriTemplate$"}, + "hydra:template": {"pattern": "^/converted_integers\\{\\?.*order\\[name_converted\\].*\\}$"}, + "hydra:variableRepresentation": {"pattern": "^BasicRepresentation$"}, + "hydra:mapping": { + "type": "array", + "items": { + "type": "object", + "properties": { + "@type": {"pattern": "^IriTemplateMapping$"}, + "variable": { + "oneOf": [ + {"pattern": "^order\\[name_converted\\]$"}, + {"pattern": "^name_converted(\\[(between|gt|gte|lt|lte)?\\])?$"} + ] + }, + "property": {"pattern": "^name_converted$"}, + "required": {"type": "boolean"} + }, + "required": ["@type", "variable", "property", "required"], + "additionalProperties": false + }, + "minItems": 8, + "maxItems": 8, + "uniqueItems": true + } + }, + "additionalProperties": false, + "required": ["@type", "hydra:template", "hydra:variableRepresentation", "hydra:mapping"] + }, + "additionalProperties": false, + "required": ["@context", "@id", "@type", "hydra:member", "hydra:totalItems", "hydra:view", "hydra:search"] + } + } + """ diff --git a/features/doctrine/range_filter.feature b/features/doctrine/range_filter.feature index 4069ffc5855..ae53bf99fb5 100644 --- a/features/doctrine/range_filter.feature +++ b/features/doctrine/range_filter.feature @@ -379,3 +379,165 @@ Feature: Range filter on collections } } """ + + @createSchema + Scenario: Get collection filtered using a name converter + Given there are 5 convertedInteger objects + When I send a "GET" request to "/converted_integers?name_converted[lte]=2" + Then the response status code should be 200 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the JSON should be equal to: + """ + { + "@context": "/contexts/ConvertedInteger", + "@id": "/converted_integers", + "@type": "hydra:Collection", + "hydra:member": [ + { + "@id": "/converted_integers/1", + "@type": "ConvertedInteger", + "name_converted": 1, + "id": 1 + }, + { + "@id": "/converted_integers/2", + "@type": "ConvertedInteger", + "name_converted": 2, + "id": 2 + } + ], + "hydra:totalItems": 2, + "hydra:view": { + "@id": "/converted_integers?name_converted%5Blte%5D=2", + "@type": "hydra:PartialCollectionView" + }, + "hydra:search": { + "@type": "hydra:IriTemplate", + "hydra:template": "/converted_integers{?name_converted,name_converted[],name_converted[between],name_converted[gt],name_converted[gte],name_converted[lt],name_converted[lte],order[name_converted]}", + "hydra:variableRepresentation": "BasicRepresentation", + "hydra:mapping": [ + { + "@type": "IriTemplateMapping", + "variable": "name_converted", + "property": "name_converted", + "required": false + }, + { + "@type": "IriTemplateMapping", + "variable": "name_converted[]", + "property": "name_converted", + "required": false + }, + { + "@type": "IriTemplateMapping", + "variable": "name_converted[between]", + "property": "name_converted", + "required": false + }, + { + "@type": "IriTemplateMapping", + "variable": "name_converted[gt]", + "property": "name_converted", + "required": false + }, + { + "@type": "IriTemplateMapping", + "variable": "name_converted[gte]", + "property": "name_converted", + "required": false + }, + { + "@type": "IriTemplateMapping", + "variable": "name_converted[lt]", + "property": "name_converted", + "required": false + }, + { + "@type": "IriTemplateMapping", + "variable": "name_converted[lte]", + "property": "name_converted", + "required": false + }, + { + "@type": "IriTemplateMapping", + "variable": "order[name_converted]", + "property": "name_converted", + "required": false + } + ] + } + } + """ + And the JSON should be valid according to this schema: + """ + { + "type": "object", + "properties": { + "@context": {"pattern": "^/contexts/ConvertedInteger$"}, + "@id": {"pattern": "^/converted_integers$"}, + "@type": {"pattern": "^hydra:Collection$"}, + "hydra:member": { + "type": "array", + "items": { + "type": "object", + "properties": { + "@id": {"pattern": "^/converted_integers/(1|2)$"}, + "@type": {"pattern": "^ConvertedInteger$"}, + "name_converted": {"type": "integer"}, + "id": {"type": "integer", "minimum":1, "maximum": 2} + }, + "required": ["@id", "@type", "name_converted", "id"], + "additionalProperties": false + }, + "minItems": 2, + "maxItems": 2, + "uniqueItems": true + }, + "hydra:totalItems": {"type": "integer", "minimum": 2, "maximum": 2}, + "hydra:view": { + "type": "object", + "properties": { + "@id": {"pattern": "^/converted_integers\\?name_converted%5Blte%5D=2$"}, + "@type": {"pattern": "^hydra:PartialCollectionView$"} + }, + "required": ["@id", "@type"], + "additionalProperties": false + }, + "hydra:search": { + "type": "object", + "properties": { + "@type": {"pattern": "^hydra:IriTemplate$"}, + "hydra:template": {"pattern": "^/converted_integers\\{\\?.*name_converted\\[between\\],name_converted\\[gt\\],name_converted\\[gte\\],name_converted\\[lt\\],name_converted\\[lte\\].*\\}$"}, + "hydra:variableRepresentation": {"pattern": "^BasicRepresentation$"}, + "hydra:mapping": { + "type": "array", + "items": { + "type": "object", + "properties": { + "@type": {"pattern": "^IriTemplateMapping$"}, + "variable": { + "oneOf": [ + {"pattern": "^name_converted(\\[(between|gt|gte|lt|lte)?\\])?$"}, + {"pattern": "^order\\[name_converted\\]$"} + ] + }, + "property": {"pattern": "^name_converted$"}, + "required": {"type": "boolean"} + }, + "required": ["@type", "variable", "property", "required"], + "additionalProperties": false + }, + "minItems": 8, + "maxItems": 8, + "uniqueItems": true + } + }, + "additionalProperties": false, + "required": ["@type", "hydra:template", "hydra:variableRepresentation", "hydra:mapping"] + }, + "additionalProperties": false, + "required": ["@context", "@id", "@type", "hydra:member", "hydra:totalItems", "hydra:view", "hydra:search"] + } + } + """ diff --git a/features/doctrine/search_filter.feature b/features/doctrine/search_filter.feature index 9b1bc249503..9e966825956 100644 --- a/features/doctrine/search_filter.feature +++ b/features/doctrine/search_filter.feature @@ -728,3 +728,72 @@ Feature: Search filter on collections } """ + @createSchema + Scenario: Search collection on a property using a name converted + Given there are 30 dummy objects + When I send a "GET" request to "/dummies?name_converted=Converted 3" + Then the response status code should be 200 + 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/Dummy$"}, + "@id": {"pattern": "^/dummies$"}, + "@type": {"pattern": "^hydra:Collection$"}, + "hydra:member": { + "type": "array", + "items": { + "type": "object", + "properties": { + "@id": { + "oneOf": [ + {"pattern": "^/dummies/3$"}, + {"pattern": "^/dummies/30$"} + ] + }, + "required": ["@id"] + } + }, + "minItems": 2, + "maxItems": 2 + }, + "hydra:view": { + "type": "object", + "properties": { + "@id": {"pattern": "^/dummies\\?name_converted=Converted%203"}, + "@type": {"pattern": "^hydra:PartialCollectionView$"} + } + }, + "hydra:search": { + "type": "object", + "properties": { + "@type": {"pattern": "^hydra:IriTemplate$"}, + "hydra:template": {"pattern": "^/dummies\\{\\?.*name_converted.*}$"}, + "hydra:variableRepresentation": {"pattern": "^BasicRepresentation$"}, + "hydra:mapping": { + "type": "array", + "items": { + "type": "object", + "properties": { + "@type": {"pattern": "^IriTemplateMapping$"}, + "variable": {"pattern": "^name_converted$"}, + "property": {"pattern": "^name_converted$"}, + "required": {"type": "boolean"} + }, + "required": ["@type", "variable", "property", "required"], + "additionalProperties": false + }, + "additionalItems": true, + "uniqueItems": true + } + }, + "additionalProperties": false, + "required": ["@type", "hydra:template", "hydra:variableRepresentation", "hydra:mapping"] + }, + "additionalProperties": false, + "required": ["@context", "@id", "@type", "hydra:member", "hydra:totalItems", "hydra:view", "hydra:search"] + } + } + """ diff --git a/features/filter/property_filter.feature b/features/filter/property_filter.feature index 722ced764ae..367d1e1689c 100644 --- a/features/filter/property_filter.feature +++ b/features/filter/property_filter.feature @@ -6,10 +6,11 @@ Feature: Set properties to include @createSchema Scenario: Test properties filter Given there are 1 dummy objects with relatedDummy and its thirdLevel - When I send a "GET" request to "/dummies/1?properties[]=name&properties[]=alias&properties[]=relatedDummy" + When I send a "GET" request to "/dummies/1?properties[]=name&properties[]=alias&properties[]=relatedDummy&properties[]=name_converted" And the JSON node "name" should be equal to "Dummy #1" And the JSON node "alias" should be equal to "Alias #0" And the JSON node "relatedDummies" should not exist + And the JSON node "name_converted" should exist Scenario: Test relation embedding When I send a "GET" request to "/dummies/1?properties[]=name&properties[]=alias&properties[relatedDummy][]=name" diff --git a/features/hal/collection.feature b/features/hal/collection.feature index 944fb839072..8507c886621 100644 --- a/features/hal/collection.feature +++ b/features/hal/collection.feature @@ -78,7 +78,7 @@ Feature: HAL Collections support "dummyPrice": null, "jsonData": [], "arrayData": [], - "name_converted": null, + "name_converted": "Converted 1", "id": 1, "name": "Dummy #1", "alias": "Alias #9", @@ -98,7 +98,7 @@ Feature: HAL Collections support "dummyPrice": null, "jsonData": [], "arrayData": [], - "name_converted": null, + "name_converted": "Converted 2", "id": 2, "name": "Dummy #2", "alias": "Alias #8", @@ -118,7 +118,7 @@ Feature: HAL Collections support "dummyPrice": null, "jsonData": [], "arrayData": [], - "name_converted": null, + "name_converted": "Converted 3", "id": 3, "name": "Dummy #3", "alias": "Alias #7", @@ -185,7 +185,7 @@ Feature: HAL Collections support "dummyPrice": null, "jsonData": [], "arrayData": [], - "name_converted": null, + "name_converted": "Converted 7", "id": 7, "name": "Dummy #7", "alias": "Alias #3", @@ -205,7 +205,7 @@ Feature: HAL Collections support "dummyPrice": null, "jsonData": [], "arrayData": [], - "name_converted": null, + "name_converted": "Converted 8", "id": 8, "name": "Dummy #8", "alias": "Alias #2", @@ -225,7 +225,7 @@ Feature: HAL Collections support "dummyPrice": null, "jsonData": [], "arrayData": [], - "name_converted": null, + "name_converted": "Converted 9", "id": 9, "name": "Dummy #9", "alias": "Alias #1", @@ -283,7 +283,7 @@ Feature: HAL Collections support "dummyPrice": null, "jsonData": [], "arrayData": [], - "name_converted": null, + "name_converted": "Converted 10", "id": 10, "name": "Dummy #10", "alias": "Alias #0", @@ -344,7 +344,7 @@ Feature: HAL Collections support "dummyPrice": null, "jsonData": [], "arrayData": [], - "name_converted": null, + "name_converted": "Converted 4", "id": 4, "name": "Dummy #4", "alias": "Alias #6", @@ -364,7 +364,7 @@ Feature: HAL Collections support "dummyPrice": null, "jsonData": [], "arrayData": [], - "name_converted": null, + "name_converted": "Converted 5", "id": 5, "name": "Dummy #5", "alias": "Alias #5", @@ -384,7 +384,7 @@ Feature: HAL Collections support "dummyPrice": null, "jsonData": [], "arrayData": [], - "name_converted": null, + "name_converted": "Converted 6", "id": 6, "name": "Dummy #6", "alias": "Alias #4", @@ -506,7 +506,7 @@ Feature: HAL Collections support "dummyPrice": null, "jsonData": [], "arrayData": [], - "name_converted": null, + "name_converted": "Converted 2", "id": 2, "name": "Dummy #2", "alias": "Alias #8", @@ -555,7 +555,7 @@ Feature: HAL Collections support "dummyPrice": null, "jsonData": [], "arrayData": [], - "name_converted": null, + "name_converted": "Converted 8", "id": 8, "name": "Dummy #8", "alias": "Alias #2", @@ -604,7 +604,7 @@ Feature: HAL Collections support "dummyPrice": null, "jsonData": [], "arrayData": [], - "name_converted": null, + "name_converted": "Converted 8", "id": 8, "name": "Dummy #8", "alias": "Alias #2", diff --git a/features/jsonapi/related-resouces-inclusion.feature b/features/jsonapi/related-resouces-inclusion.feature index a2864df22eb..2c125f6065e 100644 --- a/features/jsonapi/related-resouces-inclusion.feature +++ b/features/jsonapi/related-resouces-inclusion.feature @@ -24,7 +24,8 @@ Feature: JSON API Inclusion of Related Resources "_id": 1, "foo": "Foo #1", "bar": "Bar #1", - "baz": "Baz #1" + "baz": "Baz #1", + "name_converted": "NameConverted #1" }, "relationships": { "group": { @@ -65,7 +66,8 @@ Feature: JSON API Inclusion of Related Resources "_id": 1, "foo": "Foo #1", "bar": "Bar #1", - "baz": "Baz #1" + "baz": "Baz #1", + "name_converted": "NameConverted #1" }, "relationships": { "group": { @@ -166,7 +168,8 @@ Feature: JSON API Inclusion of Related Resources "_id": 1, "foo": "Foo #1", "bar": "Bar #1", - "baz": "Baz #1" + "baz": "Baz #1", + "name_converted": null }, "relationships": { "group": { @@ -245,7 +248,8 @@ Feature: JSON API Inclusion of Related Resources "_id": 1, "foo": "Foo #1", "bar": "Bar #1", - "baz": "Baz #1" + "baz": "Baz #1", + "name_converted": null }, "relationships": { "group": { @@ -413,7 +417,8 @@ Feature: JSON API Inclusion of Related Resources "_id": 1, "foo": "Foo #1", "bar": "Bar #1", - "baz": "Baz #1" + "baz": "Baz #1", + "name_converted": "NameConverted #1" }, "relationships": { "group": { @@ -431,7 +436,8 @@ Feature: JSON API Inclusion of Related Resources "_id": 2, "foo": "Foo #2", "bar": "Bar #2", - "baz": "Baz #2" + "baz": "Baz #2", + "name_converted": "NameConverted #2" }, "relationships": { "group": { @@ -449,7 +455,8 @@ Feature: JSON API Inclusion of Related Resources "_id": 3, "foo": "Foo #3", "bar": "Bar #3", - "baz": "Baz #3" + "baz": "Baz #3", + "name_converted": "NameConverted #3" }, "relationships": { "group": { @@ -522,7 +529,8 @@ Feature: JSON API Inclusion of Related Resources "_id": 1, "foo": "Foo #1", "bar": "Bar #1", - "baz": "Baz #1" + "baz": "Baz #1", + "name_converted": null }, "relationships": { "group": { @@ -540,7 +548,8 @@ Feature: JSON API Inclusion of Related Resources "_id": 2, "foo": "Foo #2", "bar": "Bar #2", - "baz": "Baz #2" + "baz": "Baz #2", + "name_converted": null }, "relationships": { "group": { @@ -558,7 +567,8 @@ Feature: JSON API Inclusion of Related Resources "_id": 3, "foo": "Foo #3", "bar": "Bar #3", - "baz": "Baz #3" + "baz": "Baz #3", + "name_converted": null }, "relationships": { "group": { @@ -610,7 +620,8 @@ Feature: JSON API Inclusion of Related Resources "_id": 1, "foo": "Foo #1", "bar": "Bar #1", - "baz": "Baz #1" + "baz": "Baz #1", + "name_converted": null }, "relationships": { "groups": { @@ -627,7 +638,8 @@ Feature: JSON API Inclusion of Related Resources "_id": 2, "foo": "Foo #2", "bar": "Bar #2", - "baz": "Baz #2" + "baz": "Baz #2", + "name_converted": null }, "relationships": { "groups": { diff --git a/features/main/crud.feature b/features/main/crud.feature index ea6ddd52b3a..2618739c53a 100644 --- a/features/main/crud.feature +++ b/features/main/crud.feature @@ -137,7 +137,7 @@ Feature: Create-Retrieve-Update-Delete "hydra:totalItems": 1, "hydra:search": { "@type": "hydra:IriTemplate", - "hydra:template": "/dummies{?dummyBoolean,relatedDummy.embeddedDummy.dummyBoolean,dummyDate[before],dummyDate[strictly_before],dummyDate[after],dummyDate[strictly_after],relatedDummy.dummyDate[before],relatedDummy.dummyDate[strictly_before],relatedDummy.dummyDate[after],relatedDummy.dummyDate[strictly_after],exists[alias],exists[description],exists[relatedDummy.name],exists[dummyBoolean],exists[relatedDummy],dummyFloat,dummyFloat[],dummyPrice,dummyPrice[],order[id],order[name],order[description],order[relatedDummy.name],order[relatedDummy.symfony],order[dummyDate],dummyFloat[between],dummyFloat[gt],dummyFloat[gte],dummyFloat[lt],dummyFloat[lte],dummyPrice[between],dummyPrice[gt],dummyPrice[gte],dummyPrice[lt],dummyPrice[lte],id,id[],name,alias,description,relatedDummy.name,relatedDummy.name[],relatedDummies,relatedDummies[],dummy,relatedDummies.name,relatedDummy.thirdLevel.level,relatedDummy.thirdLevel.level[],relatedDummy.thirdLevel.fourthLevel.level,relatedDummy.thirdLevel.fourthLevel.level[],relatedDummy.thirdLevel.badFourthLevel.level,relatedDummy.thirdLevel.badFourthLevel.level[],relatedDummy.thirdLevel.fourthLevel.badThirdLevel.level,relatedDummy.thirdLevel.fourthLevel.badThirdLevel.level[],properties[]}", + "hydra:template": "/dummies{?dummyBoolean,relatedDummy.embeddedDummy.dummyBoolean,dummyDate[before],dummyDate[strictly_before],dummyDate[after],dummyDate[strictly_after],relatedDummy.dummyDate[before],relatedDummy.dummyDate[strictly_before],relatedDummy.dummyDate[after],relatedDummy.dummyDate[strictly_after],exists[alias],exists[description],exists[relatedDummy.name],exists[dummyBoolean],exists[relatedDummy],dummyFloat,dummyFloat[],dummyPrice,dummyPrice[],order[id],order[name],order[description],order[relatedDummy.name],order[relatedDummy.symfony],order[dummyDate],dummyFloat[between],dummyFloat[gt],dummyFloat[gte],dummyFloat[lt],dummyFloat[lte],dummyPrice[between],dummyPrice[gt],dummyPrice[gte],dummyPrice[lt],dummyPrice[lte],id,id[],name,alias,description,relatedDummy.name,relatedDummy.name[],relatedDummies,relatedDummies[],dummy,relatedDummies.name,relatedDummy.thirdLevel.level,relatedDummy.thirdLevel.level[],relatedDummy.thirdLevel.fourthLevel.level,relatedDummy.thirdLevel.fourthLevel.level[],relatedDummy.thirdLevel.badFourthLevel.level,relatedDummy.thirdLevel.badFourthLevel.level[],relatedDummy.thirdLevel.fourthLevel.badThirdLevel.level,relatedDummy.thirdLevel.fourthLevel.badThirdLevel.level[],name_converted,properties[]}", "hydra:variableRepresentation": "BasicRepresentation", "hydra:mapping": [ { @@ -464,6 +464,12 @@ Feature: Create-Retrieve-Update-Delete "property": "relatedDummy.thirdLevel.fourthLevel.badThirdLevel.level", "required": false }, + { + "@type": "IriTemplateMapping", + "variable": "name_converted", + "property": "name_converted", + "required": false + }, { "@type": "IriTemplateMapping", "variable": "properties[]", diff --git a/features/serializer/property_filter.feature b/features/serializer/property_filter.feature index b8b1926e991..932427f255c 100644 --- a/features/serializer/property_filter.feature +++ b/features/serializer/property_filter.feature @@ -6,7 +6,7 @@ Feature: Filter with serialization attributes on items and collections @createSchema Scenario: Get a collection of resources by attributes id, foo and bar Given there are 10 dummy property objects - When I send a "GET" request to "/dummy_properties?properties[]=id&properties[]=foo&properties[]=bar" + When I send a "GET" request to "/dummy_properties?properties[]=id&properties[]=foo&properties[]=bar&properties[]=name_converted" 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" @@ -20,52 +20,27 @@ Feature: Filter with serialization attributes on items and collections "@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"] + "items": { + "type": "object", + "properties": { + "@id": {}, + "@type": {}, + "id": {}, + "foo": {}, + "bar": {}, + "name_converted": {} }, - { - "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, + "additionalProperties": false, + "required": ["@id", "@type", "id", "foo", "bar", "name_converted"] + }, + "uniqueItems": true, "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$"}, + "@id": {"pattern": "^/dummy_properties\\?properties%5B%5D=id&properties%5B%5D=foo&properties%5B%5D=bar&properties%5B%5D=name_converted&page=1$"}, "@type": {"pattern": "^hydra:PartialCollectionView$"} } } @@ -88,72 +63,27 @@ Feature: Filter with serialization attributes on items and collections "@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"] + "items": { + "type": "object", + "properties": { + "@id": {}, + "@type": {}, + "foo": {}, + "bar": {}, + "group": { + "type": "object", + "properties": { + "@id": {}, + "@type": {}, + "baz": {} + }, + "additionalProperties": false, + "required": ["@id", "@type", "baz"] + } }, - { - "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, + "additionalProperties": false, + "required": ["@id", "@type", "foo", "bar"] + }, "maxItems": 3, "minItems": 3 }, @@ -169,7 +99,7 @@ Feature: Filter with serialization attributes on items and collections """ Scenario: Get a collection of resources by attributes foo, bar - When I send a "GET" request to "/dummy_properties?whitelisted_properties[]=foo&whitelisted_properties[]=bar" + When I send a "GET" request to "/dummy_properties?whitelisted_properties[]=foo&whitelisted_properties[]=bar&whitelisted_properties[]=name_converted" 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" @@ -183,46 +113,24 @@ Feature: Filter with serialization attributes on items and collections "@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"] + "items": { + "type": "object", + "properties": { + "@id": {}, + "@type": {}, + "foo": {}, + "name_converted": {} }, - { - "type": "object", - "properties": { - "@id": {}, - "@type": {}, - "foo": {} - }, - "additionalProperties": false, - "required": ["@id", "@type", "foo"] - } - ], - "additionalItems": false, + "additionalProperties": false, + "required": ["@id", "@type", "foo", "name_converted"] + }, "maxItems": 3, "minItems": 3 }, "hydra:view": { "type": "object", "properties": { - "@id": {"pattern": "^/dummy_properties\\?whitelisted_properties%5B%5D=foo&whitelisted_properties%5B%5D=bar&page=1$"}, + "@id": {"pattern": "^/dummy_properties\\?whitelisted_properties%5B%5D=foo&whitelisted_properties%5B%5D=bar&whitelisted_properties%5B%5D=name_converted&page=1$"}, "@type": {"pattern": "^hydra:PartialCollectionView$"} } } @@ -245,69 +153,26 @@ Feature: Filter with serialization attributes on items and collections "@type": {"pattern": "^hydra:Collection$"}, "hydra:member": { "type": "array", - "items": [ - { - "type": "object", - "properties": { - "@id": {}, - "@type": {}, - "foo": {}, - "group": { - "type": "object", - "properties": { - "@id": {}, - "@type": {}, - "baz": {} - }, - "additionalProperties": false, - "required": ["@id", "@type", "baz"] - } - }, - "additionalProperties": false, - "required": ["@id", "@type", "foo"] + "items": { + "type": "object", + "properties": { + "@id": {}, + "@type": {}, + "foo": {}, + "group": { + "type": "object", + "properties": { + "@id": {}, + "@type": {}, + "baz": {} + }, + "additionalProperties": false, + "required": ["@id", "@type", "baz"] + } }, - { - "type": "object", - "properties": { - "@id": {}, - "@type": {}, - "foo": {}, - "group": { - "type": "object", - "properties": { - "@id": {}, - "@type": {}, - "baz": {} - }, - "additionalProperties": false, - "required": ["@id", "@type", "baz"] - } - }, - "additionalProperties": false, - "required": ["@id", "@type", "foo"] - }, - { - "type": "object", - "properties": { - "@id": {}, - "@type": {}, - "foo": {}, - "group": { - "type": "object", - "properties": { - "@id": {}, - "@type": {}, - "baz": {} - }, - "additionalProperties": false, - "required": ["@id", "@type"] - } - }, - "additionalProperties": false, - "required": ["@id", "@type", "foo"] - } - ], - "additionalItems": false, + "additionalProperties": false, + "required": ["@id", "@type", "foo"] + }, "maxItems": 3, "minItems": 3 }, @@ -337,36 +202,15 @@ Feature: Filter with serialization attributes on items and collections "@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"] + "items": { + "type": "object", + "properties": { + "@id": {}, + "@type": {} }, - { - "type": "object", - "properties": { - "@id": {}, - "@type": {} - }, - "additionalProperties": false, - "required": ["@id", "@type"] - } - ], - "additionalItems": false, + "additionalProperties": false, + "required": ["@id", "@type"] + }, "maxItems": 3, "minItems": 3 }, @@ -396,63 +240,24 @@ Feature: Filter with serialization attributes on items and collections "@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"] + "items": { + "type": "object", + "properties": { + "@id": {}, + "@type": {}, + "group": { + "type": "object", + "properties": { + "@id": {}, + "@type": {} + }, + "additionalProperties": false, + "required": ["@id", "@type"] + } }, - { - "type": "object", - "properties": { - "@id": {}, - "@type": {}, - "group": { - "type": "object", - "properties": { - "@id": {}, - "@type": {} - }, - "additionalProperties": false, - "required": ["@id", "@type"] - } - }, - "additionalProperties": false, - "required": ["@id", "@type", "group"] - } - ], - "additionalItems": false, + "additionalProperties": false, + "required": ["@id", "@type", "group"] + }, "maxItems": 3, "minItems": 3 }, diff --git a/phpstan.neon.dist b/phpstan.neon.dist index f7e17f05a21..aba4a36cabb 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -78,7 +78,6 @@ parameters: path: %currentWorkingDirectory%/src/Bridge/Doctrine/Common/Filter/RangeFilterTrait.php # Expected, due to deprecations - - '#Method ApiPlatform\\Core\\Bridge\\Doctrine\\Orm\\Filter\\ExistsFilter::filterProperty\(\) invoked with 7 parameters, 5-6 required\.#' - '#Method ApiPlatform\\Core\\Bridge\\Doctrine\\Orm\\Extension\\QueryResult(Item|Collection)ExtensionInterface::getResult\(\) invoked with 4 parameters, 1 required\.#' - '#Method ApiPlatform\\Core\\Bridge\\Doctrine\\Orm\\Extension\\QueryResult(Item|Collection)ExtensionInterface::supportsResult\(\) invoked with 3 parameters, 1-2 required\.#' - '#Method ApiPlatform\\Core\\Bridge\\Doctrine\\Orm\\Filter\\AbstractFilter::apply\(\) invoked with 5 parameters, 3-4 required\.#' diff --git a/src/Bridge/Doctrine/Common/Filter/BooleanFilterTrait.php b/src/Bridge/Doctrine/Common/Filter/BooleanFilterTrait.php index cf09b77b0d4..df74e3d8e64 100644 --- a/src/Bridge/Doctrine/Common/Filter/BooleanFilterTrait.php +++ b/src/Bridge/Doctrine/Common/Filter/BooleanFilterTrait.php @@ -50,9 +50,9 @@ public function getDescription(string $resourceClass): array if (!$this->isPropertyMapped($property, $resourceClass) || !$this->isBooleanField($property, $resourceClass)) { continue; } - - $description[$property] = [ - 'property' => $property, + $propertyName = $this->normalizePropertyName($property); + $description[$propertyName] = [ + 'property' => $propertyName, 'type' => 'bool', 'required' => false, ]; @@ -65,6 +65,8 @@ abstract protected function getProperties(): ?array; abstract protected function getLogger(): LoggerInterface; + abstract protected function normalizePropertyName(string $property): string; + /** * Determines whether the given property refers to a boolean field. */ diff --git a/src/Bridge/Doctrine/Common/Filter/DateFilterTrait.php b/src/Bridge/Doctrine/Common/Filter/DateFilterTrait.php index 8b2492067a3..3b379fa0041 100644 --- a/src/Bridge/Doctrine/Common/Filter/DateFilterTrait.php +++ b/src/Bridge/Doctrine/Common/Filter/DateFilterTrait.php @@ -54,6 +54,8 @@ public function getDescription(string $resourceClass): array abstract protected function getProperties(): ?array; + abstract protected function normalizePropertyName(string $property): string; + /** * Determines whether the given property refers to a date field. */ @@ -67,9 +69,11 @@ protected function isDateField(string $property, string $resourceClass): bool */ protected function getFilterDescription(string $property, string $period): array { + $propertyName = $this->normalizePropertyName($property); + return [ - sprintf('%s[%s]', $property, $period) => [ - 'property' => $property, + sprintf('%s[%s]', $propertyName, $period) => [ + 'property' => $propertyName, 'type' => \DateTimeInterface::class, 'required' => false, ], diff --git a/src/Bridge/Doctrine/Common/Filter/ExistsFilterTrait.php b/src/Bridge/Doctrine/Common/Filter/ExistsFilterTrait.php index 8c86c0982bc..327138f8459 100644 --- a/src/Bridge/Doctrine/Common/Filter/ExistsFilterTrait.php +++ b/src/Bridge/Doctrine/Common/Filter/ExistsFilterTrait.php @@ -48,9 +48,9 @@ public function getDescription(string $resourceClass): array if (!$this->isPropertyMapped($property, $resourceClass, true) || !$this->isNullableField($property, $resourceClass)) { continue; } - - $description[sprintf('%s[%s]', $this->existsParameterName, $property)] = [ - 'property' => $property, + $propertyName = $this->normalizePropertyName($property); + $description[sprintf('%s[%s]', $this->existsParameterName, $propertyName)] = [ + 'property' => $propertyName, 'type' => 'bool', 'required' => false, ]; @@ -68,6 +68,8 @@ abstract protected function getProperties(): ?array; abstract protected function getLogger(): LoggerInterface; + abstract protected function normalizePropertyName(string $property): string; + private function normalizeValue($value, string $property): ?bool { if (\is_array($value) && isset($value[self::QUERY_PARAMETER_KEY])) { diff --git a/src/Bridge/Doctrine/Common/Filter/NumericFilterTrait.php b/src/Bridge/Doctrine/Common/Filter/NumericFilterTrait.php index bd6f5ee8525..ce41e7b76a6 100644 --- a/src/Bridge/Doctrine/Common/Filter/NumericFilterTrait.php +++ b/src/Bridge/Doctrine/Common/Filter/NumericFilterTrait.php @@ -45,11 +45,11 @@ public function getDescription(string $resourceClass): array continue; } - $filterParameterNames = [$property, $property.'[]']; - + $propertyName = $this->normalizePropertyName($property); + $filterParameterNames = [$propertyName, $propertyName.'[]']; foreach ($filterParameterNames as $filterParameterName) { $description[$filterParameterName] = [ - 'property' => $property, + 'property' => $propertyName, 'type' => $this->getType((string) $this->getDoctrineFieldType($property, $resourceClass)), 'required' => false, 'is_collection' => '[]' === substr((string) $filterParameterName, -2), @@ -69,6 +69,8 @@ abstract protected function getProperties(): ?array; abstract protected function getLogger(): LoggerInterface; + abstract protected function normalizePropertyName(string $property): string; + /** * Determines whether the given property refers to a numeric field. */ diff --git a/src/Bridge/Doctrine/Common/Filter/OrderFilterTrait.php b/src/Bridge/Doctrine/Common/Filter/OrderFilterTrait.php index 53ca563057e..6ebf9e13396 100644 --- a/src/Bridge/Doctrine/Common/Filter/OrderFilterTrait.php +++ b/src/Bridge/Doctrine/Common/Filter/OrderFilterTrait.php @@ -47,9 +47,9 @@ public function getDescription(string $resourceClass): array if (!$this->isPropertyMapped($property, $resourceClass)) { continue; } - - $description[sprintf('%s[%s]', $this->orderParameterName, $property)] = [ - 'property' => $property, + $propertyName = $this->normalizePropertyName($property); + $description[sprintf('%s[%s]', $this->orderParameterName, $propertyName)] = [ + 'property' => $propertyName, 'type' => 'string', 'required' => false, ]; @@ -60,6 +60,8 @@ public function getDescription(string $resourceClass): array abstract protected function getProperties(): ?array; + abstract protected function normalizePropertyName(string $property): string; + private function normalizeValue($value, string $property): ?string { if (empty($value) && null !== $defaultDirection = $this->getProperties()[$property]['default_direction'] ?? null) { diff --git a/src/Bridge/Doctrine/Common/Filter/RangeFilterTrait.php b/src/Bridge/Doctrine/Common/Filter/RangeFilterTrait.php index a44cbdd0d28..1f7c1010f22 100644 --- a/src/Bridge/Doctrine/Common/Filter/RangeFilterTrait.php +++ b/src/Bridge/Doctrine/Common/Filter/RangeFilterTrait.php @@ -58,14 +58,18 @@ abstract protected function getProperties(): ?array; abstract protected function getLogger(): LoggerInterface; + abstract protected function normalizePropertyName(string $property): string; + /** * Gets filter description. */ protected function getFilterDescription(string $fieldName, string $operator): array { + $propertyName = $this->normalizePropertyName($fieldName); + return [ - sprintf('%s[%s]', $fieldName, $operator) => [ - 'property' => $fieldName, + sprintf('%s[%s]', $propertyName, $operator) => [ + 'property' => $propertyName, 'type' => 'string', 'required' => false, ], diff --git a/src/Bridge/Doctrine/Common/Filter/SearchFilterTrait.php b/src/Bridge/Doctrine/Common/Filter/SearchFilterTrait.php index 42b3aba9e15..e96fa514c51 100644 --- a/src/Bridge/Doctrine/Common/Filter/SearchFilterTrait.php +++ b/src/Bridge/Doctrine/Common/Filter/SearchFilterTrait.php @@ -59,18 +59,19 @@ public function getDescription(string $resourceClass): array $metadata = $this->getClassMetadata($resourceClass); } + $propertyName = $this->normalizePropertyName($property); if ($metadata->hasField($field)) { $typeOfField = $this->getType($metadata->getTypeOfField($field)); $strategy = $this->getProperties()[$property] ?? self::STRATEGY_EXACT; - $filterParameterNames = [$property]; + $filterParameterNames = [$propertyName]; if (self::STRATEGY_EXACT === $strategy) { - $filterParameterNames[] = $property.'[]'; + $filterParameterNames[] = $propertyName.'[]'; } foreach ($filterParameterNames as $filterParameterName) { $description[$filterParameterName] = [ - 'property' => $property, + 'property' => $propertyName, 'type' => $typeOfField, 'required' => false, 'strategy' => $strategy, @@ -79,13 +80,13 @@ public function getDescription(string $resourceClass): array } } elseif ($metadata->hasAssociation($field)) { $filterParameterNames = [ - $property, - $property.'[]', + $propertyName, + $propertyName.'[]', ]; foreach ($filterParameterNames as $filterParameterName) { $description[$filterParameterName] = [ - 'property' => $property, + 'property' => $propertyName, 'type' => 'string', 'required' => false, 'strategy' => self::STRATEGY_EXACT, @@ -111,6 +112,8 @@ abstract protected function getIriConverter(): IriConverterInterface; abstract protected function getPropertyAccessor(): PropertyAccessorInterface; + abstract protected function normalizePropertyName(string $property): string; + /** * Gets the ID from an IRI or a raw ID. */ diff --git a/src/Bridge/Doctrine/MongoDbOdm/Filter/AbstractFilter.php b/src/Bridge/Doctrine/MongoDbOdm/Filter/AbstractFilter.php index a5fe637fdc7..7e75cb7c373 100644 --- a/src/Bridge/Doctrine/MongoDbOdm/Filter/AbstractFilter.php +++ b/src/Bridge/Doctrine/MongoDbOdm/Filter/AbstractFilter.php @@ -19,6 +19,7 @@ use Doctrine\ODM\MongoDB\Aggregation\Builder; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; /** * {@inheritdoc} @@ -37,12 +38,14 @@ abstract class AbstractFilter implements FilterInterface protected $managerRegistry; protected $logger; protected $properties; + protected $nameConverter; - public function __construct(ManagerRegistry $managerRegistry, LoggerInterface $logger = null, array $properties = null) + public function __construct(ManagerRegistry $managerRegistry, LoggerInterface $logger = null, array $properties = null, NameConverterInterface $nameConverter = null) { $this->managerRegistry = $managerRegistry; $this->logger = $logger ?? new NullLogger(); $this->properties = $properties; + $this->nameConverter = $nameConverter; } /** @@ -51,7 +54,7 @@ public function __construct(ManagerRegistry $managerRegistry, LoggerInterface $l public function apply(Builder $aggregationBuilder, string $resourceClass, string $operationName = null, array &$context = []) { foreach ($context['filters'] as $property => $value) { - $this->filterProperty($property, $value, $aggregationBuilder, $resourceClass, $operationName, $context); + $this->filterProperty($this->denormalizePropertyName($property), $value, $aggregationBuilder, $resourceClass, $operationName, $context); } } @@ -87,4 +90,14 @@ protected function isPropertyEnabled(string $property, string $resourceClass): b return \array_key_exists($property, $this->properties); } + + protected function denormalizePropertyName(string $property): string + { + return null !== $this->nameConverter ? $this->nameConverter->denormalize($property) : $property; + } + + protected function normalizePropertyName(string $property): string + { + return null !== $this->nameConverter ? $this->nameConverter->normalize($property) : $property; + } } diff --git a/src/Bridge/Doctrine/MongoDbOdm/Filter/ExistsFilter.php b/src/Bridge/Doctrine/MongoDbOdm/Filter/ExistsFilter.php index 73eef88c5ad..762f3837192 100644 --- a/src/Bridge/Doctrine/MongoDbOdm/Filter/ExistsFilter.php +++ b/src/Bridge/Doctrine/MongoDbOdm/Filter/ExistsFilter.php @@ -19,6 +19,7 @@ use Doctrine\ODM\MongoDB\Aggregation\Builder; use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; use Psr\Log\LoggerInterface; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; /** * Filters the collection by whether a property value exists or not. @@ -39,9 +40,9 @@ final class ExistsFilter extends AbstractFilter implements ExistsFilterInterface { use ExistsFilterTrait; - public function __construct(ManagerRegistry $managerRegistry, LoggerInterface $logger = null, array $properties = null, string $existsParameterName = self::QUERY_PARAMETER_KEY) + public function __construct(ManagerRegistry $managerRegistry, LoggerInterface $logger = null, array $properties = null, string $existsParameterName = self::QUERY_PARAMETER_KEY, NameConverterInterface $nameConverter = null) { - parent::__construct($managerRegistry, $logger, $properties); + parent::__construct($managerRegistry, $logger, $properties, $nameConverter); $this->existsParameterName = $existsParameterName; } @@ -59,7 +60,7 @@ public function apply(Builder $aggregationBuilder, string $resourceClass, string } foreach ($context['filters'][$this->existsParameterName] as $property => $value) { - $this->filterProperty($property, $value, $aggregationBuilder, $resourceClass, $operationName, $context); + $this->filterProperty($this->denormalizePropertyName($property), $value, $aggregationBuilder, $resourceClass, $operationName, $context); } } diff --git a/src/Bridge/Doctrine/MongoDbOdm/Filter/OrderFilter.php b/src/Bridge/Doctrine/MongoDbOdm/Filter/OrderFilter.php index 1d5c5b6c762..bd382e71d18 100644 --- a/src/Bridge/Doctrine/MongoDbOdm/Filter/OrderFilter.php +++ b/src/Bridge/Doctrine/MongoDbOdm/Filter/OrderFilter.php @@ -18,6 +18,7 @@ use Doctrine\Common\Persistence\ManagerRegistry; use Doctrine\ODM\MongoDB\Aggregation\Builder; use Psr\Log\LoggerInterface; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; /** * Order the collection by given properties. @@ -39,7 +40,7 @@ final class OrderFilter extends AbstractFilter implements OrderFilterInterface { use OrderFilterTrait; - public function __construct(ManagerRegistry $managerRegistry, string $orderParameterName = 'order', LoggerInterface $logger = null, array $properties = null) + public function __construct(ManagerRegistry $managerRegistry, string $orderParameterName = 'order', LoggerInterface $logger = null, array $properties = null, NameConverterInterface $nameConverter = null) { if (null !== $properties) { $properties = array_map(function ($propertyOptions) { @@ -54,7 +55,7 @@ public function __construct(ManagerRegistry $managerRegistry, string $orderParam }, $properties); } - parent::__construct($managerRegistry, $logger, $properties); + parent::__construct($managerRegistry, $logger, $properties, $nameConverter); $this->orderParameterName = $orderParameterName; } @@ -75,7 +76,7 @@ public function apply(Builder $aggregationBuilder, string $resourceClass, string } foreach ($context['filters'][$this->orderParameterName] as $property => $value) { - $this->filterProperty($property, $value, $aggregationBuilder, $resourceClass, $operationName, $context); + $this->filterProperty($this->denormalizePropertyName($property), $value, $aggregationBuilder, $resourceClass, $operationName, $context); } } diff --git a/src/Bridge/Doctrine/MongoDbOdm/Filter/SearchFilter.php b/src/Bridge/Doctrine/MongoDbOdm/Filter/SearchFilter.php index e954e3ec786..3597f4297ae 100644 --- a/src/Bridge/Doctrine/MongoDbOdm/Filter/SearchFilter.php +++ b/src/Bridge/Doctrine/MongoDbOdm/Filter/SearchFilter.php @@ -27,6 +27,7 @@ use Psr\Log\LoggerInterface; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; /** * Filter the collection by given properties. @@ -42,9 +43,9 @@ final class SearchFilter extends AbstractFilter implements SearchFilterInterface public const DOCTRINE_INTEGER_TYPE = MongoDbType::INTEGER; - public function __construct(ManagerRegistry $managerRegistry, IriConverterInterface $iriConverter, IdentifiersExtractorInterface $identifiersExtractor, PropertyAccessorInterface $propertyAccessor = null, LoggerInterface $logger = null, array $properties = null) + public function __construct(ManagerRegistry $managerRegistry, IriConverterInterface $iriConverter, IdentifiersExtractorInterface $identifiersExtractor, PropertyAccessorInterface $propertyAccessor = null, LoggerInterface $logger = null, array $properties = null, NameConverterInterface $nameConverter = null) { - parent::__construct($managerRegistry, $logger, $properties); + parent::__construct($managerRegistry, $logger, $properties, $nameConverter); $this->iriConverter = $iriConverter; $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor(); diff --git a/src/Bridge/Doctrine/Orm/Filter/AbstractContextAwareFilter.php b/src/Bridge/Doctrine/Orm/Filter/AbstractContextAwareFilter.php index a896f59cc6e..312175040cf 100644 --- a/src/Bridge/Doctrine/Orm/Filter/AbstractContextAwareFilter.php +++ b/src/Bridge/Doctrine/Orm/Filter/AbstractContextAwareFilter.php @@ -30,7 +30,7 @@ public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $q } foreach ($context['filters'] as $property => $value) { - $this->filterProperty($property, $value, $queryBuilder, $queryNameGenerator, $resourceClass, $operationName, $context); + $this->filterProperty($this->denormalizePropertyName($property), $value, $queryBuilder, $queryNameGenerator, $resourceClass, $operationName, $context); } } } diff --git a/src/Bridge/Doctrine/Orm/Filter/AbstractFilter.php b/src/Bridge/Doctrine/Orm/Filter/AbstractFilter.php index 3e3aff8f591..daf5f7a4d35 100644 --- a/src/Bridge/Doctrine/Orm/Filter/AbstractFilter.php +++ b/src/Bridge/Doctrine/Orm/Filter/AbstractFilter.php @@ -23,6 +23,7 @@ use Psr\Log\NullLogger; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; /** * {@inheritdoc} @@ -41,8 +42,9 @@ abstract class AbstractFilter implements FilterInterface protected $requestStack; protected $logger; protected $properties; + protected $nameConverter; - public function __construct(ManagerRegistry $managerRegistry, ?RequestStack $requestStack = null, LoggerInterface $logger = null, array $properties = null) + public function __construct(ManagerRegistry $managerRegistry, ?RequestStack $requestStack = null, LoggerInterface $logger = null, array $properties = null, NameConverterInterface $nameConverter = null) { if (null !== $requestStack) { @trigger_error(sprintf('Passing an instance of "%s" is deprecated since 2.2. Use "filters" context key instead.', RequestStack::class), E_USER_DEPRECATED); @@ -52,6 +54,7 @@ public function __construct(ManagerRegistry $managerRegistry, ?RequestStack $req $this->requestStack = $requestStack; $this->logger = $logger ?? new NullLogger(); $this->properties = $properties; + $this->nameConverter = $nameConverter; } /** @@ -138,4 +141,14 @@ protected function extractProperties(Request $request/*, string $resourceClass*/ return $request->query->all(); } + + protected function denormalizePropertyName(string $property): string + { + return null !== $this->nameConverter ? $this->nameConverter->denormalize($property) : $property; + } + + protected function normalizePropertyName(string $property): string + { + return null !== $this->nameConverter ? $this->nameConverter->normalize($property) : $property; + } } diff --git a/src/Bridge/Doctrine/Orm/Filter/ExistsFilter.php b/src/Bridge/Doctrine/Orm/Filter/ExistsFilter.php index 871a4704489..4dd9f8b7d1f 100644 --- a/src/Bridge/Doctrine/Orm/Filter/ExistsFilter.php +++ b/src/Bridge/Doctrine/Orm/Filter/ExistsFilter.php @@ -24,6 +24,7 @@ use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; /** * Filters the collection by whether a property value exists or not. @@ -41,9 +42,9 @@ class ExistsFilter extends AbstractContextAwareFilter implements ExistsFilterInt { use ExistsFilterTrait; - public function __construct(ManagerRegistry $managerRegistry, ?RequestStack $requestStack = null, LoggerInterface $logger = null, array $properties = null, string $existsParameterName = self::QUERY_PARAMETER_KEY) + public function __construct(ManagerRegistry $managerRegistry, ?RequestStack $requestStack = null, LoggerInterface $logger = null, array $properties = null, string $existsParameterName = self::QUERY_PARAMETER_KEY, NameConverterInterface $nameConverter = null) { - parent::__construct($managerRegistry, $requestStack, $logger, $properties); + parent::__construct($managerRegistry, $requestStack, $logger, $properties, $nameConverter); $this->existsParameterName = $existsParameterName; } @@ -61,7 +62,7 @@ public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $q } foreach ($context['filters'][$this->existsParameterName] as $property => $value) { - $this->filterProperty($property, $value, $queryBuilder, $queryNameGenerator, $resourceClass, $operationName, $context); + $this->filterProperty($this->denormalizePropertyName($property), $value, $queryBuilder, $queryNameGenerator, $resourceClass, $operationName, $context); } } diff --git a/src/Bridge/Doctrine/Orm/Filter/OrderFilter.php b/src/Bridge/Doctrine/Orm/Filter/OrderFilter.php index d47895f1bb2..f019c562676 100644 --- a/src/Bridge/Doctrine/Orm/Filter/OrderFilter.php +++ b/src/Bridge/Doctrine/Orm/Filter/OrderFilter.php @@ -22,6 +22,7 @@ use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; /** * Order the collection by given properties. @@ -40,7 +41,7 @@ class OrderFilter extends AbstractContextAwareFilter implements OrderFilterInter { use OrderFilterTrait; - public function __construct(ManagerRegistry $managerRegistry, ?RequestStack $requestStack = null, string $orderParameterName = 'order', LoggerInterface $logger = null, array $properties = null) + public function __construct(ManagerRegistry $managerRegistry, ?RequestStack $requestStack = null, string $orderParameterName = 'order', LoggerInterface $logger = null, array $properties = null, NameConverterInterface $nameConverter = null) { if (null !== $properties) { $properties = array_map(function ($propertyOptions) { @@ -55,7 +56,7 @@ public function __construct(ManagerRegistry $managerRegistry, ?RequestStack $req }, $properties); } - parent::__construct($managerRegistry, $requestStack, $logger, $properties); + parent::__construct($managerRegistry, $requestStack, $logger, $properties, $nameConverter); $this->orderParameterName = $orderParameterName; } @@ -76,7 +77,7 @@ public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $q } foreach ($context['filters'][$this->orderParameterName] as $property => $value) { - $this->filterProperty($property, $value, $queryBuilder, $queryNameGenerator, $resourceClass, $operationName, $context); + $this->filterProperty($this->denormalizePropertyName($property), $value, $queryBuilder, $queryNameGenerator, $resourceClass, $operationName, $context); } } diff --git a/src/Bridge/Doctrine/Orm/Filter/SearchFilter.php b/src/Bridge/Doctrine/Orm/Filter/SearchFilter.php index c3f24e5cc41..df5f6aeb773 100644 --- a/src/Bridge/Doctrine/Orm/Filter/SearchFilter.php +++ b/src/Bridge/Doctrine/Orm/Filter/SearchFilter.php @@ -28,6 +28,7 @@ use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; /** * Filter the collection by given properties. @@ -40,9 +41,9 @@ class SearchFilter extends AbstractContextAwareFilter implements SearchFilterInt public const DOCTRINE_INTEGER_TYPE = DBALType::INTEGER; - public function __construct(ManagerRegistry $managerRegistry, ?RequestStack $requestStack, IriConverterInterface $iriConverter, PropertyAccessorInterface $propertyAccessor = null, LoggerInterface $logger = null, array $properties = null, IdentifiersExtractorInterface $identifiersExtractor = null) + public function __construct(ManagerRegistry $managerRegistry, ?RequestStack $requestStack, IriConverterInterface $iriConverter, PropertyAccessorInterface $propertyAccessor = null, LoggerInterface $logger = null, array $properties = null, IdentifiersExtractorInterface $identifiersExtractor = null, NameConverterInterface $nameConverter = null) { - parent::__construct($managerRegistry, $requestStack, $logger, $properties); + parent::__construct($managerRegistry, $requestStack, $logger, $properties, $nameConverter); if (null === $identifiersExtractor) { @trigger_error('Not injecting ItemIdentifiersExtractor is deprecated since API Platform 2.5 and can lead to unexpected behaviors, it will not be possible anymore in API Platform 3.0.', E_USER_DEPRECATED); diff --git a/src/Bridge/Symfony/Bundle/Resources/config/api.xml b/src/Bridge/Symfony/Bundle/Resources/config/api.xml index 33b5b84e188..f7537107b66 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/api.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/api.xml @@ -83,7 +83,12 @@ - + + properties + false + null + + diff --git a/src/Bridge/Symfony/Bundle/Resources/config/doctrine_mongodb_odm.xml b/src/Bridge/Symfony/Bundle/Resources/config/doctrine_mongodb_odm.xml index f95ffc1964f..fa300e4a700 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/doctrine_mongodb_odm.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/doctrine_mongodb_odm.xml @@ -67,6 +67,7 @@ + @@ -74,12 +75,14 @@ + + @@ -87,12 +90,14 @@ %api_platform.collection.exists_parameter_name% + + @@ -100,12 +105,14 @@ %api_platform.collection.order_parameter_name% + + diff --git a/src/Bridge/Symfony/Bundle/Resources/config/doctrine_orm.xml b/src/Bridge/Symfony/Bundle/Resources/config/doctrine_orm.xml index 51f7dd53978..ad6301b2bdf 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/doctrine_orm.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/doctrine_orm.xml @@ -56,6 +56,7 @@ + @@ -64,6 +65,7 @@ null %api_platform.collection.order_parameter_name% + @@ -71,6 +73,7 @@ null + @@ -78,6 +81,7 @@ null + @@ -85,6 +89,7 @@ null + @@ -92,6 +97,7 @@ null + @@ -100,6 +106,7 @@ null %api_platform.collection.exists_parameter_name% + diff --git a/src/Serializer/Filter/PropertyFilter.php b/src/Serializer/Filter/PropertyFilter.php index c60464e1c62..43e527c8db2 100644 --- a/src/Serializer/Filter/PropertyFilter.php +++ b/src/Serializer/Filter/PropertyFilter.php @@ -14,6 +14,7 @@ namespace ApiPlatform\Core\Serializer\Filter; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; /** @@ -26,12 +27,14 @@ final class PropertyFilter implements FilterInterface private $overrideDefaultProperties; private $parameterName; private $whitelist; + private $nameConverter; - public function __construct(string $parameterName = 'properties', bool $overrideDefaultProperties = false, array $whitelist = null) + public function __construct(string $parameterName = 'properties', bool $overrideDefaultProperties = false, array $whitelist = null, NameConverterInterface $nameConverter = null) { $this->overrideDefaultProperties = $overrideDefaultProperties; $this->parameterName = $parameterName; $this->whitelist = null === $whitelist ? null : $this->formatWhitelist($whitelist); + $this->nameConverter = $nameConverter; } /** @@ -51,6 +54,8 @@ public function apply(Request $request, bool $normalization, array $attributes, return; } + $properties = $this->denormalizeProperties($properties); + if (null !== $this->whitelist) { $properties = $this->getProperties($properties, $this->whitelist); } @@ -129,18 +134,42 @@ private function getProperties(array $properties, array $whitelist = null): arra foreach ($properties as $key => $value) { if (is_numeric($key)) { - if (\in_array($value, $whitelist, true)) { - $result[] = $value; + if (\in_array($propertyName = $this->denormalizePropertyName($value), $whitelist, true)) { + $result[] = $propertyName; } continue; } if (\is_array($value) && isset($whitelist[$key]) && $recursiveResult = $this->getProperties($value, $whitelist[$key])) { - $result[$key] = $recursiveResult; + $result[$this->denormalizePropertyName($key)] = $recursiveResult; + } + } + + return $result; + } + + private function denormalizeProperties(array $properties): array + { + if (null === $this->nameConverter || !$properties) { + return $properties; + } + + $result = []; + foreach ($properties as $key => $value) { + if (is_numeric($key)) { + $result[] = $this->denormalizePropertyName($value); + continue; } + + $result[$this->denormalizePropertyName($key)] = \is_array($value) ? $this->denormalizeProperties($value) : $value; } return $result; } + + private function denormalizePropertyName(string $property): string + { + return null !== $this->nameConverter ? $this->nameConverter->denormalize($property) : $property; + } } diff --git a/tests/Bridge/Doctrine/Common/Filter/SearchFilterTestTrait.php b/tests/Bridge/Doctrine/Common/Filter/SearchFilterTestTrait.php index b38f528ad16..1cc6e81420e 100644 --- a/tests/Bridge/Doctrine/Common/Filter/SearchFilterTestTrait.php +++ b/tests/Bridge/Doctrine/Common/Filter/SearchFilterTestTrait.php @@ -134,15 +134,15 @@ public function testGetDescription() 'strategy' => 'exact', 'is_collection' => true, ], - 'nameConverted' => [ - 'property' => 'nameConverted', + 'name_converted' => [ + 'property' => 'name_converted', 'type' => 'string', 'required' => false, 'strategy' => 'exact', 'is_collection' => false, ], - 'nameConverted[]' => [ - 'property' => 'nameConverted', + 'name_converted[]' => [ + 'property' => 'name_converted', 'type' => 'string', 'required' => false, 'strategy' => 'exact', diff --git a/tests/Bridge/Doctrine/MongoDbOdm/Filter/OrderFilterTest.php b/tests/Bridge/Doctrine/MongoDbOdm/Filter/OrderFilterTest.php index d11e1fc1f7c..f874fa4f7c2 100644 --- a/tests/Bridge/Doctrine/MongoDbOdm/Filter/OrderFilterTest.php +++ b/tests/Bridge/Doctrine/MongoDbOdm/Filter/OrderFilterTest.php @@ -16,6 +16,7 @@ use ApiPlatform\Core\Bridge\Doctrine\MongoDbOdm\Filter\OrderFilter; use ApiPlatform\Core\Test\DoctrineMongoDbOdmFilterTestCase; use ApiPlatform\Core\Tests\Bridge\Doctrine\Common\Filter\OrderFilterTestTrait; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Serializer\NameConverter\CustomConverter; use Doctrine\Common\Persistence\ManagerRegistry; /** @@ -82,8 +83,8 @@ public function testGetDescriptionDefaultFields() 'type' => 'string', 'required' => false, ], - 'order[nameConverted]' => [ - 'property' => 'nameConverted', + 'order[name_converted]' => [ + 'property' => 'name_converted', 'type' => 'string', 'required' => false, ], @@ -352,6 +353,6 @@ public function provideApplyTestData(): array protected function buildFilter(?array $properties = null) { - return new $this->filterClass($this->managerRegistry, 'order', null, $properties); + return new $this->filterClass($this->managerRegistry, 'order', null, $properties, new CustomConverter()); } } diff --git a/tests/Bridge/Doctrine/MongoDbOdm/Filter/SearchFilterTest.php b/tests/Bridge/Doctrine/MongoDbOdm/Filter/SearchFilterTest.php index 0124673dbf2..3b10e839f88 100644 --- a/tests/Bridge/Doctrine/MongoDbOdm/Filter/SearchFilterTest.php +++ b/tests/Bridge/Doctrine/MongoDbOdm/Filter/SearchFilterTest.php @@ -20,6 +20,7 @@ use ApiPlatform\Core\Test\DoctrineMongoDbOdmFilterTestCase; use ApiPlatform\Core\Tests\Bridge\Doctrine\Common\Filter\SearchFilterTestTrait; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\RelatedDummy; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Serializer\NameConverter\CustomConverter; use Doctrine\Common\Persistence\ManagerRegistry; use MongoDB\BSON\Regex; use Prophecy\Argument; @@ -178,15 +179,15 @@ public function testGetDescriptionDefaultFields() 'strategy' => 'exact', 'is_collection' => true, ], - 'nameConverted' => [ - 'property' => 'nameConverted', + 'name_converted' => [ + 'property' => 'name_converted', 'type' => 'string', 'required' => false, 'strategy' => 'exact', 'is_collection' => false, ], - 'nameConverted[]' => [ - 'property' => 'nameConverted', + 'name_converted[]' => [ + 'property' => 'name_converted', 'type' => 'string', 'required' => false, 'strategy' => 'exact', @@ -572,6 +573,6 @@ protected function buildSearchFilter(ManagerRegistry $managerRegistry, ?array $p $identifierExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class); $identifierExtractorProphecy->getIdentifiersFromResourceClass(Argument::type('string'))->willReturn(['id']); - return new SearchFilter($managerRegistry, $iriConverter, $identifierExtractorProphecy->reveal(), $propertyAccessor, null, $properties); + return new SearchFilter($managerRegistry, $iriConverter, $identifierExtractorProphecy->reveal(), $propertyAccessor, null, $properties, new CustomConverter()); } } diff --git a/tests/Bridge/Doctrine/Orm/Filter/ExistsFilterTest.php b/tests/Bridge/Doctrine/Orm/Filter/ExistsFilterTest.php index ac8ff833dfa..172544577cd 100644 --- a/tests/Bridge/Doctrine/Orm/Filter/ExistsFilterTest.php +++ b/tests/Bridge/Doctrine/Orm/Filter/ExistsFilterTest.php @@ -17,6 +17,7 @@ use ApiPlatform\Core\Test\DoctrineOrmFilterTestCase; use ApiPlatform\Core\Tests\Bridge\Doctrine\Common\Filter\ExistsFilterTestTrait; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Serializer\NameConverter\CustomConverter; use Doctrine\Common\Persistence\ManagerRegistry; use Symfony\Component\HttpFoundation\RequestStack; @@ -79,8 +80,8 @@ public function testGetDescriptionDefaultFields() 'type' => 'bool', 'required' => false, ], - 'exists[nameConverted]' => [ - 'property' => 'nameConverted', + 'exists[name_converted]' => [ + 'property' => 'name_converted', 'type' => 'bool', 'required' => false, ], @@ -273,7 +274,7 @@ public function testLegacyRequest() sprintf('SELECT o FROM %s o WHERE o.description IS NOT NULL', Dummy::class), null, function (ManagerRegistry $managerRegistry, array $properties = null, RequestStack $requestStack = null): ExistsFilter { - return new ExistsFilter($managerRegistry, $requestStack, null, $properties, 'exists'); + return new ExistsFilter($managerRegistry, $requestStack, null, $properties, 'exists', new CustomConverter()); }, ]; @@ -282,6 +283,6 @@ function (ManagerRegistry $managerRegistry, array $properties = null, RequestSta protected function buildFilter(?array $properties = null) { - return new $this->filterClass($this->managerRegistry, null, null, $properties, 'exists'); + return new $this->filterClass($this->managerRegistry, null, null, $properties, 'exists', new CustomConverter()); } } diff --git a/tests/Bridge/Doctrine/Orm/Filter/OrderFilterTest.php b/tests/Bridge/Doctrine/Orm/Filter/OrderFilterTest.php index f44b558a13b..0d44a18fc09 100644 --- a/tests/Bridge/Doctrine/Orm/Filter/OrderFilterTest.php +++ b/tests/Bridge/Doctrine/Orm/Filter/OrderFilterTest.php @@ -17,6 +17,7 @@ use ApiPlatform\Core\Test\DoctrineOrmFilterTestCase; use ApiPlatform\Core\Tests\Bridge\Doctrine\Common\Filter\OrderFilterTestTrait; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Serializer\NameConverter\CustomConverter; use Doctrine\Common\Persistence\ManagerRegistry; use Symfony\Component\HttpFoundation\RequestStack; @@ -85,8 +86,8 @@ public function testGetDescriptionDefaultFields() 'type' => 'string', 'required' => false, ], - 'order[nameConverted]' => [ - 'property' => 'nameConverted', + 'order[name_converted]' => [ + 'property' => 'name_converted', 'type' => 'string', 'required' => false, ], @@ -196,6 +197,6 @@ public function provideApplyTestData(): array protected function buildFilter(?array $properties = null) { - return new $this->filterClass($this->managerRegistry, null, 'order', null, $properties); + return new $this->filterClass($this->managerRegistry, null, 'order', null, $properties, new CustomConverter()); } } diff --git a/tests/Bridge/Doctrine/Orm/Filter/SearchFilterTest.php b/tests/Bridge/Doctrine/Orm/Filter/SearchFilterTest.php index c98b885be46..5b5e6f418e5 100644 --- a/tests/Bridge/Doctrine/Orm/Filter/SearchFilterTest.php +++ b/tests/Bridge/Doctrine/Orm/Filter/SearchFilterTest.php @@ -22,6 +22,7 @@ use ApiPlatform\Core\Tests\Bridge\Doctrine\Common\Filter\SearchFilterTestTrait; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\RelatedDummy; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Serializer\NameConverter\CustomConverter; use Doctrine\Common\Persistence\ManagerRegistry; use Prophecy\Argument; use Symfony\Component\HttpFoundation\Request; @@ -183,15 +184,15 @@ public function testGetDescriptionDefaultFields() 'strategy' => 'exact', 'is_collection' => true, ], - 'nameConverted' => [ - 'property' => 'nameConverted', + 'name_converted' => [ + 'property' => 'name_converted', 'type' => 'string', 'required' => false, 'strategy' => 'exact', 'is_collection' => false, ], - 'nameConverted[]' => [ - 'property' => 'nameConverted', + 'name_converted[]' => [ + 'property' => 'name_converted', 'type' => 'string', 'required' => false, 'strategy' => 'exact', @@ -515,6 +516,6 @@ protected function buildSearchFilter(ManagerRegistry $managerRegistry, ?array $p $identifierExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class); $identifierExtractorProphecy->getIdentifiersFromResourceClass(Argument::type('string'))->willReturn(['id']); - return new SearchFilter($managerRegistry, $requestStack, $iriConverter, $propertyAccessor, null, $properties, $identifierExtractorProphecy->reveal()); + return new SearchFilter($managerRegistry, $requestStack, $iriConverter, $propertyAccessor, null, $properties, $identifierExtractorProphecy->reveal(), new CustomConverter()); } } diff --git a/tests/Fixtures/TestBundle/Document/ConvertedBoolean.php b/tests/Fixtures/TestBundle/Document/ConvertedBoolean.php new file mode 100644 index 00000000000..de7f702a873 --- /dev/null +++ b/tests/Fixtures/TestBundle/Document/ConvertedBoolean.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\Document; + +use ApiPlatform\Core\Annotation\ApiFilter; +use ApiPlatform\Core\Annotation\ApiResource; +use ApiPlatform\Core\Bridge\Doctrine\MongoDbOdm\Filter\BooleanFilter; +use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; + +/** + * @ApiResource + * @ODM\Document + * @ApiFilter(BooleanFilter::class) + */ +class ConvertedBoolean +{ + /** + * @var int + * + * @ODM\Id(strategy="INCREMENT", type="integer") + */ + private $id; + + /** + * @var bool + * + * @ODM\Field(type="boolean") + */ + public $nameConverted; + + public function getId() + { + return $this->id; + } +} diff --git a/tests/Fixtures/TestBundle/Document/ConvertedDate.php b/tests/Fixtures/TestBundle/Document/ConvertedDate.php new file mode 100644 index 00000000000..f16d2813f70 --- /dev/null +++ b/tests/Fixtures/TestBundle/Document/ConvertedDate.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\Document; + +use ApiPlatform\Core\Annotation\ApiFilter; +use ApiPlatform\Core\Annotation\ApiResource; +use ApiPlatform\Core\Bridge\Doctrine\MongoDbOdm\Filter\DateFilter; +use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; + +/** + * @ApiResource + * @ODM\Document + * @ApiFilter(DateFilter::class) + */ +class ConvertedDate +{ + /** + * @var int + * + * @ODM\Id(strategy="INCREMENT", type="integer") + */ + private $id; + + /** + * @var \DateTime + * + * @ODM\Field(type="date") + */ + public $nameConverted; + + public function getId() + { + return $this->id; + } +} diff --git a/tests/Fixtures/TestBundle/Document/ConvertedInteger.php b/tests/Fixtures/TestBundle/Document/ConvertedInteger.php new file mode 100644 index 00000000000..9fd02e2529e --- /dev/null +++ b/tests/Fixtures/TestBundle/Document/ConvertedInteger.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\ApiFilter; +use ApiPlatform\Core\Annotation\ApiResource; +use ApiPlatform\Core\Bridge\Doctrine\MongoDbOdm\Filter\NumericFilter; +use ApiPlatform\Core\Bridge\Doctrine\MongoDbOdm\Filter\OrderFilter; +use ApiPlatform\Core\Bridge\Doctrine\MongoDbOdm\Filter\RangeFilter; +use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; + +/** + * @ApiResource + * @ODM\Document + * @ApiFilter(NumericFilter::class, properties={"nameConverted"}) + * @ApiFilter(RangeFilter::class, properties={"nameConverted"}) + * @ApiFilter(OrderFilter::class, properties={"nameConverted"}) + */ +class ConvertedInteger +{ + /** + * @var int + * + * @ODM\Id(strategy="INCREMENT", type="integer") + */ + private $id; + + /** + * @var int + * + * @ODM\Field(type="integer") + */ + public $nameConverted; + + public function getId() + { + return $this->id; + } +} diff --git a/tests/Fixtures/TestBundle/Document/ConvertedString.php b/tests/Fixtures/TestBundle/Document/ConvertedString.php new file mode 100644 index 00000000000..057e35d0234 --- /dev/null +++ b/tests/Fixtures/TestBundle/Document/ConvertedString.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\Document; + +use ApiPlatform\Core\Annotation\ApiFilter; +use ApiPlatform\Core\Annotation\ApiResource; +use ApiPlatform\Core\Bridge\Doctrine\MongoDbOdm\Filter\ExistsFilter; +use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; + +/** + * @ApiResource + * @ODM\Document + * @ApiFilter(ExistsFilter::class, properties={"nameConverted"}) + */ +class ConvertedString +{ + /** + * @var int + * + * @ODM\Id(strategy="INCREMENT", type="integer") + */ + private $id; + + /** + * @var string|null + * + * @ODM\Field(type="string", nullable=true) + */ + public $nameConverted; + + public function getId() + { + return $this->id; + } +} diff --git a/tests/Fixtures/TestBundle/Document/DummyProperty.php b/tests/Fixtures/TestBundle/Document/DummyProperty.php index e0dda8378d9..a13540fb41c 100644 --- a/tests/Fixtures/TestBundle/Document/DummyProperty.php +++ b/tests/Fixtures/TestBundle/Document/DummyProperty.php @@ -99,6 +99,15 @@ class DummyProperty */ public $groups; + /** + * @var string + * + * @ODM\Field(nullable=true) + * + * @Groups({"dummy_read", "dummy_write"}) + */ + public $nameConverted; + /** * @return int */ diff --git a/tests/Fixtures/TestBundle/Entity/ConvertedBoolean.php b/tests/Fixtures/TestBundle/Entity/ConvertedBoolean.php new file mode 100644 index 00000000000..52211e28852 --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/ConvertedBoolean.php @@ -0,0 +1,48 @@ + + * + * 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\ApiFilter; +use ApiPlatform\Core\Annotation\ApiResource; +use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\BooleanFilter; +use Doctrine\ORM\Mapping as ORM; + +/** + * @ApiResource + * @ORM\Entity + * @ApiFilter(BooleanFilter::class) + */ +class ConvertedBoolean +{ + /** + * @var int + * + * @ORM\Column(type="integer", nullable=true) + * @ORM\Id + * @ORM\GeneratedValue(strategy="AUTO") + */ + private $id; + + /** + * @var bool + * + * @ORM\Column(type="boolean") + */ + public $nameConverted; + + public function getId() + { + return $this->id; + } +} diff --git a/tests/Fixtures/TestBundle/Entity/ConvertedDate.php b/tests/Fixtures/TestBundle/Entity/ConvertedDate.php new file mode 100644 index 00000000000..e08357f5159 --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/ConvertedDate.php @@ -0,0 +1,48 @@ + + * + * 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\ApiFilter; +use ApiPlatform\Core\Annotation\ApiResource; +use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\DateFilter; +use Doctrine\ORM\Mapping as ORM; + +/** + * @ApiResource + * @ORM\Entity + * @ApiFilter(DateFilter::class) + */ +class ConvertedDate +{ + /** + * @var int + * + * @ORM\Column(type="integer", nullable=true) + * @ORM\Id + * @ORM\GeneratedValue(strategy="AUTO") + */ + private $id; + + /** + * @var \DateTime + * + * @ORM\Column(type="date") + */ + public $nameConverted; + + public function getId() + { + return $this->id; + } +} diff --git a/tests/Fixtures/TestBundle/Entity/ConvertedInteger.php b/tests/Fixtures/TestBundle/Entity/ConvertedInteger.php new file mode 100644 index 00000000000..8db6f254b75 --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/ConvertedInteger.php @@ -0,0 +1,52 @@ + + * + * 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\ApiFilter; +use ApiPlatform\Core\Annotation\ApiResource; +use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\NumericFilter; +use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\OrderFilter; +use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\RangeFilter; +use Doctrine\ORM\Mapping as ORM; + +/** + * @ApiResource + * @ORM\Entity + * @ApiFilter(NumericFilter::class, properties={"nameConverted"}) + * @ApiFilter(RangeFilter::class, properties={"nameConverted"}) + * @ApiFilter(OrderFilter::class, properties={"nameConverted"}) + */ +class ConvertedInteger +{ + /** + * @var int + * + * @ORM\Column(type="integer", nullable=true) + * @ORM\Id + * @ORM\GeneratedValue(strategy="AUTO") + */ + private $id; + + /** + * @var int + * + * @ORM\Column(type="integer") + */ + public $nameConverted; + + public function getId() + { + return $this->id; + } +} diff --git a/tests/Fixtures/TestBundle/Entity/ConvertedString.php b/tests/Fixtures/TestBundle/Entity/ConvertedString.php new file mode 100644 index 00000000000..cc50a3a393c --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/ConvertedString.php @@ -0,0 +1,48 @@ + + * + * 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\ApiFilter; +use ApiPlatform\Core\Annotation\ApiResource; +use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\ExistsFilter; +use Doctrine\ORM\Mapping as ORM; + +/** + * @ApiResource + * @ORM\Entity + * @ApiFilter(ExistsFilter::class, properties={"nameConverted"}) + */ +class ConvertedString +{ + /** + * @var int + * + * @ORM\Column(type="integer", nullable=true) + * @ORM\Id + * @ORM\GeneratedValue(strategy="AUTO") + */ + private $id; + + /** + * @var string|null + * + * @ORM\Column(type="string", nullable=true) + */ + public $nameConverted; + + public function getId() + { + return $this->id; + } +} diff --git a/tests/Fixtures/TestBundle/Entity/DummyProperty.php b/tests/Fixtures/TestBundle/Entity/DummyProperty.php index b8a11a6e957..4d9e85c9bb5 100644 --- a/tests/Fixtures/TestBundle/Entity/DummyProperty.php +++ b/tests/Fixtures/TestBundle/Entity/DummyProperty.php @@ -101,6 +101,15 @@ class DummyProperty */ public $groups; + /** + * @var string + * + * @ORM\Column(nullable=true) + * + * @Groups({"dummy_read"}) + */ + public $nameConverted; + /** * @return int */ diff --git a/tests/Fixtures/app/config/config_common.yml b/tests/Fixtures/app/config/config_common.yml index 09931dfd7af..bf0e7199c33 100644 --- a/tests/Fixtures/app/config/config_common.yml +++ b/tests/Fixtures/app/config/config_common.yml @@ -151,7 +151,7 @@ services: app.entity.filter.dummy_property.whitelist_property: parent: 'api_platform.serializer.property_filter' - arguments: [ 'whitelisted_properties', false, [foo] ] + arguments: [ 'whitelisted_properties', false, [foo, nameConverted] ] tags: [ { name: 'api_platform.filter', id: 'dummy_property.whitelist_property' } ] app.entity.filter.dummy_property.whitelist_nested_property: diff --git a/tests/Fixtures/app/config/config_orm.yml b/tests/Fixtures/app/config/config_orm.yml index bdf30443a30..a98632f722e 100644 --- a/tests/Fixtures/app/config/config_orm.yml +++ b/tests/Fixtures/app/config/config_orm.yml @@ -4,7 +4,7 @@ imports: services: app.my_dummy_resource.search_filter: parent: 'api_platform.doctrine.orm.search_filter' - arguments: [ { 'id': 'exact', 'name': 'partial', 'alias': 'start', 'description': 'word_start', 'relatedDummy.name': 'exact', 'relatedDummies': 'exact', 'dummy': 'ipartial', 'relatedDummies.name': 'start', 'embeddedDummy.dummyName': 'partial', 'relatedDummy.thirdLevel.level': 'exact', 'relatedDummy.thirdLevel.fourthLevel.level': 'exact', 'relatedDummy.thirdLevel.badFourthLevel.level': 'exact', 'relatedDummy.thirdLevel.fourthLevel.badThirdLevel.level': 'exact' } ] + arguments: [ { 'id': 'exact', 'name': 'partial', 'alias': 'start', 'description': 'word_start', 'relatedDummy.name': 'exact', 'relatedDummies': 'exact', 'dummy': 'ipartial', 'relatedDummies.name': 'start', 'embeddedDummy.dummyName': 'partial', 'relatedDummy.thirdLevel.level': 'exact', 'relatedDummy.thirdLevel.fourthLevel.level': 'exact', 'relatedDummy.thirdLevel.badFourthLevel.level': 'exact', 'relatedDummy.thirdLevel.fourthLevel.badThirdLevel.level': 'exact', 'nameConverted': 'partial' } ] tags: [ { name: 'api_platform.filter', id: 'my_dummy.search' } ] # Tests if the id default to the service name, do not add id attributes here diff --git a/tests/Fixtures/app/config/config_services_mongodb.yml b/tests/Fixtures/app/config/config_services_mongodb.yml index 0bfe3e46953..af4098081d9 100644 --- a/tests/Fixtures/app/config/config_services_mongodb.yml +++ b/tests/Fixtures/app/config/config_services_mongodb.yml @@ -28,7 +28,7 @@ services: tags: [ { name: 'api_platform.filter', id: 'my_dummy.mongodb.range' } ] app.my_dummy_resource.mongodb.search_filter: parent: 'api_platform.doctrine_mongodb.odm.search_filter' - arguments: [ { 'id': 'exact', 'name': 'partial', 'alias': 'start', 'description': 'word_start', 'relatedDummy.name': 'exact', 'relatedDummies': 'exact', 'dummy': 'ipartial', 'relatedDummies.name': 'start', 'embeddedDummy.dummyName': 'partial', 'relatedDummy.thirdLevel.level': 'exact', 'relatedDummy.thirdLevel.fourthLevel.level': 'exact', 'relatedDummy.thirdLevel.badFourthLevel.level': 'exact', 'relatedDummy.thirdLevel.fourthLevel.badThirdLevel.level': 'exact' } ] + arguments: [ { 'id': 'exact', 'name': 'partial', 'alias': 'start', 'description': 'word_start', 'relatedDummy.name': 'exact', 'relatedDummies': 'exact', 'dummy': 'ipartial', 'relatedDummies.name': 'start', 'embeddedDummy.dummyName': 'partial', 'relatedDummy.thirdLevel.level': 'exact', 'relatedDummy.thirdLevel.fourthLevel.level': 'exact', 'relatedDummy.thirdLevel.badFourthLevel.level': 'exact', 'relatedDummy.thirdLevel.fourthLevel.badThirdLevel.level': 'exact', 'nameConverted': 'partial' } ] tags: [ { name: 'api_platform.filter', id: 'my_dummy.mongodb.search' } ] app.related_dummy_resource.mongodb.search_filter: parent: 'api_platform.doctrine_mongodb.odm.search_filter' diff --git a/tests/Serializer/Filter/PropertyFilterTest.php b/tests/Serializer/Filter/PropertyFilterTest.php index 2ee36d3fc59..81ecc75eb5d 100644 --- a/tests/Serializer/Filter/PropertyFilterTest.php +++ b/tests/Serializer/Filter/PropertyFilterTest.php @@ -15,6 +15,7 @@ use ApiPlatform\Core\Serializer\Filter\PropertyFilter; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyProperty; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Serializer\NameConverter\CustomConverter; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; @@ -66,7 +67,7 @@ public function testApplyWithPropertiesWhitelist() $this->assertEquals(['attributes' => ['qux', 'foo', 'bar']], $context); } - public function testApplyWithPropertiesWhitelistWithNestedProperty() + public function testApplyWithPropertiesWhitelistAndNestedProperty() { $request = new Request(['properties' => ['foo', 'bar', 'group' => ['baz' => ['baz', 'qux'], 'qux']]]); $context = ['attributes' => ['qux']]; @@ -88,7 +89,7 @@ public function testApplyWithPropertiesWhitelistNotMatchingAnyProperty() $this->assertEquals(['attributes' => ['qux']], $context); } - public function testApplyWithoutPropertiesWhitelistWithOverriding() + public function testApplyWithPropertiesWhitelistAndOverriding() { $request = new Request(['properties' => ['foo', 'bar', 'baz']]); $context = ['attributes' => ['qux']]; @@ -121,6 +122,93 @@ public function testApplyWithInvalidPropertiesInRequest() $this->assertEquals(['attributes' => ['foo', 'bar']], $context); } + public function testApplyWithNameConverter() + { + $request = new Request(['properties' => ['foo', 'bar', 'name_converted']]); + $context = ['attributes' => ['foo', 'name_converted']]; + + $propertyFilter = new PropertyFilter('properties', false, null, new CustomConverter()); + $propertyFilter->apply($request, true, [], $context); + + $this->assertEquals(['attributes' => ['foo', 'name_converted', 'foo', 'bar', 'nameConverted']], $context); + } + + public function testApplyWithOverridingAndNameConverter() + { + $request = new Request(['custom_properties' => ['foo', 'bar', 'name_converted']]); + $context = ['attributes' => ['foo', 'qux']]; + + $propertyFilter = new PropertyFilter('custom_properties', true, null, new CustomConverter()); + $propertyFilter->apply($request, false, [], $context); + + $this->assertEquals(['attributes' => ['foo', 'bar', 'nameConverted']], $context); + } + + public function testApplyWithoutPropertiesInRequestAndNameConverter() + { + $context = ['attributes' => ['foo', 'name_converted']]; + + $propertyFilter = new PropertyFilter('properties', false, null, new CustomConverter()); + $propertyFilter->apply(new Request(), false, [], $context); + + $this->assertEquals(['attributes' => ['foo', 'name_converted']], $context); + } + + public function testApplyWithPropertiesWhitelistAndNameConverter() + { + $request = new Request(['properties' => ['foo', 'name_converted', 'baz']]); + $context = ['attributes' => ['qux']]; + + $propertyFilter = new PropertyFilter('properties', false, ['nameConverted', 'fuz', 'foo'], new CustomConverter()); + $propertyFilter->apply($request, true, [], $context); + + $this->assertEquals(['attributes' => ['qux', 'foo', 'nameConverted']], $context); + } + + public function testApplyWithPropertiesWhitelistWithNestedPropertyAndNameConverter() + { + $request = new Request(['properties' => ['foo', 'bar', 'name_converted' => ['baz' => ['baz', 'name_converted'], 'qux']]]); + $context = ['attributes' => ['qux']]; + + $propertyFilter = new PropertyFilter('properties', false, ['foo' => null, 'nameConverted' => ['baz' => ['nameConverted']]], new CustomConverter()); + $propertyFilter->apply($request, true, [], $context); + + $this->assertEquals(['attributes' => ['qux', 'foo', 'nameConverted' => ['baz' => ['nameConverted']]]], $context); + } + + public function testApplyWithPropertiesWhitelistNotMatchingAnyPropertyAndNameConverter() + { + $request = new Request(['properties' => ['foo', 'bar', 'name_converted' => ['baz' => ['baz', 'name_converted'], 'qux']]]); + $context = ['attributes' => ['qux']]; + + $propertyFilter = new PropertyFilter('properties', false, ['fuz', 'fiz'], new CustomConverter()); + $propertyFilter->apply($request, true, [], $context); + + $this->assertEquals(['attributes' => ['qux']], $context); + } + + public function testApplyWithPropertiesWhitelistAndOverridingAndNameConverter() + { + $request = new Request(['properties' => ['foo', 'bar', 'name_converted']]); + $context = ['attributes' => ['qux']]; + + $propertyFilter = new PropertyFilter('properties', true, ['foo', 'nameConverted'], new CustomConverter()); + $propertyFilter->apply($request, true, [], $context); + + $this->assertEquals(['attributes' => ['foo', 'nameConverted']], $context); + } + + public function testApplyWithPropertiesInPropertyFilterAttributeAndNameConverter() + { + $request = new Request(['properties' => ['foo', 'bar', 'baz']], [], ['_api_filters' => ['properties' => ['name_converted']]]); + $context = ['attributes' => ['foo', 'qux']]; + + $propertyFilter = new PropertyFilter('properties', false, null, new CustomConverter()); + $propertyFilter->apply($request, true, [], $context); + + $this->assertEquals(['attributes' => ['foo', 'qux', 'nameConverted']], $context); + } + public function testGetDescription() { $propertyFilter = new PropertyFilter('custom_properties');