Skip to content

Commit c3c6892

Browse files
committed
Throw LogicException when readOnly is used with toOne associations
1 parent 9cdfecf commit c3c6892

File tree

5 files changed

+32
-20
lines changed

5 files changed

+32
-20
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ foreach ($categories as $category) {
101101

102102
- **Batch Size:** Set a custom batch size for preloading to optimize memory usage.
103103
- **Max Fetch Join Same Field Count:** Define the maximum number of join fetches allowed per field.
104-
- **Read Only:** Mark preloaded entities as read-only to disable change tracking and improve performance.
104+
- **Read Only:** Mark preloaded entities as read-only to disable change tracking and improve performance (only supported for `#[OneToMany]` and `#[ManyToMany]` associations).
105105

106106
```php
107107
$preloader->preload(

src/EntityPreloader.php

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -64,22 +64,26 @@ public function preload(
6464
throw new LogicException('Preloading of indexed associations is not supported');
6565
}
6666

67+
$isToOne = $associationMapping['type'] === ClassMetadata::ONE_TO_ONE
68+
|| $associationMapping['type'] === ClassMetadata::MANY_TO_ONE;
69+
70+
if ($readOnly && $isToOne) {
71+
throw new LogicException('The readOnly option is not supported for toOne associations');
72+
}
73+
6774
$maxFetchJoinSameFieldCount ??= 1;
6875
$sourceEntities = $this->loadProxies(
6976
$sourceClassMetadata,
7077
$sourceEntities,
7178
$batchSize ?? self::PRELOAD_ENTITY_DEFAULT_BATCH_SIZE,
7279
$maxFetchJoinSameFieldCount,
73-
$readOnly,
7480
);
7581

76-
$preloader = match ($associationMapping['type']) {
77-
ClassMetadata::ONE_TO_ONE, ClassMetadata::MANY_TO_ONE => $this->preloadToOne(...),
78-
ClassMetadata::ONE_TO_MANY, ClassMetadata::MANY_TO_MANY => $this->preloadToMany(...),
79-
default => throw new LogicException("Unsupported association mapping type {$associationMapping['type']}"),
80-
};
82+
if ($isToOne) {
83+
return $this->preloadToOne($sourceEntities, $sourceClassMetadata, $sourcePropertyName, $targetClassMetadata, $batchSize, $maxFetchJoinSameFieldCount);
84+
}
8185

82-
return $preloader($sourceEntities, $sourceClassMetadata, $sourcePropertyName, $targetClassMetadata, $batchSize, $maxFetchJoinSameFieldCount, $readOnly);
86+
return $this->preloadToMany($sourceEntities, $sourceClassMetadata, $sourcePropertyName, $targetClassMetadata, $batchSize, $maxFetchJoinSameFieldCount, $readOnly);
8387
}
8488

8589
/**
@@ -122,7 +126,6 @@ private function loadProxies(
122126
array $entities,
123127
int $batchSize,
124128
int $maxFetchJoinSameFieldCount,
125-
bool $readOnly,
126129
): array
127130
{
128131
$identifierAccessor = $this->getSingleIdPropertyAccessor($classMetadata); // e.g. Order::$id reflection
@@ -146,7 +149,7 @@ private function loadProxies(
146149
}
147150

148151
foreach (array_chunk($uninitializedIds, $batchSize) as $idsChunk) {
149-
$this->loadEntitiesBy($classMetadata, $identifierName, $classMetadata, $idsChunk, $maxFetchJoinSameFieldCount, readOnly: $readOnly);
152+
$this->loadEntitiesBy($classMetadata, $identifierName, $classMetadata, $idsChunk, $maxFetchJoinSameFieldCount);
150153
}
151154

152155
return array_values($uniqueEntities);
@@ -383,7 +386,6 @@ private function preloadToOne(
383386
ClassMetadata $targetClassMetadata,
384387
?int $batchSize,
385388
int $maxFetchJoinSameFieldCount,
386-
bool $readOnly,
387389
): array
388390
{
389391
$sourcePropertyAccessor = $this->getPropertyAccessor($sourceClassMetadata, $sourcePropertyName); // e.g. Item::$order reflection
@@ -405,7 +407,7 @@ private function preloadToOne(
405407
$targetEntities[] = $targetEntity;
406408
}
407409

408-
return $this->loadProxies($targetClassMetadata, $targetEntities, $batchSize, $maxFetchJoinSameFieldCount, $readOnly);
410+
return $this->loadProxies($targetClassMetadata, $targetEntities, $batchSize, $maxFetchJoinSameFieldCount);
409411
}
410412

411413
/**

tests/EntityPreloadBlogManyHasManyTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,12 @@ public function testManyHasManyWithPreloadReadOnly(DbalType $primaryKey): void
125125

126126
self::assertCount(25, $tags);
127127

128+
// Verify that preloaded tags are marked as read-only
129+
$unitOfWork = $this->getEntityManager()->getUnitOfWork();
130+
foreach ($tags as $tag) {
131+
self::assertTrue($unitOfWork->isReadOnly($tag), 'Tag should be marked as read-only');
132+
}
133+
128134
self::assertAggregatedQueries([
129135
['count' => 1, 'query' => 'SELECT * FROM article t0'],
130136
['count' => 1, 'query' => 'SELECT * FROM article a0_ INNER JOIN article_tag a2_ ON a0_.id = a2_.article_id INNER JOIN tag t1_ ON t1_.id = a2_.tag_id WHERE a0_.id IN (?, ?, ?, ?, ?)'],

tests/EntityPreloadBlogManyHasOneTest.php

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -119,19 +119,17 @@ public function testManyHasOneWithPreload(DbalType $primaryKey): void
119119
}
120120

121121
#[DataProvider('providePrimaryKeyTypes')]
122-
public function testManyHasOneWithPreloadReadOnly(DbalType $primaryKey): void
122+
public function testManyHasOneWithPreloadReadOnlyThrowsException(DbalType $primaryKey): void
123123
{
124124
$this->createDummyBlogData($primaryKey, categoryCount: 5, articleInEachCategoryCount: 5);
125125

126126
$articles = $this->getEntityManager()->getRepository(Article::class)->findAll();
127-
$categories = $this->getEntityPreloader()->preload($articles, 'category', readOnly: true);
128127

129-
self::assertCount(5, $categories);
130-
131-
self::assertAggregatedQueries([
132-
['count' => 1, 'query' => 'SELECT * FROM article t0'],
133-
['count' => 1, 'query' => 'SELECT * FROM category c0_ WHERE c0_.id IN (?, ?, ?, ?, ?)'],
134-
]);
128+
self::assertException(
129+
\LogicException::class,
130+
'The readOnly option is not supported for toOne associations',
131+
fn () => $this->getEntityPreloader()->preload($articles, 'category', readOnly: true),
132+
);
135133
}
136134

137135
/**

tests/EntityPreloadBlogOneHasManyTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,12 @@ public function testOneHasManyWithPreloadReadOnly(DbalType $primaryKey): void
149149

150150
self::assertCount(25, $articles);
151151

152+
// Verify that preloaded articles are marked as read-only
153+
$unitOfWork = $this->getEntityManager()->getUnitOfWork();
154+
foreach ($articles as $article) {
155+
self::assertTrue($unitOfWork->isReadOnly($article), 'Article should be marked as read-only');
156+
}
157+
152158
self::assertAggregatedQueries([
153159
['count' => 1, 'query' => 'SELECT * FROM category t0'],
154160
['count' => 1, 'query' => 'SELECT * FROM article a0_ WHERE a0_.category_id IN (?, ?, ?, ?, ?)'],

0 commit comments

Comments
 (0)