Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EntityManager::getReference() should handle a PK which is also a FK #11601

Open
wants to merge 8 commits into
base: 2.20.x
Choose a base branch
from
2 changes: 1 addition & 1 deletion docs/en/reference/advanced-configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,7 @@ for the ``$identifier`` parameter is passed. ``$identifier`` values are
not checked and there is no guarantee that the requested entity instance even
exists – the method will still return a proxy object.

Its only when the proxy has to be fully initialized or associations cannot
It is only when the proxy has to be fully initialized or associations cannot
be written to the database that invalid ``$identifier`` values may lead to
exceptions.

Expand Down
141 changes: 69 additions & 72 deletions src/EntityManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -434,50 +434,9 @@ public function find($className, $id, $lockMode = null, $lockVersion = null)
$this->checkLockRequirements($lockMode, $class);
}

if (! is_array($id)) {
if ($class->isIdentifierComposite) {
throw ORMInvalidArgumentException::invalidCompositeIdentifier();
}

$id = [$class->identifier[0] => $id];
}

foreach ($id as $i => $value) {
if (is_object($value)) {
$className = DefaultProxyClassNameResolver::getClass($value);
if ($this->metadataFactory->hasMetadataFor($className)) {
$id[$i] = $this->unitOfWork->getSingleIdentifierValue($value);

if ($id[$i] === null) {
throw ORMInvalidArgumentException::invalidIdentifierBindingEntity($className);
}
}
}
}

$sortedId = [];

foreach ($class->identifier as $identifier) {
if (! isset($id[$identifier])) {
throw MissingIdentifierField::fromFieldAndClass($identifier, $class->name);
}

if ($id[$identifier] instanceof BackedEnum) {
$sortedId[$identifier] = $id[$identifier]->value;
} else {
$sortedId[$identifier] = $id[$identifier];
}

unset($id[$identifier]);
}

if ($id) {
throw UnrecognizedIdentifierFields::fromClassAndFieldNames($class->name, array_keys($id));
}

$unitOfWork = $this->getUnitOfWork();
$canonicalId = $this->getCanonicalId($class, $id);

$entity = $unitOfWork->tryGetById($sortedId, $class->rootEntityName);
$entity = $this->unitOfWork->tryGetById($canonicalId, $class->rootEntityName);

// Check identity map first
if ($entity !== false) {
Expand All @@ -493,32 +452,32 @@ public function find($className, $id, $lockMode = null, $lockVersion = null)
case $lockMode === LockMode::NONE:
case $lockMode === LockMode::PESSIMISTIC_READ:
case $lockMode === LockMode::PESSIMISTIC_WRITE:
$persister = $unitOfWork->getEntityPersister($class->name);
$persister->refresh($sortedId, $entity, $lockMode);
$persister = $this->unitOfWork->getEntityPersister($class->name);
$persister->refresh($canonicalId, $entity, $lockMode);
break;
}

return $entity; // Hit!
}

$persister = $unitOfWork->getEntityPersister($class->name);
$persister = $this->unitOfWork->getEntityPersister($class->name);

switch (true) {
case $lockMode === LockMode::OPTIMISTIC:
$entity = $persister->load($sortedId);
$entity = $persister->load($canonicalId);

if ($entity !== null) {
$unitOfWork->lock($entity, $lockMode, $lockVersion);
$this->unitOfWork->lock($entity, $lockMode, $lockVersion);
}

return $entity;

case $lockMode === LockMode::PESSIMISTIC_READ:
case $lockMode === LockMode::PESSIMISTIC_WRITE:
return $persister->load($sortedId, null, null, [], $lockMode);
return $persister->load($canonicalId, null, null, [], $lockMode);

default:
return $persister->loadById($sortedId);
return $persister->loadById($canonicalId);
}
}

Expand All @@ -529,39 +488,22 @@ public function getReference($entityName, $id)
{
$class = $this->metadataFactory->getMetadataFor(ltrim($entityName, '\\'));

if (! is_array($id)) {
$id = [$class->identifier[0] => $id];
}

$sortedId = [];

foreach ($class->identifier as $identifier) {
if (! isset($id[$identifier])) {
throw MissingIdentifierField::fromFieldAndClass($identifier, $class->name);
}

$sortedId[$identifier] = $id[$identifier];
unset($id[$identifier]);
}

if ($id) {
throw UnrecognizedIdentifierFields::fromClassAndFieldNames($class->name, array_keys($id));
}
$canonicalId = $this->getCanonicalId($class, $id);

$entity = $this->unitOfWork->tryGetById($sortedId, $class->rootEntityName);
$entity = $this->unitOfWork->tryGetById($canonicalId, $class->rootEntityName);

// Check identity map first, if its already in there just return it.
if ($entity !== false) {
return $entity instanceof $class->name ? $entity : null;
}

if ($class->subClasses) {
return $this->find($entityName, $sortedId);
return $this->find($entityName, $canonicalId);
}

$entity = $this->proxyFactory->getProxy($class->name, $sortedId);
$entity = $this->proxyFactory->getProxy($class->name, $canonicalId);

$this->unitOfWork->registerManaged($entity, $sortedId, []);
$this->unitOfWork->registerManaged($entity, $canonicalId, []);

return $entity;
}
Expand Down Expand Up @@ -1117,4 +1059,59 @@ private function configureLegacyMetadataCache(): void
// Wrap doctrine/cache to provide PSR-6 interface
$this->metadataFactory->setCache(CacheAdapter::wrap($metadataCache));
}

