diff --git a/lib/Doctrine/ORM/Query.php b/lib/Doctrine/ORM/Query.php index 385b8c8ea..222586a1f 100644 --- a/lib/Doctrine/ORM/Query.php +++ b/lib/Doctrine/ORM/Query.php @@ -27,6 +27,7 @@ use Doctrine\ORM\Query\QueryException; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Query\ParameterTypeInferer; use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\ORM\Utility\HierarchyDiscriminatorResolver; /** * A Query object represents a DQL query. @@ -398,6 +399,10 @@ final class Query extends AbstractQuery $value = $value->getMetadataValue($rsm->metadataParameterMapping[$key]); } + if (isset($rsm->discriminatorParameters[$key]) && $value instanceof ClassMetadata) { + $value = array_keys(HierarchyDiscriminatorResolver::resolveDiscriminatorsForClass($value, $this->_em)); + } + $value = $this->processParameterValue($value); $type = ($parameter->getValue() === $value) ? $parameter->getType() diff --git a/lib/Doctrine/ORM/Query/ResultSetMapping.php b/lib/Doctrine/ORM/Query/ResultSetMapping.php index efffa674c..8e4f766bd 100644 --- a/lib/Doctrine/ORM/Query/ResultSetMapping.php +++ b/lib/Doctrine/ORM/Query/ResultSetMapping.php @@ -168,6 +168,13 @@ class ResultSetMapping */ public $metadataParameterMapping = []; + /** + * Contains query parameter names to be resolved as discriminator values + * + * @var array + */ + public $discriminatorParameters = []; + /** * Adds an entity result to this ResultSetMapping. * diff --git a/lib/Doctrine/ORM/Query/SqlWalker.php b/lib/Doctrine/ORM/Query/SqlWalker.php index 6e2e41ddc..dc9a56ca0 100644 --- a/lib/Doctrine/ORM/Query/SqlWalker.php +++ b/lib/Doctrine/ORM/Query/SqlWalker.php @@ -25,6 +25,7 @@ use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Mapping\ClassMetadataInfo; use Doctrine\ORM\OptimisticLockException; use Doctrine\ORM\Query; +use Doctrine\ORM\Utility\HierarchyDiscriminatorResolver; use Doctrine\ORM\Utility\PersisterHelper; /** @@ -37,7 +38,6 @@ use Doctrine\ORM\Utility\PersisterHelper; * @author Alexander * @author Fabio B. Silva * @since 2.0 - * @todo Rename: SQLWalker */ class SqlWalker implements TreeWalker { @@ -2035,6 +2035,7 @@ class SqlWalker implements TreeWalker /** * {@inheritdoc} + * @throws \Doctrine\ORM\Query\QueryException */ public function walkInstanceOfExpression($instanceOfExpr) { @@ -2052,36 +2053,7 @@ class SqlWalker implements TreeWalker } $sql .= $class->discriminatorColumn['name'] . ($instanceOfExpr->not ? ' NOT IN ' : ' IN '); - - $sqlParameterList = []; - - foreach ($instanceOfExpr->value as $parameter) { - if ($parameter instanceof AST\InputParameter) { - $this->rsm->addMetadataParameterMapping($parameter->name, 'discriminatorValue'); - - $sqlParameterList[] = $this->walkInputParameter($parameter); - - continue; - } - - // Get name from ClassMetadata to resolve aliases. - $entityClassName = $this->em->getClassMetadata($parameter)->name; - $discriminatorValue = $class->discriminatorValue; - - if ($entityClassName !== $class->name) { - $discrMap = array_flip($class->discriminatorMap); - - if ( ! isset($discrMap[$entityClassName])) { - throw QueryException::instanceOfUnrelatedClass($entityClassName, $class->rootEntityName); - } - - $discriminatorValue = $discrMap[$entityClassName]; - } - - $sqlParameterList[] = $this->conn->quote($discriminatorValue); - } - - $sql .= '(' . implode(', ', $sqlParameterList) . ')'; + $sql .= $this->getChildDiscriminatorsFromClassMetadata($discrClass, $instanceOfExpr); return $sql; } @@ -2315,4 +2287,37 @@ class SqlWalker implements TreeWalker return $resultAlias; } + + /** + * @param ClassMetadataInfo $rootClass + * @param AST\InstanceOfExpression $instanceOfExpr + * @return string The list in parentheses of valid child discriminators from the given class + * @throws QueryException + */ + private function getChildDiscriminatorsFromClassMetadata(ClassMetadataInfo $rootClass, AST\InstanceOfExpression $instanceOfExpr): string + { + $sqlParameterList = []; + $discriminators = []; + foreach ($instanceOfExpr->value as $parameter) { + if ($parameter instanceof AST\InputParameter) { + $this->rsm->discriminatorParameters[$parameter->name] = $parameter->name; + $sqlParameterList[] = $this->walkInParameter($parameter); + continue; + } + + $metadata = $this->em->getClassMetadata($parameter); + + if ($metadata->getName() !== $rootClass->name && ! $metadata->getReflectionClass()->isSubclassOf($rootClass->name)) { + throw QueryException::instanceOfUnrelatedClass($parameter, $rootClass->name); + } + + $discriminators += HierarchyDiscriminatorResolver::resolveDiscriminatorsForClass($metadata, $this->em); + } + + foreach (array_keys($discriminators) as $dis) { + $sqlParameterList[] = $this->conn->quote($dis); + } + + return '(' . implode(', ', $sqlParameterList) . ')'; + } } diff --git a/lib/Doctrine/ORM/Utility/HierarchyDiscriminatorResolver.php b/lib/Doctrine/ORM/Utility/HierarchyDiscriminatorResolver.php new file mode 100644 index 000000000..9b08b7f17 --- /dev/null +++ b/lib/Doctrine/ORM/Utility/HierarchyDiscriminatorResolver.php @@ -0,0 +1,41 @@ +subClasses; + $hierarchyClasses[] = $rootClassMetadata->name; + + $discriminators = []; + + foreach ($hierarchyClasses as $class) { + $currentMetadata = $entityManager->getClassMetadata($class); + $currentDiscriminator = $currentMetadata->discriminatorValue; + + if (null !== $currentDiscriminator) { + $discriminators[$currentDiscriminator] = null; + } + } + + return $discriminators; + } +} diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1995Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1995Test.php index fb32b99fe..8bb975a05 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1995Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1995Test.php @@ -72,10 +72,9 @@ class DDC1995Test extends \Doctrine\Tests\OrmFunctionalTestCase ->getResult(); $this->assertCount(1, $result1); - $this->assertCount(1, $result2); + $this->assertCount(2, $result2); $this->assertInstanceOf(CompanyEmployee::class, $result1[0]); - $this->assertInstanceOf(CompanyPerson::class, $result2[0]); - $this->assertNotInstanceOf(CompanyEmployee::class, $result2[0]); + $this->assertContainsOnlyInstancesOf(CompanyPerson::class, $result2); } } diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/Ticket4646InstanceOfAbstractTest.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/Ticket4646InstanceOfAbstractTest.php new file mode 100644 index 000000000..5fa5cdc86 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/Ticket4646InstanceOfAbstractTest.php @@ -0,0 +1,64 @@ +_schemaTool->createSchema([ + $this->_em->getClassMetadata(PersonTicket4646Abstract::class), + $this->_em->getClassMetadata(EmployeeTicket4646Abstract::class), + ]); + } + + public function testInstanceOf(): void + { + $this->_em->persist(new EmployeeTicket4646Abstract()); + $this->_em->flush(); + + $dql = 'SELECT p FROM Doctrine\Tests\ORM\Functional\Ticket\PersonTicket4646Abstract p + WHERE p INSTANCE OF Doctrine\Tests\ORM\Functional\Ticket\PersonTicket4646Abstract'; + $query = $this->_em->createQuery($dql); + $result = $query->getResult(); + + self::assertCount(1, $result); + self::assertContainsOnlyInstancesOf(PersonTicket4646Abstract::class, $result); + } +} + +/** + * @Entity() + * @Table(name="instance_of_abstract_test_person") + * @InheritanceType(value="JOINED") + * @DiscriminatorColumn(name="kind", type="string") + * @DiscriminatorMap(value={ + * "employee": EmployeeTicket4646Abstract::class + * }) + */ +abstract class PersonTicket4646Abstract +{ + /** + * @Id() + * @GeneratedValue() + * @Column(type="integer") + */ + private $id; + + public function getId(): ?int + { + return $this->id; + } +} + +/** + * @Entity() + * @Table(name="instance_of_abstract_test_employee") + */ +class EmployeeTicket4646Abstract extends PersonTicket4646Abstract +{ +} diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/Ticket4646InstanceOfMultiLevelTest.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/Ticket4646InstanceOfMultiLevelTest.php new file mode 100644 index 000000000..2c111a9e6 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/Ticket4646InstanceOfMultiLevelTest.php @@ -0,0 +1,77 @@ +_schemaTool->createSchema([ + $this->_em->getClassMetadata(PersonTicket4646MultiLevel::class), + $this->_em->getClassMetadata(EmployeeTicket4646MultiLevel::class), + $this->_em->getClassMetadata(EngineerTicket4646MultiLevel::class), + ]); + } + + public function testInstanceOf(): void + { + $this->_em->persist(new PersonTicket4646MultiLevel()); + $this->_em->persist(new EmployeeTicket4646MultiLevel()); + $this->_em->persist(new EngineerTicket4646MultiLevel()); + $this->_em->flush(); + + $dql = 'SELECT p FROM Doctrine\Tests\ORM\Functional\Ticket\PersonTicket4646MultiLevel p + WHERE p INSTANCE OF Doctrine\Tests\ORM\Functional\Ticket\PersonTicket4646MultiLevel'; + $query = $this->_em->createQuery($dql); + $result = $query->getResult(); + + self::assertCount(3, $result); + self::assertContainsOnlyInstancesOf(PersonTicket4646MultiLevel::class, $result); + } +} + +/** + * @Entity() + * @Table(name="instance_of_multi_level_test_person") + * @InheritanceType(value="JOINED") + * @DiscriminatorColumn(name="kind", type="string") + * @DiscriminatorMap(value={ + * "person": "Doctrine\Tests\ORM\Functional\Ticket\PersonTicket4646MultiLevel", + * "employee": "Doctrine\Tests\ORM\Functional\Ticket\EmployeeTicket4646MultiLevel", + * "engineer": "Doctrine\Tests\ORM\Functional\Ticket\EngineerTicket4646MultiLevel", + * }) + */ +class PersonTicket4646MultiLevel +{ + /** + * @Id() + * @GeneratedValue() + * @Column(type="integer") + */ + private $id; + + public function getId(): ?int + { + return $this->id; + } +} + +/** + * @Entity() + * @Table(name="instance_of_multi_level_employee") + */ +class EmployeeTicket4646MultiLevel extends PersonTicket4646MultiLevel +{ +} + +/** + * @Entity() + * @Table(name="instance_of_multi_level_engineer") + */ +class EngineerTicket4646MultiLevel extends EmployeeTicket4646MultiLevel +{ +} diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/Ticket4646InstanceOfParametricTest.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/Ticket4646InstanceOfParametricTest.php new file mode 100644 index 000000000..bf11c3a85 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/Ticket4646InstanceOfParametricTest.php @@ -0,0 +1,67 @@ +_schemaTool->createSchema([ + $this->_em->getClassMetadata(PersonTicket4646Parametric::class), + $this->_em->getClassMetadata(EmployeeTicket4646Parametric::class), + ]); + } + + public function testInstanceOf(): void + { + $this->_em->persist(new PersonTicket4646Parametric()); + $this->_em->persist(new EmployeeTicket4646Parametric()); + $this->_em->flush(); + $dql = 'SELECT p FROM Doctrine\Tests\ORM\Functional\Ticket\PersonTicket4646Parametric p + WHERE p INSTANCE OF :parameter'; + $query = $this->_em->createQuery($dql); + $query->setParameter( + 'parameter', + $this->_em->getClassMetadata(PersonTicket4646Parametric::class) + ); + $result = $query->getResult(); + self::assertCount(2, $result); + self::assertContainsOnlyInstancesOf(PersonTicket4646Parametric::class, $result); + } +} + +/** + * @Entity() + * @Table(name="instance_of_parametric_person") + * @InheritanceType(value="JOINED") + * @DiscriminatorColumn(name="kind", type="string") + * @DiscriminatorMap(value={ + * "person": "Doctrine\Tests\ORM\Functional\Ticket\PersonTicket4646Parametric", + * "employee": "Doctrine\Tests\ORM\Functional\Ticket\EmployeeTicket4646Parametric" + * }) + */ +class PersonTicket4646Parametric +{ + /** + * @Id() + * @GeneratedValue() + * @Column(type="integer") + */ + private $id; + + public function getId(): ?int + { + return $this->id; + } +} + +/** + * @Entity() + * @Table(name="instance_of_parametric_employee") + */ +class EmployeeTicket4646Parametric extends PersonTicket4646Parametric +{ +} diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/Ticket4646InstanceOfTest.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/Ticket4646InstanceOfTest.php new file mode 100644 index 000000000..0ed97243c --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/Ticket4646InstanceOfTest.php @@ -0,0 +1,66 @@ +_schemaTool->createSchema([ + $this->_em->getClassMetadata(PersonTicket4646::class), + $this->_em->getClassMetadata(EmployeeTicket4646::class), + ]); + } + + public function testInstanceOf(): void + { + $this->_em->persist(new PersonTicket4646()); + $this->_em->persist(new EmployeeTicket4646()); + $this->_em->flush(); + + $dql = 'SELECT p FROM Doctrine\Tests\ORM\Functional\Ticket\PersonTicket4646 p + WHERE p INSTANCE OF Doctrine\Tests\ORM\Functional\Ticket\PersonTicket4646'; + $query = $this->_em->createQuery($dql); + $result = $query->getResult(); + + self::assertCount(2, $result); + self::assertContainsOnlyInstancesOf(PersonTicket4646::class, $result); + } +} + +/** + * @Entity() + * @Table(name="instance_of_test_person") + * @InheritanceType(value="JOINED") + * @DiscriminatorColumn(name="kind", type="string") + * @DiscriminatorMap(value={ + * "person": "Doctrine\Tests\ORM\Functional\Ticket\PersonTicket4646", + * "employee": "Doctrine\Tests\ORM\Functional\Ticket\EmployeeTicket4646" + * }) + */ +class PersonTicket4646 +{ + /** + * @Id() + * @GeneratedValue() + * @Column(type="integer") + */ + private $id; + + public function getId(): ?int + { + return $this->id; + } +} + +/** + * @Entity() + * @Table(name="instance_of_test_employee") + */ +class EmployeeTicket4646 extends PersonTicket4646 +{ +} diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/Ticket4646InstanceOfWithMultipleParametersTest.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/Ticket4646InstanceOfWithMultipleParametersTest.php new file mode 100644 index 000000000..c8c172ab6 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/Ticket4646InstanceOfWithMultipleParametersTest.php @@ -0,0 +1,88 @@ +_schemaTool->createSchema([ + $this->_em->getClassMetadata(PersonTicket4646Multiple::class), + $this->_em->getClassMetadata(EmployeeTicket4646Multiple::class), + $this->_em->getClassMetadata(ManagerTicket4646Multiple::class), + $this->_em->getClassMetadata(InternTicket4646Multiple::class), + ]); + } + + public function testInstanceOf(): void + { + $this->_em->persist(new PersonTicket4646Multiple()); + $this->_em->persist(new EmployeeTicket4646Multiple()); + $this->_em->persist(new ManagerTicket4646Multiple()); + $this->_em->persist(new InternTicket4646Multiple()); + $this->_em->flush(); + + $dql = 'SELECT p FROM Doctrine\Tests\ORM\Functional\Ticket\PersonTicket4646Multiple p + WHERE p INSTANCE OF (Doctrine\Tests\ORM\Functional\Ticket\EmployeeTicket4646Multiple, Doctrine\Tests\ORM\Functional\Ticket\InternTicket4646Multiple)'; + $query = $this->_em->createQuery($dql); + $result = $query->getResult(); + + self::assertCount(2, $result); + self::assertContainsOnlyInstancesOf(PersonTicket4646Multiple::class, $result); + } +} + +/** + * @Entity() + * @Table(name="instance_of_test_multiple_person") + * @InheritanceType(value="JOINED") + * @DiscriminatorColumn(name="kind", type="string") + * @DiscriminatorMap(value={ + * "person": "Doctrine\Tests\ORM\Functional\Ticket\PersonTicket4646Multiple", + * "employee": "Doctrine\Tests\ORM\Functional\Ticket\EmployeeTicket4646Multiple", + * "manager": "Doctrine\Tests\ORM\Functional\Ticket\ManagerTicket4646Multiple", + * "intern": "Doctrine\Tests\ORM\Functional\Ticket\InternTicket4646Multiple" + * }) + */ +class PersonTicket4646Multiple +{ + /** + * @Id() + * @GeneratedValue() + * @Column(type="integer") + */ + private $id; + + public function getId() + { + return $this->id; + } +} + +/** + * @Entity() + * @Table(name="instance_of_test_multiple_employee") + */ +class EmployeeTicket4646Multiple extends PersonTicket4646Multiple +{ +} + +/** + * @Entity() + * @Table(name="instance_of_test_multiple_manager") + */ +class ManagerTicket4646Multiple extends PersonTicket4646Multiple +{ +} + +/** + * @Entity() + * @Table(name="instance_of_test_multiple_intern") + */ +class InternTicket4646Multiple extends PersonTicket4646Multiple +{ +} diff --git a/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php b/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php index 9e04c3183..4a093094d 100644 --- a/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php +++ b/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php @@ -500,7 +500,7 @@ class SelectSqlGenerationTest extends OrmTestCase { $this->assertSqlGeneration( "SELECT u FROM Doctrine\Tests\Models\Company\CompanyPerson u WHERE u INSTANCE OF Doctrine\Tests\Models\Company\CompanyEmployee", - "SELECT c0_.id AS id_0, c0_.name AS name_1, c0_.discr AS discr_2 FROM company_persons c0_ WHERE c0_.discr IN ('employee')" + "SELECT c0_.id AS id_0, c0_.name AS name_1, c0_.discr AS discr_2 FROM company_persons c0_ WHERE c0_.discr IN ('manager', 'employee')" ); } @@ -509,7 +509,7 @@ class SelectSqlGenerationTest extends OrmTestCase // This also uses FQCNs starting with or without a backslash in the INSTANCE OF parameter $this->assertSqlGeneration( "SELECT u FROM Doctrine\Tests\Models\Company\CompanyPerson u WHERE u INSTANCE OF (Doctrine\Tests\Models\Company\CompanyEmployee, \Doctrine\Tests\Models\Company\CompanyManager)", - "SELECT c0_.id AS id_0, c0_.name AS name_1, c0_.discr AS discr_2 FROM company_persons c0_ WHERE c0_.discr IN ('employee', 'manager')" + "SELECT c0_.id AS id_0, c0_.name AS name_1, c0_.discr AS discr_2 FROM company_persons c0_ WHERE c0_.discr IN ('manager', 'employee')" ); } @@ -520,7 +520,7 @@ class SelectSqlGenerationTest extends OrmTestCase { $this->assertSqlGeneration( "SELECT u FROM Doctrine\Tests\Models\Company\CompanyPerson u WHERE u INSTANCE OF \Doctrine\Tests\Models\Company\CompanyEmployee", - "SELECT c0_.id AS id_0, c0_.name AS name_1, c0_.discr AS discr_2 FROM company_persons c0_ WHERE c0_.discr IN ('employee')" + "SELECT c0_.id AS id_0, c0_.name AS name_1, c0_.discr AS discr_2 FROM company_persons c0_ WHERE c0_.discr IN ('manager', 'employee')" ); } diff --git a/tests/Doctrine/Tests/ORM/Utility/HierarchyDiscriminatorResolverTest.php b/tests/Doctrine/Tests/ORM/Utility/HierarchyDiscriminatorResolverTest.php new file mode 100644 index 000000000..cffb8cd3b --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Utility/HierarchyDiscriminatorResolverTest.php @@ -0,0 +1,56 @@ +name = 'Some\Class\Child\Name'; + $childClassMetadata->discriminatorValue = 'child-discriminator'; + + $classMetadata = new ClassMetadata('Entity'); + $classMetadata->subClasses = [$childClassMetadata->name]; + $classMetadata->name = 'Some\Class\Name'; + $classMetadata->discriminatorValue = 'discriminator'; + + $em = $this->prophesize(EntityManagerInterface::class); + $em->getClassMetadata($classMetadata->name) + ->shouldBeCalled() + ->willReturn($classMetadata); + $em->getClassMetadata($childClassMetadata->name) + ->shouldBeCalled() + ->willReturn($childClassMetadata); + + $discriminators = HierarchyDiscriminatorResolver::resolveDiscriminatorsForClass($classMetadata, $em->reveal()); + + $this->assertCount(2, $discriminators); + $this->assertArrayHasKey($classMetadata->discriminatorValue, $discriminators); + $this->assertArrayHasKey($childClassMetadata->discriminatorValue, $discriminators); + } + + public function testResolveDiscriminatorsForClassWithNoSubclasses() + { + $classMetadata = new ClassMetadata('Entity'); + $classMetadata->subClasses = []; + $classMetadata->name = 'Some\Class\Name'; + $classMetadata->discriminatorValue = 'discriminator'; + + $em = $this->prophesize(EntityManagerInterface::class); + $em->getClassMetadata($classMetadata->name) + ->shouldBeCalled() + ->willReturn($classMetadata); + + $discriminators = HierarchyDiscriminatorResolver::resolveDiscriminatorsForClass($classMetadata, $em->reveal()); + + $this->assertCount(1, $discriminators); + $this->assertArrayHasKey($classMetadata->discriminatorValue, $discriminators); + } +}