diff --git a/phpstan.neon b/phpstan.neon index 8cdf4363f41..a2f2965c437 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -11,4 +11,5 @@ parameters: # False positives - '#Parameter \#2 \$dqlPart of method Doctrine\\ORM\\QueryBuilder::add\(\) expects Doctrine\\ORM\\Query\\Expr\\Base, Doctrine\\ORM\\Query\\Expr\\Join\[\] given#' # Fixed in Doctrine's master + - '#Call to an undefined method Doctrine\\Common\\Persistence\\ObjectManager::getConnection\(\)#' - '#Parameter \#1 \$callable of static method Doctrine\\Common\\Annotations\\AnnotationRegistry::registerLoader\(\) expects callable, mixed\[\] given#' diff --git a/src/Bridge/Doctrine/Orm/ItemDataProvider.php b/src/Bridge/Doctrine/Orm/ItemDataProvider.php index c72c268093f..44cb11aff4f 100644 --- a/src/Bridge/Doctrine/Orm/ItemDataProvider.php +++ b/src/Bridge/Doctrine/Orm/ItemDataProvider.php @@ -15,15 +15,14 @@ use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryItemExtensionInterface; use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryResultItemExtensionInterface; +use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\IdentifierManagerTrait; use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGenerator; use ApiPlatform\Core\DataProvider\ItemDataProviderInterface; -use ApiPlatform\Core\Exception\PropertyNotFoundException; use ApiPlatform\Core\Exception\ResourceClassNotSupportedException; use ApiPlatform\Core\Exception\RuntimeException; use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use Doctrine\Common\Persistence\ManagerRegistry; -use Doctrine\Common\Persistence\ObjectManager; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\QueryBuilder; @@ -35,9 +34,9 @@ */ class ItemDataProvider implements ItemDataProviderInterface { + use IdentifierManagerTrait; + private $managerRegistry; - private $propertyNameCollectionFactory; - private $propertyMetadataFactory; private $itemExtensions; /** @@ -116,55 +115,4 @@ private function addWhereForIdentifiers(array $identifiers, QueryBuilder $queryB $queryBuilder->setParameter($placeholder, $value); } } - - /** - * Transform and check the identifier, composite or not. - * - * @param int|string $id - * @param ObjectManager $manager - * @param string $resourceClass - * - * @throws PropertyNotFoundException - * - * @return array - */ - private function normalizeIdentifiers($id, ObjectManager $manager, string $resourceClass): array - { - $identifierValues = [$id]; - $doctrineMetadataIdentifier = $manager->getClassMetadata($resourceClass)->getIdentifier(); - - if (count($doctrineMetadataIdentifier) >= 2) { - $identifiers = explode(';', $id); - $identifiersMap = []; - - // first transform identifiers to a proper key/value array - foreach ($identifiers as $identifier) { - $keyValue = explode('=', $identifier); - $identifiersMap[$keyValue[0]] = $keyValue[1]; - } - } - - $identifiers = []; - $i = 0; - - foreach ($this->propertyNameCollectionFactory->create($resourceClass) as $propertyName) { - $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName); - - $identifier = $propertyMetadata->isIdentifier(); - if (null === $identifier || false === $identifier) { - continue; - } - - $identifier = !isset($identifiersMap) ? $identifierValues[$i] ?? null : $identifiersMap[$propertyName] ?? null; - - if (null === $identifier) { - throw new PropertyNotFoundException(sprintf('Invalid identifier "%s", "%s" has not been found.', $id, $propertyName)); - } - - $identifiers[$propertyName] = $identifier; - ++$i; - } - - return $identifiers; - } } diff --git a/src/Bridge/Doctrine/Orm/Util/IdentifierManagerTrait.php b/src/Bridge/Doctrine/Orm/Util/IdentifierManagerTrait.php new file mode 100644 index 00000000000..941c9287e72 --- /dev/null +++ b/src/Bridge/Doctrine/Orm/Util/IdentifierManagerTrait.php @@ -0,0 +1,85 @@ + + * + * 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\Doctrine\Orm\Util; + +use ApiPlatform\Core\Exception\PropertyNotFoundException; +use Doctrine\Common\Persistence\ObjectManager; +use Doctrine\DBAL\Types\Type as DBALType; +use Doctrine\ORM\EntityManagerInterface; + +/** + * @internal + */ +trait IdentifierManagerTrait +{ + private $propertyNameCollectionFactory; + private $propertyMetadataFactory; + + /** + * Transform and check the identifier, composite or not. + * + * @param int|string $id + * @param ObjectManager $manager + * @param string $resourceClass + * + * @throws PropertyNotFoundException + * + * @return array + */ + private function normalizeIdentifiers($id, ObjectManager $manager, string $resourceClass): array + { + $identifierValues = [$id]; + $doctrineClassMetadata = $manager->getClassMetadata($resourceClass); + $doctrineIdentifierFields = $doctrineClassMetadata->getIdentifier(); + $isOrm = interface_exists(EntityManagerInterface::class) && $manager instanceof EntityManagerInterface; + $platform = $isOrm ? $manager->getConnection()->getDatabasePlatform() : null; + + if (count($doctrineIdentifierFields) > 1) { + $identifiersMap = []; + + // first transform identifiers to a proper key/value array + foreach (explode(';', $id) as $identifier) { + $identifierPair = explode('=', $identifier); + $identifiersMap[$identifierPair[0]] = $identifierPair[1]; + } + } + + $identifiers = []; + $i = 0; + + foreach ($this->propertyNameCollectionFactory->create($resourceClass) as $propertyName) { + $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName); + + if (!$propertyMetadata->isIdentifier()) { + continue; + } + + $identifier = !isset($identifiersMap) ? $identifierValues[$i] ?? null : $identifiersMap[$propertyName] ?? null; + if (null === $identifier) { + 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; + } + + return $identifiers; + } +} diff --git a/tests/Bridge/Doctrine/Orm/ItemDataProviderTest.php b/tests/Bridge/Doctrine/Orm/ItemDataProviderTest.php index aca90a30daa..f65a89b6395 100644 --- a/tests/Bridge/Doctrine/Orm/ItemDataProviderTest.php +++ b/tests/Bridge/Doctrine/Orm/ItemDataProviderTest.php @@ -23,9 +23,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; @@ -58,29 +61,19 @@ public function testGetItemSingleIdentifier() $queryBuilder = $queryBuilderProphecy->reveal(); - $repositoryProphecy = $this->prophesize(EntityRepository::class); - $repositoryProphecy->createQueryBuilder('o')->willReturn($queryBuilder)->shouldBeCalled(); - - $managerProphecy = $this->prophesize(ObjectManager::class); - $managerProphecy->getRepository(Dummy::class)->willReturn($repositoryProphecy->reveal())->shouldBeCalled(); - $classMetadataProphecy = $this->prophesize(ClassMetadata::class); - $classMetadataProphecy->getIdentifier()->willReturn(['id'])->shouldBeCalled(); - $managerProphecy->getClassMetadata(Dummy::class)->willReturn($classMetadataProphecy->reveal())->shouldBeCalled(); - - $managerRegistryProphecy = $this->prophesize(ManagerRegistry::class); - $managerRegistryProphecy->getManagerForClass(Dummy::class)->willReturn($managerProphecy->reveal())->shouldBeCalled(); + 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(); - $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); - $propertyNameCollectionFactoryProphecy->create(Dummy::class)->willReturn(new PropertyNameCollection(['id']))->shouldBeCalled(); - $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $metadata = new PropertyMetadata(); - $metadata = $metadata->withIdentifier(true); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'id')->willReturn($metadata)->shouldBeCalled(); - - $dataProvider = new ItemDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), [$extensionProphecy->reveal()]); + $dataProvider = new ItemDataProvider($managerRegistry, $propertyNameCollectionFactory, $propertyMetadataFactory, [$extensionProphecy->reveal()]); $this->assertEquals([], $dataProvider->getItem(Dummy::class, 1, 'foo', $context)); } @@ -107,61 +100,27 @@ public function testGetItemDoubleIdentifier() $queryBuilder = $queryBuilderProphecy->reveal(); - $repositoryProphecy = $this->prophesize(EntityRepository::class); - $repositoryProphecy->createQueryBuilder('o')->willReturn($queryBuilder)->shouldBeCalled(); - - $managerProphecy = $this->prophesize(ObjectManager::class); - $managerProphecy->getRepository(Dummy::class)->willReturn($repositoryProphecy->reveal())->shouldBeCalled(); - $classMetadataProphecy = $this->prophesize(ClassMetadata::class); - $classMetadataProphecy->getIdentifier()->willReturn(['ida', 'idb'])->shouldBeCalled(); - $managerProphecy->getClassMetadata(Dummy::class)->willReturn($classMetadataProphecy->reveal())->shouldBeCalled(); - - $managerRegistryProphecy = $this->prophesize(ManagerRegistry::class); - $managerRegistryProphecy->getManagerForClass(Dummy::class)->willReturn($managerProphecy->reveal())->shouldBeCalled(); + 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(); - $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); - $propertyNameCollectionFactoryProphecy->create(Dummy::class)->willReturn(new PropertyNameCollection(['nonid', 'ida', 'idb']))->shouldBeCalled(); - $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $metadata = new PropertyMetadata(); - $metadata = $metadata->withIdentifier(true); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'nonid')->willReturn(new PropertyMetadata())->shouldBeCalled(); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'ida')->willReturn($metadata)->shouldBeCalled(); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'idb')->willReturn($metadata)->shouldBeCalled(); - - $dataProvider = new ItemDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), [$extensionProphecy->reveal()]); + $dataProvider = new ItemDataProvider($managerRegistry, $propertyNameCollectionFactory, $propertyMetadataFactory, [$extensionProphecy->reveal()]); $this->assertEquals([], $dataProvider->getItem(Dummy::class, 'ida=1;idb=2', 'foo')); } - /** - * @expectedException \ApiPlatform\Core\Exception\PropertyNotFoundException - * @expectedExceptionMessage Invalid identifier "idbad=1;idb=2", "ida" has not been found. - */ - public function testInvalidIdentifier() - { - $managerProphecy = $this->prophesize(ObjectManager::class); - $classMetadataProphecy = $this->prophesize(ClassMetadata::class); - $classMetadataProphecy->getIdentifier()->willReturn(['ida', 'idb'])->shouldBeCalled(); - $managerProphecy->getClassMetadata(Dummy::class)->willReturn($classMetadataProphecy->reveal())->shouldBeCalled(); - - $managerRegistryProphecy = $this->prophesize(ManagerRegistry::class); - $managerRegistryProphecy->getManagerForClass(Dummy::class)->willReturn($managerProphecy->reveal())->shouldBeCalled(); - - $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); - $propertyNameCollectionFactoryProphecy->create(Dummy::class)->willReturn(new PropertyNameCollection(['ida', 'idb']))->shouldBeCalled(); - $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $metadata = new PropertyMetadata(); - $metadata = $metadata->withIdentifier(true); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'ida')->willReturn($metadata)->shouldBeCalled(); - - $dataProvider = new ItemDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), []); - - $this->assertEquals([], $dataProvider->getItem(Dummy::class, 'idbad=1;idb=2', 'foo')); - } - public function testQueryResultExtension() { $comparisonProphecy = $this->prophesize(Comparison::class); @@ -177,32 +136,21 @@ public function testQueryResultExtension() $queryBuilder = $queryBuilderProphecy->reveal(); - $repositoryProphecy = $this->prophesize(EntityRepository::class); - $repositoryProphecy->createQueryBuilder('o')->willReturn($queryBuilder)->shouldBeCalled(); - - $managerProphecy = $this->prophesize(ObjectManager::class); - $managerProphecy->getRepository(Dummy::class)->willReturn($repositoryProphecy->reveal())->shouldBeCalled(); - $classMetadataProphecy = $this->prophesize(ClassMetadata::class); - $classMetadataProphecy->getIdentifier()->willReturn(['id'])->shouldBeCalled(); - $managerProphecy->getClassMetadata(Dummy::class)->willReturn($classMetadataProphecy->reveal())->shouldBeCalled(); - - $managerRegistryProphecy = $this->prophesize(ManagerRegistry::class); - $managerRegistryProphecy->getManagerForClass(Dummy::class)->willReturn($managerProphecy->reveal())->shouldBeCalled(); + 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(); $extensionProphecy->supportsResult(Dummy::class, 'foo')->willReturn(true)->shouldBeCalled(); $extensionProphecy->getResult($queryBuilder)->willReturn([])->shouldBeCalled(); - $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); - $propertyNameCollectionFactoryProphecy->create(Dummy::class)->willReturn(new PropertyNameCollection(['id']))->shouldBeCalled(); - $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - - $metadata = new PropertyMetadata(); - $metadata = $metadata->withIdentifier(true); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'id')->willReturn($metadata)->shouldBeCalled(); - - $dataProvider = new ItemDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), [$extensionProphecy->reveal()]); + $dataProvider = new ItemDataProvider($managerRegistry, $propertyNameCollectionFactory, $propertyMetadataFactory, [$extensionProphecy->reveal()]); $this->assertEquals([], $dataProvider->getItem(Dummy::class, 1, 'foo')); } @@ -215,12 +163,13 @@ public function testThrowResourceClassNotSupportedException() $managerRegistryProphecy = $this->prophesize(ManagerRegistry::class); $managerRegistryProphecy->getManagerForClass(Dummy::class)->willReturn(null)->shouldBeCalled(); - $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); - $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $extensionProphecy = $this->prophesize(QueryItemExtensionInterface::class); - $dataProvider = new ItemDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), [$extensionProphecy->reveal()]); + list($propertyNameCollectionFactory, $propertyMetadataFactory) = $this->getMetadataFactories(Dummy::class, [ + 'id', + ]); + + $dataProvider = new ItemDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory, [$extensionProphecy->reveal()]); $dataProvider->getItem(Dummy::class, 'foo'); } @@ -231,25 +180,99 @@ public function testThrowResourceClassNotSupportedException() public function testCannotCreateQueryBuilder() { $repositoryProphecy = $this->prophesize(ObjectRepository::class); - $classMetadataProphecy = $this->prophesize(ClassMetadata::class); - $classMetadataProphecy->getIdentifier()->willReturn(['id'])->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(ObjectManager::class); - $managerProphecy->getRepository(Dummy::class)->willReturn($repositoryProphecy->reveal())->shouldBeCalled(); - $managerProphecy->getClassMetadata(Dummy::class)->willReturn($classMetadataProphecy->reveal())->shouldBeCalled(); + $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()); - $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); - $propertyNameCollectionFactoryProphecy->create(Dummy::class)->willReturn(new PropertyNameCollection(['id']))->shouldBeCalled(); + $extensionProphecy = $this->prophesize(QueryItemExtensionInterface::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); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'id')->willReturn((new PropertyMetadata())->withIdentifier(true))->shouldBeCalled(); - $extensionProphecy = $this->prophesize(QueryItemExtensionInterface::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()); - (new ItemDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), [$extensionProphecy->reveal()]))->getItem(Dummy::class, 'foo'); + return $managerRegistryProphecy->reveal(); } } diff --git a/tests/Bridge/Doctrine/Util/IdentifierManagerTraitTest.php b/tests/Bridge/Doctrine/Util/IdentifierManagerTraitTest.php new file mode 100644 index 00000000000..618d04b4ae6 --- /dev/null +++ b/tests/Bridge/Doctrine/Util/IdentifierManagerTraitTest.php @@ -0,0 +1,165 @@ + + * + * 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\Doctrine\Util; + +use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\IdentifierManagerTrait; +use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; +use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; +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\DBAL\Connection; +use Doctrine\DBAL\Platforms\AbstractPlatform; +use Doctrine\DBAL\Types\Type as DBALType; +use Doctrine\ORM\EntityManagerInterface; + +class IdentifierManagerTraitTest extends \PHPUnit_Framework_TestCase +{ + private function getIdentifierManagerTraitImpl(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory) + { + return new class($propertyNameCollectionFactory, $propertyMetadataFactory) { + use IdentifierManagerTrait { + IdentifierManagerTrait::normalizeIdentifiers as public; + } + + public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory) + { + $this->propertyNameCollectionFactory = $propertyNameCollectionFactory; + $this->propertyMetadataFactory = $propertyMetadataFactory; + } + }; + } + + public function testSingleIdentifier() + { + list($propertyNameCollectionFactory, $propertyMetadataFactory) = $this->getMetadataFactories(Dummy::class, [ + 'id', + ]); + $objectManager = $this->getObjectManager(Dummy::class, [ + 'id' => [ + 'type' => DBALType::INTEGER, + ], + ]); + + $identifierManager = $this->getIdentifierManagerTraitImpl($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 = $this->getIdentifierManagerTraitImpl($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 = $this->getIdentifierManagerTraitImpl($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); + + $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 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(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); + + $managerProphecy = $this->prophesize(EntityManagerInterface::class); + $managerProphecy->getClassMetadata($resourceClass)->willReturn($classMetadataProphecy->reveal()); + $managerProphecy->getConnection()->willReturn($connectionProphecy); + + return $managerProphecy->reveal(); + } +}