/**
* @param mixed $id The identitifiers to sort.
*
* @return mixed The sorted identifiers.
*
* @throws ORMInvalidArgumentException
* @throws MissingIdentifierField
* @throws UnrecognizedIdentifierFields
*/
private function getCanonicalId(ClassMetadata $class, $id)
{
if (! is_array($id)) {
if ($class->isIdentifierComposite) {
throw ORMInvalidArgumentException::invalidCompositeIdentifier();
}

$id = [$class->identifier[0] => $id];
}

foreach ($id as $i => $value) {
if (is_object($value)) {
$className = DefaultProxyClassNameResolver::getClass($value);
if ($this->metadataFactory->hasMetadataFor($className)) {
$id[$i] = $this->unitOfWork->getSingleIdentifierValue($value);

if ($id[$i] === null) {
throw ORMInvalidArgumentException::invalidIdentifierBindingEntity($className);
}
}
}
}

$canonicalId = [];

foreach ($class->identifier as $identifier) {
if (! isset($id[$identifier])) {
throw MissingIdentifierField::fromFieldAndClass($identifier, $class->name);
}

if ($id[$identifier] instanceof BackedEnum) {
$canonicalId[$identifier] = $id[$identifier]->value;
} else {
$canonicalId[$identifier] = $id[$identifier];
}

unset($id[$identifier]);
}

if ($id) {
throw UnrecognizedIdentifierFields::fromClassAndFieldNames($class->name, array_keys($id));
}

return $canonicalId;
}
}
16 changes: 14 additions & 2 deletions src/Proxy/ProxyFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
use function array_combine;
use function array_flip;
use function array_intersect_key;
use function assert;
use function bin2hex;
use function chmod;
use function class_exists;
Expand Down Expand Up @@ -465,8 +466,9 @@ private function getProxyFactory(string $className): Closure
$initializer = $this->createLazyInitializer($class, $entityPersister, $this->identifierFlattener);
$proxyClassName = $this->loadProxyClass($class);
$identifierFields = array_intersect_key($class->getReflectionProperties(), $identifiers);
$em = $this->em;

$proxyFactory = Closure::bind(static function (array $identifier) use ($initializer, $skippedProperties, $identifierFields, $className): InternalProxy {
$proxyFactory = Closure::bind(static function (array $identifier) use ($initializer, $skippedProperties, $identifierFields, $className, $class, $em): InternalProxy {
$proxy = self::createLazyGhost(static function (InternalProxy $object) use ($initializer, $identifier): void {
$initializer($object, $identifier);
}, $skippedProperties);
Expand All @@ -476,7 +478,17 @@ private function getProxyFactory(string $className): Closure
throw ORMInvalidArgumentException::missingPrimaryKeyValue($className, $idField);
}

$reflector->setValue($proxy, $identifier[$idField]);
assert($reflector !== null);

$idValue = $identifier[$idField];
if ($class->hasAssociation($idField)) {
$idValue = $em->getReference(
$class->getAssociationTargetClass($idField),
$idValue
);
}

$reflector->setValue($proxy, $idValue);
}

return $proxy;
Expand Down
29 changes: 29 additions & 0 deletions tests/Tests/Models/RelationAsId/Group.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

namespace Doctrine\Tests\Models\RelationAsId;

use Doctrine\ORM\Mapping as ORM;

/**
* @ORM\Entity
* @ORM\Table(name="relation_as_id_group")
*/
class Group
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
*
* @var int
*/
public $id;

/**
* @ORM\Column(type="string")
*
* @var string
*/
public $name;
}
37 changes: 37 additions & 0 deletions tests/Tests/Models/RelationAsId/Membership.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

namespace Doctrine\Tests\Models\RelationAsId;

use Doctrine\ORM\Mapping as ORM;

/**
* @ORM\Entity
* @ORM\Table(name="relation_as_id_membership")
*/
class Membership
{
/**
* @ORM\Id
* @ORM\ManyToOne(targetEntity=User::class)
*
* @var User
*/
public $user;

/**
* @ORM\Id
* @ORM\ManyToOne(targetEntity=Group::class)
*
* @var Group
*/
public $group;

/**
* @ORM\Column(type="string")
*
* @var srtring
*/
public $role;
}
29 changes: 29 additions & 0 deletions tests/Tests/Models/RelationAsId/Profile.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

namespace Doctrine\Tests\Models\RelationAsId;

use Doctrine\ORM\Mapping as ORM;

/**
* @ORM\Entity
* @ORM\Table(name="relation_as_id_profile")
*/
class Profile
{
/**
* @ORM\Id
* @ORM\OneToOne(targetEntity="User")
*
* @var User
*/
public $user;

/**
* @ORM\Column(type="string")
*
* @var string
*/
public $url;
}
29 changes: 29 additions & 0 deletions tests/Tests/Models/RelationAsId/User.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

namespace Doctrine\Tests\Models\RelationAsId;

use Doctrine\ORM\Mapping as ORM;

/**
* @ORM\Entity
* @ORM\Table(name="relation_as_id_user")
*/
class User
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
*
* @var int
*/
public $id;

/**
* @ORM\Column(type="string")
*
* @var string
*/
public $name;
}
Loading