Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Internal/Hydration/ObjectHydrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ private function getEntity(array $data, string $dqlAlias): object
}

$this->hints['fetchAlias'] = $dqlAlias;
$this->hints['isPartial'] = $this->rsm->partialAliases[$dqlAlias] ?? false;

return $this->uow->createEntity($className, $data, $this->hints);
}
Expand Down
2 changes: 2 additions & 0 deletions src/Internal/Hydration/SimpleObjectHydrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ protected function prepare(): void
}

$this->class = $this->getClassMetadata(reset($this->resultSetMapping()->aliasMap));

$this->hints['isPartial'] = count($this->rsm->partialAliases) > 0;
}

protected function cleanup(): void
Expand Down
8 changes: 5 additions & 3 deletions src/Proxy/ProxyFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ public function __construct(
* @param class-string $className
* @param array<mixed> $identifier
*/
public function getProxy(string $className, array $identifier): object
public function getProxy(string $className, array $identifier, bool $assignIdentifiers = true): object
{
if ($this->em->getConfiguration()->isNativeLazyObjectsEnabled()) {
$classMetadata = $this->em->getClassMetadata($className);
Expand All @@ -228,8 +228,10 @@ public function getProxy(string $className, array $identifier): object
}
}, ReflectionClass::SKIP_INITIALIZATION_ON_SERIALIZE);

