Skip to content

Commit 831a1eb

Browse files
authored
Merge pull request #11581 from greg0ire/3.2.x
Merge 2.19.x up into 3.2.x
2 parents 205b2f5 + 3a82b15 commit 831a1eb

File tree

11 files changed

+351
-17
lines changed

11 files changed

+351
-17
lines changed

docs/en/reference/working-with-objects.rst

+5-4
Original file line numberDiff line numberDiff line change
@@ -338,10 +338,11 @@ Performance of different deletion strategies
338338
Deleting an object with all its associated objects can be achieved
339339
in multiple ways with very different performance impacts.
340340

341-
1. If an association is marked as ``CASCADE=REMOVE`` Doctrine ORM
342-
will fetch this association. If its a Single association it will
343-
pass this entity to
344-
``EntityManager#remove()``. If the association is a collection, Doctrine will loop over all its elements and pass them to``EntityManager#remove()``.
341+
1. If an association is marked as ``CASCADE=REMOVE`` Doctrine ORM will
342+
fetch this association. If it's a Single association it will pass
343+
this entity to ``EntityManager#remove()``. If the association is a
344+
collection, Doctrine will loop over all its elements and pass them to
345+
``EntityManager#remove()``.
345346
In both cases the cascade remove semantics are applied recursively.
346347
For large object graphs this removal strategy can be very costly.
347348
2. Using a DQL ``DELETE`` statement allows you to delete multiple

docs/en/tutorials/getting-started.rst

+2-2
Original file line numberDiff line numberDiff line change
@@ -139,12 +139,12 @@ step:
139139
140140
// Create a simple "default" Doctrine ORM configuration for Attributes
141141
$config = ORMSetup::createAttributeMetadataConfiguration(
142-
paths: array(__DIR__."/src"),
142+
paths: [__DIR__ . '/src'],
143143
isDevMode: true,
144144
);
145145
// or if you prefer XML
146146
// $config = ORMSetup::createXMLMetadataConfiguration(
147-
// paths: array(__DIR__."/config/xml"),
147+
// paths: [__DIR__ . '/config/xml'],
148148
// isDevMode: true,
149149
//);
150150

src/NativeQuery.php

+9-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,15 @@ protected function _doExecute(): Result|int
4040
$types = [];
4141

