Skip to content

Commit 168ac31

Browse files
authored
Merge pull request #11109 from mcurland/Fix11108
Original entity data resolves inverse 1-1 joins
2 parents 8ac6a13 + fe4a2e8 commit 168ac31

File tree

5 files changed

+204
-7
lines changed

5 files changed

+204
-7
lines changed

src/Persisters/Entity/BasicEntityPersister.php

+32-7
Original file line numberDiff line numberDiff line change
@@ -832,17 +832,42 @@ public function loadOneToOneEntity(array $assoc, $sourceEntity, array $identifie
832832

833833
$computedIdentifier = [];
834834

835+
/** @var array<string,mixed>|null $sourceEntityData */
836+
$sourceEntityData = null;
837+
835838
// TRICKY: since the association is specular source and target are flipped
836839
foreach ($owningAssoc['targetToSourceKeyColumns'] as $sourceKeyColumn => $targetKeyColumn) {
837840
if (! isset($sourceClass->fieldNames[$sourceKeyColumn])) {
838-
throw MappingException::joinColumnMustPointToMappedField(
839-
$sourceClass->name,
840-
$sourceKeyColumn
841-
);
842-
}
841+
// The likely case here is that the column is a join column
842+
// in an association mapping. However, there is no guarantee
843+
// at this point that a corresponding (generally identifying)
844+
// association has been mapped in the source entity. To handle
845+
// this case we directly reference the column-keyed data used
846+
// to initialize the source entity before throwing an exception.
847+
$resolvedSourceData = false;
848+
if (! isset($sourceEntityData)) {
849+
$sourceEntityData = $this->em->getUnitOfWork()->getOriginalEntityData($sourceEntity);
850+
}
851+
852+
if (isset($sourceEntityData[$sourceKeyColumn])) {
853+
$dataValue = $sourceEntityData[$sourceKeyColumn];
854+
if ($dataValue !== null) {
855+
$resolvedSourceData = true;
856+
$computedIdentifier[$targetClass->getFieldForColumn($targetKeyColumn)] =
857+
$dataValue;
858+
}
859+
}
843860

844-
$computedIdentifier[$targetClass->getFieldForColumn($targetKeyColumn)] =
845-
$sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity);
861+
if (! $resolvedSourceData) {
862+
throw MappingException::joinColumnMustPointToMappedField(
863+
$sourceClass->name,
864+
$sourceKeyColumn
865+
);
866+
}
867+
} else {
868+
$computedIdentifier[$targetClass->getFieldForColumn($targetKeyColumn)] =
869+
$sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity);
870+
}
846871
}
847872

848873
$targetEntity = $this->load($computedIdentifier, null, $assoc);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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+
/**
14+
* @Entity()
15+
* @Table(name="one_to_one_inverse_side_assoc_id_load_inverse")
16+
*/
17+
class InverseSide
18+
{
19+
/**
20+
* Associative id (owning identifier)
21+
*
22+
* @var InverseSideIdTarget
23+
* @Id()
24+
* @OneToOne(targetEntity=InverseSideIdTarget::class, inversedBy="inverseSide")
25+
* @JoinColumn(nullable=false, name="associativeId")
26+
*/
27+
public $associativeId;
28+
29+
/**
30+
* @var OwningSide
31+
* @OneToOne(targetEntity=OwningSide::class, mappedBy="inverse")
32+
*/
33+
public $owning;
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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+
/**
15+
* @Entity()
16+
* @Table(name="one_to_one_inverse_side_assoc_id_load_inverse_id_target")
17+
*/
18+
class InverseSideIdTarget
19+
{
20+
/**
21+
* @var string
22+
* @Id()
23+
* @Column(type="string", length=255)
24+
* @GeneratedValue(strategy="NONE")
25+
*/
26+
public $id;
27+
28+
/**
29+
* @var InverseSide
30+
* @OneToOne(targetEntity=InverseSide::class, mappedBy="associativeId")
31+
*/
32+
public $inverseSide;
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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+
/**
16+
* @Entity()
17+
* @Table(name="one_to_one_inverse_side_assoc_id_load_owning")
18+
*/
19+
class OwningSide
20+
{
21+
/**
22+
* @var string
23+
* @Id()
24+
* @Column(type="string", length=255)
25+
* @GeneratedValue(strategy="NONE")
26+
*/
27+
public $id;
28+
29+
/**
30+
* Owning side
31+
*
32+
* @var InverseSide
33+
* @OneToOne(targetEntity=InverseSide::class, inversedBy="owning")
34+
* @JoinColumn(name="inverse", referencedColumnName="associativeId")
35+
*/
36+
public $inverse;
37+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
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+
12+
use function assert;
13+
14+
class OneToOneInverseSideWithAssociativeIdLoadAfterDqlQueryTest extends OrmFunctionalTestCase
15+
{
16+
protected function setUp(): void
17+
{
18+
parent::setUp();
19+
20+
$this->createSchemaForModels(OwningSide::class, InverseSideIdTarget::class, InverseSide::class);
21+
}
22+
23+
/** @group GH-11108 */
24+
public function testInverseSideWithAssociativeIdOneToOneLoadedAfterDqlQuery(): void
25+
{
26+
$owner = new OwningSide();
27+
$inverseId = new InverseSideIdTarget();
28+
$inverse = new InverseSide();
29+
30+
$owner->id = 'owner';
31+
$inverseId->id = 'inverseId';
32+
$inverseId->inverseSide = $inverse;
33+
$inverse->associativeId = $inverseId;
34+
$owner->inverse = $inverse;
35+
$inverse->owning = $owner;
36+
37+
$this->_em->persist($owner);
38+
$this->_em->persist($inverseId);
39+
$this->_em->persist($inverse);
40+
$this->_em->flush();
41+
$this->_em->clear();
42+
43+
$fetchedInverse = $this
44+
->_em
45+
->createQueryBuilder()
46+
->select('inverse')
47+
->from(InverseSide::class, 'inverse')
48+
->andWhere('inverse.associativeId = :associativeId')
49+
->setParameter('associativeId', 'inverseId')
50+
->getQuery()
51+
->getSingleResult();
52+
assert($fetchedInverse instanceof InverseSide);
53+
54+
self::assertInstanceOf(InverseSide::class, $fetchedInverse);
55+
self::assertInstanceOf(InverseSideIdTarget::class, $fetchedInverse->associativeId);
56+
self::assertInstanceOf(OwningSide::class, $fetchedInverse->owning);
57+
58+
$this->assertSQLEquals(
59+
'select o0_.associativeid as associativeid_0 from one_to_one_inverse_side_assoc_id_load_inverse o0_ where o0_.associativeid = ?',
60+
$this->getLastLoggedQuery(1)['sql']
61+
);
62+
63+
$this->assertSQLEquals(
64+
'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 = ?',
65+
$this->getLastLoggedQuery()['sql']
66+
);
67+
}
68+
}

0 commit comments

Comments
 (0)