foreach ($identifier as $idField => $value) {
$classMetadata->propertyAccessors[$idField]->setValue($proxy, $value);
if ($assignIdentifiers) {
foreach ($identifier as $idField => $value) {
$classMetadata->propertyAccessors[$idField]->setValue($proxy, $value);
}
}

return $proxy;
Expand Down
19 changes: 17 additions & 2 deletions src/Query/ResultSetMapping.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ class ResultSetMapping
*/
public array $aliasMap = [];

/**
* Weather this alias is for a partially loaded entity
*
* @var array<string, bool>
*/
public array $partialAliases = [];

/**
* Maps alias names to related association field names.
*
Expand Down Expand Up @@ -207,7 +214,7 @@ class ResultSetMapping
*
* @todo Rename: addRootEntity
*/
public function addEntityResult(string $class, string $alias, string|null $resultAlias = null): static
public function addEntityResult(string $class, string $alias, string|null $resultAlias = null, bool $isPartial = false): static
{
$this->aliasMap[$alias] = $class;
$this->entityMappings[$alias] = $resultAlias;
Expand All @@ -216,6 +223,10 @@ public function addEntityResult(string $class, string $alias, string|null $resul
$this->isMixed = true;
}

if ($isPartial) {
$this->partialAliases[$alias] = true;
}

return $this;
}

Expand Down Expand Up @@ -388,12 +399,16 @@ public function getColumnAliasByField(string $alias, string $fieldName): string
*
* @todo Rename: addJoinedEntity
*/
public function addJoinedEntityResult(string $class, string $alias, string $parentAlias, string $relation): static
public function addJoinedEntityResult(string $class, string $alias, string $parentAlias, string $relation, bool $isPartial = false): static
{
$this->aliasMap[$alias] = $class;
$this->parentAliasMap[$alias] = $parentAlias;
$this->relationMap[$alias] = $relation;

if ($isPartial) {
$this->partialAliases[$alias] = true;
}

return $this;
}

Expand Down
7 changes: 5 additions & 2 deletions src/Query/SqlWalker.php
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
<?php

declare(strict_types=1);
Expand Down Expand Up @@ -112,9 +112,9 @@
/**
* A list of classes that appear in non-scalar SelectExpressions.
*
* @phpstan-var array<string, array{class: ClassMetadata, dqlAlias: string, resultAlias: string|null}>
* @phpstan-var array<string, array{class: ClassMetadata, dqlAlias: string, resultAlias: string|null, partial: bool}>>

Check failure on line 115 in src/Query/SqlWalker.php

View workflow job for this annotation

GitHub Actions / Static Analysis with PHPStan (default, phpstan.neon)

PHPDoc tag @phpstan-var has invalid value (array<string, array{class: ClassMetadata, dqlAlias: string, resultAlias: string|null, partial: bool}>>): Unexpected token ">", expected TOKEN_HORIZONTAL_WS at offset 202 on line 4

Check failure on line 115 in src/Query/SqlWalker.php

View workflow job for this annotation

GitHub Actions / Static Analysis with PHPStan (3.8.2, phpstan-dbal3.neon)

PHPDoc tag @phpstan-var has invalid value (array<string, array{class: ClassMetadata, dqlAlias: string, resultAlias: string|null, partial: bool}>>): Unexpected token ">", expected TOKEN_HORIZONTAL_WS at offset 202 on line 4
*/
private array $selectedClasses = [];

Check failure on line 117 in src/Query/SqlWalker.php

View workflow job for this annotation

GitHub Actions / Static Analysis with PHPStan (default, phpstan.neon)

Property Doctrine\ORM\Query\SqlWalker::$selectedClasses type has no value type specified in iterable type array.

Check failure on line 117 in src/Query/SqlWalker.php

View workflow job for this annotation

GitHub Actions / Static Analysis with PHPStan (3.8.2, phpstan-dbal3.neon)

Property Doctrine\ORM\Query\SqlWalker::$selectedClasses type has no value type specified in iterable type array.

Check failure on line 117 in src/Query/SqlWalker.php

View workflow job for this annotation

GitHub Actions / coding-standards / Coding Standards (PHP: 8.4)

@var annotation of property \Doctrine\ORM\Query\SqlWalker::$selectedClasses does not specify type hint for its items.

/**
* The DQL alias of the root class of the currently traversed query.
Expand Down Expand Up @@ -679,10 +679,11 @@
$class = $selectedClass['class'];
$dqlAlias = $selectedClass['dqlAlias'];
$resultAlias = $selectedClass['resultAlias'];
$isPartial = $selectedClass['partial'];

// Register as entity or joined entity result
if (! isset($this->queryComponents[$dqlAlias]['relation'])) {
$this->rsm->addEntityResult($class->name, $dqlAlias, $resultAlias);
$this->rsm->addEntityResult($class->name, $dqlAlias, $resultAlias, $isPartial);
} else {
assert(isset($this->queryComponents[$dqlAlias]['parent']));

Expand All @@ -691,6 +692,7 @@
$dqlAlias,
$this->queryComponents[$dqlAlias]['parent'],
$this->queryComponents[$dqlAlias]['relation']->fieldName,
$isPartial,
);
}

Expand Down Expand Up @@ -760,7 +762,7 @@
if ($assoc->isToOneOwningSide()) {
$targetClass = $this->em->getClassMetadata($assoc->targetEntity);

foreach ($assoc->joinColumns as $joinColumn) {

Check failure on line 765 in src/Query/SqlWalker.php

View workflow job for this annotation

GitHub Actions / Static Analysis with PHPStan (default, phpstan.neon)

Ignored error pattern #^Access to an undefined property Doctrine\\ORM\\Mapping\\ManyToManyInverseSideMapping\|Doctrine\\ORM\\Mapping\\ManyToManyOwningSideMapping\|Doctrine\\ORM\\Mapping\\ManyToOneAssociationMapping\|Doctrine\\ORM\\Mapping\\OneToManyAssociationMapping\|Doctrine\\ORM\\Mapping\\OneToOneInverseSideMapping\|Doctrine\\ORM\\Mapping\\OneToOneOwningSideMapping\:\:\$joinColumns\.$# (property.notFound) in path /home/runner/work/orm/orm/src/Query/SqlWalker.php is expected to occur 2 times, but occurred only 1 time.
$columnName = $joinColumn->name;
$columnAlias = $this->getSQLColumnAlias($columnName);
$columnType = PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $targetClass, $this->em);
Expand Down Expand Up @@ -1386,6 +1388,7 @@
'class' => $class,
'dqlAlias' => $dqlAlias,
'resultAlias' => $resultAlias,
'partial' => $partialFieldSet !== [],
];
}

Expand Down
17 changes: 10 additions & 7 deletions src/UnitOfWork.php
Original file line number Diff line number Diff line change
Expand Up @@ -2395,19 +2395,22 @@ public function createEntity(string $className, array $data, array &$hints = [])

Hydrator::hydrate($entity, (array) $class->reflClass->newInstanceWithoutConstructor());
}
} else {
if (
} elseif (
! isset($hints[Query::HINT_REFRESH])
|| (isset($hints[Query::HINT_REFRESH_ENTITY]) && $hints[Query::HINT_REFRESH_ENTITY] !== $entity)
) {
return $entity;
}
) {
return $entity;
}

$this->originalEntityData[$oid] = $data;
} else {
$entity = $class->newInstance();
$oid = spl_object_id($entity);
if ($this->em->getConfiguration()->isNativeLazyObjectsEnabled() && isset($hints['isPartial']) && $hints['isPartial']) {
$entity = $this->em->getProxyFactory()->getProxy($class->name, $id, false);
} else {
$entity = $class->newInstance();
}

$oid = spl_object_id($entity);
$this->registerManaged($entity, $id, $data);

if (isset($hints[Query::HINT_READ_ONLY]) && $hints[Query::HINT_READ_ONLY] === true) {
Expand Down
49 changes: 49 additions & 0 deletions tests/Tests/ORM/Functional/QueryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,55 @@
self::assertEquals('Symfony 2', $users[0]->articles[1]->topic);
}

public function testJoinPartialObjectHydration(): void
{
if (!$this->_em->getConfiguration()->isNativeLazyObjectsEnabled()) {

Check failure on line 114 in tests/Tests/ORM/Functional/QueryTest.php

View workflow job for this annotation

GitHub Actions / coding-standards / Coding Standards (PHP: 8.4)

Expected 1 space after NOT operator; 0 found
$this->markTestSkipped('Test requires native lazy objects to be enabled.');
}

$user = new CmsUser();
$user->name = 'Guilherme';
$user->username = 'gblanco';
$user->status = 'developer';

$article1 = new CmsArticle();
$article1->topic = 'Doctrine 2';
$article1->text = 'This is an introduction to Doctrine 2.';
$user->addArticle($article1);

$article2 = new CmsArticle();
$article2->topic = 'Symfony 2';
$article2->text = 'This is an introduction to Symfony 2.';
$user->addArticle($article2);

$this->_em->persist($user);
$this->_em->persist($article1);
$this->_em->persist($article2);

$this->_em->flush();
$this->_em->clear();

$query = $this->_em->createQuery('select partial u.{id, username}, partial a.{id, topic} from ' . CmsUser::class . ' u join u.articles a ORDER BY a.topic');
$users = $query->getResult();

$queries = count($this->getQueryLog()->queries);
$topicsByUsername = [];
foreach ($users as $user) {
$topicsByUsername[$user->username] = $user->articles->map(static fn ($article) => $article->topic)->toArray();
}

self::assertQueryCount($queries);
self::assertEquals(['gblanco' => ['Doctrine 2', 'Symfony 2']], $topicsByUsername);

$userNames = [];
foreach ($users as $user) {
$userNames[] = $user->name;
}

self::assertQueryCount($queries + 1);
self::assertEquals(['Guilherme'], $userNames);
}

public function testJoinPartialArrayHydration(): void
{
$user = new CmsUser();
Expand Down
6 changes: 6 additions & 0 deletions tests/Tests/ORM/Functional/ValueObjectsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,10 @@

public function testPartialDqlOnEmbeddedObjectsField(): void
{
if (!$this->_em->getConfiguration()->isNativeLazyObjectsEnabled()) {

Check failure on line 185 in tests/Tests/ORM/Functional/ValueObjectsTest.php

View workflow job for this annotation

GitHub Actions / coding-standards / Coding Standards (PHP: 8.4)

Expected 1 space after NOT operator; 0 found
$this->markTestSkipped('Test requires native lazy objects to be enabled.');
}

$person = new DDC93Person('Karl', new DDC93Address('Foo', '12345', 'Gosport', new DDC93Country('England')));
$this->_em->persist($person);
$this->_em->flush();
Expand Down Expand Up @@ -226,9 +230,11 @@

// Selected field must be equal, all other fields must be null.
self::assertEquals('Gosport', $person->address->city);
// TODO: What about lazy loading embeddables? *shrug*
self::assertNull($person->address->street);
self::assertNull($person->address->zip);
self::assertNull($person->address->country);
// this actually loads the name field lazily
self::assertNull($person->name);
}

Expand Down
8 changes: 4 additions & 4 deletions tests/Tests/ORM/Hydration/ObjectHydratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1029,12 +1029,12 @@ public function testCreatesProxyForLazyLoadingWithForeignKeys(): void
'Proxies',
ProxyFactory::AUTOGENERATE_ALWAYS,
) extends ProxyFactory {
public function getProxy(string $className, array $identifier): object
public function getProxy(string $className, array $identifier, bool $assignIdentifiers = false): object
{
TestCase::assertSame(ECommerceShipping::class, $className);
TestCase::assertSame(['id' => 42], $identifier);

return parent::getProxy($className, $identifier);
return parent::getProxy($className, $identifier, $assignIdentifiers);
}
};

Expand Down Expand Up @@ -1083,12 +1083,12 @@ public function testCreatesProxyForLazyLoadingWithForeignKeysWithAliasedProductE
'Proxies',
ProxyFactory::AUTOGENERATE_ALWAYS,
) extends ProxyFactory {
public function getProxy(string $className, array $identifier): object
public function getProxy(string $className, array $identifier, bool $assignIdentifiers = false): object
{
TestCase::assertSame(ECommerceShipping::class, $className);
TestCase::assertSame(['id' => 42], $identifier);

return parent::getProxy($className, $identifier);
return parent::getProxy($className, $identifier, $assignIdentifiers);
}
};

Expand Down
Loading