From 9ce38f3a241218a3aa1db756fc61eebc048c1301 Mon Sep 17 00:00:00 2001 From: Daniel West Date: Fri, 3 Apr 2020 11:37:12 +0100 Subject: [PATCH 01/29] Include extended JsonLD data if DTO outputs original object In `ApiPlatform\Core\DataTransformer\DataTransformerInterface` there is a comment that states that DTOs should be allowed to return the same original object if no transformation is done. This resulted in missing LD data. (No @id, @context etc.). ```php /** * Transforms the given object to something else, usually another object. * This must return the original object if no transformation has been done. * * @param object $object * * @return object */ public function transform($object, string $to, array $context = []); ``` This update checks if the output class is the same as the original, and if so populated the extended metadata in the JsonLd\ItemNormalizer as it would not be added using the JsonLd\ObjectNormalizer --- CHANGELOG.md | 1 + src/JsonLd/Serializer/ItemNormalizer.php | 4 +++- src/Serializer/AbstractItemNormalizer.php | 25 ++++++++++++++++------- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce819873b8f..c687788a4d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * GraphQL: Do not allow empty cursor values on `before` or `after` * Filter: Improve the RangeFilter query in case the values are equals using the between operator +* Bug fix: Allow Data Transformers to return the original class if no transformation has been made, or the transformation was to the original object ## 2.5.4 diff --git a/src/JsonLd/Serializer/ItemNormalizer.php b/src/JsonLd/Serializer/ItemNormalizer.php index aca10e346d7..02ce3c61cb6 100644 --- a/src/JsonLd/Serializer/ItemNormalizer.php +++ b/src/JsonLd/Serializer/ItemNormalizer.php @@ -65,7 +65,9 @@ public function supportsNormalization($data, $format = null): bool */ public function normalize($object, $format = null, array $context = []) { - if (null !== $this->getOutputClass($this->getObjectClass($object), $context)) { + $objectClass = $this->getObjectClass($object); + $outputClass = $this->getOutputClass($objectClass, $context); + if (null !== $outputClass && !isset($context[self::IS_TRANSFORMED_TO_SAME_CLASS_CONTEXT_KEY])) { return parent::normalize($object, $format, $context); } diff --git a/src/Serializer/AbstractItemNormalizer.php b/src/Serializer/AbstractItemNormalizer.php index 2e513af8ff3..a1f71f5d9a2 100644 --- a/src/Serializer/AbstractItemNormalizer.php +++ b/src/Serializer/AbstractItemNormalizer.php @@ -51,6 +51,8 @@ abstract class AbstractItemNormalizer extends AbstractObjectNormalizer use ContextTrait; use InputOutputMetadataTrait; + public const IS_TRANSFORMED_TO_SAME_CLASS_CONTEXT_KEY = 'is_transformed_to_same_class'; + protected $propertyNameCollectionFactory; protected $propertyMetadataFactory; protected $iriConverter; @@ -112,18 +114,24 @@ public function hasCacheableSupportsMethod(): bool */ public function normalize($object, $format = null, array $context = []) { - if ($object !== $transformed = $this->transformOutput($object, $context)) { + if (!($isTransformed = isset($context[self::IS_TRANSFORMED_TO_SAME_CLASS_CONTEXT_KEY])) && $outputClass = $this->getOutputClass($this->getObjectClass($object), $context)) { if (!$this->serializer instanceof NormalizerInterface) { throw new LogicException('Cannot normalize the output because the injected serializer is not a normalizer'); } - $context['api_normalize'] = true; - $context['api_resource'] = $object; - unset($context['output']); - unset($context['resource_class']); + if ($object !== $transformed = $this->transformOutput($object, $context, $outputClass)) { + $context['api_normalize'] = true; + $context['api_resource'] = $object; + unset($context['output'], $context['resource_class']); + } else { + $context[self::IS_TRANSFORMED_TO_SAME_CLASS_CONTEXT_KEY] = true; + } return $this->serializer->normalize($transformed, $format, $context); } + if ($isTransformed) { + unset($context[self::IS_TRANSFORMED_TO_SAME_CLASS_CONTEXT_KEY]); + } $resourceClass = $this->resourceClassResolver->getResourceClass($object, $context['resource_class'] ?? null); $context = $this->initContext($resourceClass, $context); @@ -637,9 +645,12 @@ protected function getDataTransformer($data, string $to, array $context = []): ? * For a given resource, it returns an output representation if any * If not, the resource is returned. */ - protected function transformOutput($object, array $context = []) + protected function transformOutput($object, array $context = [], string $outputClass = null) { - $outputClass = $this->getOutputClass($this->getObjectClass($object), $context); + if (null === $outputClass) { + $outputClass = $this->getOutputClass($this->getObjectClass($object), $context); + } + if (null !== $outputClass && null !== $dataTransformer = $this->getDataTransformer($object, $outputClass, $context)) { return $dataTransformer->transform($object, $outputClass, $context); } From 33d2da76e980c2bc3dd3d4fdb7425fb3ebe01891 Mon Sep 17 00:00:00 2001 From: Daniel West Date: Fri, 3 Apr 2020 12:16:09 +0100 Subject: [PATCH 02/29] Added behat test --- features/bootstrap/DoctrineContext.php | 14 +++++ features/jsonld/input_output.feature | 19 +++++++ .../Entity/DummyDtoOutputSameClass.php | 56 +++++++++++++++++++ 3 files changed, 89 insertions(+) create mode 100644 tests/Fixtures/TestBundle/Entity/DummyDtoOutputSameClass.php diff --git a/features/bootstrap/DoctrineContext.php b/features/bootstrap/DoctrineContext.php index 3e519762e76..8ff543895ba 100644 --- a/features/bootstrap/DoctrineContext.php +++ b/features/bootstrap/DoctrineContext.php @@ -90,6 +90,7 @@ use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyDtoCustom; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyDtoNoInput; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyDtoNoOutput; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyDtoOutputSameClass; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyFriend; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyGroup; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyImmutableDate; @@ -1379,6 +1380,19 @@ public function thereAreNbDummyDtoCustom($nb) $this->manager->clear(); } + /** + * @Given there is a DummyDtoOutputSameClass + */ + public function thereIsADummyDtoOutputSameClass() + { + $dto = new DummyDtoOutputSameClass(); + $dto->lorem = 'test'; + $dto->ipsum = '1'; + $this->manager->persist($dto); + $this->manager->flush(); + $this->manager->clear(); + } + /** * @Given there is an order with same customer and recipient */ diff --git a/features/jsonld/input_output.feature b/features/jsonld/input_output.feature index 12f1492f870..1bc4ab36c27 100644 --- a/features/jsonld/input_output.feature +++ b/features/jsonld/input_output.feature @@ -83,6 +83,25 @@ Feature: JSON-LD DTO input and output } """ + @createSchema + Scenario: Get an item with same class as custom output + Given there is a DummyDtoOutputSameClass + When I send a "GET" request to "/dummy_dto_output_same_classes/1" + Then the response status code should be 200 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the JSON should be equal to: + """ + { + "@context": "/contexts/DummyDtoOutputSameClass", + "@id": "/dummy_dto_output_same_classes/1", + "@type": "DummyDtoOutputSameClass", + "lorem": "test", + "ipsum": "1", + "id": 1 + } + """ + @createSchema Scenario: Create a DummyDtoCustom object without output When I send a "POST" request to "/dummy_dto_custom_post_without_output" with body: diff --git a/tests/Fixtures/TestBundle/Entity/DummyDtoOutputSameClass.php b/tests/Fixtures/TestBundle/Entity/DummyDtoOutputSameClass.php new file mode 100644 index 00000000000..7a4f389ee29 --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/DummyDtoOutputSameClass.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity; + +use ApiPlatform\Core\Annotation\ApiResource; +use Doctrine\ORM\Mapping as ORM; + +/** + * Dummy InputOutput. + * + * @author Kévin Dunglas + * + * @ApiResource(attributes={"output"=DummyDtoOutputSameClass::class}) + * @ORM\Entity + */ +class DummyDtoOutputSameClass +{ + /** + * @var int The id + * + * @ORM\Column(type="integer") + * @ORM\Id + * @ORM\GeneratedValue(strategy="AUTO") + */ + private $id; + + /** + * @var string + * + * @ORM\Column + */ + public $lorem; + + /** + * @var string + * + * @ORM\Column + */ + public $ipsum; + + public function getId() + { + return $this->id; + } +} From 4413451a82eb533a54497ac6458f43e1bee49b00 Mon Sep 17 00:00:00 2001 From: Daniel West Date: Fri, 3 Apr 2020 14:23:57 +0100 Subject: [PATCH 03/29] Add failing test where DataTransformer may fallback to returning the original object --- features/bootstrap/DoctrineContext.php | 46 +++++++++------ features/jsonld/input_output.feature | 19 +++++++ .../OutputDtoUnmodifiedDataTransformer.php | 42 ++++++++++++++ .../TestBundle/Dto/OutputDtoDummy.php | 22 +++++++ .../DummyDtoOutputFallbackToSameClass.php | 57 +++++++++++++++++++ 5 files changed, 170 insertions(+), 16 deletions(-) create mode 100644 tests/Fixtures/TestBundle/DataTransformer/OutputDtoUnmodifiedDataTransformer.php create mode 100644 tests/Fixtures/TestBundle/Dto/OutputDtoDummy.php create mode 100644 tests/Fixtures/TestBundle/Entity/DummyDtoOutputFallbackToSameClass.php diff --git a/features/bootstrap/DoctrineContext.php b/features/bootstrap/DoctrineContext.php index 8ff543895ba..4ed58c18248 100644 --- a/features/bootstrap/DoctrineContext.php +++ b/features/bootstrap/DoctrineContext.php @@ -90,6 +90,7 @@ use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyDtoCustom; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyDtoNoInput; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyDtoNoOutput; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyDtoOutputFallbackToSameClass; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyDtoOutputSameClass; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyFriend; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyGroup; @@ -591,7 +592,7 @@ public function thereAreDummyObjectsWithDummyDate(int $nb) $descriptions = ['Smart dummy.', 'Not so smart dummy.']; for ($i = 1; $i <= $nb; ++$i) { - $date = new \DateTime(sprintf('2015-04-%d', $i), new \DateTimeZone('UTC')); + $date = new DateTime(sprintf('2015-04-%d', $i), new DateTimeZone('UTC')); $dummy = $this->buildDummy(); $dummy->setName('Dummy #'.$i); @@ -622,11 +623,11 @@ public function thereAreDummyObjectsWithDummyDateAndDummyBoolean(int $nb, string $bool = false; } else { $expected = ['true', 'false', '1', '0']; - throw new \InvalidArgumentException(sprintf('Invalid boolean value for "%s" property, expected one of ( "%s" )', $bool, implode('" | "', $expected))); + throw new InvalidArgumentException(sprintf('Invalid boolean value for "%s" property, expected one of ( "%s" )', $bool, implode('" | "', $expected))); } for ($i = 1; $i <= $nb; ++$i) { - $date = new \DateTime(sprintf('2015-04-%d', $i), new \DateTimeZone('UTC')); + $date = new DateTime(sprintf('2015-04-%d', $i), new DateTimeZone('UTC')); $dummy = $this->buildDummy(); $dummy->setName('Dummy #'.$i); @@ -651,7 +652,7 @@ public function thereAreDummyObjectsWithDummyDateAndDummyBoolean(int $nb, string public function thereAreDummyObjectsWithDummyDateAndRelatedDummy(int $nb) { for ($i = 1; $i <= $nb; ++$i) { - $date = new \DateTime(sprintf('2015-04-%d', $i), new \DateTimeZone('UTC')); + $date = new DateTime(sprintf('2015-04-%d', $i), new DateTimeZone('UTC')); $relatedDummy = $this->buildRelatedDummy(); $relatedDummy->setName('RelatedDummy #'.$i); @@ -679,7 +680,7 @@ public function thereAreDummyObjectsWithDummyDateAndRelatedDummy(int $nb) public function thereAreDummyObjectsWithDummyDateAndEmbeddedDummy(int $nb) { for ($i = 1; $i <= $nb; ++$i) { - $date = new \DateTime(sprintf('2015-04-%d', $i), new \DateTimeZone('UTC')); + $date = new DateTime(sprintf('2015-04-%d', $i), new DateTimeZone('UTC')); $embeddableDummy = $this->buildEmbeddableDummy(); $embeddableDummy->setDummyName('Embeddable #'.$i); @@ -706,7 +707,7 @@ 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')); + $convertedDate->nameConverted = new DateTime(sprintf('2015-04-%d', $i), new DateTimeZone('UTC')); $this->manager->persist($convertedDate); } @@ -792,7 +793,7 @@ public function thereAreDummyObjectsWithDummyBoolean(int $nb, string $bool) $bool = false; } else { $expected = ['true', 'false', '1', '0']; - throw new \InvalidArgumentException(sprintf('Invalid boolean value for "%s" property, expected one of ( "%s" )', $bool, implode('" | "', $expected))); + throw new InvalidArgumentException(sprintf('Invalid boolean value for "%s" property, expected one of ( "%s" )', $bool, implode('" | "', $expected))); } $descriptions = ['Smart dummy.', 'Not so smart dummy.']; @@ -820,7 +821,7 @@ public function thereAreDummyObjectsWithEmbeddedDummyBoolean(int $nb, string $bo $bool = false; } else { $expected = ['true', 'false', '1', '0']; - throw new \InvalidArgumentException(sprintf('Invalid boolean value for "%s" property, expected one of ( "%s" )', $bool, implode('" | "', $expected))); + throw new InvalidArgumentException(sprintf('Invalid boolean value for "%s" property, expected one of ( "%s" )', $bool, implode('" | "', $expected))); } for ($i = 1; $i <= $nb; ++$i) { @@ -847,7 +848,7 @@ public function thereAreDummyObjectsWithRelationEmbeddedDummyBoolean(int $nb, st $bool = false; } else { $expected = ['true', 'false', '1', '0']; - throw new \InvalidArgumentException(sprintf('Invalid boolean value for "%s" property, expected one of ( "%s" )', $bool, implode('" | "', $expected))); + throw new InvalidArgumentException(sprintf('Invalid boolean value for "%s" property, expected one of ( "%s" )', $bool, implode('" | "', $expected))); } for ($i = 1; $i <= $nb; ++$i) { @@ -977,7 +978,7 @@ public function thereIsAFooEntityWithRelatedBars() $foo = $this->buildDummyCar(); $foo->setName('mustli'); $foo->setCanSell(true); - $foo->setAvailableAt(new \DateTime()); + $foo->setAvailableAt(new DateTime()); $this->manager->persist($foo); $bar1 = $this->buildDummyCarColor(); @@ -1078,7 +1079,7 @@ public function thePasswordForUserShouldBeHashed(string $password, string $user) { $user = $this->doctrine->getRepository($this->isOrm() ? User::class : UserDocument::class)->find($user); if (!$this->passwordEncoder->isPasswordValid($user, $password)) { - throw new \Exception('User password mismatch'); + throw new Exception('User password mismatch'); } } @@ -1143,7 +1144,7 @@ public function createPeopleWithPets() public function thereAreDummyDateObjectsWithDummyDate(int $nb) { for ($i = 1; $i <= $nb; ++$i) { - $date = new \DateTime(sprintf('2015-04-%d', $i), new \DateTimeZone('UTC')); + $date = new DateTime(sprintf('2015-04-%d', $i), new DateTimeZone('UTC')); $dummy = $this->buildDummyDate(); $dummy->dummyDate = $date; @@ -1161,7 +1162,7 @@ public function thereAreDummyDateObjectsWithDummyDate(int $nb) public function thereAreDummyDateObjectsWithNullableDateIncludeNullAfter(int $nb) { for ($i = 1; $i <= $nb; ++$i) { - $date = new \DateTime(sprintf('2015-04-%d', $i), new \DateTimeZone('UTC')); + $date = new DateTime(sprintf('2015-04-%d', $i), new DateTimeZone('UTC')); $dummy = $this->buildDummyDate(); $dummy->dummyDate = $date; @@ -1180,7 +1181,7 @@ public function thereAreDummyDateObjectsWithNullableDateIncludeNullAfter(int $nb public function thereAreDummyDateObjectsWithNullableDateIncludeNullBefore(int $nb) { for ($i = 1; $i <= $nb; ++$i) { - $date = new \DateTime(sprintf('2015-04-%d', $i), new \DateTimeZone('UTC')); + $date = new DateTime(sprintf('2015-04-%d', $i), new DateTimeZone('UTC')); $dummy = $this->buildDummyDate(); $dummy->dummyDate = $date; @@ -1199,7 +1200,7 @@ public function thereAreDummyDateObjectsWithNullableDateIncludeNullBefore(int $n public function thereAreDummyDateObjectsWithNullableDateIncludeNullBeforeAndAfter(int $nb) { for ($i = 1; $i <= $nb; ++$i) { - $date = new \DateTime(sprintf('2015-04-%d', $i), new \DateTimeZone('UTC')); + $date = new DateTime(sprintf('2015-04-%d', $i), new DateTimeZone('UTC')); $dummy = $this->buildDummyDate(); $dummy->dummyDate = $date; @@ -1217,7 +1218,7 @@ public function thereAreDummyDateObjectsWithNullableDateIncludeNullBeforeAndAfte public function thereAreDummyImmutableDateObjectsWithDummyDate(int $nb) { for ($i = 1; $i <= $nb; ++$i) { - $date = new \DateTimeImmutable(sprintf('2015-04-%d', $i), new \DateTimeZone('UTC')); + $date = new DateTimeImmutable(sprintf('2015-04-%d', $i), new DateTimeZone('UTC')); $dummy = new DummyImmutableDate(); $dummy->dummyDate = $date; @@ -1393,6 +1394,19 @@ public function thereIsADummyDtoOutputSameClass() $this->manager->clear(); } + /** + * @Given there is a DummyDtoOutputFallbackToSameClass + */ + public function thereIsADummyDtoOutputFallbackToSameClass() + { + $dto = new DummyDtoOutputFallbackToSameClass(); + $dto->lorem = 'test'; + $dto->ipsum = '1'; + $this->manager->persist($dto); + $this->manager->flush(); + $this->manager->clear(); + } + /** * @Given there is an order with same customer and recipient */ diff --git a/features/jsonld/input_output.feature b/features/jsonld/input_output.feature index 1bc4ab36c27..90ed4e38944 100644 --- a/features/jsonld/input_output.feature +++ b/features/jsonld/input_output.feature @@ -102,6 +102,25 @@ Feature: JSON-LD DTO input and output } """ + @createSchema + Scenario: Get an item with a data transformer that will return the original class as a fallback + Given there is a DummyDtoOutputFallbackToSameClass + When I send a "GET" request to "/dummy_dto_output_fallback_to_same_classes/1" + Then the response status code should be 200 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the JSON should be equal to: + """ + { + "@context": "/contexts/DummyDtoOutputFallbackToSameClass", + "@id": "/dummy_dto_output_fallback_to_same_classes/1", + "@type": "DummyDtoOutputFallbackToSameClass", + "lorem": "test", + "ipsum": "1", + "id": 1 + } + """ + @createSchema Scenario: Create a DummyDtoCustom object without output When I send a "POST" request to "/dummy_dto_custom_post_without_output" with body: diff --git a/tests/Fixtures/TestBundle/DataTransformer/OutputDtoUnmodifiedDataTransformer.php b/tests/Fixtures/TestBundle/DataTransformer/OutputDtoUnmodifiedDataTransformer.php new file mode 100644 index 00000000000..c4bf9d4a136 --- /dev/null +++ b/tests/Fixtures/TestBundle/DataTransformer/OutputDtoUnmodifiedDataTransformer.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\DataTransformer; + +use ApiPlatform\Core\DataTransformer\DataTransformerInterface; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\OutputDtoDummy; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyDtoOutputFallbackToSameClass; + +/** + * OutputDtoUnmodifiedDataTransformer + * + * @author Daniel West + */ +final class OutputDtoUnmodifiedDataTransformer implements DataTransformerInterface +{ + /** + * {@inheritdoc} + */ + public function transform($object, string $to, array $context = []) + { + return $object; + } + + /** + * {@inheritdoc} + */ + public function supportsTransformation($data, string $to, array $context = []): bool + { + return $data instanceof DummyDtoOutputFallbackToSameClass && OutputDtoDummy::class === $to; + } +} diff --git a/tests/Fixtures/TestBundle/Dto/OutputDtoDummy.php b/tests/Fixtures/TestBundle/Dto/OutputDtoDummy.php new file mode 100644 index 00000000000..262d4781132 --- /dev/null +++ b/tests/Fixtures/TestBundle/Dto/OutputDtoDummy.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto; + +/** + * OutputDtoDummy + * + * @author Daniel West + */ +class OutputDtoDummy +{} diff --git a/tests/Fixtures/TestBundle/Entity/DummyDtoOutputFallbackToSameClass.php b/tests/Fixtures/TestBundle/Entity/DummyDtoOutputFallbackToSameClass.php new file mode 100644 index 00000000000..c7e37206b69 --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/DummyDtoOutputFallbackToSameClass.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity; + +use ApiPlatform\Core\Annotation\ApiResource; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\OutputDtoDummy; +use Doctrine\ORM\Mapping as ORM; + +/** + * Dummy InputOutput. + * + * @author Kévin Dunglas + * + * @ApiResource(attributes={"output"=OutputDtoDummy::class}) + * @ORM\Entity + */ +class DummyDtoOutputFallbackToSameClass +{ + /** + * @var int The id + * + * @ORM\Column(type="integer") + * @ORM\Id + * @ORM\GeneratedValue(strategy="AUTO") + */ + private $id; + + /** + * @var string + * + * @ORM\Column + */ + public $lorem; + + /** + * @var string + * + * @ORM\Column + */ + public $ipsum; + + public function getId() + { + return $this->id; + } +} From 27e545b74c1996e7b2eae3843e3337d3da28b21c Mon Sep 17 00:00:00 2001 From: Daniel West Date: Fri, 3 Apr 2020 14:42:32 +0100 Subject: [PATCH 04/29] Use context variable to detect whether transformed When a DataTransformer returns the original resource as a fallback, a context key is set so when repeating the serialization/normalization we can populate the extended jsonld data --- src/JsonLd/Serializer/ItemNormalizer.php | 1 + src/Serializer/AbstractItemNormalizer.php | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/JsonLd/Serializer/ItemNormalizer.php b/src/JsonLd/Serializer/ItemNormalizer.php index 02ce3c61cb6..117be69f20a 100644 --- a/src/JsonLd/Serializer/ItemNormalizer.php +++ b/src/JsonLd/Serializer/ItemNormalizer.php @@ -65,6 +65,7 @@ public function supportsNormalization($data, $format = null): bool */ public function normalize($object, $format = null, array $context = []) { + $objectClass = $this->getObjectClass($object); $outputClass = $this->getOutputClass($objectClass, $context); if (null !== $outputClass && !isset($context[self::IS_TRANSFORMED_TO_SAME_CLASS_CONTEXT_KEY])) { diff --git a/src/Serializer/AbstractItemNormalizer.php b/src/Serializer/AbstractItemNormalizer.php index a1f71f5d9a2..449da118b74 100644 --- a/src/Serializer/AbstractItemNormalizer.php +++ b/src/Serializer/AbstractItemNormalizer.php @@ -114,6 +114,7 @@ public function hasCacheableSupportsMethod(): bool */ public function normalize($object, $format = null, array $context = []) { + if (!($isTransformed = isset($context[self::IS_TRANSFORMED_TO_SAME_CLASS_CONTEXT_KEY])) && $outputClass = $this->getOutputClass($this->getObjectClass($object), $context)) { if (!$this->serializer instanceof NormalizerInterface) { throw new LogicException('Cannot normalize the output because the injected serializer is not a normalizer'); @@ -645,6 +646,7 @@ protected function getDataTransformer($data, string $to, array $context = []): ? * For a given resource, it returns an output representation if any * If not, the resource is returned. */ + protected function transformOutput($object, array $context = [], string $outputClass = null) { if (null === $outputClass) { From 1acad918ce03bfa0b44d2c2f4d9da0279a5b7eed Mon Sep 17 00:00:00 2001 From: Daniel West Date: Fri, 3 Apr 2020 14:53:21 +0100 Subject: [PATCH 05/29] Optimise serialization if output dto defined as same class Previous push reverted a change which is beneficial to performance. Instead of re-looping over the serializer/normalizers, if the DTO output class is defined as the same as the resource, we can detect this in the 1st round of serialization --- src/JsonLd/Serializer/ItemNormalizer.php | 2 +- src/Serializer/AbstractItemNormalizer.php | 12 +++--------- .../OutputDtoUnmodifiedDataTransformer.php | 2 +- tests/Fixtures/TestBundle/Dto/OutputDtoDummy.php | 5 +++-- 4 files changed, 8 insertions(+), 13 deletions(-) diff --git a/src/JsonLd/Serializer/ItemNormalizer.php b/src/JsonLd/Serializer/ItemNormalizer.php index 117be69f20a..1ebb2038dea 100644 --- a/src/JsonLd/Serializer/ItemNormalizer.php +++ b/src/JsonLd/Serializer/ItemNormalizer.php @@ -65,9 +65,9 @@ public function supportsNormalization($data, $format = null): bool */ public function normalize($object, $format = null, array $context = []) { - $objectClass = $this->getObjectClass($object); $outputClass = $this->getOutputClass($objectClass, $context); + if (null !== $outputClass && !isset($context[self::IS_TRANSFORMED_TO_SAME_CLASS_CONTEXT_KEY])) { return parent::normalize($object, $format, $context); } diff --git a/src/Serializer/AbstractItemNormalizer.php b/src/Serializer/AbstractItemNormalizer.php index 449da118b74..ffd3fd3597f 100644 --- a/src/Serializer/AbstractItemNormalizer.php +++ b/src/Serializer/AbstractItemNormalizer.php @@ -92,7 +92,7 @@ public function __construct(PropertyNameCollectionFactoryInterface $propertyName */ public function supportsNormalization($data, $format = null) { - if (!\is_object($data)) { + if (!\is_object($data) || $data instanceof \Traversable) { return false; } @@ -114,13 +114,12 @@ public function hasCacheableSupportsMethod(): bool */ public function normalize($object, $format = null, array $context = []) { - if (!($isTransformed = isset($context[self::IS_TRANSFORMED_TO_SAME_CLASS_CONTEXT_KEY])) && $outputClass = $this->getOutputClass($this->getObjectClass($object), $context)) { if (!$this->serializer instanceof NormalizerInterface) { throw new LogicException('Cannot normalize the output because the injected serializer is not a normalizer'); } - if ($object !== $transformed = $this->transformOutput($object, $context, $outputClass)) { + if ($object !== $transformed = $this->transformOutput($object, $outputClass, $context)) { $context['api_normalize'] = true; $context['api_resource'] = $object; unset($context['output'], $context['resource_class']); @@ -646,13 +645,8 @@ protected function getDataTransformer($data, string $to, array $context = []): ? * For a given resource, it returns an output representation if any * If not, the resource is returned. */ - - protected function transformOutput($object, array $context = [], string $outputClass = null) + protected function transformOutput($object, string $outputClass, array $context = []) { - if (null === $outputClass) { - $outputClass = $this->getOutputClass($this->getObjectClass($object), $context); - } - if (null !== $outputClass && null !== $dataTransformer = $this->getDataTransformer($object, $outputClass, $context)) { return $dataTransformer->transform($object, $outputClass, $context); } diff --git a/tests/Fixtures/TestBundle/DataTransformer/OutputDtoUnmodifiedDataTransformer.php b/tests/Fixtures/TestBundle/DataTransformer/OutputDtoUnmodifiedDataTransformer.php index c4bf9d4a136..2db33f74828 100644 --- a/tests/Fixtures/TestBundle/DataTransformer/OutputDtoUnmodifiedDataTransformer.php +++ b/tests/Fixtures/TestBundle/DataTransformer/OutputDtoUnmodifiedDataTransformer.php @@ -18,7 +18,7 @@ use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyDtoOutputFallbackToSameClass; /** - * OutputDtoUnmodifiedDataTransformer + * OutputDtoUnmodifiedDataTransformer. * * @author Daniel West */ diff --git a/tests/Fixtures/TestBundle/Dto/OutputDtoDummy.php b/tests/Fixtures/TestBundle/Dto/OutputDtoDummy.php index 262d4781132..78bf2998742 100644 --- a/tests/Fixtures/TestBundle/Dto/OutputDtoDummy.php +++ b/tests/Fixtures/TestBundle/Dto/OutputDtoDummy.php @@ -14,9 +14,10 @@ namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto; /** - * OutputDtoDummy + * OutputDtoDummy. * * @author Daniel West */ class OutputDtoDummy -{} +{ +} From 4b0b05077c840ea02c34d804ee23d39879bd2593 Mon Sep 17 00:00:00 2001 From: Daniel West Date: Fri, 3 Apr 2020 15:00:39 +0100 Subject: [PATCH 06/29] Add property to OutputDtoDummy to prevent tests failing with unsupported class from PropertyInfo extractor --- tests/Fixtures/TestBundle/Dto/OutputDtoDummy.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Fixtures/TestBundle/Dto/OutputDtoDummy.php b/tests/Fixtures/TestBundle/Dto/OutputDtoDummy.php index 78bf2998742..3f319e4d987 100644 --- a/tests/Fixtures/TestBundle/Dto/OutputDtoDummy.php +++ b/tests/Fixtures/TestBundle/Dto/OutputDtoDummy.php @@ -20,4 +20,5 @@ */ class OutputDtoDummy { + public $foo = 'foo'; } From d5d1104eb6f8be9c82abd09d00acd2f3067d2d1d Mon Sep 17 00:00:00 2001 From: Daniel West Date: Fri, 3 Apr 2020 15:16:12 +0100 Subject: [PATCH 07/29] Add ODM docs for tests --- .../DummyDtoOutputFallbackToSameClass.php | 57 +++++++++++++++++++ .../Document/DummyDtoOutputSameClass.php | 56 ++++++++++++++++++ 2 files changed, 113 insertions(+) create mode 100644 tests/Fixtures/TestBundle/Document/DummyDtoOutputFallbackToSameClass.php create mode 100644 tests/Fixtures/TestBundle/Document/DummyDtoOutputSameClass.php diff --git a/tests/Fixtures/TestBundle/Document/DummyDtoOutputFallbackToSameClass.php b/tests/Fixtures/TestBundle/Document/DummyDtoOutputFallbackToSameClass.php new file mode 100644 index 00000000000..01cf53b164b --- /dev/null +++ b/tests/Fixtures/TestBundle/Document/DummyDtoOutputFallbackToSameClass.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Document; + +use ApiPlatform\Core\Annotation\ApiResource; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\OutputDtoDummy; +use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; + +/** + * Dummy InputOutput. + * + * @author Kévin Dunglas + * + * @ApiResource(attributes={"output"=OutputDtoDummy::class}) + * @ODM\Document + */ +class DummyDtoOutputFallbackToSameClass +{ + /** + * @var int The id + * + * @ORM\Column(type="integer") + * @ORM\Id + * @ORM\GeneratedValue(strategy="AUTO") + */ + private $id; + + /** + * @var string + * + * @ORM\Column + */ + public $lorem; + + /** + * @var string + * + * @ORM\Column + */ + public $ipsum; + + public function getId() + { + return $this->id; + } +} diff --git a/tests/Fixtures/TestBundle/Document/DummyDtoOutputSameClass.php b/tests/Fixtures/TestBundle/Document/DummyDtoOutputSameClass.php new file mode 100644 index 00000000000..d12b5c787f5 --- /dev/null +++ b/tests/Fixtures/TestBundle/Document/DummyDtoOutputSameClass.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Document; + +use ApiPlatform\Core\Annotation\ApiResource; +use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; + +/** + * Dummy InputOutput. + * + * @author Kévin Dunglas + * + * @ApiResource(attributes={"output"=DummyDtoOutputSameClass::class}) + * @ODM\Document + */ +class DummyDtoOutputSameClass +{ + /** + * @var int The id + * + * @ORM\Column(type="integer") + * @ORM\Id + * @ORM\GeneratedValue(strategy="AUTO") + */ + private $id; + + /** + * @var string + * + * @ORM\Column + */ + public $lorem; + + /** + * @var string + * + * @ORM\Column + */ + public $ipsum; + + public function getId() + { + return $this->id; + } +} From f6703b062923122ff1ef42ef8d17d19954477c08 Mon Sep 17 00:00:00 2001 From: Daniel West Date: Fri, 3 Apr 2020 15:16:12 +0100 Subject: [PATCH 08/29] Add ODM docs for tests --- .../Document/DummyDtoOutputFallbackToSameClass.php | 12 ++++++++++++ .../TestBundle/Document/DummyDtoOutputSameClass.php | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/tests/Fixtures/TestBundle/Document/DummyDtoOutputFallbackToSameClass.php b/tests/Fixtures/TestBundle/Document/DummyDtoOutputFallbackToSameClass.php index 01cf53b164b..22d33de997e 100644 --- a/tests/Fixtures/TestBundle/Document/DummyDtoOutputFallbackToSameClass.php +++ b/tests/Fixtures/TestBundle/Document/DummyDtoOutputFallbackToSameClass.php @@ -30,23 +30,35 @@ class DummyDtoOutputFallbackToSameClass /** * @var int The id * +<<<<<<< HEAD * @ORM\Column(type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") +======= + * @ODM\Id(strategy="INCREMENT", type="integer", nullable=true) +>>>>>>> Add ODM docs for tests */ private $id; /** * @var string * +<<<<<<< HEAD * @ORM\Column +======= + * @ODM\Field +>>>>>>> Add ODM docs for tests */ public $lorem; /** * @var string * +<<<<<<< HEAD * @ORM\Column +======= + * @ODM\Field +>>>>>>> Add ODM docs for tests */ public $ipsum; diff --git a/tests/Fixtures/TestBundle/Document/DummyDtoOutputSameClass.php b/tests/Fixtures/TestBundle/Document/DummyDtoOutputSameClass.php index d12b5c787f5..02323cf5f02 100644 --- a/tests/Fixtures/TestBundle/Document/DummyDtoOutputSameClass.php +++ b/tests/Fixtures/TestBundle/Document/DummyDtoOutputSameClass.php @@ -29,23 +29,35 @@ class DummyDtoOutputSameClass /** * @var int The id * +<<<<<<< HEAD * @ORM\Column(type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") +======= + * @ODM\Id(strategy="INCREMENT", type="integer", nullable=true) +>>>>>>> Add ODM docs for tests */ private $id; /** * @var string * +<<<<<<< HEAD * @ORM\Column +======= + * @ODM\Field +>>>>>>> Add ODM docs for tests */ public $lorem; /** * @var string * +<<<<<<< HEAD * @ORM\Column +======= + * @ODM\Field +>>>>>>> Add ODM docs for tests */ public $ipsum; From 658f925afbcf433f8ea6d603e4ffbf3cb9383dc3 Mon Sep 17 00:00:00 2001 From: Daniel West Date: Fri, 3 Apr 2020 15:48:30 +0100 Subject: [PATCH 09/29] Behat Doctrine Context use document instead of ORM entity if running mongo tests --- features/bootstrap/DoctrineContext.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/features/bootstrap/DoctrineContext.php b/features/bootstrap/DoctrineContext.php index 4ed58c18248..17d537be209 100644 --- a/features/bootstrap/DoctrineContext.php +++ b/features/bootstrap/DoctrineContext.php @@ -35,6 +35,8 @@ use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\DummyDtoCustom as DummyDtoCustomDocument; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\DummyDtoNoInput as DummyDtoNoInputDocument; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\DummyDtoNoOutput as DummyDtoNoOutputDocument; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\DummyDtoOutputFallbackToSameClass as DummyDtoOutputFallbackToSameClassDocument; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\DummyDtoOutputSameClass as DummyDtoOutputSameClassDocument; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\DummyFriend as DummyFriendDocument; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\DummyGroup as DummyGroupDocument; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\DummyOffer as DummyOfferDocument; @@ -1386,7 +1388,7 @@ public function thereAreNbDummyDtoCustom($nb) */ public function thereIsADummyDtoOutputSameClass() { - $dto = new DummyDtoOutputSameClass(); + $dto = $this->isOrm() ? new DummyDtoOutputSameClass() : new DummyDtoOutputSameClassDocument(); $dto->lorem = 'test'; $dto->ipsum = '1'; $this->manager->persist($dto); @@ -1399,7 +1401,7 @@ public function thereIsADummyDtoOutputSameClass() */ public function thereIsADummyDtoOutputFallbackToSameClass() { - $dto = new DummyDtoOutputFallbackToSameClass(); + $dto = $this->isOrm() ? new DummyDtoOutputFallbackToSameClass() : new DummyDtoOutputFallbackToSameClassDocument(); $dto->lorem = 'test'; $dto->ipsum = '1'; $this->manager->persist($dto); From c5425e2303974a8f5728b20e2bba71b0a43795eb Mon Sep 17 00:00:00 2001 From: Daniel West Date: Tue, 7 Apr 2020 12:04:20 +0100 Subject: [PATCH 10/29] Transformer should still be called if the same class is returned --- src/JsonLd/Serializer/ItemNormalizer.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/JsonLd/Serializer/ItemNormalizer.php b/src/JsonLd/Serializer/ItemNormalizer.php index 1ebb2038dea..02ce3c61cb6 100644 --- a/src/JsonLd/Serializer/ItemNormalizer.php +++ b/src/JsonLd/Serializer/ItemNormalizer.php @@ -67,7 +67,6 @@ public function normalize($object, $format = null, array $context = []) { $objectClass = $this->getObjectClass($object); $outputClass = $this->getOutputClass($objectClass, $context); - if (null !== $outputClass && !isset($context[self::IS_TRANSFORMED_TO_SAME_CLASS_CONTEXT_KEY])) { return parent::normalize($object, $format, $context); } From 0bb0bb53d000428f7560dbab7edad66bd46d4e7e Mon Sep 17 00:00:00 2001 From: Daniel West Date: Tue, 7 Apr 2020 12:30:04 +0100 Subject: [PATCH 11/29] Improve test so that the DTO can fallback to the same class but with some modified data if needed --- features/jsonld/input_output.feature | 2 +- src/JsonLd/Serializer/ItemNormalizer.php | 1 + ...ormer.php => OutputDtoFallbackSameClassTransformer.php} | 7 ++++++- tests/Fixtures/app/config/config_common.yml | 6 ++++++ 4 files changed, 14 insertions(+), 2 deletions(-) rename tests/Fixtures/TestBundle/DataTransformer/{OutputDtoUnmodifiedDataTransformer.php => OutputDtoFallbackSameClassTransformer.php} (80%) diff --git a/features/jsonld/input_output.feature b/features/jsonld/input_output.feature index 90ed4e38944..835a9c33913 100644 --- a/features/jsonld/input_output.feature +++ b/features/jsonld/input_output.feature @@ -116,7 +116,7 @@ Feature: JSON-LD DTO input and output "@id": "/dummy_dto_output_fallback_to_same_classes/1", "@type": "DummyDtoOutputFallbackToSameClass", "lorem": "test", - "ipsum": "1", + "ipsum": "modified", "id": 1 } """ diff --git a/src/JsonLd/Serializer/ItemNormalizer.php b/src/JsonLd/Serializer/ItemNormalizer.php index 02ce3c61cb6..1ebb2038dea 100644 --- a/src/JsonLd/Serializer/ItemNormalizer.php +++ b/src/JsonLd/Serializer/ItemNormalizer.php @@ -67,6 +67,7 @@ public function normalize($object, $format = null, array $context = []) { $objectClass = $this->getObjectClass($object); $outputClass = $this->getOutputClass($objectClass, $context); + if (null !== $outputClass && !isset($context[self::IS_TRANSFORMED_TO_SAME_CLASS_CONTEXT_KEY])) { return parent::normalize($object, $format, $context); } diff --git a/tests/Fixtures/TestBundle/DataTransformer/OutputDtoUnmodifiedDataTransformer.php b/tests/Fixtures/TestBundle/DataTransformer/OutputDtoFallbackSameClassTransformer.php similarity index 80% rename from tests/Fixtures/TestBundle/DataTransformer/OutputDtoUnmodifiedDataTransformer.php rename to tests/Fixtures/TestBundle/DataTransformer/OutputDtoFallbackSameClassTransformer.php index 2db33f74828..631d645d8c7 100644 --- a/tests/Fixtures/TestBundle/DataTransformer/OutputDtoUnmodifiedDataTransformer.php +++ b/tests/Fixtures/TestBundle/DataTransformer/OutputDtoFallbackSameClassTransformer.php @@ -22,13 +22,18 @@ * * @author Daniel West */ -final class OutputDtoUnmodifiedDataTransformer implements DataTransformerInterface +final class OutputDtoFallbackSameClassTransformer implements DataTransformerInterface { /** * {@inheritdoc} */ public function transform($object, string $to, array $context = []) { + if (!$object instanceof DummyDtoOutputFallbackToSameClass) { + throw new \InvalidArgumentException(); + } + $object->ipsum = 'modified'; + return $object; } diff --git a/tests/Fixtures/app/config/config_common.yml b/tests/Fixtures/app/config/config_common.yml index e4c861cbb4a..bc898f0d090 100644 --- a/tests/Fixtures/app/config/config_common.yml +++ b/tests/Fixtures/app/config/config_common.yml @@ -226,6 +226,12 @@ services: tags: - { name: 'api_platform.data_transformer' } + app.data_transformer.custom_output_dto_fallback_same_class: + class: 'ApiPlatform\Core\Tests\Fixtures\TestBundle\DataTransformer\OutputDtoFallbackSameClassTransformer' + public: false + tags: + - { name: 'api_platform.data_transformer' } + app.data_transformer.input_dto: class: 'ApiPlatform\Core\Tests\Fixtures\TestBundle\DataTransformer\InputDtoDataTransformer' public: false From 693b321bad3a2a05131673afd7b96aabba18fab2 Mon Sep 17 00:00:00 2001 From: Daniel West Date: Tue, 7 Apr 2020 12:54:24 +0100 Subject: [PATCH 12/29] Improve test, ensure transformer is always called if output class is set no matter how the same class/object is returned --- features/jsonld/input_output.feature | 2 +- src/JsonLd/Serializer/ItemNormalizer.php | 1 - ...sTransformer.php => OutputDtoSameClassTransformer.php} | 8 +++++--- tests/Fixtures/app/config/config_common.yml | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) rename tests/Fixtures/TestBundle/DataTransformer/{OutputDtoFallbackSameClassTransformer.php => OutputDtoSameClassTransformer.php} (72%) diff --git a/features/jsonld/input_output.feature b/features/jsonld/input_output.feature index 835a9c33913..256eb610a22 100644 --- a/features/jsonld/input_output.feature +++ b/features/jsonld/input_output.feature @@ -97,7 +97,7 @@ Feature: JSON-LD DTO input and output "@id": "/dummy_dto_output_same_classes/1", "@type": "DummyDtoOutputSameClass", "lorem": "test", - "ipsum": "1", + "ipsum": "modified", "id": 1 } """ diff --git a/src/JsonLd/Serializer/ItemNormalizer.php b/src/JsonLd/Serializer/ItemNormalizer.php index 1ebb2038dea..02ce3c61cb6 100644 --- a/src/JsonLd/Serializer/ItemNormalizer.php +++ b/src/JsonLd/Serializer/ItemNormalizer.php @@ -67,7 +67,6 @@ public function normalize($object, $format = null, array $context = []) { $objectClass = $this->getObjectClass($object); $outputClass = $this->getOutputClass($objectClass, $context); - if (null !== $outputClass && !isset($context[self::IS_TRANSFORMED_TO_SAME_CLASS_CONTEXT_KEY])) { return parent::normalize($object, $format, $context); } diff --git a/tests/Fixtures/TestBundle/DataTransformer/OutputDtoFallbackSameClassTransformer.php b/tests/Fixtures/TestBundle/DataTransformer/OutputDtoSameClassTransformer.php similarity index 72% rename from tests/Fixtures/TestBundle/DataTransformer/OutputDtoFallbackSameClassTransformer.php rename to tests/Fixtures/TestBundle/DataTransformer/OutputDtoSameClassTransformer.php index 631d645d8c7..18d6bdee4f6 100644 --- a/tests/Fixtures/TestBundle/DataTransformer/OutputDtoFallbackSameClassTransformer.php +++ b/tests/Fixtures/TestBundle/DataTransformer/OutputDtoSameClassTransformer.php @@ -16,20 +16,21 @@ use ApiPlatform\Core\DataTransformer\DataTransformerInterface; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\OutputDtoDummy; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyDtoOutputFallbackToSameClass; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyDtoOutputSameClass; /** * OutputDtoUnmodifiedDataTransformer. * * @author Daniel West */ -final class OutputDtoFallbackSameClassTransformer implements DataTransformerInterface +final class OutputDtoSameClassTransformer implements DataTransformerInterface { /** * {@inheritdoc} */ public function transform($object, string $to, array $context = []) { - if (!$object instanceof DummyDtoOutputFallbackToSameClass) { + if (!$object instanceof DummyDtoOutputFallbackToSameClass && !$object instanceof DummyDtoOutputSameClass) { throw new \InvalidArgumentException(); } $object->ipsum = 'modified'; @@ -42,6 +43,7 @@ public function transform($object, string $to, array $context = []) */ public function supportsTransformation($data, string $to, array $context = []): bool { - return $data instanceof DummyDtoOutputFallbackToSameClass && OutputDtoDummy::class === $to; + return ($data instanceof DummyDtoOutputFallbackToSameClass && OutputDtoDummy::class === $to) || + ($data instanceof DummyDtoOutputSameClass && DummyDtoOutputSameClass::class === $to); } } diff --git a/tests/Fixtures/app/config/config_common.yml b/tests/Fixtures/app/config/config_common.yml index bc898f0d090..e3f614ad159 100644 --- a/tests/Fixtures/app/config/config_common.yml +++ b/tests/Fixtures/app/config/config_common.yml @@ -227,7 +227,7 @@ services: - { name: 'api_platform.data_transformer' } app.data_transformer.custom_output_dto_fallback_same_class: - class: 'ApiPlatform\Core\Tests\Fixtures\TestBundle\DataTransformer\OutputDtoFallbackSameClassTransformer' + class: 'ApiPlatform\Core\Tests\Fixtures\TestBundle\DataTransformer\OutputDtoSameClassTransformer' public: false tags: - { name: 'api_platform.data_transformer' } From 1dd21ed0f2567b60448d5d8eede9a3946d9ea76c Mon Sep 17 00:00:00 2001 From: Daniel West Date: Tue, 7 Apr 2020 16:43:33 +0100 Subject: [PATCH 13/29] Revert un-namespaced imports --- features/bootstrap/DoctrineContext.php | 30 +++++++++++++------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/features/bootstrap/DoctrineContext.php b/features/bootstrap/DoctrineContext.php index 17d537be209..a1ee0fd41b1 100644 --- a/features/bootstrap/DoctrineContext.php +++ b/features/bootstrap/DoctrineContext.php @@ -594,7 +594,7 @@ public function thereAreDummyObjectsWithDummyDate(int $nb) $descriptions = ['Smart dummy.', 'Not so smart dummy.']; for ($i = 1; $i <= $nb; ++$i) { - $date = new DateTime(sprintf('2015-04-%d', $i), new DateTimeZone('UTC')); + $date = new \DateTime(sprintf('2015-04-%d', $i), new \DateTimeZone('UTC')); $dummy = $this->buildDummy(); $dummy->setName('Dummy #'.$i); @@ -625,11 +625,11 @@ public function thereAreDummyObjectsWithDummyDateAndDummyBoolean(int $nb, string $bool = false; } else { $expected = ['true', 'false', '1', '0']; - throw new InvalidArgumentException(sprintf('Invalid boolean value for "%s" property, expected one of ( "%s" )', $bool, implode('" | "', $expected))); + throw new \InvalidArgumentException(sprintf('Invalid boolean value for "%s" property, expected one of ( "%s" )', $bool, implode('" | "', $expected))); } for ($i = 1; $i <= $nb; ++$i) { - $date = new DateTime(sprintf('2015-04-%d', $i), new DateTimeZone('UTC')); + $date = new \DateTime(sprintf('2015-04-%d', $i), new \DateTimeZone('UTC')); $dummy = $this->buildDummy(); $dummy->setName('Dummy #'.$i); @@ -654,7 +654,7 @@ public function thereAreDummyObjectsWithDummyDateAndDummyBoolean(int $nb, string public function thereAreDummyObjectsWithDummyDateAndRelatedDummy(int $nb) { for ($i = 1; $i <= $nb; ++$i) { - $date = new DateTime(sprintf('2015-04-%d', $i), new DateTimeZone('UTC')); + $date = new \DateTime(sprintf('2015-04-%d', $i), new \DateTimeZone('UTC')); $relatedDummy = $this->buildRelatedDummy(); $relatedDummy->setName('RelatedDummy #'.$i); @@ -682,7 +682,7 @@ public function thereAreDummyObjectsWithDummyDateAndRelatedDummy(int $nb) public function thereAreDummyObjectsWithDummyDateAndEmbeddedDummy(int $nb) { for ($i = 1; $i <= $nb; ++$i) { - $date = new DateTime(sprintf('2015-04-%d', $i), new DateTimeZone('UTC')); + $date = new \DateTime(sprintf('2015-04-%d', $i), new \DateTimeZone('UTC')); $embeddableDummy = $this->buildEmbeddableDummy(); $embeddableDummy->setDummyName('Embeddable #'.$i); @@ -709,7 +709,7 @@ 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')); + $convertedDate->nameConverted = new \DateTime(sprintf('2015-04-%d', $i), new \DateTimeZone('UTC')); $this->manager->persist($convertedDate); } @@ -795,7 +795,7 @@ public function thereAreDummyObjectsWithDummyBoolean(int $nb, string $bool) $bool = false; } else { $expected = ['true', 'false', '1', '0']; - throw new InvalidArgumentException(sprintf('Invalid boolean value for "%s" property, expected one of ( "%s" )', $bool, implode('" | "', $expected))); + throw new \InvalidArgumentException(sprintf('Invalid boolean value for "%s" property, expected one of ( "%s" )', $bool, implode('" | "', $expected))); } $descriptions = ['Smart dummy.', 'Not so smart dummy.']; @@ -823,7 +823,7 @@ public function thereAreDummyObjectsWithEmbeddedDummyBoolean(int $nb, string $bo $bool = false; } else { $expected = ['true', 'false', '1', '0']; - throw new InvalidArgumentException(sprintf('Invalid boolean value for "%s" property, expected one of ( "%s" )', $bool, implode('" | "', $expected))); + throw new \InvalidArgumentException(sprintf('Invalid boolean value for "%s" property, expected one of ( "%s" )', $bool, implode('" | "', $expected))); } for ($i = 1; $i <= $nb; ++$i) { @@ -850,7 +850,7 @@ public function thereAreDummyObjectsWithRelationEmbeddedDummyBoolean(int $nb, st $bool = false; } else { $expected = ['true', 'false', '1', '0']; - throw new InvalidArgumentException(sprintf('Invalid boolean value for "%s" property, expected one of ( "%s" )', $bool, implode('" | "', $expected))); + throw new \InvalidArgumentException(sprintf('Invalid boolean value for "%s" property, expected one of ( "%s" )', $bool, implode('" | "', $expected))); } for ($i = 1; $i <= $nb; ++$i) { @@ -980,7 +980,7 @@ public function thereIsAFooEntityWithRelatedBars() $foo = $this->buildDummyCar(); $foo->setName('mustli'); $foo->setCanSell(true); - $foo->setAvailableAt(new DateTime()); + $foo->setAvailableAt(new \DateTime()); $this->manager->persist($foo); $bar1 = $this->buildDummyCarColor(); @@ -1146,7 +1146,7 @@ public function createPeopleWithPets() public function thereAreDummyDateObjectsWithDummyDate(int $nb) { for ($i = 1; $i <= $nb; ++$i) { - $date = new DateTime(sprintf('2015-04-%d', $i), new DateTimeZone('UTC')); + $date = new \DateTime(sprintf('2015-04-%d', $i), new \DateTimeZone('UTC')); $dummy = $this->buildDummyDate(); $dummy->dummyDate = $date; @@ -1164,7 +1164,7 @@ public function thereAreDummyDateObjectsWithDummyDate(int $nb) public function thereAreDummyDateObjectsWithNullableDateIncludeNullAfter(int $nb) { for ($i = 1; $i <= $nb; ++$i) { - $date = new DateTime(sprintf('2015-04-%d', $i), new DateTimeZone('UTC')); + $date = new \DateTime(sprintf('2015-04-%d', $i), new \DateTimeZone('UTC')); $dummy = $this->buildDummyDate(); $dummy->dummyDate = $date; @@ -1183,7 +1183,7 @@ public function thereAreDummyDateObjectsWithNullableDateIncludeNullAfter(int $nb public function thereAreDummyDateObjectsWithNullableDateIncludeNullBefore(int $nb) { for ($i = 1; $i <= $nb; ++$i) { - $date = new DateTime(sprintf('2015-04-%d', $i), new DateTimeZone('UTC')); + $date = new \DateTime(sprintf('2015-04-%d', $i), new \DateTimeZone('UTC')); $dummy = $this->buildDummyDate(); $dummy->dummyDate = $date; @@ -1202,7 +1202,7 @@ public function thereAreDummyDateObjectsWithNullableDateIncludeNullBefore(int $n public function thereAreDummyDateObjectsWithNullableDateIncludeNullBeforeAndAfter(int $nb) { for ($i = 1; $i <= $nb; ++$i) { - $date = new DateTime(sprintf('2015-04-%d', $i), new DateTimeZone('UTC')); + $date = new \DateTime(sprintf('2015-04-%d', $i), new \DateTimeZone('UTC')); $dummy = $this->buildDummyDate(); $dummy->dummyDate = $date; @@ -1220,7 +1220,7 @@ public function thereAreDummyDateObjectsWithNullableDateIncludeNullBeforeAndAfte public function thereAreDummyImmutableDateObjectsWithDummyDate(int $nb) { for ($i = 1; $i <= $nb; ++$i) { - $date = new DateTimeImmutable(sprintf('2015-04-%d', $i), new DateTimeZone('UTC')); + $date = new \DateTimeImmutable(sprintf('2015-04-%d', $i), new \DateTimeZone('UTC')); $dummy = new DummyImmutableDate(); $dummy->dummyDate = $date; From e2182be2d4c719a9e966d1bd9b74d05b539b637f Mon Sep 17 00:00:00 2001 From: Daniel West Date: Thu, 9 Apr 2020 11:00:51 +0100 Subject: [PATCH 14/29] Test DataTransformer for same class should support Documents as well as Entities --- .../OutputDtoSameClassTransformer.php | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/tests/Fixtures/TestBundle/DataTransformer/OutputDtoSameClassTransformer.php b/tests/Fixtures/TestBundle/DataTransformer/OutputDtoSameClassTransformer.php index 18d6bdee4f6..00a9ad57189 100644 --- a/tests/Fixtures/TestBundle/DataTransformer/OutputDtoSameClassTransformer.php +++ b/tests/Fixtures/TestBundle/DataTransformer/OutputDtoSameClassTransformer.php @@ -14,6 +14,8 @@ namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\DataTransformer; use ApiPlatform\Core\DataTransformer\DataTransformerInterface; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\DummyDtoOutputFallbackToSameClass as DummyDtoOutputFallbackToSameClassDocument; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\DummyDtoOutputSameClass as DummyDtoOutputSameClassDocument; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\OutputDtoDummy; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyDtoOutputFallbackToSameClass; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyDtoOutputSameClass; @@ -30,7 +32,12 @@ final class OutputDtoSameClassTransformer implements DataTransformerInterface */ public function transform($object, string $to, array $context = []) { - if (!$object instanceof DummyDtoOutputFallbackToSameClass && !$object instanceof DummyDtoOutputSameClass) { + if ( + !$object instanceof DummyDtoOutputFallbackToSameClass && + !$object instanceof DummyDtoOutputFallbackToSameClassDocument && + !$object instanceof DummyDtoOutputSameClass && + !$object instanceof DummyDtoOutputSameClassDocument + ) { throw new \InvalidArgumentException(); } $object->ipsum = 'modified'; @@ -43,7 +50,7 @@ public function transform($object, string $to, array $context = []) */ public function supportsTransformation($data, string $to, array $context = []): bool { - return ($data instanceof DummyDtoOutputFallbackToSameClass && OutputDtoDummy::class === $to) || - ($data instanceof DummyDtoOutputSameClass && DummyDtoOutputSameClass::class === $to); + return (($data instanceof DummyDtoOutputFallbackToSameClass || $data instanceof DummyDtoOutputFallbackToSameClassDocument) && OutputDtoDummy::class === $to) || + (($data instanceof DummyDtoOutputSameClass || $data instanceof DummyDtoOutputSameClassDocument) && DummyDtoOutputSameClass::class === $to); } } From ca8f6f78faf36e05206dca42fad4a601556eb00b Mon Sep 17 00:00:00 2001 From: Daniel West Date: Mon, 20 Apr 2020 13:13:02 +0100 Subject: [PATCH 15/29] Apply review comments - Prevent BC change on AbstractItemNormalizer::transformOutput - Remove useless documentation --- src/Serializer/AbstractItemNormalizer.php | 8 ++++++-- .../DataTransformer/OutputDtoSameClassTransformer.php | 2 -- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Serializer/AbstractItemNormalizer.php b/src/Serializer/AbstractItemNormalizer.php index ffd3fd3597f..e49e1b00052 100644 --- a/src/Serializer/AbstractItemNormalizer.php +++ b/src/Serializer/AbstractItemNormalizer.php @@ -119,7 +119,7 @@ public function normalize($object, $format = null, array $context = []) throw new LogicException('Cannot normalize the output because the injected serializer is not a normalizer'); } - if ($object !== $transformed = $this->transformOutput($object, $outputClass, $context)) { + if ($object !== $transformed = $this->transformOutput($object, $context, $outputClass)) { $context['api_normalize'] = true; $context['api_resource'] = $object; unset($context['output'], $context['resource_class']); @@ -645,8 +645,12 @@ protected function getDataTransformer($data, string $to, array $context = []): ? * For a given resource, it returns an output representation if any * If not, the resource is returned. */ - protected function transformOutput($object, string $outputClass, array $context = []) + protected function transformOutput($object, array $context = [], string $outputClass = null) { + if (null === $outputClass) { + $outputClass = $this->getOutputClass($this->getObjectClass($object), $context); + } + if (null !== $outputClass && null !== $dataTransformer = $this->getDataTransformer($object, $outputClass, $context)) { return $dataTransformer->transform($object, $outputClass, $context); } diff --git a/tests/Fixtures/TestBundle/DataTransformer/OutputDtoSameClassTransformer.php b/tests/Fixtures/TestBundle/DataTransformer/OutputDtoSameClassTransformer.php index 00a9ad57189..7a2844cbcb4 100644 --- a/tests/Fixtures/TestBundle/DataTransformer/OutputDtoSameClassTransformer.php +++ b/tests/Fixtures/TestBundle/DataTransformer/OutputDtoSameClassTransformer.php @@ -21,8 +21,6 @@ use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyDtoOutputSameClass; /** - * OutputDtoUnmodifiedDataTransformer. - * * @author Daniel West */ final class OutputDtoSameClassTransformer implements DataTransformerInterface From 09680fc10fab0b0ddc545e24549329e30dc11006 Mon Sep 17 00:00:00 2001 From: Daniel West Date: Mon, 20 Apr 2020 13:53:29 +0100 Subject: [PATCH 16/29] Fix errors in rebasing --- features/bootstrap/DoctrineContext.php | 2 +- src/Serializer/AbstractItemNormalizer.php | 2 +- .../Document/DummyDtoOutputFallbackToSameClass.php | 14 -------------- .../Document/DummyDtoOutputSameClass.php | 14 -------------- 4 files changed, 2 insertions(+), 30 deletions(-) diff --git a/features/bootstrap/DoctrineContext.php b/features/bootstrap/DoctrineContext.php index a1ee0fd41b1..da723f6e55c 100644 --- a/features/bootstrap/DoctrineContext.php +++ b/features/bootstrap/DoctrineContext.php @@ -1081,7 +1081,7 @@ public function thePasswordForUserShouldBeHashed(string $password, string $user) { $user = $this->doctrine->getRepository($this->isOrm() ? User::class : UserDocument::class)->find($user); if (!$this->passwordEncoder->isPasswordValid($user, $password)) { - throw new Exception('User password mismatch'); + throw new \Exception('User password mismatch'); } } diff --git a/src/Serializer/AbstractItemNormalizer.php b/src/Serializer/AbstractItemNormalizer.php index e49e1b00052..a1f71f5d9a2 100644 --- a/src/Serializer/AbstractItemNormalizer.php +++ b/src/Serializer/AbstractItemNormalizer.php @@ -92,7 +92,7 @@ public function __construct(PropertyNameCollectionFactoryInterface $propertyName */ public function supportsNormalization($data, $format = null) { - if (!\is_object($data) || $data instanceof \Traversable) { + if (!\is_object($data)) { return false; } diff --git a/tests/Fixtures/TestBundle/Document/DummyDtoOutputFallbackToSameClass.php b/tests/Fixtures/TestBundle/Document/DummyDtoOutputFallbackToSameClass.php index 22d33de997e..031e2887d0f 100644 --- a/tests/Fixtures/TestBundle/Document/DummyDtoOutputFallbackToSameClass.php +++ b/tests/Fixtures/TestBundle/Document/DummyDtoOutputFallbackToSameClass.php @@ -30,35 +30,21 @@ class DummyDtoOutputFallbackToSameClass /** * @var int The id * -<<<<<<< HEAD - * @ORM\Column(type="integer") - * @ORM\Id - * @ORM\GeneratedValue(strategy="AUTO") -======= * @ODM\Id(strategy="INCREMENT", type="integer", nullable=true) ->>>>>>> Add ODM docs for tests */ private $id; /** * @var string * -<<<<<<< HEAD - * @ORM\Column -======= * @ODM\Field ->>>>>>> Add ODM docs for tests */ public $lorem; /** * @var string * -<<<<<<< HEAD - * @ORM\Column -======= * @ODM\Field ->>>>>>> Add ODM docs for tests */ public $ipsum; diff --git a/tests/Fixtures/TestBundle/Document/DummyDtoOutputSameClass.php b/tests/Fixtures/TestBundle/Document/DummyDtoOutputSameClass.php index 02323cf5f02..b8e66071224 100644 --- a/tests/Fixtures/TestBundle/Document/DummyDtoOutputSameClass.php +++ b/tests/Fixtures/TestBundle/Document/DummyDtoOutputSameClass.php @@ -29,35 +29,21 @@ class DummyDtoOutputSameClass /** * @var int The id * -<<<<<<< HEAD - * @ORM\Column(type="integer") - * @ORM\Id - * @ORM\GeneratedValue(strategy="AUTO") -======= * @ODM\Id(strategy="INCREMENT", type="integer", nullable=true) ->>>>>>> Add ODM docs for tests */ private $id; /** * @var string * -<<<<<<< HEAD - * @ORM\Column -======= * @ODM\Field ->>>>>>> Add ODM docs for tests */ public $lorem; /** * @var string * -<<<<<<< HEAD - * @ORM\Column -======= * @ODM\Field ->>>>>>> Add ODM docs for tests */ public $ipsum; From b522a523543b3399e5db716ec9291cc354c30c3f Mon Sep 17 00:00:00 2001 From: soyuka Date: Thu, 16 Apr 2020 09:55:03 +0200 Subject: [PATCH 17/29] Fix ramsey uuid denormalization #3473 --- features/main/uuid.feature | 14 +++++++++ .../Serializer/UuidDenormalizer.php | 31 +++++++++++++++++++ .../Bundle/Resources/config/ramsey_uuid.xml | 4 +++ .../ApiPlatformExtensionTest.php | 1 + 4 files changed, 50 insertions(+) create mode 100644 src/Bridge/RamseyUuid/Serializer/UuidDenormalizer.php diff --git a/features/main/uuid.feature b/features/main/uuid.feature index ca5bc9ec9fc..9278cb15ee5 100644 --- a/features/main/uuid.feature +++ b/features/main/uuid.feature @@ -130,3 +130,17 @@ Feature: Using uuid identifier on resource Then the response status code should be 404 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + + @!mongodb + @createSchema + Scenario: Create a resource identified by Ramsey\Uuid\Uuid + When I add "Content-Type" header equal to "application/ld+json" + And I send a "POST" request to "/ramsey_uuid_dummies" with body: + """ + { + "id": "41b29566-144b-11e6-a148-3e1d05defe78" + } + """ + 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" diff --git a/src/Bridge/RamseyUuid/Serializer/UuidDenormalizer.php b/src/Bridge/RamseyUuid/Serializer/UuidDenormalizer.php new file mode 100644 index 00000000000..87090190a85 --- /dev/null +++ b/src/Bridge/RamseyUuid/Serializer/UuidDenormalizer.php @@ -0,0 +1,31 @@ + + * + * 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\Bridge\RamseyUuid\Serializer; + +use Ramsey\Uuid\Uuid; +use Ramsey\Uuid\UuidInterface; +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; + +final class UuidDenormalizer implements DenormalizerInterface +{ + public function denormalize($data, $type, $format = null, array $context = []) + { + return Uuid::fromString($data); + } + + public function supportsDenormalization($data, $type, $format = null) + { + return \is_string($data) && is_a($type, UuidInterface::class, true); + } +} diff --git a/src/Bridge/Symfony/Bundle/Resources/config/ramsey_uuid.xml b/src/Bridge/Symfony/Bundle/Resources/config/ramsey_uuid.xml index a5d0626ab06..bbf81f232fa 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/ramsey_uuid.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/ramsey_uuid.xml @@ -8,5 +8,9 @@ + + + + diff --git a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php index c378d7897d7..026f8ce9c7a 100644 --- a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php +++ b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php @@ -949,6 +949,7 @@ private function getPartialContainerBuilderProphecy($configuration = null) 'api_platform.serializer.group_filter', 'api_platform.serializer.normalizer.item', 'api_platform.serializer.property_filter', + 'api_platform.serializer.uuid_denormalizer', 'api_platform.serializer_locator', 'api_platform.subresource_data_provider', 'api_platform.subresource_operation_factory', From 55b5e25151e5af5ff761847f7d27a597c341e3ea Mon Sep 17 00:00:00 2001 From: Daniel West Date: Wed, 22 Apr 2020 08:37:33 +0100 Subject: [PATCH 18/29] Apply Vincent's review updates `src/Serializer/AbstractItemNormalizer.php`: Update constant name Change author phpdoc on test documents and entities --- src/JsonLd/Serializer/ItemNormalizer.php | 2 +- src/Serializer/AbstractItemNormalizer.php | 8 ++++---- .../Document/DummyDtoOutputFallbackToSameClass.php | 2 +- .../TestBundle/Document/DummyDtoOutputSameClass.php | 2 +- .../Entity/DummyDtoOutputFallbackToSameClass.php | 2 +- .../TestBundle/Entity/DummyDtoOutputSameClass.php | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/JsonLd/Serializer/ItemNormalizer.php b/src/JsonLd/Serializer/ItemNormalizer.php index 02ce3c61cb6..832c23b8b59 100644 --- a/src/JsonLd/Serializer/ItemNormalizer.php +++ b/src/JsonLd/Serializer/ItemNormalizer.php @@ -67,7 +67,7 @@ public function normalize($object, $format = null, array $context = []) { $objectClass = $this->getObjectClass($object); $outputClass = $this->getOutputClass($objectClass, $context); - if (null !== $outputClass && !isset($context[self::IS_TRANSFORMED_TO_SAME_CLASS_CONTEXT_KEY])) { + if (null !== $outputClass && !isset($context[self::IS_TRANSFORMED_TO_SAME_CLASS])) { return parent::normalize($object, $format, $context); } diff --git a/src/Serializer/AbstractItemNormalizer.php b/src/Serializer/AbstractItemNormalizer.php index a1f71f5d9a2..bdd14483f4e 100644 --- a/src/Serializer/AbstractItemNormalizer.php +++ b/src/Serializer/AbstractItemNormalizer.php @@ -51,7 +51,7 @@ abstract class AbstractItemNormalizer extends AbstractObjectNormalizer use ContextTrait; use InputOutputMetadataTrait; - public const IS_TRANSFORMED_TO_SAME_CLASS_CONTEXT_KEY = 'is_transformed_to_same_class'; + public const IS_TRANSFORMED_TO_SAME_CLASS = 'is_transformed_to_same_class'; protected $propertyNameCollectionFactory; protected $propertyMetadataFactory; @@ -114,7 +114,7 @@ public function hasCacheableSupportsMethod(): bool */ public function normalize($object, $format = null, array $context = []) { - if (!($isTransformed = isset($context[self::IS_TRANSFORMED_TO_SAME_CLASS_CONTEXT_KEY])) && $outputClass = $this->getOutputClass($this->getObjectClass($object), $context)) { + if (!($isTransformed = isset($context[self::IS_TRANSFORMED_TO_SAME_CLASS])) && $outputClass = $this->getOutputClass($this->getObjectClass($object), $context)) { if (!$this->serializer instanceof NormalizerInterface) { throw new LogicException('Cannot normalize the output because the injected serializer is not a normalizer'); } @@ -124,13 +124,13 @@ public function normalize($object, $format = null, array $context = []) $context['api_resource'] = $object; unset($context['output'], $context['resource_class']); } else { - $context[self::IS_TRANSFORMED_TO_SAME_CLASS_CONTEXT_KEY] = true; + $context[self::IS_TRANSFORMED_TO_SAME_CLASS] = true; } return $this->serializer->normalize($transformed, $format, $context); } if ($isTransformed) { - unset($context[self::IS_TRANSFORMED_TO_SAME_CLASS_CONTEXT_KEY]); + unset($context[self::IS_TRANSFORMED_TO_SAME_CLASS]); } $resourceClass = $this->resourceClassResolver->getResourceClass($object, $context['resource_class'] ?? null); diff --git a/tests/Fixtures/TestBundle/Document/DummyDtoOutputFallbackToSameClass.php b/tests/Fixtures/TestBundle/Document/DummyDtoOutputFallbackToSameClass.php index 031e2887d0f..7a7d796d4a0 100644 --- a/tests/Fixtures/TestBundle/Document/DummyDtoOutputFallbackToSameClass.php +++ b/tests/Fixtures/TestBundle/Document/DummyDtoOutputFallbackToSameClass.php @@ -20,7 +20,7 @@ /** * Dummy InputOutput. * - * @author Kévin Dunglas + * @author Daniel West * * @ApiResource(attributes={"output"=OutputDtoDummy::class}) * @ODM\Document diff --git a/tests/Fixtures/TestBundle/Document/DummyDtoOutputSameClass.php b/tests/Fixtures/TestBundle/Document/DummyDtoOutputSameClass.php index b8e66071224..fe121b03439 100644 --- a/tests/Fixtures/TestBundle/Document/DummyDtoOutputSameClass.php +++ b/tests/Fixtures/TestBundle/Document/DummyDtoOutputSameClass.php @@ -19,7 +19,7 @@ /** * Dummy InputOutput. * - * @author Kévin Dunglas + * @author Daniel West * * @ApiResource(attributes={"output"=DummyDtoOutputSameClass::class}) * @ODM\Document diff --git a/tests/Fixtures/TestBundle/Entity/DummyDtoOutputFallbackToSameClass.php b/tests/Fixtures/TestBundle/Entity/DummyDtoOutputFallbackToSameClass.php index c7e37206b69..a34152e2730 100644 --- a/tests/Fixtures/TestBundle/Entity/DummyDtoOutputFallbackToSameClass.php +++ b/tests/Fixtures/TestBundle/Entity/DummyDtoOutputFallbackToSameClass.php @@ -20,7 +20,7 @@ /** * Dummy InputOutput. * - * @author Kévin Dunglas + * @author Daniel West * * @ApiResource(attributes={"output"=OutputDtoDummy::class}) * @ORM\Entity diff --git a/tests/Fixtures/TestBundle/Entity/DummyDtoOutputSameClass.php b/tests/Fixtures/TestBundle/Entity/DummyDtoOutputSameClass.php index 7a4f389ee29..4bd43af9659 100644 --- a/tests/Fixtures/TestBundle/Entity/DummyDtoOutputSameClass.php +++ b/tests/Fixtures/TestBundle/Entity/DummyDtoOutputSameClass.php @@ -19,7 +19,7 @@ /** * Dummy InputOutput. * - * @author Kévin Dunglas + * @author Daniel West * * @ApiResource(attributes={"output"=DummyDtoOutputSameClass::class}) * @ORM\Entity From 7724bdeaa8a7265fb8649971408b570c6c735ccd Mon Sep 17 00:00:00 2001 From: Daniel West Date: Wed, 22 Apr 2020 18:51:10 +0100 Subject: [PATCH 19/29] Fix DataTransformer in test to support document same class test --- .../DataTransformer/OutputDtoSameClassTransformer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Fixtures/TestBundle/DataTransformer/OutputDtoSameClassTransformer.php b/tests/Fixtures/TestBundle/DataTransformer/OutputDtoSameClassTransformer.php index 7a2844cbcb4..53a222e3cb0 100644 --- a/tests/Fixtures/TestBundle/DataTransformer/OutputDtoSameClassTransformer.php +++ b/tests/Fixtures/TestBundle/DataTransformer/OutputDtoSameClassTransformer.php @@ -49,6 +49,6 @@ public function transform($object, string $to, array $context = []) public function supportsTransformation($data, string $to, array $context = []): bool { return (($data instanceof DummyDtoOutputFallbackToSameClass || $data instanceof DummyDtoOutputFallbackToSameClassDocument) && OutputDtoDummy::class === $to) || - (($data instanceof DummyDtoOutputSameClass || $data instanceof DummyDtoOutputSameClassDocument) && DummyDtoOutputSameClass::class === $to); + (($data instanceof DummyDtoOutputSameClass || $data instanceof DummyDtoOutputSameClassDocument) && (DummyDtoOutputSameClass::class === $to || DummyDtoOutputSameClassDocument::class === $to)); } } From a9e94fb42b7f3347b8e2cff894f7e611ef5ddae9 Mon Sep 17 00:00:00 2001 From: soyuka Date: Fri, 24 Apr 2020 10:10:55 +0200 Subject: [PATCH 20/29] Revert "Passing custom doctrine type to addWhereByStrategy" This reverts commit 57ec463c8259fddc7505eae73d284406dfee998d. --- .../Doctrine/Orm/Filter/SearchFilter.php | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/Bridge/Doctrine/Orm/Filter/SearchFilter.php b/src/Bridge/Doctrine/Orm/Filter/SearchFilter.php index 6b539999645..da0b8f3b505 100644 --- a/src/Bridge/Doctrine/Orm/Filter/SearchFilter.php +++ b/src/Bridge/Doctrine/Orm/Filter/SearchFilter.php @@ -92,11 +92,12 @@ protected function filterProperty(string $property, $value, QueryBuilder $queryB $caseSensitive = true; $metadata = $this->getNestedMetadata($resourceClass, $associations); - $doctrineTypeField = $this->getDoctrineFieldType($property, $resourceClass); - $values = array_map([$this, 'getIdFromValue'], $values); - if ($metadata->hasField($field)) { - if (!$this->hasValidValues($values, $doctrineTypeField)) { + if ('id' === $field) { + $values = array_map([$this, 'getIdFromValue'], $values); + } + + if (!$this->hasValidValues($values, $this->getDoctrineFieldType($property, $resourceClass))) { $this->logger->notice('Invalid filter ignored', [ 'exception' => new InvalidArgumentException(sprintf('Values for field "%s" are not valid according to the doctrine type.', $field)), ]); @@ -113,7 +114,7 @@ protected function filterProperty(string $property, $value, QueryBuilder $queryB } if (1 === \count($values)) { - $this->addWhereByStrategy($strategy, $queryBuilder, $queryNameGenerator, $alias, $field, $doctrineTypeField, $values[0], $caseSensitive); + $this->addWhereByStrategy($strategy, $queryBuilder, $queryNameGenerator, $alias, $field, $values[0], $caseSensitive); return; } @@ -139,7 +140,9 @@ protected function filterProperty(string $property, $value, QueryBuilder $queryB return; } + $values = array_map([$this, 'getIdFromValue'], $values); $associationFieldIdentifier = 'id'; + $doctrineTypeField = $this->getDoctrineFieldType($property, $resourceClass); if (null !== $this->identifiersExtractor) { $associationResourceClass = $metadata->getAssociationTargetClass($field); @@ -168,11 +171,11 @@ protected function filterProperty(string $property, $value, QueryBuilder $queryB if (1 === \count($values)) { $queryBuilder ->andWhere(sprintf('%s.%s = :%s', $associationAlias, $associationField, $valueParameter)) - ->setParameter($valueParameter, $values[0], $doctrineTypeField); + ->setParameter($valueParameter, $values[0]); } else { $queryBuilder ->andWhere(sprintf('%s.%s IN (:%s)', $associationAlias, $associationField, $valueParameter)) - ->setParameter($valueParameter, $values, $doctrineTypeField); + ->setParameter($valueParameter, $values); } } @@ -181,7 +184,7 @@ protected function filterProperty(string $property, $value, QueryBuilder $queryB * * @throws InvalidArgumentException If strategy does not exist */ - protected function addWhereByStrategy(string $strategy, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $alias, string $field, $fieldType, $value, bool $caseSensitive) + protected function addWhereByStrategy(string $strategy, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $alias, string $field, $value, bool $caseSensitive) { $wrapCase = $this->createWrapCase($caseSensitive); $valueParameter = $queryNameGenerator->generateParameterName($field); @@ -191,27 +194,27 @@ protected function addWhereByStrategy(string $strategy, QueryBuilder $queryBuild case self::STRATEGY_EXACT: $queryBuilder ->andWhere(sprintf($wrapCase('%s.%s').' = '.$wrapCase(':%s'), $alias, $field, $valueParameter)) - ->setParameter($valueParameter, $value, $fieldType); + ->setParameter($valueParameter, $value); break; case self::STRATEGY_PARTIAL: $queryBuilder ->andWhere(sprintf($wrapCase('%s.%s').' LIKE '.$wrapCase('CONCAT(\'%%\', :%s, \'%%\')'), $alias, $field, $valueParameter)) - ->setParameter($valueParameter, $value, $fieldType); + ->setParameter($valueParameter, $value); break; case self::STRATEGY_START: $queryBuilder ->andWhere(sprintf($wrapCase('%s.%s').' LIKE '.$wrapCase('CONCAT(:%s, \'%%\')'), $alias, $field, $valueParameter)) - ->setParameter($valueParameter, $value, $fieldType); + ->setParameter($valueParameter, $value); break; case self::STRATEGY_END: $queryBuilder ->andWhere(sprintf($wrapCase('%s.%s').' LIKE '.$wrapCase('CONCAT(\'%%\', :%s)'), $alias, $field, $valueParameter)) - ->setParameter($valueParameter, $value, $fieldType); + ->setParameter($valueParameter, $value); break; case self::STRATEGY_WORD_START: $queryBuilder ->andWhere(sprintf($wrapCase('%1$s.%2$s').' LIKE '.$wrapCase('CONCAT(:%3$s, \'%%\')').' OR '.$wrapCase('%1$s.%2$s').' LIKE '.$wrapCase('CONCAT(\'%% \', :%3$s, \'%%\')'), $alias, $field, $valueParameter)) - ->setParameter($valueParameter, $value, $fieldType); + ->setParameter($valueParameter, $value); break; default: throw new InvalidArgumentException(sprintf('strategy %s does not exist.', $strategy)); From 3137ed00f07bb537a1e5db5a26ae9c542ad2d33f Mon Sep 17 00:00:00 2001 From: Alan Poulain Date: Sun, 26 Apr 2020 13:38:58 +0200 Subject: [PATCH 21/29] [GraphQL] Resource with no operation should be available through relations (#3532) --- features/bootstrap/DoctrineContext.php | 21 ------ features/graphql/introspection.feature | 27 +++++++- src/GraphQl/Type/TypeConverter.php | 5 +- .../TestBundle/Document/VoDummyInspection.php | 11 +-- .../Fixtures/TestBundle/Entity/Container.php | 68 ------------------- tests/Fixtures/TestBundle/Entity/Node.php | 62 ----------------- .../TestBundle/Entity/VoDummyInspection.php | 11 +-- tests/GraphQl/Type/TypeConverterTest.php | 13 ++++ 8 files changed, 57 insertions(+), 161 deletions(-) delete mode 100644 tests/Fixtures/TestBundle/Entity/Container.php delete mode 100644 tests/Fixtures/TestBundle/Entity/Node.php diff --git a/features/bootstrap/DoctrineContext.php b/features/bootstrap/DoctrineContext.php index 3e519762e76..6f5095eb648 100644 --- a/features/bootstrap/DoctrineContext.php +++ b/features/bootstrap/DoctrineContext.php @@ -71,7 +71,6 @@ use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\CompositeLabel; 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; @@ -107,7 +106,6 @@ use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Greeting; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\InternalUser; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\MaxDepthDummy; -use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Node; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Order; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Person; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\PersonToPet; @@ -1051,25 +1049,6 @@ public function thereIsAnAnswerToTheQuestion(string $a, string $q) $this->manager->clear(); } - /** - * @Given there are :nb nodes in a container :uuid - */ - public function thereAreNodesInAContainer(int $nb, string $uuid) - { - $container = new Container(); - $container->setId($uuid); - $this->manager->persist($container); - - for ($i = 0; $i < $nb; ++$i) { - $node = new Node(); - $node->setContainer($container); - $node->setSerial($i); - $this->manager->persist($node); - } - - $this->manager->flush(); - } - /** * @Then the password :password for user :user should be hashed */ diff --git a/features/graphql/introspection.feature b/features/graphql/introspection.feature index 0c093266985..03d1e36389d 100644 --- a/features/graphql/introspection.feature +++ b/features/graphql/introspection.feature @@ -426,7 +426,7 @@ Feature: GraphQL introspection support And the JSON node "data.typeCreatePayloadData.fields[3].type.name" should be equal to "createDummyGroupNestedPayload" And the JSON node "data.typeCreateNestedPayload.fields[0].name" should be equal to "id" - Scenario: Retrieve an item through a GraphQL query + Scenario: Retrieve a type name through a GraphQL query Given there are 4 dummy objects with relatedDummy When I send the following GraphQL request: """ @@ -447,3 +447,28 @@ Feature: GraphQL introspection support And the JSON node "data.dummy.name" should be equal to "Dummy #3" And the JSON node "data.dummy.relatedDummy.name" should be equal to "RelatedDummy #3" And the JSON node "data.dummy.relatedDummy.__typename" should be equal to "RelatedDummy" + + Scenario: Introspect a type available only through relations + When I send the following GraphQL request: + """ + { + typeNotAvailable: __type(name: "VoDummyInspectionConnection") { + description + } + typeOwner: __type(name: "VoDummyCar") { + description, + fields { + name + type { + name + } + } + } + } + """ + Then the response status code should be 200 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/json" + And the JSON node "errors[0].debugMessage" should be equal to 'Type with id "VoDummyInspectionConnection" is not present in the types container' + And the JSON node "data.typeNotAvailable" should be null + And the JSON node "data.typeOwner.fields[3].type.name" should be equal to "VoDummyInspectionConnection" diff --git a/src/GraphQl/Type/TypeConverter.php b/src/GraphQl/Type/TypeConverter.php index b6f09a8a8d7..17617f86e88 100644 --- a/src/GraphQl/Type/TypeConverter.php +++ b/src/GraphQl/Type/TypeConverter.php @@ -105,9 +105,12 @@ private function getResourceType(Type $type, bool $input, ?string $queryName, ?s try { $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass); - if ([] === ($resourceMetadata->getGraphql() ?? [])) { + if (null === $resourceMetadata->getGraphql()) { return null; } + if ('Node' === $resourceMetadata->getShortName()) { + throw new \UnexpectedValueException('A "Node" resource cannot be used with GraphQL because the type is already used by the Relay specification.'); + } } catch (ResourceClassNotFoundException $e) { // Skip objects that are not resources for now return null; diff --git a/tests/Fixtures/TestBundle/Document/VoDummyInspection.php b/tests/Fixtures/TestBundle/Document/VoDummyInspection.php index 60fd02c77fb..6e0ad90c45a 100644 --- a/tests/Fixtures/TestBundle/Document/VoDummyInspection.php +++ b/tests/Fixtures/TestBundle/Document/VoDummyInspection.php @@ -19,10 +19,13 @@ use Symfony\Component\Serializer\Annotation\Groups; /** - * @ApiResource(attributes={ - * "normalization_context"={"groups"={"inspection_read"}}, - * "denormalization_context"={"groups"={"inspection_write"}} - * }) + * @ApiResource( + * attributes={ + * "normalization_context"={"groups"={"inspection_read"}}, + * "denormalization_context"={"groups"={"inspection_write"}} + * }, + * graphql={} + * ) * @ODM\Document */ class VoDummyInspection diff --git a/tests/Fixtures/TestBundle/Entity/Container.php b/tests/Fixtures/TestBundle/Entity/Container.php deleted file mode 100644 index 09e0c75922d..00000000000 --- a/tests/Fixtures/TestBundle/Entity/Container.php +++ /dev/null @@ -1,68 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity; - -use ApiPlatform\Core\Annotation\ApiResource; -use ApiPlatform\Core\Annotation\ApiSubresource; -use Doctrine\Common\Collections\ArrayCollection; -use Doctrine\ORM\Mapping as ORM; - -/** - * @ApiResource - * @ORM\Entity - */ -class Container -{ - /** - * @ORM\Id - * @ORM\Column(type="guid") - * - * @var string UUID - */ - private $id; - - /** - * @ApiSubresource - * @ORM\OneToMany( - * targetEntity="Node", - * mappedBy="container", - * indexBy="serial", - * fetch="LAZY", - * cascade={}, - * orphanRemoval=false - * ) - * @ORM\OrderBy({"serial"="ASC"}) - * - * @var ArrayCollection|Node[] - */ - private $nodes; - - public function getId(): string - { - return $this->id; - } - - public function setId(string $id) - { - $this->id = $id; - } - - /** - * @return ArrayCollection|Node[] - */ - public function getNodes() - { - return $this->nodes; - } -} diff --git a/tests/Fixtures/TestBundle/Entity/Node.php b/tests/Fixtures/TestBundle/Entity/Node.php deleted file mode 100644 index e0985dc8853..00000000000 --- a/tests/Fixtures/TestBundle/Entity/Node.php +++ /dev/null @@ -1,62 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity; - -use ApiPlatform\Core\Annotation\ApiResource; -use Doctrine\ORM\Mapping as ORM; - -/** - * @see https://github.com/api-platform/core/pull/904#issuecomment-294132077 - * @ApiResource(graphql={}) - * @ORM\Entity - */ -class Node -{ - /** - * @ORM\Id - * @ORM\Column(name="serial", type="integer") - * - * @var int Node serial - */ - private $serial; - - /** - * @ORM\Id - * @ORM\ManyToOne(targetEntity="Container", fetch="LAZY") - * @ORM\JoinColumn(name="container_id", referencedColumnName="id", onDelete="RESTRICT") - * - * @var Container - */ - private $container; - - public function setContainer(Container $container) - { - $this->container = $container; - } - - public function getContainer(): Container - { - return $this->container; - } - - public function setSerial(int $serial) - { - $this->serial = $serial; - } - - public function getSerial(): int - { - return $this->serial; - } -} diff --git a/tests/Fixtures/TestBundle/Entity/VoDummyInspection.php b/tests/Fixtures/TestBundle/Entity/VoDummyInspection.php index a0aca6022a6..dcb13c5d6ca 100644 --- a/tests/Fixtures/TestBundle/Entity/VoDummyInspection.php +++ b/tests/Fixtures/TestBundle/Entity/VoDummyInspection.php @@ -19,10 +19,13 @@ use Symfony\Component\Serializer\Annotation\Groups; /** - * @ApiResource(attributes={ - * "normalization_context"={"groups"={"inspection_read"}}, - * "denormalization_context"={"groups"={"inspection_write"}} - * }) + * @ApiResource( + * attributes={ + * "normalization_context"={"groups"={"inspection_read"}}, + * "denormalization_context"={"groups"={"inspection_write"}} + * }, + * graphql={} + * ) * @ORM\Entity */ class VoDummyInspection diff --git a/tests/GraphQl/Type/TypeConverterTest.php b/tests/GraphQl/Type/TypeConverterTest.php index 69cb0eae469..7c03c3f724d 100644 --- a/tests/GraphQl/Type/TypeConverterTest.php +++ b/tests/GraphQl/Type/TypeConverterTest.php @@ -96,6 +96,19 @@ public function testConvertTypeNoGraphQlResourceMetadata(): void $this->assertNull($graphqlType); } + public function testConvertTypeNodeResource(): void + { + $type = new Type(Type::BUILTIN_TYPE_OBJECT, false, 'node'); + + $this->typeBuilderProphecy->isCollection($type)->shouldBeCalled()->willReturn(false); + $this->resourceMetadataFactoryProphecy->create('node')->shouldBeCalled()->willReturn((new ResourceMetadata('Node'))->withGraphql(['test'])); + + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessage('A "Node" resource cannot be used with GraphQL because the type is already used by the Relay specification.'); + + $this->typeConverter->convertType($type, false, null, null, 'resourceClass', 'rootClass', null, 0); + } + public function testConvertTypeResourceClassNotFound(): void { $type = new Type(Type::BUILTIN_TYPE_OBJECT, false, 'dummy'); From d5916f265151b5bc0c27ee922eac87d0eeb05475 Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Tue, 12 May 2020 15:37:18 +0200 Subject: [PATCH 22/29] Fix: Allow objects without properties (#3544) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix: Allow objects without properties * Refactor tests Co-authored-by: Andreas Möller * Fix: CS * Add testcases Co-authored-by: Andreas Möller --- ...pertyInfoPropertyNameCollectionFactory.php | 8 +- ...yInfoPropertyNameCollectionFactoryTest.php | 73 +++++++++++++++++++ .../DummyObjectWithOnlyPrivateProperty.php | 19 +++++ .../DummyObjectWithOnlyPublicProperty.php | 19 +++++ ...ummyObjectWithPublicAndPrivateProperty.php | 20 +++++ tests/Fixtures/DummyObjectWithoutProperty.php | 18 +++++ 6 files changed, 150 insertions(+), 7 deletions(-) create mode 100644 tests/Bridge/Symfony/PropertyInfo/Metadata/Property/PropertyInfoPropertyNameCollectionFactoryTest.php create mode 100644 tests/Fixtures/DummyObjectWithOnlyPrivateProperty.php create mode 100644 tests/Fixtures/DummyObjectWithOnlyPublicProperty.php create mode 100644 tests/Fixtures/DummyObjectWithPublicAndPrivateProperty.php create mode 100644 tests/Fixtures/DummyObjectWithoutProperty.php diff --git a/src/Bridge/Symfony/PropertyInfo/Metadata/Property/PropertyInfoPropertyNameCollectionFactory.php b/src/Bridge/Symfony/PropertyInfo/Metadata/Property/PropertyInfoPropertyNameCollectionFactory.php index f988850c080..f603e08f3a1 100644 --- a/src/Bridge/Symfony/PropertyInfo/Metadata/Property/PropertyInfoPropertyNameCollectionFactory.php +++ b/src/Bridge/Symfony/PropertyInfo/Metadata/Property/PropertyInfoPropertyNameCollectionFactory.php @@ -13,7 +13,6 @@ namespace ApiPlatform\Core\Bridge\Symfony\PropertyInfo\Metadata\Property; -use ApiPlatform\Core\Exception\RuntimeException; use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Core\Metadata\Property\PropertyNameCollection; use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface; @@ -36,16 +35,11 @@ public function __construct(PropertyInfoExtractorInterface $propertyInfo) /** * {@inheritdoc} - * - * @throws RuntimeException */ public function create(string $resourceClass, array $options = []): PropertyNameCollection { $properties = $this->propertyInfo->getProperties($resourceClass, $options); - if (null === $properties) { - throw new RuntimeException(sprintf('There is no PropertyInfo extractor supporting the class "%s".', $resourceClass)); - } - return new PropertyNameCollection($properties); + return new PropertyNameCollection($properties ?? []); } } diff --git a/tests/Bridge/Symfony/PropertyInfo/Metadata/Property/PropertyInfoPropertyNameCollectionFactoryTest.php b/tests/Bridge/Symfony/PropertyInfo/Metadata/Property/PropertyInfoPropertyNameCollectionFactoryTest.php new file mode 100644 index 00000000000..27384cccb64 --- /dev/null +++ b/tests/Bridge/Symfony/PropertyInfo/Metadata/Property/PropertyInfoPropertyNameCollectionFactoryTest.php @@ -0,0 +1,73 @@ + + * + * 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\Bridge\Symfony\PropertyInfo\Metadata\Property; + +use ApiPlatform\Core\Bridge\Symfony\PropertyInfo\Metadata\Property\PropertyInfoPropertyNameCollectionFactory; +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 PHPUnit\Framework\TestCase; +use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; +use Symfony\Component\PropertyInfo\PropertyInfoExtractor; + +/** + * @author Oskar Stark + */ +class PropertyInfoPropertyNameCollectionFactoryTest extends TestCase +{ + public function testCreateMethodReturnsEmptyPropertyNameCollectionForObjectWithOnlyPrivateProperty() + { + $factory = new PropertyInfoPropertyNameCollectionFactory(new PropertyInfoExtractor([ + new ReflectionExtractor(), + ])); + + $collection = $factory->create(DummyObjectWithOnlyPrivateProperty::class); + + self::assertCount(0, $collection->getIterator()); + } + + public function testCreateMethodReturnsEmptyPropertyNameCollectionForObjectWithoutProperties() + { + $factory = new PropertyInfoPropertyNameCollectionFactory(new PropertyInfoExtractor([ + new ReflectionExtractor(), + ])); + + $collection = $factory->create(DummyObjectWithoutProperty::class); + + self::assertCount(0, $collection->getIterator()); + } + + public function testCreateMethodReturnsProperPropertyNameCollectionForObjectWithPublicAndPrivateProperty() + { + $factory = new PropertyInfoPropertyNameCollectionFactory(new PropertyInfoExtractor([ + new ReflectionExtractor(), + ])); + + $collection = $factory->create(DummyObjectWithPublicAndPrivateProperty::class); + + self::assertCount(1, $collection->getIterator()); + } + + public function testCreateMethodReturnsProperPropertyNameCollectionForObjectWithPublicProperty() + { + $factory = new PropertyInfoPropertyNameCollectionFactory(new PropertyInfoExtractor([ + new ReflectionExtractor(), + ])); + + $collection = $factory->create(DummyObjectWithOnlyPublicProperty::class); + + self::assertCount(1, $collection->getIterator()); + } +} diff --git a/tests/Fixtures/DummyObjectWithOnlyPrivateProperty.php b/tests/Fixtures/DummyObjectWithOnlyPrivateProperty.php new file mode 100644 index 00000000000..1cc3c6dfaf9 --- /dev/null +++ b/tests/Fixtures/DummyObjectWithOnlyPrivateProperty.php @@ -0,0 +1,19 @@ + + * + * 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; + +class DummyObjectWithOnlyPrivateProperty +{ + private $foo; +} diff --git a/tests/Fixtures/DummyObjectWithOnlyPublicProperty.php b/tests/Fixtures/DummyObjectWithOnlyPublicProperty.php new file mode 100644 index 00000000000..4a4ad2a22ed --- /dev/null +++ b/tests/Fixtures/DummyObjectWithOnlyPublicProperty.php @@ -0,0 +1,19 @@ + + * + * 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; + +class DummyObjectWithOnlyPublicProperty +{ + public $foo; +} diff --git a/tests/Fixtures/DummyObjectWithPublicAndPrivateProperty.php b/tests/Fixtures/DummyObjectWithPublicAndPrivateProperty.php new file mode 100644 index 00000000000..2c146a0048e --- /dev/null +++ b/tests/Fixtures/DummyObjectWithPublicAndPrivateProperty.php @@ -0,0 +1,20 @@ + + * + * 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; + +class DummyObjectWithPublicAndPrivateProperty +{ + public $foo; + private $bar; +} diff --git a/tests/Fixtures/DummyObjectWithoutProperty.php b/tests/Fixtures/DummyObjectWithoutProperty.php new file mode 100644 index 00000000000..3171e2fd992 --- /dev/null +++ b/tests/Fixtures/DummyObjectWithoutProperty.php @@ -0,0 +1,18 @@ + + * + * 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; + +class DummyObjectWithoutProperty +{ +} From 563e01756c1e94519374f70474c84e6c2cf439fe Mon Sep 17 00:00:00 2001 From: Alan Poulain Date: Mon, 18 May 2020 17:28:35 +0200 Subject: [PATCH 23/29] Fix tests and PHPStan (#3561) --- features/bootstrap/DoctrineContext.php | 2 +- features/jsonld/interface_as_resource.feature | 8 ++++---- src/Hydra/Serializer/CollectionNormalizer.php | 12 +++++------- tests/Hal/Serializer/CollectionNormalizerTest.php | 6 +++++- tests/Hydra/Serializer/CollectionNormalizerTest.php | 5 ++++- 5 files changed, 19 insertions(+), 14 deletions(-) diff --git a/features/bootstrap/DoctrineContext.php b/features/bootstrap/DoctrineContext.php index 53ec0eb479a..5bd8487d67b 100644 --- a/features/bootstrap/DoctrineContext.php +++ b/features/bootstrap/DoctrineContext.php @@ -1481,7 +1481,7 @@ public function thereIsTheFollowingProduct(PyStringNode $dataNode): void $product = $this->isOrm() ? new Product() : new ProductDocument(); $product->setCode($data['code']); if (isset($data['mainTaxon'])) { - $mainTaxonCode = str_replace('/taxons/', '', $data['mainTaxon']); + $mainTaxonCode = str_replace('/taxa/', '', $data['mainTaxon']); $mainTaxon = $this->manager->getRepository($this->isOrm() ? Taxon::class : TaxonDocument::class)->findOneBy([ 'code' => $mainTaxonCode, ]); diff --git a/features/jsonld/interface_as_resource.feature b/features/jsonld/interface_as_resource.feature index 29ce56cce70..7236ace7ae7 100644 --- a/features/jsonld/interface_as_resource.feature +++ b/features/jsonld/interface_as_resource.feature @@ -15,7 +15,7 @@ Feature: JSON-LD using interface as resource "code": "WONDERFUL_TAXON" } """ - When I send a "GET" request to "/taxons/WONDERFUL_TAXON" + When I send a "GET" request to "/taxa/WONDERFUL_TAXON" 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" @@ -23,7 +23,7 @@ Feature: JSON-LD using interface as resource """ { "@context": "/contexts/Taxon", - "@id": "/taxons/WONDERFUL_TAXON", + "@id": "/taxa/WONDERFUL_TAXON", "@type": "Taxon", "code": "WONDERFUL_TAXON" } @@ -34,7 +34,7 @@ Feature: JSON-LD using interface as resource """ { "code": "GREAT_PRODUCT", - "mainTaxon": "/taxons/WONDERFUL_TAXON" + "mainTaxon": "/taxa/WONDERFUL_TAXON" } """ When I send a "GET" request to "/products/GREAT_PRODUCT" @@ -49,7 +49,7 @@ Feature: JSON-LD using interface as resource "@type": "Product", "code": "GREAT_PRODUCT", "mainTaxon": { - "@id": "/taxons/WONDERFUL_TAXON", + "@id": "/taxa/WONDERFUL_TAXON", "@type": "Taxon", "code": "WONDERFUL_TAXON" } diff --git a/src/Hydra/Serializer/CollectionNormalizer.php b/src/Hydra/Serializer/CollectionNormalizer.php index c7ac9d13f6c..bef56cff98d 100644 --- a/src/Hydra/Serializer/CollectionNormalizer.php +++ b/src/Hydra/Serializer/CollectionNormalizer.php @@ -87,13 +87,11 @@ public function normalize($object, $format = null, array $context = []) $data['hydra:member'][] = $this->normalizer->normalize($obj, $format, $context); } - $paginated = null; - if ( - \is_array($object) || - ($paginated = $object instanceof PaginatorInterface) || - $object instanceof \Countable && !$object instanceof PartialPaginatorInterface - ) { - $data['hydra:totalItems'] = $paginated ? $object->getTotalItems() : \count($object); + if ($object instanceof PaginatorInterface) { + $data['hydra:totalItems'] = $object->getTotalItems(); + } + if (\is_array($object) || ($object instanceof \Countable && !$object instanceof PartialPaginatorInterface)) { + $data['hydra:totalItems'] = \count($object); } return $data; diff --git a/tests/Hal/Serializer/CollectionNormalizerTest.php b/tests/Hal/Serializer/CollectionNormalizerTest.php index 96d2e1fe7ce..9c63cd8f7f1 100644 --- a/tests/Hal/Serializer/CollectionNormalizerTest.php +++ b/tests/Hal/Serializer/CollectionNormalizerTest.php @@ -112,7 +112,11 @@ public function testNormalizePartialPaginator() private function normalizePaginator($partial = false) { - $paginatorProphecy = $this->prophesize($partial ? PartialPaginatorInterface::class : PaginatorInterface::class); + $paginatorProphecy = $this->prophesize(PaginatorInterface::class); + if ($partial) { + $paginatorProphecy = $this->prophesize(PartialPaginatorInterface::class); + } + $paginatorProphecy->getCurrentPage()->willReturn(3); $paginatorProphecy->getItemsPerPage()->willReturn(12); $paginatorProphecy->rewind()->will(function () {}); diff --git a/tests/Hydra/Serializer/CollectionNormalizerTest.php b/tests/Hydra/Serializer/CollectionNormalizerTest.php index 2ef97fd951b..445204dc6d9 100644 --- a/tests/Hydra/Serializer/CollectionNormalizerTest.php +++ b/tests/Hydra/Serializer/CollectionNormalizerTest.php @@ -291,7 +291,10 @@ public function testNormalizePartialPaginator() private function normalizePaginator($partial = false) { - $paginatorProphecy = $this->prophesize($partial ? PartialPaginatorInterface::class : PaginatorInterface::class); + $paginatorProphecy = $this->prophesize(PaginatorInterface::class); + if ($partial) { + $paginatorProphecy = $this->prophesize(PartialPaginatorInterface::class); + } if (!$partial) { $paginatorProphecy->getTotalItems()->willReturn(1312); From e972102bf73f1959988380816d818c259e0f8e3b Mon Sep 17 00:00:00 2001 From: soyuka Date: Wed, 29 Apr 2020 10:16:45 +0200 Subject: [PATCH 24/29] Changelog 2.5.6 --- CHANGELOG.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eba011d738d..1081a7c5aa0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,11 @@ # Changelog -## 2.5.x-dev +## 2.5.6 -* JSON Schema: Missing LD context from Data Transformers #3479 +* JSON Schema: Missing JSON-LD context from Data Transformers #3479 +* GraphQl: Resource with no operation should be available through relations #3532 +* Fix ramsey uuid denormalization #3473 +* Revert #3331 as it breaks backwards compatibility ## 2.5.5 From 1ce0410eb2ef52955f2d35a349c05d170cf62d16 Mon Sep 17 00:00:00 2001 From: soyuka Date: Tue, 26 May 2020 14:35:46 +0200 Subject: [PATCH 25/29] fix: Uuid identifier normalizer should support only strings --- .../RamseyUuid/Identifier/Normalizer/UuidNormalizer.php | 2 +- tests/Bridge/RamseyUuid/Normalizer/UuidNormalizerTest.php | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Bridge/RamseyUuid/Identifier/Normalizer/UuidNormalizer.php b/src/Bridge/RamseyUuid/Identifier/Normalizer/UuidNormalizer.php index 8bac4e27f52..8a3ce082486 100644 --- a/src/Bridge/RamseyUuid/Identifier/Normalizer/UuidNormalizer.php +++ b/src/Bridge/RamseyUuid/Identifier/Normalizer/UuidNormalizer.php @@ -43,6 +43,6 @@ public function denormalize($data, $class, $format = null, array $context = []) */ public function supportsDenormalization($data, $type, $format = null) { - return is_a($type, UuidInterface::class, true); + return \is_string($data) && is_a($type, UuidInterface::class, true); } } diff --git a/tests/Bridge/RamseyUuid/Normalizer/UuidNormalizerTest.php b/tests/Bridge/RamseyUuid/Normalizer/UuidNormalizerTest.php index 3a74e6cfb55..cbf158da679 100644 --- a/tests/Bridge/RamseyUuid/Normalizer/UuidNormalizerTest.php +++ b/tests/Bridge/RamseyUuid/Normalizer/UuidNormalizerTest.php @@ -44,4 +44,11 @@ public function testFailDenormalizeUuid() $this->assertTrue($normalizer->supportsDenormalization($uuid, Uuid::class)); $normalizer->denormalize($uuid, Uuid::class); } + + public function testDoNotSupportNotString() + { + $uuid = Uuid::uuid4(); + $normalizer = new UuidNormalizer(); + $this->assertFalse($normalizer->supportsDenormalization($uuid, Uuid::class)); + } } From 43bddc1bdfa656de11d0b4d5d00bc114030d6de0 Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Thu, 28 May 2020 15:26:35 +0200 Subject: [PATCH 26/29] Handle deprecations from Doctrine Inflector (#3564) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Handle deprecations from Doctrine Inflector. * Move Inflector to Util Co-authored-by: Kévin Dunglas --- src/Annotation/AttributesHydratorTrait.php | 2 +- .../Factory/CatDocumentMetadataFactory.php | 2 +- .../Symfony/Routing/RouteNameGenerator.php | 2 +- src/GraphQl/Type/FieldsBuilder.php | 2 +- .../DashPathSegmentNameGenerator.php | 2 +- .../UnderscorePathSegmentNameGenerator.php | 2 +- src/Util/AnnotationFilterExtractorTrait.php | 1 - src/Util/Inflector.php | 55 +++++++++++++++++++ 8 files changed, 61 insertions(+), 7 deletions(-) create mode 100644 src/Util/Inflector.php diff --git a/src/Annotation/AttributesHydratorTrait.php b/src/Annotation/AttributesHydratorTrait.php index aba28a5fedb..1211f73b6a1 100644 --- a/src/Annotation/AttributesHydratorTrait.php +++ b/src/Annotation/AttributesHydratorTrait.php @@ -14,7 +14,7 @@ namespace ApiPlatform\Core\Annotation; use ApiPlatform\Core\Exception\InvalidArgumentException; -use Doctrine\Common\Inflector\Inflector; +use ApiPlatform\Core\Util\Inflector; /** * Hydrates attributes from annotation's parameters. diff --git a/src/Bridge/Elasticsearch/Metadata/Document/Factory/CatDocumentMetadataFactory.php b/src/Bridge/Elasticsearch/Metadata/Document/Factory/CatDocumentMetadataFactory.php index 0fb5c5260fc..c7d42878e36 100644 --- a/src/Bridge/Elasticsearch/Metadata/Document/Factory/CatDocumentMetadataFactory.php +++ b/src/Bridge/Elasticsearch/Metadata/Document/Factory/CatDocumentMetadataFactory.php @@ -16,7 +16,7 @@ use ApiPlatform\Core\Bridge\Elasticsearch\Exception\IndexNotFoundException; use ApiPlatform\Core\Bridge\Elasticsearch\Metadata\Document\DocumentMetadata; use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; -use Doctrine\Common\Inflector\Inflector; +use ApiPlatform\Core\Util\Inflector; use Elasticsearch\Client; use Elasticsearch\Common\Exceptions\Missing404Exception; diff --git a/src/Bridge/Symfony/Routing/RouteNameGenerator.php b/src/Bridge/Symfony/Routing/RouteNameGenerator.php index d765dc9ef0e..89b3ca3ac5f 100644 --- a/src/Bridge/Symfony/Routing/RouteNameGenerator.php +++ b/src/Bridge/Symfony/Routing/RouteNameGenerator.php @@ -16,7 +16,7 @@ use ApiPlatform\Core\Api\OperationType; use ApiPlatform\Core\Api\OperationTypeDeprecationHelper; use ApiPlatform\Core\Exception\InvalidArgumentException; -use Doctrine\Common\Inflector\Inflector; +use ApiPlatform\Core\Util\Inflector; /** * Generates the Symfony route name associated with an operation name and a resource short name. diff --git a/src/GraphQl/Type/FieldsBuilder.php b/src/GraphQl/Type/FieldsBuilder.php index 64d0d47a75c..237f1746e5c 100644 --- a/src/GraphQl/Type/FieldsBuilder.php +++ b/src/GraphQl/Type/FieldsBuilder.php @@ -21,7 +21,7 @@ use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; use ApiPlatform\Core\Metadata\Resource\ResourceMetadata; -use Doctrine\Common\Inflector\Inflector; +use ApiPlatform\Core\Util\Inflector; use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\NullableType; use GraphQL\Type\Definition\Type as GraphQLType; diff --git a/src/Operation/DashPathSegmentNameGenerator.php b/src/Operation/DashPathSegmentNameGenerator.php index 07acc591ecc..1a31f1fa7fc 100644 --- a/src/Operation/DashPathSegmentNameGenerator.php +++ b/src/Operation/DashPathSegmentNameGenerator.php @@ -13,7 +13,7 @@ namespace ApiPlatform\Core\Operation; -use Doctrine\Common\Inflector\Inflector; +use ApiPlatform\Core\Util\Inflector; /** * Generate a path name with a dash separator according to a string and whether it's a collection or not. diff --git a/src/Operation/UnderscorePathSegmentNameGenerator.php b/src/Operation/UnderscorePathSegmentNameGenerator.php index a48f051fa75..c0e5694ac53 100644 --- a/src/Operation/UnderscorePathSegmentNameGenerator.php +++ b/src/Operation/UnderscorePathSegmentNameGenerator.php @@ -13,7 +13,7 @@ namespace ApiPlatform\Core\Operation; -use Doctrine\Common\Inflector\Inflector; +use ApiPlatform\Core\Util\Inflector; /** * Generate a path name with an underscore separator according to a string and whether it's a collection or not. diff --git a/src/Util/AnnotationFilterExtractorTrait.php b/src/Util/AnnotationFilterExtractorTrait.php index 756dab0d376..33be708471a 100644 --- a/src/Util/AnnotationFilterExtractorTrait.php +++ b/src/Util/AnnotationFilterExtractorTrait.php @@ -15,7 +15,6 @@ use ApiPlatform\Core\Annotation\ApiFilter; use Doctrine\Common\Annotations\Reader; -use Doctrine\Common\Inflector\Inflector; /** * Generates a service id for a generic filter. diff --git a/src/Util/Inflector.php b/src/Util/Inflector.php new file mode 100644 index 00000000000..0c9cf904316 --- /dev/null +++ b/src/Util/Inflector.php @@ -0,0 +1,55 @@ + + * + * 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\Util; + +use Doctrine\Common\Inflector\Inflector as LegacyInflector; +use Doctrine\Inflector\Inflector as InflectorObject; +use Doctrine\Inflector\InflectorFactory; + +/** + * Facade for Doctrine Inflector. + * + * This class allows us to maintain compatibility with Doctrine Inflector 1.3 and 2.0 at the same time. + * + * @internal + */ +final class Inflector +{ + /** + * @var InflectorObject + */ + private static $instance; + + private static function getInstance(): InflectorObject + { + return self::$instance + ?? self::$instance = InflectorFactory::create()->build(); + } + + /** + * @see LegacyInflector::tableize() + */ + public static function tableize(string $word): string + { + return class_exists(InflectorFactory::class) ? self::getInstance()->tableize($word) : LegacyInflector::tableize($word); + } + + /** + * @see LegacyInflector::pluralize() + */ + public static function pluralize(string $word): string + { + return class_exists(InflectorFactory::class) ? self::getInstance()->pluralize($word) : LegacyInflector::pluralize($word); + } +} From 821b2562059d5907f6204ac213c5b72625dbf2bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Thu, 28 May 2020 16:10:25 +0200 Subject: [PATCH 27/29] Fix --prefer-lowest tests --- CONTRIBUTING.md | 6 ++++-- tests/Fixtures/app/AppKernel.php | 8 ++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c4505483f95..ecb7da160f1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -97,11 +97,13 @@ Coverage will be available in `coverage/index.html`. The command to launch Behat tests is: - ./vendor/bin/behat --suite=default --stop-on-failure -vvv + php -d memory_limit=-1 ./vendor/bin/behat --suite=default --stop-on-failure --format=progress If you want to launch Behat tests for MongoDB, the command is: - APP_ENV=mongodb ./vendor/bin/behat --suite=mongodb --stop-on-failure -vvv + APP_ENV=mongodb php -d memory_limit=-1 ./vendor/bin/behat --suite=mongodb --stop-on-failure --format=progress + +To get more details about an error, replace `--format=progress` by `-vvv`. ## Squash your Commits diff --git a/tests/Fixtures/app/AppKernel.php b/tests/Fixtures/app/AppKernel.php index 6ab28fbb047..e5d2e9b52de 100644 --- a/tests/Fixtures/app/AppKernel.php +++ b/tests/Fixtures/app/AppKernel.php @@ -17,6 +17,8 @@ use ApiPlatform\Core\Tests\Fixtures\TestBundle\TestBundle; use Doctrine\Bundle\DoctrineBundle\DoctrineBundle; use Doctrine\Bundle\MongoDBBundle\DoctrineMongoDBBundle; +use Doctrine\Common\Inflector\Inflector; +use Doctrine\Inflector\InflectorFactory; use FOS\UserBundle\FOSUserBundle; use Nelmio\ApiDocBundle\NelmioApiDocBundle; use Symfony\Bundle\FrameworkBundle\FrameworkBundle; @@ -48,6 +50,12 @@ public function __construct(string $environment, bool $debug) // patch for behat/symfony2-extension not supporting %env(APP_ENV)% $this->environment = $_SERVER['APP_ENV'] ?? $environment; + + // patch for old versions of Doctrine Inflector, to delete when we'll drop support for v1 + // see https://github.com/doctrine/inflector/issues/147#issuecomment-628807276 + if (!class_exists(InflectorFactory::class)) { + Inflector::rules('plural', ['/taxon/i' => 'taxa']); + } } public function registerBundles(): array From ad0d2b1ef671325abca85fadd3fc34e33a684dd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Thu, 28 May 2020 16:53:55 +0200 Subject: [PATCH 28/29] Support for Mercure 0.10 (#3584) * Support for Mercure 0.10 * Fix linters * Fix testPublishUpdate --- .../PublishMercureUpdatesListener.php | 66 +++++++++---- tests/Annotation/ApiResourceTest.php | 4 +- .../PublishMercureUpdatesListenerTest.php | 97 ++++++++++++++++++- 3 files changed, 144 insertions(+), 23 deletions(-) diff --git a/src/Bridge/Doctrine/EventListener/PublishMercureUpdatesListener.php b/src/Bridge/Doctrine/EventListener/PublishMercureUpdatesListener.php index d59051fd446..5eb2818a8bb 100644 --- a/src/Bridge/Doctrine/EventListener/PublishMercureUpdatesListener.php +++ b/src/Bridge/Doctrine/EventListener/PublishMercureUpdatesListener.php @@ -36,11 +36,19 @@ */ final class PublishMercureUpdatesListener { + private const ALLOWED_KEYS = [ + 'topics' => true, + 'data' => true, + 'private' => true, + 'id' => true, + 'type' => true, + 'retry' => true, + ]; + use DispatchTrait; use ResourceClassInfoTrait; private $iriConverter; - private $resourceMetadataFactory; private $serializer; private $publisher; private $expressionLanguage; @@ -127,60 +135,84 @@ private function storeEntityToPublish($entity, string $property): void return; } - $value = $this->resourceMetadataFactory->create($resourceClass)->getAttribute('mercure', false); - if (false === $value) { + $options = $this->resourceMetadataFactory->create($resourceClass)->getAttribute('mercure', false); + if (false === $options) { return; } - if (\is_string($value)) { + if (\is_string($options)) { if (null === $this->expressionLanguage) { throw new RuntimeException('The Expression Language component is not installed. Try running "composer require symfony/expression-language".'); } - $value = $this->expressionLanguage->evaluate($value, ['object' => $entity]); + $options = $this->expressionLanguage->evaluate($options, ['object' => $entity]); } - if (true === $value) { - $value = []; + if (true === $options) { + $options = []; } - if (!\is_array($value)) { - throw new InvalidArgumentException(sprintf('The value of the "mercure" attribute of the "%s" resource class must be a boolean, an array of targets or a valid expression, "%s" given.', $resourceClass, \gettype($value))); + if (!\is_array($options)) { + throw new InvalidArgumentException(sprintf('The value of the "mercure" attribute of the "%s" resource class must be a boolean, an array of options or an expression returning this array, "%s" given.', $resourceClass, \gettype($options))); + } + + foreach ($options as $key => $value) { + if (0 === $key) { + if (method_exists(Update::class, 'isPrivate')) { + throw new \InvalidArgumentException('Targets do not exist anymore since Mercure 0.10. Mark the update as private instead or downgrade the Mercure Component to version 0.3'); + } + + @trigger_error('Targets do not exist anymore since Mercure 0.10. Mark the update as private instead.', E_USER_DEPRECATED); + break; + } + + if (!isset(self::ALLOWED_KEYS[$key])) { + throw new InvalidArgumentException(sprintf('The option "%s" set in the "mercure" attribute of the "%s" resource does not exist. Existing options: "%s"', $key, $resourceClass, implode('", "', self::ALLOWED_KEYS))); + } } if ('deletedEntities' === $property) { $this->deletedEntities[(object) [ 'id' => $this->iriConverter->getIriFromItem($entity), 'iri' => $this->iriConverter->getIriFromItem($entity, UrlGeneratorInterface::ABS_URL), - ]] = $value; + ]] = $options; return; } - $this->{$property}[$entity] = $value; + $this->{$property}[$entity] = $options; } /** * @param object $entity */ - private function publishUpdate($entity, array $targets): void + private function publishUpdate($entity, array $options): void { if ($entity instanceof \stdClass) { // By convention, if the entity has been deleted, we send only its IRI // This may change in the feature, because it's not JSON Merge Patch compliant, // and I'm not a fond of this approach - $iri = $entity->iri; + $iri = $options['topics'] ?? $entity->iri; /** @var string $data */ - $data = json_encode(['@id' => $entity->id]); + $data = $options['data'] ?? json_encode(['@id' => $entity->id]); } else { $resourceClass = $this->getObjectClass($entity); $context = $this->resourceMetadataFactory->create($resourceClass)->getAttribute('normalization_context', []); - $iri = $this->iriConverter->getIriFromItem($entity, UrlGeneratorInterface::ABS_URL); - $data = $this->serializer->serialize($entity, key($this->formats), $context); + $iri = $options['topics'] ?? $this->iriConverter->getIriFromItem($entity, UrlGeneratorInterface::ABS_URL); + $data = $options['data'] ?? $this->serializer->serialize($entity, key($this->formats), $context); } - $update = new Update($iri, $data, $targets); + if (method_exists(Update::class, 'isPrivate')) { + $update = new Update($iri, $data, $options['private'] ?? false, $options['id'] ?? null, $options['type'] ?? null, $options['retry'] ?? null); + } else { + /** + * Mercure Component < 0.4. + * + * @phpstan-ignore-next-line + */ + $update = new Update($iri, $data, $options); + } $this->messageBus ? $this->dispatch($update) : ($this->publisher)($update); } } diff --git a/tests/Annotation/ApiResourceTest.php b/tests/Annotation/ApiResourceTest.php index 1fdc1fefe2c..5f24c6aacca 100644 --- a/tests/Annotation/ApiResourceTest.php +++ b/tests/Annotation/ApiResourceTest.php @@ -43,7 +43,7 @@ public function testConstruct() 'input' => 'Foo', 'iri' => 'http://example.com/res', 'itemOperations' => ['foo' => ['bar']], - 'mercure' => '[\'foo\', object.owner]', + 'mercure' => ['private' => true], 'messenger' => true, 'normalizationContext' => ['groups' => ['bar']], 'order' => ['foo', 'bar' => 'ASC'], @@ -85,7 +85,7 @@ public function testConstruct() 'formats' => ['foo', 'bar' => ['application/bar']], 'filters' => ['foo', 'bar'], 'input' => 'Foo', - 'mercure' => '[\'foo\', object.owner]', + 'mercure' => ['private' => true], 'messenger' => true, 'normalization_context' => ['groups' => ['bar']], 'order' => ['foo', 'bar' => 'ASC'], diff --git a/tests/Bridge/Doctrine/EventListener/PublishMercureUpdatesListenerTest.php b/tests/Bridge/Doctrine/EventListener/PublishMercureUpdatesListenerTest.php index a4d38a1a79a..0238647395e 100644 --- a/tests/Bridge/Doctrine/EventListener/PublishMercureUpdatesListenerTest.php +++ b/tests/Bridge/Doctrine/EventListener/PublishMercureUpdatesListenerTest.php @@ -37,8 +37,12 @@ */ class PublishMercureUpdatesListenerTest extends TestCase { - public function testPublishUpdate() + public function testLegacyPublishUpdate(): void { + if (method_exists(Update::class, 'isPrivate')) { + $this->markTestSkipped(); + } + $toInsert = new Dummy(); $toInsert->setId(1); $toInsertNotResource = new NotAResource('foo', 'bar'); @@ -84,7 +88,7 @@ public function testPublishUpdate() $targets = []; $publisher = function (Update $update) use (&$topics, &$targets): string { $topics = array_merge($topics, $update->getTopics()); - $targets[] = $update->getTargets(); + $targets[] = $update->getTargets(); // @phpstan-ignore-line return 'id'; }; @@ -115,7 +119,92 @@ public function testPublishUpdate() $this->assertSame([[], [], [], ['foo', 'bar']], $targets); } - public function testNoPublisher() + public function testPublishUpdate(): void + { + if (!method_exists(Update::class, 'isPrivate')) { + $this->markTestSkipped(); + } + + $toInsert = new Dummy(); + $toInsert->setId(1); + $toInsertNotResource = new NotAResource('foo', 'bar'); + + $toUpdate = new Dummy(); + $toUpdate->setId(2); + $toUpdateNoMercureAttribute = new DummyCar(); + + $toDelete = new Dummy(); + $toDelete->setId(3); + $toDeleteExpressionLanguage = new DummyFriend(); + $toDeleteExpressionLanguage->setId(4); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass(Argument::type(Dummy::class))->willReturn(Dummy::class); + $resourceClassResolverProphecy->getResourceClass(Argument::type(DummyCar::class))->willReturn(DummyCar::class); + $resourceClassResolverProphecy->getResourceClass(Argument::type(DummyFriend::class))->willReturn(DummyFriend::class); + $resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true); + $resourceClassResolverProphecy->isResourceClass(NotAResource::class)->willReturn(false); + $resourceClassResolverProphecy->isResourceClass(DummyCar::class)->willReturn(true); + $resourceClassResolverProphecy->isResourceClass(DummyFriend::class)->willReturn(true); + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + $iriConverterProphecy->getIriFromItem($toInsert, UrlGeneratorInterface::ABS_URL)->willReturn('http://example.com/dummies/1')->shouldBeCalled(); + $iriConverterProphecy->getIriFromItem($toUpdate, UrlGeneratorInterface::ABS_URL)->willReturn('http://example.com/dummies/2')->shouldBeCalled(); + $iriConverterProphecy->getIriFromItem($toDelete, UrlGeneratorInterface::ABS_URL)->willReturn('http://example.com/dummies/3')->shouldBeCalled(); + $iriConverterProphecy->getIriFromItem($toDelete)->willReturn('/dummies/3')->shouldBeCalled(); + $iriConverterProphecy->getIriFromItem($toDeleteExpressionLanguage)->willReturn('/dummy_friends/4')->shouldBeCalled(); + $iriConverterProphecy->getIriFromItem($toDeleteExpressionLanguage, UrlGeneratorInterface::ABS_URL)->willReturn('http://example.com/dummy_friends/4')->shouldBeCalled(); + + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); + $resourceMetadataFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadata(null, null, null, null, null, ['mercure' => true, 'normalization_context' => ['groups' => ['foo', 'bar']]])); + $resourceMetadataFactoryProphecy->create(DummyCar::class)->willReturn(new ResourceMetadata()); + $resourceMetadataFactoryProphecy->create(DummyFriend::class)->willReturn(new ResourceMetadata(null, null, null, null, null, ['mercure' => ['private' => true, 'retry' => 10]])); + + $serializerProphecy = $this->prophesize(SerializerInterface::class); + $serializerProphecy->serialize($toInsert, 'jsonld', ['groups' => ['foo', 'bar']])->willReturn('1'); + $serializerProphecy->serialize($toUpdate, 'jsonld', ['groups' => ['foo', 'bar']])->willReturn('2'); + + $formats = ['jsonld' => ['application/ld+json'], 'jsonhal' => ['application/hal+json']]; + + $topics = []; + $private = []; + $retry = []; + $publisher = function (Update $update) use (&$topics, &$private, &$retry): string { + $topics = array_merge($topics, $update->getTopics()); + $private[] = $update->isPrivate(); + $retry[] = $update->getRetry(); + + return 'id'; + }; + + $listener = new PublishMercureUpdatesListener( + $resourceClassResolverProphecy->reveal(), + $iriConverterProphecy->reveal(), + $resourceMetadataFactoryProphecy->reveal(), + $serializerProphecy->reveal(), + $formats, + null, + $publisher + ); + + $uowProphecy = $this->prophesize(UnitOfWork::class); + $uowProphecy->getScheduledEntityInsertions()->willReturn([$toInsert, $toInsertNotResource])->shouldBeCalled(); + $uowProphecy->getScheduledEntityUpdates()->willReturn([$toUpdate, $toUpdateNoMercureAttribute])->shouldBeCalled(); + $uowProphecy->getScheduledEntityDeletions()->willReturn([$toDelete, $toDeleteExpressionLanguage])->shouldBeCalled(); + + $emProphecy = $this->prophesize(EntityManagerInterface::class); + $emProphecy->getUnitOfWork()->willReturn($uowProphecy->reveal())->shouldBeCalled(); + $eventArgs = new OnFlushEventArgs($emProphecy->reveal()); + + $listener->onFlush($eventArgs); + $listener->postFlush(); + + $this->assertSame(['http://example.com/dummies/1', 'http://example.com/dummies/2', 'http://example.com/dummies/3', 'http://example.com/dummy_friends/4'], $topics); + $this->assertSame([false, false, false, true], $private); + $this->assertSame([null, null, null, 10], $retry); + } + + public function testNoPublisher(): void { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('A message bus or a publisher must be provided.'); @@ -134,7 +223,7 @@ public function testNoPublisher() public function testInvalidMercureAttribute() { $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('The value of the "mercure" attribute of the "ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy" resource class must be a boolean, an array of targets or a valid expression, "integer" given.'); + $this->expectExceptionMessage('The value of the "mercure" attribute of the "ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy" resource class must be a boolean, an array of options or an expression returning this array, "integer" given.'); $toInsert = new Dummy(); From 91e4fb828e98fe42db986abf2a53b489d3805520 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Thu, 28 May 2020 16:54:15 +0200 Subject: [PATCH 29/29] Add changelog for v2.5.6 --- CHANGELOG.md | 107 ++++++++++++++++++++++++++------------------------- 1 file changed, 55 insertions(+), 52 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1081a7c5aa0..0eebe5616a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,33 +2,36 @@ ## 2.5.6 -* JSON Schema: Missing JSON-LD context from Data Transformers #3479 -* GraphQl: Resource with no operation should be available through relations #3532 -* Fix ramsey uuid denormalization #3473 -* Revert #3331 as it breaks backwards compatibility +* Add support for Mercure 0.10 (#3584) +* Allow objects without properties (#3544) +* Fix Ramsey uuid denormalization (#3473) +* Revert #3331 as it breaks backwards compatibility +* Handle deprecations from Doctrine Inflector (#3564) +* JSON Schema: Missing JSON-LD context from Data Transformers (#3479) +* GraphQL: Resource with no operations should be available through relations (#3532) ## 2.5.5 -* Filter: Improve the RangeFilter query in case the values are equals using the between operator #3488 -* Pagination: Fix bug with large values #3451 -* Doctrine: use the correct type within `setParameter` of the SearchFilter #3331 -* Allow `\Traversable` resources #3463 -* Hydra: `hydra:writable` => `hydra:writeable` #3481 -* Hydra: Show `hydra:next` only when it's available #3457 -* Swagger UI: Missing default context argument #3443 -* Swagger UI: Fix API docs path in swagger ui #3475 -* OpenAPI: Export with unescaped slashes #3368 -* OpenAPI: OAuth flows fix #3333 -* JSON Schema: Fix metadata options #3425 -* JSON Schema: Allow decoration #3417 -* JSON Schema: Add DateInterval type #3351 -* JSON Schema: Correct schema generation for many types #3402 -* Validation: Use API Platform's `ValidationException` instead of Symfony's #3414 -* Validation: Fix a bug preventing to serialize validator's payload #3375 -* Subresources: Improve queries when there's only one level #3396 -* HTTP: Location header is only set on POST with a 201 or between 300 and 400 #3497 -* GraphQL: Do not allow empty cursor values on `before` or `after` #3360 -* Bump versions of Swagger UI, GraphiQL and GraphQL Playground #3510 +* Filter: Improve the RangeFilter query in case the values are equals using the between operator (#3488) +* Pagination: Fix bug with large values (#3451) +* Doctrine: use the correct type within `setParameter` of the SearchFilter (#3331) +* Allow `\Traversable` resources (#3463) +* Hydra: `hydra:writable` => `hydra:writeable` (#3481) +* Hydra: Show `hydra:next` only when it's available (#3457) +* Swagger UI: Missing default context argument (#3443) +* Swagger UI: Fix API docs path in swagger ui (#3475) +* OpenAPI: Export with unescaped slashes (#3368) +* OpenAPI: OAuth flows fix (#3333) +* JSON Schema: Fix metadata options (#3425) +* JSON Schema: Allow decoration (#3417) +* JSON Schema: Add DateInterval type (#3351) +* JSON Schema: Correct schema generation for many types (#3402) +* Validation: Use API Platform's `ValidationException` instead of Symfony's (#3414) +* Validation: Fix a bug preventing to serialize validator's payload (#3375) +* Subresources: Improve queries when there's only one level (#3396) +* HTTP: Location header is only set on POST with a 201 or between 300 and 400 (#3497) +* GraphQL: Do not allow empty cursor values on `before` or `after` (#3360) +* Bump versions of Swagger UI, GraphiQL and GraphQL Playground (#3510) ## 2.5.4 @@ -54,7 +57,7 @@ * Compatibility with Symfony 5 beta * Fix a notice in `SerializerContextBuilder` * Fix dashed path segment generation -* Fix support for custom filters without constructor in the `@ApiFilter` annotation +* Fix support for custom filters without constructors in the `@ApiFilter` annotation * Fix a bug that was preventing to disable Swagger/OpenAPI * Return a `404` HTTP status code instead of `500` whe the identifier is invalid (e.g.: invalid UUID) * Add links to the documentation in `@ApiResource` annotation's attributes to improve DX @@ -82,9 +85,9 @@ * Allow to not declare GET item operation * Add support for the Accept-Patch header -* Make the the `maximum_items_per_page` attribute consistent with other attributes controlling pagination +* Make the `maximum_items_per_page` attribute consistent with other attributes controlling pagination * Allow to use a string instead of an array for serializer groups -* Test: Add an helper method to find the IRI of a resource +* Test: Add a helper method to find the IRI of a resource * Test: Add assertions for testing response against JSON Schema from API resource * GraphQL: Add support for multipart request so user can create custom file upload mutations (#3041) * GraphQL: Add support for name converter (#2765) @@ -97,7 +100,7 @@ * Add infrastructure to generate a JSON Schema from a Resource `ApiPlatform\Core\JsonSchema\SchemaFactoryInterface` (#2983) * Replaces `access_control` by `security` and adds a `security_post_denormalize` attribute (#2992) * Add basic infrastructure for cursor-based pagination (#2532) -* Change ExistsFilter syntax to `exists[property]`, old syntax still supported see #2243, fixes it's behavior on GraphQL (also related #2640). +* Change ExistsFilter syntax to `exists[property]`, old syntax still supported see #2243, fixes its behavior on GraphQL (also related #2640). * Pagination with subresources (#2698) * Improve search filter id's management (#1844) * Add support of name converter in filters (#2751, #2897), filter signature in abstract methods has changed see b42dfd198b1644904fd6a684ab2cedaf530254e3 @@ -141,10 +144,10 @@ Please read #2825 if you have issues with the behavior of Readable/Writable Link * Varnish: Prevent cache miss by generating IRI for child related resources * Messenger: Unwrap exception thrown in handler for Symfony Messenger 4.3 * Fix remaining Symfony 4.3 deprecation notices -* Prevent cloning non clonable objects in `previous_data` +* Prevent cloning non cloneable objects in `previous_data` * Return a 415 HTTP status code instead of a 406 one when a faulty `Content-Type` is sent * Fix `WriteListener` trying to generate IRI for non-resources -* Allow to extract blank values from composite identifier +* Allow extracting blank values from composite identifier ## 2.4.5 @@ -312,18 +315,18 @@ Please read #2825 if you have issues with the behavior of Readable/Writable Link * OpenAPI: support generating documentation using [ReDoc](https://github.com/Rebilly/ReDoc) * OpenAPI: basic hypermedia hints using OpenAPI v3 links * OpenAPI: expose the pagination controls -* Allow to use custom classes for input and output (DTO) with the `input_class` and `output_class` attributes -* Allow to disable the input or the output by setting `input_class` and `output_class` to false +* Allow using custom classes for input and output (DTO) with the `input_class` and `output_class` attributes +* Allow disabling the input or the output by setting `input_class` and `output_class` to false * Guess and automatically set the appropriate Schema.org IRIs for common validation constraints -* Allow to set custom cache HTTP headers using the `cache_headers` attribute -* Allow to set the HTTP status code to send to the client through the `status` attribute +* Allow setting custom cache HTTP headers using the `cache_headers` attribute +* Allow setting the HTTP status code to send to the client through the `status` attribute * Add support for the `Sunset` HTTP header using the `sunset` attribute * Set the `Content-Location` and `Location` headers when appropriate for better RFC7231 conformance * Display the matching data provider and data persister in the debug panel * GraphQL: improve performance by lazy loading types * Add the `api_persist` request attribute to enable or disable the `WriteListener` -* Allow to set a default context in all normalizers -* Permit to use a string instead of an array when there is only one serialization group +* Allow setting a default context in all normalizers +* Permit using a string instead of an array when there is only one serialization group * Add support for setting relations using the constructor of the resource classes * Automatically set a [409 Conflict](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/409) HTTP status code when an `OptimisticLockException` is thrown * Resolve Dependency Injection Container parameters in the XML and YAML files for the resource class configuration @@ -348,7 +351,7 @@ Please read #2825 if you have issues with the behavior of Readable/Writable Link * OpenAPI/Swagger: add a description for the `properties[]` filter * OpenAPI/Swagger: Leverage advanced name converters * JSON-LD: Prevent an error in `ItemNormalizer` when `$context['resource_class']` is not defined -* Allow to pass a the serialization group to use a string instead of as an array of one element +* Allow to pass the serialization group to use a string instead of as an array of one element * Modernize the code base to use PHP 7.1 features when possible * Bump minimal dependencies of the used Symfony components * Improve the Packagist description @@ -367,7 +370,7 @@ Please read #2825 if you have issues with the behavior of Readable/Writable Link * Throw an `InvalidArgumentException` when trying to get an item from a collection route * Improve the debug bar panel visibility * Take into account the `route_prefix` attribute in subresources -* Allow to use multiple values with `NumericFilter` +* Allow using multiple values with `NumericFilter` * Improve exception handling in `ReadListener` by adding the previous exception ## 2.3.3 @@ -406,7 +409,7 @@ Please read #2825 if you have issues with the behavior of Readable/Writable Link * Add support for deprecating resources, operations and fields in GraphQL, Hydra and Swagger * Add API Platform panels in the Symfony profiler and in the web debug toolbar * Make resource class's constructor parameters writable -* Add support for interface as a resource +* Add support for interfaces as resources * Add a shortcut syntax to define attributes at the root of `@ApiResource` and `@ApiProperty` annotations * Throw an exception if a required filter isn't set * Allow to specify the message when access is denied using the `access_control_message` attribute @@ -481,7 +484,7 @@ Please read #2825 if you have issues with the behavior of Readable/Writable Link ## 2.2.5 -* Fix a various issues preventing the metadata cache to work properly (performance fix) +* Fix various issues preventing the metadata cache to work properly (performance fix) * Fix a cache corruption issue when using subresources * Fix non-standard outputs when using the HAL format * Persist data in Doctrine DataPersister only if needed @@ -499,7 +502,7 @@ Please read #2825 if you have issues with the behavior of Readable/Writable Link ## 2.2.3 * Fix object state inconsistency after persistence -* Allow to use multiple `@ApiFilter` annotations on the same class +* Allow using multiple `@ApiFilter` annotations on the same class * Fix a BC break when the serialization context builder depends of the retrieved data * Fix a bug regarding collections handling in the GraphQL endpoint @@ -534,8 +537,8 @@ Please read #2825 if you have issues with the behavior of Readable/Writable Link * Deprecate the `ApiPlatform\Core\Bridge\Doctrine\EventListener\WriteListener` class in favor of the new `ApiPlatform\Core\EventListener\WriteListener` class. * Remove the `api_platform.doctrine.listener.view.write` event listener service. * Add a data persistence layer with a new `ApiPlatform\Core\DataPersister\DataPersisterInterface` interface. -* Add the a new configuration to disable the API entrypoint and the documentation -* Allow to set maximum items per page at operation/resource level +* Add a new configuration to disable the API entrypoint and the documentation +* Allow setting maximum items per page at operation/resource level * Add the ability to customize the message when configuring an access control rule trough the `access_control_message` attribute * Allow empty operations in XML configs @@ -554,9 +557,9 @@ Please read #2825 if you have issues with the behavior of Readable/Writable Link * Add support for the immutable date and time types introduced in Doctrine * Fix the Doctrine query generated to retrieve nested subresources * Fix several bugs in the automatic eager loading support -* Fix a bug occurring when passing neither an IRI nor an array in an embedded relation -* Allow to request `0` items per page in collections -* Also copy the `Host` from the Symfony Router +* Fix a bug occurring when passing neither an IRI, nor an array in an embedded relation +* Allow requesting `0` items per page in collections +* Copy the `Host` from the Symfony Router * `Paginator::getLastPage()` now always returns a `float` * Minor performance improvements * Minor quality fixes @@ -566,7 +569,7 @@ Please read #2825 if you have issues with the behavior of Readable/Writable Link * Symfony 3.4 and 4.0 compatibility * Autowiring strict mode compatibility * Fix a bug preventing to create resource classes in the global namespace -* Fix Doctrine type conversion in filter's WHERE clauses +* Fix Doctrine type conversion in filters WHERE clauses * Fix filters when using eager loading and non-association composite identifier * Fix Doctrine type resolution for identifiers (for custom DBALType) * Add missing Symfony Routing options to operations configuration @@ -621,9 +624,9 @@ Please read #2825 if you have issues with the behavior of Readable/Writable Link * Add a flag to disable all request listeners * Add a default order option in the configuration * Allow to disable all operations using the XML configuration format and deprecate the previous format -* Allow upper cased property names +* Allow upper-cased property names * Improve the overall performance by optimizing `RequestAttributesExtractor` -* Improve the performance of the filters subsystem by using a PSR-11 service locator and deprecate the `FilterCollection` class +* Improve the performance of the filter subsystem by using a PSR-11 service locator and deprecate the `FilterCollection` class * Add compatibility with Symfony Flex and Symfony 4 * Allow the Symfony Dependency Injection component to autoconfigure data providers and query extensions * Allow to use service for dynamic validation groups @@ -721,7 +724,7 @@ Please read #2825 if you have issues with the behavior of Readable/Writable Link * Fix the support of the Symfony's serializer @MaxDepth annotation * Fix property range of relations in the Hydra doc when an IRI is used * Fix an error "api:swagger:export" command when decorating the Swagger normalizer -* Fix an an error in the Swagger documentation generator when a property has several serialization groups +* Fix an error in the Swagger documentation generator when a property has several serialization groups ## 2.0.1 @@ -759,7 +762,7 @@ Please read #2825 if you have issues with the behavior of Readable/Writable Link * Add a range filter * Search filter: add a case sensitivity setting * Search filter: fix the behavior of the search filter when 0 is provided as value -* Search filter: allow to use identifiers different than id +* Search filter: allow using identifiers different from id * Exclude tests from classmap * Fix some deprecations and tests @@ -805,4 +808,4 @@ Please read #2825 if you have issues with the behavior of Readable/Writable Link ## 1.0.0 beta 2 * Preserve indexes when normalizing and denormalizing associative arrays -* Allow to set default order for property when registering a `Doctrine\Orm\Filter\OrderFilter` instance +* Allow setting default order for property when registering a `Doctrine\Orm\Filter\OrderFilter` instance