1
0
mirror of synced 2025-02-02 21:41:45 +03:00

Merge pull request #6392 from Jean85/pr_1441_rebased

Correct DQL `INSTANCE OF` to filter all possible child classes
This commit is contained in:
Marco Pivetta 2017-08-18 21:35:54 +02:00 committed by GitHub
commit 1a0bb82e1d
12 changed files with 512 additions and 37 deletions

View File

@ -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()

View File

@ -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.
*

View File

@ -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 <iam.asm89@gmail.com>
* @author Fabio B. Silva <fabio.bat.silva@gmail.com>
* @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) . ')';
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace Doctrine\ORM\Utility;
use Doctrine\Common\Persistence\Mapping\ClassMetadata;
use Doctrine\ORM\EntityManagerInterface;
/**
* @internal This class exists only to avoid code duplication, do not reuse it externally
*/
final class HierarchyDiscriminatorResolver
{
private function __construct()
{
}
/**
* This method is needed to make INSTANCEOF work correctly with inheritance: if the class at hand has inheritance,
* it extracts all the discriminators from the child classes and returns them
*/
public static function resolveDiscriminatorsForClass(
ClassMetadata $rootClassMetadata,
EntityManagerInterface $entityManager
): array {
$hierarchyClasses = $rootClassMetadata->subClasses;
$hierarchyClasses[] = $rootClassMetadata->name;
$discriminators = [];
foreach ($hierarchyClasses as $class) {
$currentMetadata = $entityManager->getClassMetadata($class);
$currentDiscriminator = $currentMetadata->discriminatorValue;
if (null !== $currentDiscriminator) {
$discriminators[$currentDiscriminator] = null;
}
}
return $discriminators;
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,64 @@
<?php
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\Tests\OrmFunctionalTestCase;
class Ticket4646InstanceOfAbstractTest extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->_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
{
}

View File

@ -0,0 +1,77 @@
<?php
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\Tests\OrmFunctionalTestCase;
class Ticket4646InstanceOfMultiLevelTest extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->_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
{
}

View File

@ -0,0 +1,67 @@
<?php
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\Tests\OrmFunctionalTestCase;
class Ticket4646InstanceOfParametricTest extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->_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
{
}

View File

@ -0,0 +1,66 @@
<?php
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\Tests\OrmFunctionalTestCase;
class Ticket4646InstanceOfTest extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->_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
{
}

View File

@ -0,0 +1,88 @@
<?php
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\Tests\OrmFunctionalTestCase;
class Ticket4646MultipleInstanceOfWithMultipleParametersTest extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->_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
{
}

View File

@ -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')"
);
}

View File

@ -0,0 +1,56 @@
<?php
namespace Doctrine\Tests\ORM\Utility;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Utility\HierarchyDiscriminatorResolver;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
class HierarchyDiscriminatorResolverTest extends TestCase
{
public function testResolveDiscriminatorsForClass()
{
$childClassMetadata = new ClassMetadata('ChildEntity');
$childClassMetadata->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);
}
}