diff --git a/src/Bridge/Doctrine/Orm/Util/IdentifierManagerTrait.php b/src/Bridge/Doctrine/Orm/Util/IdentifierManagerTrait.php index e8f3afbf71e..c7343eccdf9 100644 --- a/src/Bridge/Doctrine/Orm/Util/IdentifierManagerTrait.php +++ b/src/Bridge/Doctrine/Orm/Util/IdentifierManagerTrait.php @@ -13,6 +13,8 @@ use ApiPlatform\Core\Exception\PropertyNotFoundException; use Doctrine\Common\Persistence\ObjectManager; +use Doctrine\DBAL\Types\Type as DBALType; +use Doctrine\ORM\EntityManagerInterface; /** * @internal @@ -33,19 +35,21 @@ trait IdentifierManagerTrait * * @return array */ - public function normalizeIdentifiers($id, ObjectManager $manager, string $resourceClass): array + private function normalizeIdentifiers($id, ObjectManager $manager, string $resourceClass): array { $identifierValues = [$id]; - $doctrineMetadataIdentifier = $manager->getClassMetadata($resourceClass)->getIdentifier(); + $doctrineClassMetadata = $manager->getClassMetadata($resourceClass); + $doctrineIdentifierFields = $doctrineClassMetadata->getIdentifier(); + $isOrm = interface_exists(EntityManagerInterface::class) && $manager instanceof EntityManagerInterface; + $platform = $isOrm ? $manager->getConnection()->getDatabasePlatform() : null; - if (2 <= count($doctrineMetadataIdentifier)) { - $identifiers = explode(';', $id); + if (count($doctrineIdentifierFields) > 1) { $identifiersMap = []; // first transform identifiers to a proper key/value array - foreach ($identifiers as $identifier) { - $keyValue = explode('=', $identifier); - $identifiersMap[$keyValue[0]] = $keyValue[1]; + foreach (explode(';', $id) as $identifier) { + $identifierPair = explode('=', $identifier); + $identifiersMap[$identifierPair[0]] = $identifierPair[1]; } } @@ -55,8 +59,7 @@ public function normalizeIdentifiers($id, ObjectManager $manager, string $resour foreach ($this->propertyNameCollectionFactory->create($resourceClass) as $propertyName) { $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName); - $identifier = $propertyMetadata->isIdentifier(); - if (null === $identifier || false === $identifier) { + if (!$propertyMetadata->isIdentifier()) { continue; } @@ -65,6 +68,12 @@ public function normalizeIdentifiers($id, ObjectManager $manager, string $resour throw new PropertyNotFoundException(sprintf('Invalid identifier "%s", "%s" has not been found.', $id, $propertyName)); } + $doctrineTypeName = $doctrineClassMetadata->getTypeOfField($propertyName); + + if ($isOrm && null !== $doctrineTypeName && DBALType::hasType($doctrineTypeName)) { + $identifier = DBALType::getType($doctrineTypeName)->convertToPHPValue($identifier, $platform); + } + $identifiers[$propertyName] = $identifier; ++$i; } diff --git a/tests/Bridge/Doctrine/Orm/ItemDataProviderTest.php b/tests/Bridge/Doctrine/Orm/ItemDataProviderTest.php index 7ee30e33b2a..930ea624609 100644 --- a/tests/Bridge/Doctrine/Orm/ItemDataProviderTest.php +++ b/tests/Bridge/Doctrine/Orm/ItemDataProviderTest.php @@ -21,9 +21,12 @@ use ApiPlatform\Core\Metadata\Property\PropertyNameCollection; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy; use Doctrine\Common\Persistence\ManagerRegistry; -use Doctrine\Common\Persistence\ObjectManager; use Doctrine\Common\Persistence\ObjectRepository; +use Doctrine\DBAL\Connection; +use Doctrine\DBAL\Platforms\AbstractPlatform; +use Doctrine\DBAL\Types\Type as DBALType; use Doctrine\ORM\AbstractQuery; +use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Query\Expr; @@ -36,47 +39,6 @@ */ class ItemDataProviderTest extends \PHPUnit_Framework_TestCase { - private function getManagerRegistryProphecy(QueryBuilder $queryBuilder, array $identifiers, string $resourceClass) - { - $classMetadataProphecy = $this->prophesize(ClassMetadata::class); - $classMetadataProphecy->getIdentifier()->willReturn($identifiers)->shouldBeCalled(); - - $repositoryProphecy = $this->prophesize(EntityRepository::class); - $repositoryProphecy->createQueryBuilder('o')->willReturn($queryBuilder)->shouldBeCalled(); - - $managerProphecy = $this->prophesize(ObjectManager::class); - $managerProphecy->getClassMetadata($resourceClass)->shouldBeCalled()->willReturn($classMetadataProphecy->reveal()); - $managerProphecy->getRepository($resourceClass)->willReturn($repositoryProphecy->reveal())->shouldBeCalled(); - - $managerRegistryProphecy = $this->prophesize(ManagerRegistry::class); - $managerRegistryProphecy->getManagerForClass(Dummy::class)->willReturn($managerProphecy->reveal())->shouldBeCalled(); - - return $managerRegistryProphecy->reveal(); - } - - private function getMetadataProphecies(array $identifiers, string $resourceClass) - { - $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); - $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - - $nameCollection = ['foobar']; - - foreach ($identifiers as $identifier) { - $metadata = new PropertyMetadata(); - $metadata = $metadata->withIdentifier(true); - $propertyMetadataFactoryProphecy->create($resourceClass, $identifier)->willReturn($metadata); - - $nameCollection[] = $identifier; - } - - //random property to prevent the use of non-identifiers metadata while looping - $propertyMetadataFactoryProphecy->create($resourceClass, 'foobar')->willReturn(new PropertyMetadata()); - - $propertyNameCollectionFactoryProphecy->create($resourceClass)->willReturn(new PropertyNameCollection($nameCollection)); - - return [$propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal()]; - } - public function testGetItemSingleIdentifier() { $context = ['foo' => 'bar', 'fetch_data' => true]; @@ -97,9 +59,14 @@ public function testGetItemSingleIdentifier() $queryBuilder = $queryBuilderProphecy->reveal(); - $identifiers = ['id']; - list($propertyNameCollectionFactory, $propertyMetadataFactory) = $this->getMetadataProphecies($identifiers, Dummy::class); - $managerRegistry = $this->getManagerRegistryProphecy($queryBuilder, $identifiers, Dummy::class); + list($propertyNameCollectionFactory, $propertyMetadataFactory) = $this->getMetadataFactories(Dummy::class, [ + 'id', + ]); + $managerRegistry = $this->getManagerRegistry(Dummy::class, [ + 'id' => [ + 'type' => DBALType::INTEGER, + ], + ], $queryBuilder); $extensionProphecy = $this->prophesize(QueryItemExtensionInterface::class); $extensionProphecy->applyToItem($queryBuilder, Argument::type(QueryNameGeneratorInterface::class), Dummy::class, ['id' => 1], 'foo', $context)->shouldBeCalled(); @@ -131,9 +98,18 @@ public function testGetItemDoubleIdentifier() $queryBuilder = $queryBuilderProphecy->reveal(); - $identifiers = ['ida', 'idb']; - list($propertyNameCollectionFactory, $propertyMetadataFactory) = $this->getMetadataProphecies($identifiers, Dummy::class); - $managerRegistry = $this->getManagerRegistryProphecy($queryBuilder, $identifiers, Dummy::class); + list($propertyNameCollectionFactory, $propertyMetadataFactory) = $this->getMetadataFactories(Dummy::class, [ + 'ida', + 'idb', + ]); + $managerRegistry = $this->getManagerRegistry(Dummy::class, [ + 'ida' => [ + 'type' => DBALType::INTEGER, + ], + 'idb' => [ + 'type' => DBALType::INTEGER, + ], + ], $queryBuilder); $extensionProphecy = $this->prophesize(QueryItemExtensionInterface::class); $extensionProphecy->applyToItem($queryBuilder, Argument::type(QueryNameGeneratorInterface::class), Dummy::class, ['ida' => 1, 'idb' => 2], 'foo', [])->shouldBeCalled(); @@ -158,9 +134,14 @@ public function testQueryResultExtension() $queryBuilder = $queryBuilderProphecy->reveal(); - $identifiers = ['id']; - list($propertyNameCollectionFactory, $propertyMetadataFactory) = $this->getMetadataProphecies($identifiers, Dummy::class); - $managerRegistry = $this->getManagerRegistryProphecy($queryBuilder, $identifiers, Dummy::class); + list($propertyNameCollectionFactory, $propertyMetadataFactory) = $this->getMetadataFactories(Dummy::class, [ + 'id', + ]); + $managerRegistry = $this->getManagerRegistry(Dummy::class, [ + 'id' => [ + 'type' => DBALType::INTEGER, + ], + ], $queryBuilder); $extensionProphecy = $this->prophesize(QueryResultItemExtensionInterface::class); $extensionProphecy->applyToItem($queryBuilder, Argument::type(QueryNameGeneratorInterface::class), Dummy::class, ['id' => 1], 'foo', [])->shouldBeCalled(); @@ -182,8 +163,9 @@ public function testThrowResourceClassNotSupportedException() $extensionProphecy = $this->prophesize(QueryItemExtensionInterface::class); - $identifiers = ['id']; - list($propertyNameCollectionFactory, $propertyMetadataFactory) = $this->getMetadataProphecies($identifiers, Dummy::class); + list($propertyNameCollectionFactory, $propertyMetadataFactory) = $this->getMetadataFactories(Dummy::class, [ + 'id', + ]); $dataProvider = new ItemDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory, [$extensionProphecy->reveal()]); $dataProvider->getItem(Dummy::class, 'foo'); @@ -195,22 +177,100 @@ public function testThrowResourceClassNotSupportedException() */ public function testCannotCreateQueryBuilder() { - $identifiers = ['id']; - $repositoryProphecy = $this->prophesize(ObjectRepository::class); $classMetadataProphecy = $this->prophesize(ClassMetadata::class); - $classMetadataProphecy->getIdentifier()->willReturn($identifiers)->shouldBeCalled(); - $managerProphecy = $this->prophesize(ObjectManager::class); - $managerProphecy->getClassMetadata(Dummy::class)->shouldBeCalled()->willReturn($classMetadataProphecy->reveal()); - $managerProphecy->getRepository(Dummy::class)->willReturn($repositoryProphecy->reveal())->shouldBeCalled(); + $classMetadataProphecy->getIdentifier()->willReturn([ + 'id', + ]); + $classMetadataProphecy->getTypeOfField('id')->willReturn(DBALType::INTEGER); + + $platformProphecy = $this->prophesize(AbstractPlatform::class); + + $connectionProphecy = $this->prophesize(Connection::class); + $connectionProphecy->getDatabasePlatform()->willReturn($platformProphecy); + + $managerProphecy = $this->prophesize(EntityManagerInterface::class); + $managerProphecy->getClassMetadata(Dummy::class)->willReturn($classMetadataProphecy->reveal()); + $managerProphecy->getConnection()->willReturn($connectionProphecy); + $managerProphecy->getRepository(Dummy::class)->willReturn($repositoryProphecy->reveal()); $managerRegistryProphecy = $this->prophesize(ManagerRegistry::class); - $managerRegistryProphecy->getManagerForClass(Dummy::class)->willReturn($managerProphecy->reveal())->shouldBeCalled(); + $managerRegistryProphecy->getManagerForClass(Dummy::class)->willReturn($managerProphecy->reveal()); $extensionProphecy = $this->prophesize(QueryItemExtensionInterface::class); - list($propertyNameCollectionFactory, $propertyMetadataFactory) = $this->getMetadataProphecies($identifiers, Dummy::class); + list($propertyNameCollectionFactory, $propertyMetadataFactory) = $this->getMetadataFactories(Dummy::class, [ + 'id', + ]); (new ItemDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory, [$extensionProphecy->reveal()]))->getItem(Dummy::class, 'foo'); } + + /** + * Gets mocked metadata factories. + * + * @param string $resourceClass + * @param array $identifiers + * + * @return array + */ + private function getMetadataFactories(string $resourceClass, array $identifiers): array + { + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + + $nameCollection = ['foobar']; + + foreach ($identifiers as $identifier) { + $metadata = new PropertyMetadata(); + $metadata = $metadata->withIdentifier(true); + $propertyMetadataFactoryProphecy->create($resourceClass, $identifier)->willReturn($metadata); + + $nameCollection[] = $identifier; + } + + //random property to prevent the use of non-identifiers metadata while looping + $propertyMetadataFactoryProphecy->create($resourceClass, 'foobar')->willReturn(new PropertyMetadata()); + + $propertyNameCollectionFactoryProphecy->create($resourceClass)->willReturn(new PropertyNameCollection($nameCollection)); + + return [$propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal()]; + } + + /** + * Gets a mocked manager registry. + * + * @param string $resourceClass + * @param array $identifierFields + * @param QueryBuilder $queryBuilder + * + * @return ManagerRegistry + */ + private function getManagerRegistry(string $resourceClass, array $identifierFields, QueryBuilder $queryBuilder): ManagerRegistry + { + $classMetadataProphecy = $this->prophesize(ClassMetadata::class); + $classMetadataProphecy->getIdentifier()->willReturn(array_keys($identifierFields)); + + foreach ($identifierFields as $name => $field) { + $classMetadataProphecy->getTypeOfField($name)->willReturn($field['type']); + } + + $platformProphecy = $this->prophesize(AbstractPlatform::class); + + $connectionProphecy = $this->prophesize(Connection::class); + $connectionProphecy->getDatabasePlatform()->willReturn($platformProphecy); + + $repositoryProphecy = $this->prophesize(EntityRepository::class); + $repositoryProphecy->createQueryBuilder('o')->willReturn($queryBuilder); + + $managerProphecy = $this->prophesize(EntityManagerInterface::class); + $managerProphecy->getClassMetadata($resourceClass)->willReturn($classMetadataProphecy->reveal()); + $managerProphecy->getConnection()->willReturn($connectionProphecy); + $managerProphecy->getRepository($resourceClass)->willReturn($repositoryProphecy->reveal()); + + $managerRegistryProphecy = $this->prophesize(ManagerRegistry::class); + $managerRegistryProphecy->getManagerForClass(Dummy::class)->willReturn($managerProphecy->reveal()); + + return $managerRegistryProphecy->reveal(); + } } diff --git a/tests/Bridge/Doctrine/Util/IdentifierManagerTraitTest.php b/tests/Bridge/Doctrine/Util/IdentifierManagerTraitTest.php index baee81985b9..2d1061b1457 100644 --- a/tests/Bridge/Doctrine/Util/IdentifierManagerTraitTest.php +++ b/tests/Bridge/Doctrine/Util/IdentifierManagerTraitTest.php @@ -17,12 +17,18 @@ use ApiPlatform\Core\Metadata\Property\PropertyMetadata; use ApiPlatform\Core\Metadata\Property\PropertyNameCollection; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy; +use Doctrine\Common\Persistence\Mapping\ClassMetadata; use Doctrine\Common\Persistence\ObjectManager; -use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\DBAL\Connection; +use Doctrine\DBAL\Platforms\AbstractPlatform; +use Doctrine\DBAL\Types\Type as DBALType; +use Doctrine\ORM\EntityManagerInterface; class IdentifierManagerTraitImpl { - use IdentifierManagerTrait; + use IdentifierManagerTrait { + IdentifierManagerTrait::normalizeIdentifiers as public; + } public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory) { @@ -33,7 +39,75 @@ public function __construct(PropertyNameCollectionFactoryInterface $propertyName class IdentifierManagerTraitTest extends \PHPUnit_Framework_TestCase { - private function getMetadataProphecies(array $identifiers, string $resourceClass) + public function testSingleIdentifier() + { + list($propertyNameCollectionFactory, $propertyMetadataFactory) = $this->getMetadataFactories(Dummy::class, [ + 'id', + ]); + $objectManager = $this->getObjectManager(Dummy::class, [ + 'id' => [ + 'type' => DBALType::INTEGER, + ], + ]); + + $identifierManager = new IdentifierManagerTraitImpl($propertyNameCollectionFactory, $propertyMetadataFactory); + + $this->assertEquals($identifierManager->normalizeIdentifiers(1, $objectManager, Dummy::class), ['id' => 1]); + } + + public function testCompositeIdentifier() + { + list($propertyNameCollectionFactory, $propertyMetadataFactory) = $this->getMetadataFactories(Dummy::class, [ + 'ida', + 'idb', + ]); + $objectManager = $this->getObjectManager(Dummy::class, [ + 'ida' => [ + 'type' => DBALType::INTEGER, + ], + 'idb' => [ + 'type' => DBALType::INTEGER, + ], + ]); + + $identifierManager = new IdentifierManagerTraitImpl($propertyNameCollectionFactory, $propertyMetadataFactory); + + $this->assertEquals($identifierManager->normalizeIdentifiers('ida=1;idb=2', $objectManager, Dummy::class), ['ida' => 1, 'idb' => 2]); + } + + /** + * @expectedException \ApiPlatform\Core\Exception\PropertyNotFoundException + * @expectedExceptionMessage Invalid identifier "idbad=1;idb=2", "ida" has not been found. + */ + public function testInvalidIdentifier() + { + list($propertyNameCollectionFactory, $propertyMetadataFactory) = $this->getMetadataFactories(Dummy::class, [ + 'ida', + 'idb', + ]); + $objectManager = $this->getObjectManager(Dummy::class, [ + 'ida' => [ + 'type' => DBALType::INTEGER, + ], + 'idb' => [ + 'type' => DBALType::INTEGER, + ], + ]); + + $identifierManager = new IdentifierManagerTraitImpl($propertyNameCollectionFactory, $propertyMetadataFactory); + + $identifierManager->normalizeIdentifiers('idbad=1;idb=2', $objectManager, Dummy::class); + } + + /** + * Gets mocked metadata factories. + * + * @param string $resourceClass + * @param array $identifiers + * + * @return array + */ + private function getMetadataFactories(string $resourceClass, array $identifiers): array { $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); @@ -51,55 +125,37 @@ private function getMetadataProphecies(array $identifiers, string $resourceClass //random property to prevent the use of non-identifiers metadata while looping $propertyMetadataFactoryProphecy->create($resourceClass, 'foobar')->willReturn(new PropertyMetadata()); - $propertyNameCollectionFactoryProphecy->create($resourceClass)->willReturn(new PropertyNameCollection($nameCollection))->shouldBeCalled(); + $propertyNameCollectionFactoryProphecy->create($resourceClass)->willReturn(new PropertyNameCollection($nameCollection)); return [$propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal()]; } - private function getObjectManagerProphecy(array $output, string $resourceClass) + /** + * Gets a mocked object manager. + * + * @param string $resourceClass + * @param array $identifierFields + * + * @return ObjectManager + */ + private function getObjectManager(string $resourceClass, array $identifierFields): ObjectManager { $classMetadataProphecy = $this->prophesize(ClassMetadata::class); - $classMetadataProphecy->getIdentifier()->willReturn($output)->shouldBeCalled(); - $managerProphecy = $this->prophesize(ObjectManager::class); - $managerProphecy->getClassMetadata($resourceClass)->shouldBeCalled()->willReturn($classMetadataProphecy->reveal()); - - return $managerProphecy->reveal(); - } - - public function testSingleIdentifier() - { - $identifiers = ['id']; - list($propertyNameCollectionFactory, $propertyMetadataFactory) = $this->getMetadataProphecies($identifiers, Dummy::class); - $objectManager = $this->getObjectManagerProphecy($identifiers, Dummy::class); - - $identifierManager = new IdentifierManagerTraitImpl($propertyNameCollectionFactory, $propertyMetadataFactory); - - $this->assertEquals($identifierManager->normalizeIdentifiers(1, $objectManager, Dummy::class), ['id' => 1]); - } - - public function testCompositeIdentifier() - { - $identifiers = ['ida', 'idb']; - list($propertyNameCollectionFactory, $propertyMetadataFactory) = $this->getMetadataProphecies($identifiers, Dummy::class); - $objectManager = $this->getObjectManagerProphecy($identifiers, Dummy::class); + $classMetadataProphecy->getIdentifier()->willReturn(array_keys($identifierFields)); - $identifierManager = new IdentifierManagerTraitImpl($propertyNameCollectionFactory, $propertyMetadataFactory); + foreach ($identifierFields as $name => $field) { + $classMetadataProphecy->getTypeOfField($name)->willReturn($field['type']); + } - $this->assertEquals($identifierManager->normalizeIdentifiers('ida=1;idb=2', $objectManager, Dummy::class), ['ida' => 1, 'idb' => 2]); - } + $platformProphecy = $this->prophesize(AbstractPlatform::class); - /** - * @expectedException \ApiPlatform\Core\Exception\PropertyNotFoundException - * @expectedExceptionMessage Invalid identifier "idbad=1;idb=2", "ida" has not been found. - */ - public function testInvalidIdentifier() - { - $identifiers = ['ida', 'idb']; - list($propertyNameCollectionFactory, $propertyMetadataFactory) = $this->getMetadataProphecies($identifiers, Dummy::class); - $objectManager = $this->getObjectManagerProphecy($identifiers, Dummy::class); + $connectionProphecy = $this->prophesize(Connection::class); + $connectionProphecy->getDatabasePlatform()->willReturn($platformProphecy); - $identifierManager = new IdentifierManagerTraitImpl($propertyNameCollectionFactory, $propertyMetadataFactory); + $managerProphecy = $this->prophesize(EntityManagerInterface::class); + $managerProphecy->getClassMetadata($resourceClass)->willReturn($classMetadataProphecy->reveal()); + $managerProphecy->getConnection()->willReturn($connectionProphecy); - $identifierManager->normalizeIdentifiers('idbad=1;idb=2', $objectManager, Dummy::class); + return $managerProphecy->reveal(); } }