4242
foreach ($this->getParameters() as $parameter) {
43-
$name = $parameter->getName();
43+
$name = $parameter->getName();
44+
45+
if ($parameter->typeWasSpecified()) {
46+
$parameters[$name] = $parameter->getValue();
47+
$types[$name] = $parameter->getType();
48+
49+
continue;
50+
}
51+
4452
$value = $this->processParameterValue($parameter->getValue());
4553
$type = $parameter->getValue() === $value
4654
? $parameter->getType()

src/Persisters/Collection/OneToManyPersister.php

+9-3
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,12 @@
1414
use Doctrine\ORM\PersistentCollection;
1515
use Doctrine\ORM\Utility\PersisterHelper;
1616

17+
use function array_fill;
18+
use function array_keys;
1719
use function array_reverse;
1820
use function array_values;
1921
use function assert;
22+
use function count;
2023
use function implode;
2124
use function is_int;
2225
use function is_string;
@@ -174,9 +177,12 @@ private function deleteEntityCollection(PersistentCollection $collection): int
174177

175178
if ($targetClass->isInheritanceTypeSingleTable()) {
176179
$discriminatorColumn = $targetClass->getDiscriminatorColumn();
177-
$statement .= ' AND ' . $discriminatorColumn->name . ' = ?';
178-
$parameters[] = $targetClass->discriminatorValue;
179-
$types[] = $discriminatorColumn->type;
180+
$discriminatorValues = $targetClass->discriminatorValue ? [$targetClass->discriminatorValue] : array_keys($targetClass->discriminatorMap);
181+
$statement .= ' AND ' . $discriminatorColumn->name . ' IN (' . implode(', ', array_fill(0, count($discriminatorValues), '?')) . ')';
182+
foreach ($discriminatorValues as $discriminatorValue) {
183+
$parameters[] = $discriminatorValue;
184+
$types[] = $discriminatorColumn->type;
185+
}
180186
}
181187

182188
$numAffected = $this->conn->executeStatement($statement, $parameters, $types);

src/Persisters/Entity/BasicEntityPersister.php

+32-7
Original file line numberDiff line numberDiff line change
@@ -792,17 +792,42 @@ public function loadOneToOneEntity(AssociationMapping $assoc, object $sourceEnti
792792

793793
$computedIdentifier = [];
794794

795+
/** @var array<string,mixed>|null $sourceEntityData */
796+
$sourceEntityData = null;
797+
795798
// TRICKY: since the association is specular source and target are flipped
796799
foreach ($owningAssoc->targetToSourceKeyColumns as $sourceKeyColumn => $targetKeyColumn) {
797800
if (! isset($sourceClass->fieldNames[$sourceKeyColumn])) {
798-
throw MappingException::joinColumnMustPointToMappedField(
799-
$sourceClass->name,
800-
$sourceKeyColumn,
801-
);
802-
}
801+
// The likely case here is that the column is a join column
802+
// in an association mapping. However, there is no guarantee
803+
// at this point that a corresponding (generally identifying)
804+
// association has been mapped in the source entity. To handle
805+
// this case we directly reference the column-keyed data used
806+
// to initialize the source entity before throwing an exception.
807+
$resolvedSourceData = false;
808+
if (! isset($sourceEntityData)) {
809+
$sourceEntityData = $this->em->getUnitOfWork()->getOriginalEntityData($sourceEntity);
810+
}
811+
812+
if (isset($sourceEntityData[$sourceKeyColumn])) {
813+
$dataValue = $sourceEntityData[$sourceKeyColumn];
814+
if ($dataValue !== null) {
815+
$resolvedSourceData = true;
816+
$computedIdentifier[$targetClass->getFieldForColumn($targetKeyColumn)] =
817+
$dataValue;
818+
}
819+
}
803820

804-
$computedIdentifier[$targetClass->getFieldForColumn($targetKeyColumn)] =
805-
$sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity);
821+
if (! $resolvedSourceData) {
822+
throw MappingException::joinColumnMustPointToMappedField(
823+
$sourceClass->name,
824+
$sourceKeyColumn,
825+
);
826+
}
827+
} else {
828+
$computedIdentifier[$targetClass->getFieldForColumn($targetKeyColumn)] =
829+
$sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity);
830+
}
806831
}
807832

