1
0
mirror of synced 2025-02-09 08:49:26 +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\Mapping\ClassMetadata;
use Doctrine\ORM\Query\ParameterTypeInferer; use Doctrine\ORM\Query\ParameterTypeInferer;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Utility\HierarchyDiscriminatorResolver;
/** /**
* A Query object represents a DQL query. * A Query object represents a DQL query.
@ -398,6 +399,10 @@ final class Query extends AbstractQuery
$value = $value->getMetadataValue($rsm->metadataParameterMapping[$key]); $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); $value = $this->processParameterValue($value);
$type = ($parameter->getValue() === $value) $type = ($parameter->getValue() === $value)
? $parameter->getType() ? $parameter->getType()

View File

@ -168,6 +168,13 @@ class ResultSetMapping
*/ */
public $metadataParameterMapping = []; public $metadataParameterMapping = [];
/**
* Contains query parameter names to be resolved as discriminator values
*
* @var array
*/
public $discriminatorParameters = [];
/** /**
* Adds an entity result to this ResultSetMapping. * 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\Mapping\ClassMetadataInfo;
use Doctrine\ORM\OptimisticLockException; use Doctrine\ORM\OptimisticLockException;
use Doctrine\ORM\Query; use Doctrine\ORM\Query;
use Doctrine\ORM\Utility\HierarchyDiscriminatorResolver;
use Doctrine\ORM\Utility\PersisterHelper; use Doctrine\ORM\Utility\PersisterHelper;
/** /**
@ -37,7 +38,6 @@ use Doctrine\ORM\Utility\PersisterHelper;
* @author Alexander <iam.asm89@gmail.com> * @author Alexander <iam.asm89@gmail.com>
* @author Fabio B. Silva <fabio.bat.silva@gmail.com> * @author Fabio B. Silva <fabio.bat.silva@gmail.com>
* @since 2.0 * @since 2.0
* @todo Rename: SQLWalker
*/ */
class SqlWalker implements TreeWalker class SqlWalker implements TreeWalker
{ {
@ -2035,6 +2035,7 @@ class SqlWalker implements TreeWalker
/** /**
* {@inheritdoc} * {@inheritdoc}
* @throws \Doctrine\ORM\Query\QueryException
*/ */
public function walkInstanceOfExpression($instanceOfExpr) public function walkInstanceOfExpression($instanceOfExpr)
{ {
@ -2052,36 +2053,7 @@ class SqlWalker implements TreeWalker
} }
$sql .= $class->discriminatorColumn['name'] . ($instanceOfExpr->not ? ' NOT IN ' : ' IN '); $sql .= $class->discriminatorColumn['name'] . ($instanceOfExpr->not ? ' NOT IN ' : ' IN ');
$sql .= $this->getChildDiscriminatorsFromClassMetadata($discrClass, $instanceOfExpr);
$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) . ')';
return $sql; return $sql;
} }
@ -2315,4 +2287,37 @@ class SqlWalker implements TreeWalker
return $resultAlias; 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(); ->getResult();
$this->assertCount(1, $result1); $this->assertCount(1, $result1);
$this->assertCount(1, $result2); $this->assertCount(2, $result2);
$this->assertInstanceOf(CompanyEmployee::class, $result1[0]); $this->assertInstanceOf(CompanyEmployee::class, $result1[0]);
$this->assertInstanceOf(CompanyPerson::class, $result2[0]); $this->assertContainsOnlyInstancesOf(CompanyPerson::class, $result2);
$this->assertNotInstanceOf(CompanyEmployee::class, $result2[0]);
} }
} }

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( $this->assertSqlGeneration(
"SELECT u FROM Doctrine\Tests\Models\Company\CompanyPerson u WHERE u INSTANCE OF Doctrine\Tests\Models\Company\CompanyEmployee", "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 also uses FQCNs starting with or without a backslash in the INSTANCE OF parameter
$this->assertSqlGeneration( $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 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( $this->assertSqlGeneration(
"SELECT u FROM Doctrine\Tests\Models\Company\CompanyPerson u WHERE u INSTANCE OF \Doctrine\Tests\Models\Company\CompanyEmployee", "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);
}
}