diff --git a/behat.yml.dist b/behat.yml.dist index 75db6117f57..c3af2501185 100644 --- a/behat.yml.dist +++ b/behat.yml.dist @@ -11,6 +11,7 @@ default: - 'ApiPlatform\Core\Tests\Behat\HttpCacheContext' - 'ApiPlatform\Core\Tests\Behat\JsonApiContext' - 'ApiPlatform\Core\Tests\Behat\JsonHalContext' + - 'ApiPlatform\Core\Tests\Behat\XmlContext' - 'Behat\MinkExtension\Context\MinkContext' - 'behatch:context:rest' filters: @@ -45,6 +46,7 @@ postgres: - 'ApiPlatform\Core\Tests\Behat\HttpCacheContext' - 'ApiPlatform\Core\Tests\Behat\JsonApiContext' - 'ApiPlatform\Core\Tests\Behat\JsonHalContext' + - 'ApiPlatform\Core\Tests\Behat\XmlContext' - 'Behat\MinkExtension\Context\MinkContext' - 'behatch:context:rest' filters: @@ -64,6 +66,7 @@ mongodb: - 'ApiPlatform\Core\Tests\Behat\HttpCacheContext' - 'ApiPlatform\Core\Tests\Behat\JsonApiContext' - 'ApiPlatform\Core\Tests\Behat\JsonHalContext' + - 'ApiPlatform\Core\Tests\Behat\XmlContext' - 'Behat\MinkExtension\Context\MinkContext' - 'behatch:context:rest' filters: @@ -98,6 +101,7 @@ default-coverage: - 'ApiPlatform\Core\Tests\Behat\JsonApiContext' - 'ApiPlatform\Core\Tests\Behat\JsonHalContext' - 'ApiPlatform\Core\Tests\Behat\CoverageContext' + - 'ApiPlatform\Core\Tests\Behat\XmlContext' - 'Behat\MinkExtension\Context\MinkContext' - 'behatch:context:rest' @@ -117,6 +121,7 @@ mongodb-coverage: - 'ApiPlatform\Core\Tests\Behat\JsonApiContext' - 'ApiPlatform\Core\Tests\Behat\JsonHalContext' - 'ApiPlatform\Core\Tests\Behat\CoverageContext' + - 'ApiPlatform\Core\Tests\Behat\XmlContext' - 'Behat\MinkExtension\Context\MinkContext' - 'behatch:context:rest' diff --git a/composer.json b/composer.json index 00990f8314f..8b0c239c7af 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,7 @@ "symfony/http-foundation": "^4.4 || ^5.1", "symfony/http-kernel": "^4.4 || ^5.1", "symfony/property-access": "^3.4.19 || ^4.4 || ^5.1", - "symfony/property-info": "^3.4 || ^4.4 || ^5.1", + "symfony/property-info": "^3.4 || ^4.4 || ^5.1@dev", "symfony/serializer": "^4.4 || ^5.1", "symfony/web-link": "^4.4 || ^5.1", "willdurand/negotiation": "^2.0.3 || ^3.0" diff --git a/features/doctrine/search_filter.feature b/features/doctrine/search_filter.feature index 0ed91ebf66d..c396159bf77 100644 --- a/features/doctrine/search_filter.feature +++ b/features/doctrine/search_filter.feature @@ -19,7 +19,7 @@ Feature: Search filter on collections Given there is a DummyCar entity with related colors When I send a "GET" request to "/dummy_cars?colors.prop=red" Then the response status code should be 200 - And the JSON should be deep equal to: + And the JSON should be equal to: """ { "@context": "/contexts/DummyCar", @@ -81,25 +81,25 @@ Feature: Search filter on collections "hydra:mapping": [ { "@type": "IriTemplateMapping", - "variable": "availableAt[after]", + "variable": "availableAt[before]", "property": "availableAt", "required": false }, { "@type": "IriTemplateMapping", - "variable": "availableAt[before]", + "variable": "availableAt[strictly_before]", "property": "availableAt", "required": false }, { "@type": "IriTemplateMapping", - "variable": "availableAt[strictly_after]", + "variable": "availableAt[after]", "property": "availableAt", "required": false }, { "@type": "IriTemplateMapping", - "variable": "availableAt[strictly_before]", + "variable": "availableAt[strictly_after]", "property": "availableAt", "required": false }, @@ -111,44 +111,38 @@ Feature: Search filter on collections }, { "@type": "IriTemplateMapping", - "variable": "colors", - "property": "colors", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "colors.prop", - "property": "colors.prop", + "variable": "foobar[]", + "property": null, "required": false }, { "@type": "IriTemplateMapping", - "variable": "colors[]", - "property": "colors", + "variable": "foobargroups[]", + "property": null, "required": false }, { "@type": "IriTemplateMapping", - "variable": "foobar[]", + "variable": "foobargroups_override[]", "property": null, "required": false }, { "@type": "IriTemplateMapping", - "variable": "foobargroups[]", - "property": null, + "variable": "colors.prop", + "property": "colors.prop", "required": false }, { "@type": "IriTemplateMapping", - "variable": "foobargroups_override[]", - "property": null, + "variable": "colors", + "property": "colors", "required": false }, { "@type": "IriTemplateMapping", - "variable": "name", - "property": "name", + "variable": "colors[]", + "property": "colors", "required": false }, { @@ -186,6 +180,12 @@ Feature: Search filter on collections "variable": "uuid[]", "property": "uuid", "required": false + }, + { + "@type": "IriTemplateMapping", + "variable": "name", + "property": "name", + "required": false } ] } @@ -278,7 +278,6 @@ Feature: Search filter on collections 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 print last JSON response And the JSON should be valid according to this schema: """ { diff --git a/features/graphql/introspection.feature b/features/graphql/introspection.feature index 1867b588c7c..9f67b60b025 100644 --- a/features/graphql/introspection.feature +++ b/features/graphql/introspection.feature @@ -71,7 +71,7 @@ Feature: GraphQL introspection support And the response should be in JSON And the header "Content-Type" should be equal to "application/json" And the JSON node "data.type1.description" should be equal to "Dummy Product." - And the JSON node "data.type1.fields[1].type.name" should be equal to "DummyAggregateOfferConnection" + And the JSON node "data.type1.fields[2].type.name" should be equal to "DummyAggregateOfferConnection" And the JSON node "data.type2.fields[0].name" should be equal to "edges" And the JSON node "data.type2.fields[0].type.ofType.name" should be equal to "DummyAggregateOfferEdge" And the JSON node "data.type3.fields[0].name" should be equal to "node" @@ -201,7 +201,7 @@ Feature: GraphQL introspection support Then the response status code should be 200 And the response should be in JSON And the header "Content-Type" should be equal to "application/json" - And the JSON should be deep equal to: + And the JSON should be equal to: """ { "data": { @@ -286,8 +286,8 @@ Feature: GraphQL introspection support Then the response status code should be 200 And the response should be in JSON And the header "Content-Type" should be equal to "application/json" - And the JSON node "data.__type.fields[9].name" should be equal to "jsonData" - And the JSON node "data.__type.fields[9].type.name" should be equal to "Iterable" + And the JSON node "data.__type.fields[13].name" should be equal to "jsonData" + And the JSON node "data.__type.fields[13].type.name" should be equal to "Iterable" Scenario: Retrieve entity - using serialization groups - fields When I send the following GraphQL request: @@ -424,8 +424,8 @@ Feature: GraphQL introspection support And the JSON node "data.typeCreatePayload.fields[0].name" should be equal to "dummyProperty" And the JSON node "data.typeCreatePayload.fields[0].type.name" should be equal to "createDummyPropertyPayloadData" And the JSON node "data.typeCreatePayload.fields[1].name" should be equal to "clientMutationId" - And the JSON node "data.typeCreatePayloadData.fields[3].name" should be equal to "group" - And the JSON node "data.typeCreatePayloadData.fields[3].type.name" should be equal to "createDummyGroupNestedPayload" + And the JSON node "data.typeCreatePayloadData.fields[4].name" should be equal to "group" + And the JSON node "data.typeCreatePayloadData.fields[4].type.name" should be equal to "createDummyGroupNestedPayload" And the JSON node "data.typeCreateNestedPayload.fields[0].name" should be equal to "id" Scenario: Retrieve a type name through a GraphQL query diff --git a/features/jsonapi/related-resouces-inclusion.feature b/features/jsonapi/related-resouces-inclusion.feature index 5857c5a1f5b..df339df10db 100644 --- a/features/jsonapi/related-resouces-inclusion.feature +++ b/features/jsonapi/related-resouces-inclusion.feature @@ -14,7 +14,7 @@ Feature: JSON API Inclusion of Related Resources Then the response status code should be 200 And the response should be in JSON And the JSON should be valid according to the JSON API schema - And the JSON should be deep equal to: + And the JSON should be equal to: """ { "data": { @@ -56,7 +56,7 @@ Feature: JSON API Inclusion of Related Resources Then the response status code should be 200 And the response should be in JSON And the JSON should be valid according to the JSON API schema - And the JSON should be deep equal to: + And the JSON should be equal to: """ { "data": { @@ -86,7 +86,7 @@ Feature: JSON API Inclusion of Related Resources Then the response status code should be 200 And the response should be in JSON And the JSON should be valid according to the JSON API schema - And the JSON should be deep equal to: + And the JSON should be equal to: """ { "data": { @@ -123,7 +123,7 @@ Feature: JSON API Inclusion of Related Resources Then the response status code should be 200 And the response should be in JSON And the JSON should be valid according to the JSON API schema - And the JSON should be deep equal to: + And the JSON should be equal to: """ { "data": { @@ -158,7 +158,7 @@ Feature: JSON API Inclusion of Related Resources Then the response status code should be 200 And the response should be in JSON And the JSON should be valid according to the JSON API schema - And the JSON should be deep equal to: + And the JSON should be equal to: """ { "data": { @@ -238,7 +238,7 @@ Feature: JSON API Inclusion of Related Resources Then the response status code should be 200 And the response should be in JSON And the JSON should be valid according to the JSON API schema - And the JSON should be deep equal to: + And the JSON should be equal to: """ { "data": { @@ -328,7 +328,7 @@ Feature: JSON API Inclusion of Related Resources Then the response status code should be 200 And the response should be in JSON And the JSON should be valid according to the JSON API schema - And the JSON should be deep equal to: + And the JSON should be equal to: """ { "data": { @@ -398,7 +398,7 @@ Feature: JSON API Inclusion of Related Resources Then the response status code should be 200 And the response should be in JSON And the JSON should be valid according to the JSON API schema - And the JSON should be deep equal to: + And the JSON should be equal to: """ { "links": { @@ -510,7 +510,7 @@ Feature: JSON API Inclusion of Related Resources Then the response status code should be 200 And the response should be in JSON And the JSON should be valid according to the JSON API schema - And the JSON should be deep equal to: + And the JSON should be equal to: """ { "links": { @@ -602,7 +602,7 @@ Feature: JSON API Inclusion of Related Resources Then the response status code should be 200 And the response should be in JSON And the JSON should be valid according to the JSON API schema - And the JSON should be deep equal to: + And the JSON should be equal to: """ { "links": { diff --git a/features/main/composite.feature b/features/main/composite.feature index 6fd4e9721ce..3997855e8b5 100644 --- a/features/main/composite.feature +++ b/features/main/composite.feature @@ -11,7 +11,7 @@ Feature: Retrieve data with Composite identifiers 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 deep equal to: + And the JSON should be equal to: """ { "@context": "/contexts/CompositeItem", diff --git a/features/main/content_negotiation.feature b/features/main/content_negotiation.feature index 71033af1f74..f6f6593c448 100644 --- a/features/main/content_negotiation.feature +++ b/features/main/content_negotiation.feature @@ -15,7 +15,8 @@ Feature: Content Negotiation support """ Then the response status code should be 201 And the header "Content-Type" should be equal to "application/xml; charset=utf-8" - And the response should be equal to + And the response should be in XML + And the XML should be equal to: """ 1XML! @@ -26,7 +27,8 @@ Feature: Content Negotiation support And I send a "GET" request to "/dummies" Then the response status code should be 200 And the header "Content-Type" should be equal to "application/xml; charset=utf-8" - And the response should be equal to + And the response should be in XML + And the XML should be equal to: """ 1XML! @@ -36,7 +38,8 @@ Feature: Content Negotiation support When I send a "GET" request to "/dummies.xml" Then the response status code should be 200 And the header "Content-Type" should be equal to "application/xml; charset=utf-8" - And the response should be equal to + And the response should be in XML + And the XML should be equal to: """ 1XML! @@ -82,7 +85,8 @@ Feature: Content Negotiation support """ Then the response status code should be 201 And the header "Content-Type" should be equal to "application/xml; charset=utf-8" - And the response should be equal to + And the response should be in XML + And the XML should be equal to: """ 2Sent in JSON @@ -134,7 +138,8 @@ Feature: Content Negotiation support """ Then the response status code should be 201 And the header "Content-Type" should be equal to "application/xml; charset=utf-8" - And the response should be equal to + And the response should be in XML + And the XML should be equal to: """ 1Kevin diff --git a/features/main/subresource.feature b/features/main/subresource.feature index 909fd5398ac..5c99830688c 100644 --- a/features/main/subresource.feature +++ b/features/main/subresource.feature @@ -193,45 +193,45 @@ Feature: Subresource support } """ - Scenario: Get the subresource relation item - When I send a "GET" request to "/dummies/1/related_dummies/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/RelatedDummy", - "@id": "/related_dummies/2", - "@type": "https://schema.org/Product", - "id": 2, - "name": null, - "symfony": "symfony", - "dummyDate": null, - "thirdLevel": { - "@id": "/third_levels/1", - "@type": "ThirdLevel", - "fourthLevel": "/fourth_levels/1" - }, - "relatedToDummyFriend": [], - "dummyBoolean": null, - "embeddedDummy": [], - "age": null - } - """ +# Scenario: Get the subresource relation item +# When I send a "GET" request to "/dummies/1/related_dummies/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/RelatedDummy", +# "@id": "/related_dummies/2", +# "@type": "https://schema.org/Product", +# "id": 2, +# "name": null, +# "symfony": "symfony", +# "dummyDate": null, +# "thirdLevel": { +# "@id": "/third_levels/1", +# "@type": "ThirdLevel", +# "fourthLevel": "/fourth_levels/1" +# }, +# "relatedToDummyFriend": [], +# "dummyBoolean": null, +# "embeddedDummy": [], +# "age": null +# } +# """ - Scenario: Create a dummy with a relation that is a subresource - When I add "Content-Type" header equal to "application/ld+json" - And I send a "POST" request to "/dummies" with body: - """ - { - "name": "Dummy with relations", - "relatedDummy": "/dummies/1/related_dummies/2" - } - """ - Then the response status code should be 201 - And the response should be in JSON - And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" +# Scenario: Create a dummy with a relation that is a subresource +# When I add "Content-Type" header equal to "application/ld+json" +# And I send a "POST" request to "/dummies" with body: +# """ +# { +# "name": "Dummy with relations", +# "relatedDummy": "/dummies/1/related_dummies/2" +# } +# """ +# Then the response status code should be 201 +# And the response should be in JSON +# And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" Scenario: Get the embedded relation subresource item at the third level When I send a "GET" request to "/dummies/1/related_dummies/1/third_level" @@ -355,7 +355,7 @@ Feature: Subresource support """ { "@context": "/contexts/Dummy", - "@id": "/dummies/3", + "@id": "/dummies/2", "@type": "Dummy", "description": null, "dummy": null, @@ -370,7 +370,7 @@ Feature: Subresource support "name_converted": null, "relatedOwnedDummy": "/related_owned_dummies/1", "relatedOwningDummy": null, - "id": 3, + "id": 2, "name": "plop", "alias": null, "foo": null @@ -387,7 +387,7 @@ Feature: Subresource support """ { "@context": "/contexts/Dummy", - "@id": "/dummies/4", + "@id": "/dummies/3", "@type": "Dummy", "description": null, "dummy": null, @@ -402,7 +402,7 @@ Feature: Subresource support "name_converted": null, "relatedOwnedDummy": null, "relatedOwningDummy": "/related_owning_dummies/1", - "id": 4, + "id": 3, "name": "plop", "alias": null, "foo": null diff --git a/src/Bridge/Symfony/PropertyInfo/Metadata/Property/PropertyInfoPropertyNameCollectionFactory.php b/src/Bridge/Symfony/PropertyInfo/Metadata/Property/PropertyInfoPropertyNameCollectionFactory.php index f603e08f3a1..50f9970acb2 100644 --- a/src/Bridge/Symfony/PropertyInfo/Metadata/Property/PropertyInfoPropertyNameCollectionFactory.php +++ b/src/Bridge/Symfony/PropertyInfo/Metadata/Property/PropertyInfoPropertyNameCollectionFactory.php @@ -38,7 +38,7 @@ public function __construct(PropertyInfoExtractorInterface $propertyInfo) */ public function create(string $resourceClass, array $options = []): PropertyNameCollection { - $properties = $this->propertyInfo->getProperties($resourceClass, $options); + $properties = $this->propertyInfo->getProperties($resourceClass, $options + ['serializer_groups' => null]); return new PropertyNameCollection($properties ?? []); } diff --git a/src/Metadata/Property/Factory/SerializerPropertyMetadataFactory.php b/src/Metadata/Property/Factory/SerializerPropertyMetadataFactory.php index 226e84c95a7..c02050cd5a0 100644 --- a/src/Metadata/Property/Factory/SerializerPropertyMetadataFactory.php +++ b/src/Metadata/Property/Factory/SerializerPropertyMetadataFactory.php @@ -18,6 +18,7 @@ use ApiPlatform\Core\Metadata\Property\PropertyMetadata; use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; use ApiPlatform\Core\Util\ResourceClassInfoTrait; +use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface as SerializerClassMetadataFactoryInterface; /** @@ -66,24 +67,26 @@ public function create(string $resourceClass, string $property, array $options = } /** - * Sets readable/writable based on matching normalization/denormalization groups. + * Sets readable/writable based on matching normalization/denormalization groups and property's ignorance. * * A false value is never reset as it could be unreadable/unwritable for other reasons. - * If normalization/denormalization groups are not specified, the property is implicitly readable/writable. + * If normalization/denormalization groups are not specified and the property is not ignored, the property is implicitly readable/writable. * * @param string[]|null $normalizationGroups * @param string[]|null $denormalizationGroups */ private function transformReadWrite(PropertyMetadata $propertyMetadata, string $resourceClass, string $propertyName, array $normalizationGroups = null, array $denormalizationGroups = null): PropertyMetadata { - $groups = $this->getPropertySerializerGroups($resourceClass, $propertyName); + $serializerAttributeMetadata = $this->getSerializerAttributeMetadata($resourceClass, $propertyName); + $groups = $serializerAttributeMetadata ? $serializerAttributeMetadata->getGroups() : []; + $ignored = $serializerAttributeMetadata && method_exists($serializerAttributeMetadata, 'isIgnored') ? $serializerAttributeMetadata->isIgnored() : false; if (false !== $propertyMetadata->isReadable()) { - $propertyMetadata = $propertyMetadata->withReadable(null === $normalizationGroups || !empty(array_intersect($normalizationGroups, $groups))); + $propertyMetadata = $propertyMetadata->withReadable(!$ignored && (null === $normalizationGroups || array_intersect($normalizationGroups, $groups))); } if (false !== $propertyMetadata->isWritable()) { - $propertyMetadata = $propertyMetadata->withWritable(null === $denormalizationGroups || !empty(array_intersect($denormalizationGroups, $groups))); + $propertyMetadata = $propertyMetadata->withWritable(!$ignored && (null === $denormalizationGroups || array_intersect($denormalizationGroups, $groups))); } return $propertyMetadata; @@ -178,22 +181,17 @@ private function getEffectiveSerializerGroups(array $options, string $resourceCl ]; } - /** - * Gets the serializer groups defined on a property. - * - * @return string[] - */ - private function getPropertySerializerGroups(string $class, string $property): array + private function getSerializerAttributeMetadata(string $class, string $attribute): ?AttributeMetadataInterface { $serializerClassMetadata = $this->serializerClassMetadataFactory->getMetadataFor($class); foreach ($serializerClassMetadata->getAttributesMetadata() as $serializerAttributeMetadata) { - if ($property === $serializerAttributeMetadata->getName()) { - return $serializerAttributeMetadata->getGroups(); + if ($attribute === $serializerAttributeMetadata->getName()) { + return $serializerAttributeMetadata; } } - return []; + return null; } /** diff --git a/src/Serializer/AbstractItemNormalizer.php b/src/Serializer/AbstractItemNormalizer.php index bafdba3a86a..72655d7be8a 100644 --- a/src/Serializer/AbstractItemNormalizer.php +++ b/src/Serializer/AbstractItemNormalizer.php @@ -356,19 +356,8 @@ protected function getAllowedAttributes($classOrObject, array $context, $attribu $options = $this->getFactoryOptions($context); $propertyNames = $this->propertyNameCollectionFactory->create($context['resource_class'], $options); - $attributesMetadata = $this->classMetadataFactory ? - $this->classMetadataFactory->getMetadataFor($context['resource_class'])->getAttributesMetadata() : - null; - $allowedAttributes = []; foreach ($propertyNames as $propertyName) { - if ( - null != $attributesMetadata && \array_key_exists($propertyName, $attributesMetadata) && - method_exists($attributesMetadata[$propertyName], 'isIgnored') && - $attributesMetadata[$propertyName]->isIgnored()) { - continue; - } - $propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $propertyName, $options); if ( diff --git a/tests/Behat/JsonContext.php b/tests/Behat/JsonContext.php index 5cb109b7b6c..9bd33f6eb56 100644 --- a/tests/Behat/JsonContext.php +++ b/tests/Behat/JsonContext.php @@ -27,25 +27,20 @@ public function __construct(HttpCallResultPool $httpCallResultPool) parent::__construct($httpCallResultPool); } - /** - * @Then /^the JSON should be deep equal to:$/ - */ - public function theJsonShouldBeDeepEqualTo(PyStringNode $content) + public function theJsonShouldBeEqualTo(PyStringNode $content): void { $actual = $this->getJson(); + try { $expected = new Json($content); } catch (\Exception $e) { throw new \Exception('The expected JSON is not a valid'); } - $actual = new Json(json_encode($this->sortArrays($actual->getContent()))); - $expected = new Json(json_encode($this->sortArrays($expected->getContent()))); - - $this->assertSame( - (string) $expected, - (string) $actual, - "The json is equal to:\n".$actual->encode() + $this->assertEquals( + $expected->getContent(), + $actual->getContent(), + "The json is equal to:\n{$actual->encode()}" ); } @@ -59,25 +54,4 @@ public function theJsonIsASupersetOf(PyStringNode $content) method_exists(Assert::class, 'assertArraySubset') ? Assert::assertArraySubset($subset, $array) : ApiTestCase::assertArraySubset($subset, $array); // @phpstan-ignore-line Compatibility with PHPUnit 7 } - - private function sortArrays($obj) - { - $isObject = \is_object($obj); - - foreach ($obj as $key => $value) { - if (null === $value || is_scalar($value)) { - continue; - } - - if (\is_array($value)) { - sort($value); - } - - $value = $this->sortArrays($value); - - $isObject ? $obj->{$key} = $value : $obj[$key] = $value; - } - - return $obj; - } } diff --git a/tests/Behat/XmlContext.php b/tests/Behat/XmlContext.php new file mode 100644 index 00000000000..8e18ffd9408 --- /dev/null +++ b/tests/Behat/XmlContext.php @@ -0,0 +1,43 @@ + + * + * 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\Behat; + +use Behat\Gherkin\Node\PyStringNode; +use Behatch\Context\XmlContext as BaseXmlContext; +use Symfony\Component\Serializer\Encoder\XmlEncoder; + +final class XmlContext extends BaseXmlContext +{ + private $xmlEncoder; + + public function __construct() + { + $this->xmlEncoder = new XmlEncoder(); + } + + /** + * @Then the XML should be equal to: + */ + public function theXmlShouldBeEqualTo(PyStringNode $content): void + { + $expected = $this->xmlEncoder->decode((string) $content, 'xml'); + $actual = $this->xmlEncoder->decode($actualXml = $this->getSession()->getPage()->getContent(), 'xml'); + + $this->assertEquals( + $expected, + $actual, + "The XML is equal to:\n{$actualXml}" + ); + } +} diff --git a/tests/Bridge/Symfony/PropertyInfo/Metadata/Property/PropertyInfoPropertyNameCollectionFactoryTest.php b/tests/Bridge/Symfony/PropertyInfo/Metadata/Property/PropertyInfoPropertyNameCollectionFactoryTest.php index 27384cccb64..cbcc1458f27 100644 --- a/tests/Bridge/Symfony/PropertyInfo/Metadata/Property/PropertyInfoPropertyNameCollectionFactoryTest.php +++ b/tests/Bridge/Symfony/PropertyInfo/Metadata/Property/PropertyInfoPropertyNameCollectionFactoryTest.php @@ -14,13 +14,19 @@ namespace ApiPlatform\Core\Tests\Bridge\Symfony\PropertyInfo\Metadata\Property; use ApiPlatform\Core\Bridge\Symfony\PropertyInfo\Metadata\Property\PropertyInfoPropertyNameCollectionFactory; +use ApiPlatform\Core\Tests\Fixtures\DummyIgnoreProperty; use ApiPlatform\Core\Tests\Fixtures\DummyObjectWithOnlyPrivateProperty; use ApiPlatform\Core\Tests\Fixtures\DummyObjectWithOnlyPublicProperty; use ApiPlatform\Core\Tests\Fixtures\DummyObjectWithoutProperty; use ApiPlatform\Core\Tests\Fixtures\DummyObjectWithPublicAndPrivateProperty; +use Doctrine\Common\Annotations\AnnotationReader; use PHPUnit\Framework\TestCase; +use Symfony\Component\PropertyInfo\DependencyInjection\PropertyInfoConstructorPass; use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; +use Symfony\Component\PropertyInfo\Extractor\SerializerExtractor; use Symfony\Component\PropertyInfo\PropertyInfoExtractor; +use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; +use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader; /** * @author Oskar Stark @@ -70,4 +76,36 @@ public function testCreateMethodReturnsProperPropertyNameCollectionForObjectWith self::assertCount(1, $collection->getIterator()); } + + public function testCreateMethodReturnsProperPropertyNameCollectionForObjectWithIgnoredProperties(): void + { + // symfony/property-info < 5.2.1 + if (!class_exists(PropertyInfoConstructorPass::class)) { + self::markTestSkipped(); + } + + $factory = new PropertyInfoPropertyNameCollectionFactory( + new PropertyInfoExtractor([ + new SerializerExtractor( + new ClassMetadataFactory( + new AnnotationLoader( + new AnnotationReader() + ) + ) + ), + ]) + ); + + self::assertObjectHasAttribute('ignored', new DummyIgnoreProperty()); + + $collection = $factory->create(DummyIgnoreProperty::class, ['serializer_groups' => ['dummy']]); + + self::assertCount(1, $collection); + self::assertNotContains('ignored', $collection); + + $collection = $factory->create(DummyIgnoreProperty::class); + + self::assertCount(2, $collection); + self::assertNotContains('ignored', $collection); + } } diff --git a/tests/Fixtures/DummyIgnoreProperty.php b/tests/Fixtures/DummyIgnoreProperty.php new file mode 100644 index 00000000000..ff267d757f3 --- /dev/null +++ b/tests/Fixtures/DummyIgnoreProperty.php @@ -0,0 +1,33 @@ + + * + * 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; + +use Symfony\Component\Serializer\Annotation\Groups; +use Symfony\Component\Serializer\Annotation\Ignore; + +class DummyIgnoreProperty +{ + public $visibleWithoutGroup; + + /** + * @Groups({"dummy"}) + */ + public $visibleWithGroup; + + /** + * @Groups({"dummy"}) + * @Ignore + */ + public $ignored; +} diff --git a/tests/Fixtures/TestBundle/Document/RelatedDummy.php b/tests/Fixtures/TestBundle/Document/RelatedDummy.php index dcf01f6e5de..45ae92e5f7d 100644 --- a/tests/Fixtures/TestBundle/Document/RelatedDummy.php +++ b/tests/Fixtures/TestBundle/Document/RelatedDummy.php @@ -34,7 +34,6 @@ class RelatedDummy extends ParentDummy { /** * @ApiProperty(writable=false) - * @ApiSubresource * @ODM\Id(strategy="INCREMENT", type="integer") * @Groups({"chicago", "friends"}) */ diff --git a/tests/Fixtures/TestBundle/Entity/RelatedDummy.php b/tests/Fixtures/TestBundle/Entity/RelatedDummy.php index 02f13cf929e..cb1fbc8f721 100644 --- a/tests/Fixtures/TestBundle/Entity/RelatedDummy.php +++ b/tests/Fixtures/TestBundle/Entity/RelatedDummy.php @@ -33,7 +33,6 @@ class RelatedDummy extends ParentDummy { /** * @ApiProperty(writable=false) - * @ApiSubresource * @ORM\Column(type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") diff --git a/tests/Metadata/Property/Factory/SerializerPropertyMetadataFactoryTest.php b/tests/Metadata/Property/Factory/SerializerPropertyMetadataFactoryTest.php index 0d571730e1f..f068623bef3 100644 --- a/tests/Metadata/Property/Factory/SerializerPropertyMetadataFactoryTest.php +++ b/tests/Metadata/Property/Factory/SerializerPropertyMetadataFactoryTest.php @@ -19,6 +19,7 @@ use ApiPlatform\Core\Metadata\Property\PropertyMetadata; use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; use ApiPlatform\Core\Metadata\Resource\ResourceMetadata; +use ApiPlatform\Core\Tests\Fixtures\DummyIgnoreProperty; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyTableInheritance; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyTableInheritanceChild; @@ -26,6 +27,7 @@ use ApiPlatform\Core\Tests\ProphecyTrait; use PHPUnit\Framework\TestCase; use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\Serializer\Annotation\Ignore; use Symfony\Component\Serializer\Mapping\AttributeMetadata as SerializerAttributeMetadata; use Symfony\Component\Serializer\Mapping\ClassMetadata as SerializerClassMetadata; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface as SerializerClassMetadataFactoryInterface; @@ -82,9 +84,6 @@ public function testCreate($readGroups, $writeGroups) $dummySerializerClassMetadata->addAttributeMetadata($nameConvertedSerializerAttributeMetadata); $serializerClassMetadataFactoryProphecy->getMetadataFor(Dummy::class)->willReturn($dummySerializerClassMetadata); $relatedDummySerializerClassMetadata = new SerializerClassMetadata(RelatedDummy::class); - $idSerializerAttributeMetadata = new SerializerAttributeMetadata('id'); - $idSerializerAttributeMetadata->addGroup('dummy_read'); - $relatedDummySerializerClassMetadata->addAttributeMetadata($idSerializerAttributeMetadata); $nameSerializerAttributeMetadata = new SerializerAttributeMetadata('name'); $nameSerializerAttributeMetadata->addGroup('dummy_read'); $relatedDummySerializerClassMetadata->addAttributeMetadata($nameSerializerAttributeMetadata); @@ -161,4 +160,49 @@ public function testCreateInherited(): void $this->assertEquals($actual->getChildInherited(), DummyTableInheritanceChild::class); } + + public function testCreateWithIgnoredProperty(): void + { + // symfony/serializer < 5.1 + if (!class_exists(Ignore::class)) { + self::markTestSkipped(); + } + + $dummyIgnorePropertyResourceMetadata = (new ResourceMetadata()) + ->withAttributes([ + 'normalization_context' => [AbstractNormalizer::GROUPS => ['dummy']], + 'denormalization_context' => [AbstractNormalizer::GROUPS => ['dummy']], + ]); + + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); + $resourceMetadataFactoryProphecy->create(DummyIgnoreProperty::class)->willReturn($dummyIgnorePropertyResourceMetadata); + + $ignoredSerializerAttributeMetadata = new SerializerAttributeMetadata('ignored'); + $ignoredSerializerAttributeMetadata->addGroup('dummy'); + $ignoredSerializerAttributeMetadata->addGroup('dummy'); + $ignoredSerializerAttributeMetadata->setIgnore(true); + + $dummyIgnorePropertySerializerClassMetadata = new SerializerClassMetadata(DummyIgnoreProperty::class); + $dummyIgnorePropertySerializerClassMetadata->addAttributeMetadata($ignoredSerializerAttributeMetadata); + + $serializerClassMetadataFactoryProphecy = $this->prophesize(SerializerClassMetadataFactoryInterface::class); + $serializerClassMetadataFactoryProphecy->getMetadataFor(DummyIgnoreProperty::class)->willReturn($dummyIgnorePropertySerializerClassMetadata); + + $ignoredPropertyMetadata = (new PropertyMetadata())->withType(new Type(Type::BUILTIN_TYPE_STRING, true)); + + $decoratedProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $decoratedProphecy->create(DummyIgnoreProperty::class, 'ignored', [])->willReturn($ignoredPropertyMetadata); + + $serializerPropertyMetadataFactory = new SerializerPropertyMetadataFactory( + $resourceMetadataFactoryProphecy->reveal(), + $serializerClassMetadataFactoryProphecy->reveal(), + $decoratedProphecy->reveal(), + $this->prophesize(ResourceClassResolverInterface::class)->reveal() + ); + + $result = $serializerPropertyMetadataFactory->create(DummyIgnoreProperty::class, 'ignored'); + + self::assertFalse($result->isReadable()); + self::assertFalse($result->isWritable()); + } } diff --git a/tests/Serializer/AbstractItemNormalizerTest.php b/tests/Serializer/AbstractItemNormalizerTest.php index e4a6c491474..7f81ac608df 100644 --- a/tests/Serializer/AbstractItemNormalizerTest.php +++ b/tests/Serializer/AbstractItemNormalizerTest.php @@ -44,9 +44,6 @@ use Symfony\Component\PropertyAccess\PropertyAccessorInterface; use Symfony\Component\PropertyInfo\Type; use Symfony\Component\Serializer\Exception\UnexpectedValueException; -use Symfony\Component\Serializer\Mapping\AttributeMetadata; -use Symfony\Component\Serializer\Mapping\ClassMetadataInterface; -use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; use Symfony\Component\Serializer\NameConverter\AdvancedNameConverterInterface; use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; @@ -1205,68 +1202,4 @@ public function testNormalizationWithDataTransformer() $propertyAccessorProphecy->setValue($actualDummy, 'name', 'Dummy')->shouldHaveBeenCalled(); } - - public function testNormalizationWithIgnoreMetadata() - { - if (!method_exists(AttributeMetadata::class, 'setIgnore')) { - $this->markTestSkipped(); - } - - $dummy = new Dummy(); - - $dummyAttributeMetadata = new AttributeMetadata('dummy'); - $dummyAttributeMetadata->setIgnore(true); - - $classMetadataProphecy = $this->prophesize(ClassMetadataInterface::class); - $classMetadataProphecy->getAttributesMetadata()->willReturn(['dummy' => $dummyAttributeMetadata]); - - $classMetadataFactoryProphecy = $this->prophesize(ClassMetadataFactoryInterface::class); - $classMetadataFactoryProphecy->getMetadataFor(Dummy::class)->willReturn($classMetadataProphecy->reveal()); - - $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); - $propertyNameCollectionFactoryProphecy->create(Dummy::class, [])->willReturn(new PropertyNameCollection(['name', 'dummy'])); - - $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', [])->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_STRING), '', true)); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'dummy', [])->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_STRING), '', true)); - - $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); - $iriConverterProphecy->getIriFromItem($dummy)->willReturn('/dummies/1'); - - $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class); - $propertyAccessorProphecy->getValue($dummy, 'name')->willReturn('foo'); - $propertyAccessorProphecy->getValue($dummy, 'dummy')->willReturn('bar'); - - $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); - $resourceClassResolverProphecy->getResourceClass($dummy, null)->willReturn(Dummy::class); - - $serializerProphecy = $this->prophesize(SerializerInterface::class); - $serializerProphecy->willImplement(NormalizerInterface::class); - $serializerProphecy->normalize('foo', null, Argument::type('array'))->willReturn('foo'); - $serializerProphecy->normalize('bar', null, Argument::type('array'))->willReturn('bar'); - - $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [ - $propertyNameCollectionFactoryProphecy->reveal(), - $propertyMetadataFactoryProphecy->reveal(), - $iriConverterProphecy->reveal(), - $resourceClassResolverProphecy->reveal(), - $propertyAccessorProphecy->reveal(), - null, - $classMetadataFactoryProphecy->reveal(), - null, - false, - [], - [], - null, - null, - ]); - $normalizer->setSerializer($serializerProphecy->reveal()); - - $expected = [ - 'name' => 'foo', - ]; - $this->assertEquals($expected, $normalizer->normalize($dummy, null, [ - 'resources' => [], - ])); - } }