808833
$targetEntity = $this->load($computedIdentifier, null, $assoc);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\Tests\Models\OneToOneInverseSideWithAssociativeIdLoad;
6+
7+
use Doctrine\ORM\Mapping\Entity;
8+
use Doctrine\ORM\Mapping\Id;
9+
use Doctrine\ORM\Mapping\JoinColumn;
10+
use Doctrine\ORM\Mapping\OneToOne;
11+
use Doctrine\ORM\Mapping\Table;
12+
13+
#[Entity]
14+
#[Table(name: 'one_to_one_inverse_side_assoc_id_load_inverse')]
15+
class InverseSide
16+
{
17+
/** Associative id (owning identifier) */
18+
#[Id]
19+
#[OneToOne(targetEntity: InverseSideIdTarget::class, inversedBy: 'inverseSide')]
20+
#[JoinColumn(nullable: false, name: 'associativeId')]
21+
public InverseSideIdTarget $associativeId;
22+
23+
#[OneToOne(targetEntity: OwningSide::class, mappedBy: 'inverse')]
24+
public OwningSide $owning;
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\Tests\Models\OneToOneInverseSideWithAssociativeIdLoad;
6+
7+
use Doctrine\ORM\Mapping\Column;
8+
use Doctrine\ORM\Mapping\Entity;
9+
use Doctrine\ORM\Mapping\GeneratedValue;
10+
use Doctrine\ORM\Mapping\Id;
11+
use Doctrine\ORM\Mapping\OneToOne;
12+
use Doctrine\ORM\Mapping\Table;
13+
14+
#[Entity]
15+
#[Table(name: 'one_to_one_inverse_side_assoc_id_load_inverse_id_target')]
16+
class InverseSideIdTarget
17+
{
18+
#[Id]
19+
#[Column(type: 'string', length: 255)]
20+
#[GeneratedValue(strategy: 'NONE')]
21+
public string $id;
22+
23+
#[OneToOne(targetEntity: InverseSide::class, mappedBy: 'associativeId')]
24+
public InverseSide $inverseSide;
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\Tests\Models\OneToOneInverseSideWithAssociativeIdLoad;
6+
7+
use Doctrine\ORM\Mapping\Column;
8+
use Doctrine\ORM\Mapping\Entity;
9+
use Doctrine\ORM\Mapping\GeneratedValue;
10+
use Doctrine\ORM\Mapping\Id;
11+
use Doctrine\ORM\Mapping\JoinColumn;
12+
use Doctrine\ORM\Mapping\OneToOne;
13+
use Doctrine\ORM\Mapping\Table;
14+
15+
#[Entity]
16+
#[Table(name: 'one_to_one_inverse_side_assoc_id_load_owning')]
17+
class OwningSide
18+
{
19+
#[Id]
20+
#[Column(type: 'string', length: 255)]
21+
#[GeneratedValue(strategy: 'NONE')]
22+
public string $id;
23+
24+
/** Owning side */
25+
#[OneToOne(targetEntity: InverseSide::class, inversedBy: 'owning')]
26+
#[JoinColumn(name: 'inverse', referencedColumnName: 'associativeId')]
27+
public InverseSide $inverse;
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\Tests\ORM\Functional;
6+
7+
use Doctrine\Tests\Models\OneToOneInverseSideWithAssociativeIdLoad\InverseSide;
8+
use Doctrine\Tests\Models\OneToOneInverseSideWithAssociativeIdLoad\InverseSideIdTarget;
9+
use Doctrine\Tests\Models\OneToOneInverseSideWithAssociativeIdLoad\OwningSide;
10+
use Doctrine\Tests\OrmFunctionalTestCase;
11+
use PHPUnit\Framework\Attributes\Group;
12+
13+
use function assert;
14+
15+
class OneToOneInverseSideWithAssociativeIdLoadAfterDqlQueryTest extends OrmFunctionalTestCase
16+
{
17+
protected function setUp(): void
18+
{
19+
parent::setUp();
20+
21+
$this->createSchemaForModels(OwningSide::class, InverseSideIdTarget::class, InverseSide::class);
22+
}
23+
24+
#[Group('GH-11108')]
25+
public function testInverseSideWithAssociativeIdOneToOneLoadedAfterDqlQuery(): void
26+
{
27+
$owner = new OwningSide();
28+
$inverseId = new InverseSideIdTarget();
29+
$inverse = new InverseSide();
30+
31+
$owner->id = 'owner';
32+
$inverseId->id = 'inverseId';
33+
$inverseId->inverseSide = $inverse;
34+
$inverse->associativeId = $inverseId;
35+
$owner->inverse = $inverse;
36+
$inverse->owning = $owner;
37+
38+
$this->_em->persist($owner);
39+
$this->_em->persist($inverseId);
40+
$this->_em->persist($inverse);
41+
$this->_em->flush();
42+
$this->_em->clear();
43+
44+
$fetchedInverse = $this
45+
->_em
46+
->createQueryBuilder()
47+
->select('inverse')
48+
->from(InverseSide::class, 'inverse')
49+
->andWhere('inverse.associativeId = :associativeId')
50+
->setParameter('associativeId', 'inverseId')
51+
->getQuery()
52+
->getSingleResult();
53+
assert($fetchedInverse instanceof InverseSide);
54+
55+
self::assertInstanceOf(InverseSide::class, $fetchedInverse);
56+
self::assertInstanceOf(InverseSideIdTarget::class, $fetchedInverse->associativeId);
57+
self::assertInstanceOf(OwningSide::class, $fetchedInverse->owning);
58+
59+
$this->assertSQLEquals(
60+
'select o0_.associativeid as associativeid_0 from one_to_one_inverse_side_assoc_id_load_inverse o0_ where o0_.associativeid = ?',
61+
$this->getLastLoggedQuery(1)['sql'],
62+
);
63+
64+
$this->assertSQLEquals(
65+
'select t0.id as id_1, t0.inverse as inverse_2 from one_to_one_inverse_side_assoc_id_load_owning t0 where t0.inverse = ?',
66+
$this->getLastLoggedQuery()['sql'],
67+
);
68+
}
69+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\Tests\ORM\Functional\Ticket;
6+
7+
use Doctrine\Common\Collections\ArrayCollection;
8+
use Doctrine\Common\Collections\Collection;
9+
use Doctrine\ORM\Exception\ORMException;
10+
use Doctrine\ORM\Mapping as ORM;
11+
use Doctrine\Tests\OrmFunctionalTestCase;
12+
13+
class GH11501Test extends OrmFunctionalTestCase
14+
{
15+
protected function setUp(): void
16+
{
17+
parent::setUp();
18+
19+
$this->setUpEntitySchema([
20+
GH11501AbstractTestEntity::class,
21+
GH11501TestEntityOne::class,
22+
GH11501TestEntityTwo::class,
23+
GH11501TestEntityHolder::class,
24+
]);
25+
}
26+
27+
/** @throws ORMException */
28+
public function testDeleteOneToManyCollectionWithSingleTableInheritance(): void
29+
{
30+
$testEntityOne = new GH11501TestEntityOne();
31+
$testEntityTwo = new GH11501TestEntityTwo();
32+
$testEntityHolder = new GH11501TestEntityHolder();
33+
34+
$testEntityOne->testEntityHolder = $testEntityHolder;
35+
$testEntityHolder->testEntities->add($testEntityOne);
36+
37+
$testEntityTwo->testEntityHolder = $testEntityHolder;
38+
$testEntityHolder->testEntities->add($testEntityTwo);
39+
40+
$em = $this->getEntityManager();
41+
$em->persist($testEntityOne);
42+
$em->persist($testEntityTwo);
43+
$em->persist($testEntityHolder);
44+
$em->flush();
45+
46+
$testEntityHolder->testEntities = new ArrayCollection();
47+
$em->persist($testEntityHolder);
48+
$em->flush();
49+
$em->refresh($testEntityHolder);
50+
51+
static::assertEmpty($testEntityHolder->testEntities->toArray(), 'All records should have been deleted');
52+
}
53+
}
54+
55+
#[ORM\Entity]
56+
#[ORM\Table(name: 'one_to_many_single_table_inheritance_test_entities_parent_join')]
57+
#[ORM\InheritanceType('SINGLE_TABLE')]
58+
#[ORM\DiscriminatorColumn(name: 'type', type: 'string')]
59+
#[ORM\DiscriminatorMap([
60+
'test_entity_one' => 'GH11501TestEntityOne',
61+
'test_entity_two' => 'GH11501TestEntityTwo',
62+
])]
63+
class GH11501AbstractTestEntity
64+
{
65+
#[ORM\Id]
66+
#[ORM\Column(type: 'integer')]
67+
#[ORM\GeneratedValue]
68+
public int $id;
69+
70+
#[ORM\ManyToOne(targetEntity: 'GH11501TestEntityHolder', inversedBy: 'testEntities')]
71+
#[ORM\JoinColumn(name: 'test_entity_holder_id', referencedColumnName: 'id')]
72+
public GH11501TestEntityHolder $testEntityHolder;
73+
}
74+
75+
76+
#[ORM\Entity]
77+
class GH11501TestEntityOne extends GH11501AbstractTestEntity
78+
{
79+
}
80+
81+
#[ORM\Entity]
82+
class GH11501TestEntityTwo extends GH11501AbstractTestEntity
83+
{
84+
}
85+
86+
#[ORM\Entity]
87+
class GH11501TestEntityHolder
88+
{
89+
#[ORM\Id]
90+
#[ORM\Column(type: 'integer')]
91+
#[ORM\GeneratedValue]
92+
public int $id;
93+
94+
#[ORM\OneToMany(
95+
targetEntity: 'GH11501AbstractTestEntity',
96+
mappedBy: 'testEntityHolder',
97+
orphanRemoval: true,
98+
)]
99+
public Collection $testEntities;
100+
101+
public function __construct()
102+
{
103+
$this->testEntities = new ArrayCollection();
104+
}
105+
}

0 commit comments

Comments
 (0)