1010use Doctrine \ORM \Mapping \ClassMetadata ;
1111use Doctrine \ORM \Mapping \PropertyAccessors \PropertyAccessor ;
1212use Doctrine \ORM \PersistentCollection ;
13+ use Doctrine \ORM \Query ;
1314use Doctrine \ORM \QueryBuilder ;
1415use LogicException ;
1516use ReflectionProperty ;
@@ -44,6 +45,7 @@ public function preload(
4445 string $ sourcePropertyName ,
4546 ?int $ batchSize = null ,
4647 ?int $ maxFetchJoinSameFieldCount = null ,
48+ bool $ readOnly = false ,
4749 ): array
4850 {
4951 $ sourceEntitiesCommonAncestor = $ this ->getCommonAncestor ($ sourceEntities );
@@ -63,15 +65,27 @@ public function preload(
6365 }
6466
6567 $ maxFetchJoinSameFieldCount ??= 1 ;
66- $ sourceEntities = $ this ->loadProxies ($ sourceClassMetadata , $ sourceEntities , $ batchSize ?? self ::PRELOAD_ENTITY_DEFAULT_BATCH_SIZE , $ maxFetchJoinSameFieldCount );
68+ $ sourceEntities = $ this ->loadProxies ($ sourceClassMetadata , $ sourceEntities , $ batchSize ?? self ::PRELOAD_ENTITY_DEFAULT_BATCH_SIZE , $ maxFetchJoinSameFieldCount, $ readOnly );
6769
6870 $ preloader = match ($ associationMapping ['type ' ]) {
6971 ClassMetadata::ONE_TO_ONE , ClassMetadata::MANY_TO_ONE => $ this ->preloadToOne (...),
7072 ClassMetadata::ONE_TO_MANY , ClassMetadata::MANY_TO_MANY => $ this ->preloadToMany (...),
7173 default => throw new LogicException ("Unsupported association mapping type {$ associationMapping ['type ' ]}" ),
7274 };
7375
74- return $ preloader ($ sourceEntities , $ sourceClassMetadata , $ sourcePropertyName , $ targetClassMetadata , $ batchSize , $ maxFetchJoinSameFieldCount );
76+ $ result = $ preloader ($ sourceEntities , $ sourceClassMetadata , $ sourcePropertyName , $ targetClassMetadata , $ batchSize , $ maxFetchJoinSameFieldCount , $ readOnly );
77+
78+ if ($ readOnly ) {
79+ $ unitOfWork = $ this ->entityManager ->getUnitOfWork ();
80+
81+ foreach ($ result as $ entity ) {
82+ if (!$ unitOfWork ->isReadOnly ($ entity )) {
83+ $ unitOfWork ->markReadOnly ($ entity );
84+ }
85+ }
86+ }
87+
88+ return $ result ;
7589 }
7690
7791 /**
@@ -114,6 +128,7 @@ private function loadProxies(
114128 array $ entities ,
115129 int $ batchSize ,
116130 int $ maxFetchJoinSameFieldCount ,
131+ bool $ readOnly ,
117132 ): array
118133 {
119134 $ identifierAccessor = $ this ->getSingleIdPropertyAccessor ($ classMetadata ); // e.g. Order::$id reflection
@@ -137,7 +152,7 @@ private function loadProxies(
137152 }
138153
139154 foreach (array_chunk ($ uninitializedIds , $ batchSize ) as $ idsChunk ) {
140- $ this ->loadEntitiesBy ($ classMetadata , $ identifierName , $ classMetadata , $ idsChunk , $ maxFetchJoinSameFieldCount );
155+ $ this ->loadEntitiesBy ($ classMetadata , $ identifierName , $ classMetadata , $ idsChunk , $ maxFetchJoinSameFieldCount, readOnly: $ readOnly );
141156 }
142157
143158 return array_values ($ uniqueEntities );
@@ -158,6 +173,7 @@ private function preloadToMany(
158173 ClassMetadata $ targetClassMetadata ,
159174 ?int $ batchSize ,
160175 int $ maxFetchJoinSameFieldCount ,
176+ bool $ readOnly ,
161177 ): array
162178 {
163179 $ sourceIdentifierAccessor = $ this ->getSingleIdPropertyAccessor ($ sourceClassMetadata ); // e.g. Order::$id reflection
@@ -213,6 +229,7 @@ private function preloadToMany(
213229 uninitializedSourceEntityIdsChunk: array_values ($ uninitializedSourceEntityIdsChunk ),
214230 uninitializedCollections: $ uninitializedCollections ,
215231 maxFetchJoinSameFieldCount: $ maxFetchJoinSameFieldCount ,
232+ readOnly: $ readOnly ,
216233 );
217234
218235 foreach ($ targetEntitiesChunk as $ targetEntityKey => $ targetEntity ) {
@@ -247,6 +264,7 @@ private function preloadOneToManyInner(
247264 array $ uninitializedSourceEntityIdsChunk ,
248265 array $ uninitializedCollections ,
249266 int $ maxFetchJoinSameFieldCount ,
267+ bool $ readOnly ,
250268 ): array
251269 {
252270 $ targetPropertyName = $ sourceClassMetadata ->getAssociationMappedByTargetField ($ sourcePropertyName ); // e.g. 'order'
@@ -264,6 +282,7 @@ private function preloadOneToManyInner(
264282 $ uninitializedSourceEntityIdsChunk ,
265283 $ maxFetchJoinSameFieldCount ,
266284 $ associationMapping ['orderBy ' ] ?? [],
285+ $ readOnly ,
267286 );
268287
269288 foreach ($ targetEntitiesList as $ targetEntity ) {
@@ -297,6 +316,7 @@ private function preloadManyToManyInner(
297316 array $ uninitializedSourceEntityIdsChunk ,
298317 array $ uninitializedCollections ,
299318 int $ maxFetchJoinSameFieldCount ,
319+ bool $ readOnly ,
300320 ): array
301321 {
302322 if (count ($ associationMapping ['orderBy ' ] ?? []) > 0 ) {
@@ -308,7 +328,7 @@ private function preloadManyToManyInner(
308328
309329 $ sourceIdentifierType = $ this ->getIdentifierFieldType ($ sourceClassMetadata );
310330
311- $ manyToManyRows = $ this ->entityManager ->createQueryBuilder ()
331+ $ manyToManyQuery = $ this ->entityManager ->createQueryBuilder ()
312332 ->select ("source. {$ sourceIdentifierName } AS sourceId " , "target. {$ targetIdentifierName } AS targetId " )
313333 ->from ($ sourceClassMetadata ->getName (), 'source ' )
314334 ->join ("source. {$ sourcePropertyName }" , 'target ' )
@@ -318,8 +338,13 @@ private function preloadManyToManyInner(
318338 $ this ->convertFieldValuesToDatabaseValues ($ sourceIdentifierType , $ uninitializedSourceEntityIdsChunk ),
319339 $ this ->deduceArrayParameterType ($ sourceIdentifierType ),
320340 )
321- ->getQuery ()
322- ->getResult ();
341+ ->getQuery ();
342+
343+ if ($ readOnly ) {
344+ $ manyToManyQuery ->setHint (Query::HINT_READ_ONLY , true );
345+ }
346+
347+ $ manyToManyRows = $ manyToManyQuery ->getResult ();
323348
324349 $ targetEntities = [];
325350 $ uninitializedTargetEntityIds = [];
@@ -339,7 +364,7 @@ private function preloadManyToManyInner(
339364 $ uninitializedTargetEntityIds [$ targetEntityKey ] = $ targetEntityId ;
340365 }
341366
342- foreach ($ this ->loadEntitiesBy ($ targetClassMetadata , $ targetIdentifierName , $ sourceClassMetadata , array_values ($ uninitializedTargetEntityIds ), $ maxFetchJoinSameFieldCount ) as $ targetEntity ) {
367+ foreach ($ this ->loadEntitiesBy ($ targetClassMetadata , $ targetIdentifierName , $ sourceClassMetadata , array_values ($ uninitializedTargetEntityIds ), $ maxFetchJoinSameFieldCount, readOnly: $ readOnly ) as $ targetEntity ) {
343368 $ targetEntityKey = (string ) $ targetIdentifierAccessor ->getValue ($ targetEntity );
344369 $ targetEntities [$ targetEntityKey ] = $ targetEntity ;
345370 }
@@ -368,6 +393,7 @@ private function preloadToOne(
368393 ClassMetadata $ targetClassMetadata ,
369394 ?int $ batchSize ,
370395 int $ maxFetchJoinSameFieldCount ,
396+ bool $ readOnly ,
371397 ): array
372398 {
373399 $ sourcePropertyAccessor = $ this ->getPropertyAccessor ($ sourceClassMetadata , $ sourcePropertyName ); // e.g. Item::$order reflection
@@ -389,7 +415,7 @@ private function preloadToOne(
389415 $ targetEntities [] = $ targetEntity ;
390416 }
391417
392- return $ this ->loadProxies ($ targetClassMetadata , $ targetEntities , $ batchSize , $ maxFetchJoinSameFieldCount );
418+ return $ this ->loadProxies ($ targetClassMetadata , $ targetEntities , $ batchSize , $ maxFetchJoinSameFieldCount, $ readOnly );
393419 }
394420
395421 /**
@@ -407,6 +433,7 @@ private function loadEntitiesBy(
407433 array $ fieldValues ,
408434 int $ maxFetchJoinSameFieldCount ,
409435 array $ orderBy = [],
436+ bool $ readOnly = false ,
410437 ): array
411438 {
412439 if (count ($ fieldValues ) === 0 ) {
@@ -432,7 +459,13 @@ private function loadEntitiesBy(
432459 $ queryBuilder ->addOrderBy ("{$ rootLevelAlias }. {$ field }" , $ direction );
433460 }
434461
435- return $ queryBuilder ->getQuery ()->getResult ();
462+ $ query = $ queryBuilder ->getQuery ();
463+
464+ if ($ readOnly ) {
465+ $ query ->setHint (Query::HINT_READ_ONLY , true );
466+ }
467+
468+ return $ query ->getResult ();
436469 }
437470
438471 private function deduceArrayParameterType (Type $ dbalType ): ArrayParameterType |int |null // @phpstan-ignore return.unusedType (old dbal compat)
0 commit comments