6
6
use Doctrine \ORM \Mapping \ClassMetadata ;
7
7
use Doctrine \ORM \PersistentCollection ;
8
8
use Doctrine \ORM \QueryBuilder ;
9
- use Doctrine \Persistence \Proxy ;
10
9
use LogicException ;
10
+ use ReflectionProperty ;
11
11
use function array_chunk ;
12
- use function array_keys ;
13
12
use function array_values ;
14
13
use function count ;
15
14
use function get_parent_class ;
21
20
class EntityPreloader
22
21
{
23
22
24
- private const BATCH_SIZE = 1_000 ;
23
+ private const PRELOAD_ENTITY_DEFAULT_BATCH_SIZE = 1_000 ;
25
24
private const PRELOAD_COLLECTION_DEFAULT_BATCH_SIZE = 100 ;
26
25
27
26
public function __construct (
@@ -65,14 +64,15 @@ public function preload(
65
64
}
66
65
67
66
$ maxFetchJoinSameFieldCount ??= 1 ;
68
- $ sourceEntities = $ this ->loadProxies ($ sourceClassMetadata , $ sourceEntities , $ batchSize ?? self ::BATCH_SIZE , $ maxFetchJoinSameFieldCount );
67
+ $ sourceEntities = $ this ->loadProxies ($ sourceClassMetadata , $ sourceEntities , $ batchSize ?? self ::PRELOAD_ENTITY_DEFAULT_BATCH_SIZE , $ maxFetchJoinSameFieldCount );
69
68
70
- return match ($ associationMapping ->type ()) {
71
- ClassMetadata::ONE_TO_MANY => $ this ->preloadOneToMany ($ sourceEntities , $ sourceClassMetadata , $ sourcePropertyName , $ targetClassMetadata , $ batchSize , $ maxFetchJoinSameFieldCount ),
72
- ClassMetadata::ONE_TO_ONE ,
73
- ClassMetadata::MANY_TO_ONE => $ this ->preloadToOne ($ sourceEntities , $ sourceClassMetadata , $ sourcePropertyName , $ targetClassMetadata , $ batchSize , $ maxFetchJoinSameFieldCount ),
69
+ $ preloader = match (true ) {
70
+ $ associationMapping ->isToOne () => $ this ->preloadToOne (...),
71
+ $ associationMapping ->isToMany () => $ this ->preloadToMany (...),
74
72
default => throw new LogicException ("Unsupported association mapping type {$ associationMapping ->type ()}" ),
75
73
};
74
+
75
+ return $ preloader ($ sourceEntities , $ sourceClassMetadata , $ sourcePropertyName , $ targetClassMetadata , $ batchSize , $ maxFetchJoinSameFieldCount );
76
76
}
77
77
78
78
/**
@@ -135,7 +135,7 @@ private function loadProxies(
135
135
$ entityKey = (string ) $ entityId ;
136
136
$ uniqueEntities [$ entityKey ] = $ entity ;
137
137
138
- if ($ entity instanceof Proxy && ! $ entity -> __isInitialized ( )) {
138
+ if ($ this -> entityManager -> isUninitializedObject ( $ entity )) {
139
139
$ uninitializedIds [$ entityKey ] = $ entityId ;
140
140
}
141
141
}
@@ -157,7 +157,7 @@ private function loadProxies(
157
157
* @template S of E
158
158
* @template T of E
159
159
*/
160
- private function preloadOneToMany (
160
+ private function preloadToMany (
161
161
array $ sourceEntities ,
162
162
ClassMetadata $ sourceClassMetadata ,
163
163
string $ sourcePropertyName ,
@@ -168,50 +168,170 @@ private function preloadOneToMany(
168
168
{
169
169
$ sourceIdentifierReflection = $ sourceClassMetadata ->getSingleIdReflectionProperty (); // e.g. Order::$id reflection
170
170
$ sourcePropertyReflection = $ sourceClassMetadata ->getReflectionProperty ($ sourcePropertyName ); // e.g. Order::$items reflection
171
- $ targetPropertyName = $ sourceClassMetadata ->getAssociationMappedByTargetField ($ sourcePropertyName ); // e.g. 'order'
172
- $ targetPropertyReflection = $ targetClassMetadata ->getReflectionProperty ($ targetPropertyName ); // e.g. Item::$order reflection
171
+ $ targetIdentifierReflection = $ targetClassMetadata ->getSingleIdReflectionProperty ();
173
172
174
- if ($ sourceIdentifierReflection === null || $ sourcePropertyReflection === null || $ targetPropertyReflection === null ) {
173
+ if ($ sourceIdentifierReflection === null || $ sourcePropertyReflection === null || $ targetIdentifierReflection === null ) {
175
174
throw new LogicException ('Doctrine should use RuntimeReflectionService which never returns null. ' );
176
175
}
177
176
178
177
$ batchSize ??= self ::PRELOAD_COLLECTION_DEFAULT_BATCH_SIZE ;
179
-
180
178
$ targetEntities = [];
179
+ $ uninitializedSourceEntityIds = [];
181
180
$ uninitializedCollections = [];
182
181
183
182
foreach ($ sourceEntities as $ sourceEntity ) {
184
- $ sourceEntityId = (string ) $ sourceIdentifierReflection ->getValue ($ sourceEntity );
183
+ $ sourceEntityId = $ sourceIdentifierReflection ->getValue ($ sourceEntity );
184
+ $ sourceEntityKey = (string ) $ sourceEntityId ;
185
185
$ sourceEntityCollection = $ sourcePropertyReflection ->getValue ($ sourceEntity );
186
186
187
187
if (
188
188
$ sourceEntityCollection instanceof PersistentCollection
189
189
&& !$ sourceEntityCollection ->isInitialized ()
190
190
&& !$ sourceEntityCollection ->isDirty () // preloading dirty collection is too hard to handle
191
191
) {
192
- $ uninitializedCollections [$ sourceEntityId ] = $ sourceEntityCollection ;
192
+ $ uninitializedSourceEntityIds [$ sourceEntityKey ] = $ sourceEntityId ;
193
+ $ uninitializedCollections [$ sourceEntityKey ] = $ sourceEntityCollection ;
193
194
continue ;
194
195
}
195
196
196
197
foreach ($ sourceEntityCollection as $ targetEntity ) {
197
- $ targetEntities [] = $ targetEntity ;
198
+ $ targetEntityKey = (string ) $ targetIdentifierReflection ->getValue ($ targetEntity );
199
+ $ targetEntities [$ targetEntityKey ] = $ targetEntity ;
198
200
}
199
201
}
200
202
201
- foreach (array_chunk ($ uninitializedCollections , $ batchSize , true ) as $ chunk ) {
202
- $ targetEntitiesChunk = $ this ->loadEntitiesBy ($ targetClassMetadata , $ targetPropertyName , array_keys ($ chunk ), $ maxFetchJoinSameFieldCount );
203
+ $ innerLoader = match ($ sourceClassMetadata ->getAssociationMapping ($ sourcePropertyName )->type ()) {
204
+ ClassMetadata::ONE_TO_MANY => $ this ->preloadOneToManyInner (...),
205
+ ClassMetadata::MANY_TO_MANY => $ this ->preloadManyToManyInner (...),
206
+ default => throw new LogicException ('Unsupported association mapping type ' ),
207
+ };
203
208
204
- foreach ($ targetEntitiesChunk as $ targetEntity ) {
205
- $ sourceEntity = $ targetPropertyReflection ->getValue ($ targetEntity );
206
- $ sourceEntityId = (string ) $ sourceIdentifierReflection ->getValue ($ sourceEntity );
207
- $ uninitializedCollections [$ sourceEntityId ]->add ($ targetEntity );
208
- $ targetEntities [] = $ targetEntity ;
209
+ foreach (array_chunk ($ uninitializedSourceEntityIds , $ batchSize , preserve_keys: true ) as $ uninitializedSourceEntityIdsChunk ) {
210
+ $ targetEntitiesChunk = $ innerLoader (
211
+ sourceClassMetadata: $ sourceClassMetadata ,
212
+ sourceIdentifierReflection: $ sourceIdentifierReflection ,
213
+ sourcePropertyName: $ sourcePropertyName ,
214
+ targetClassMetadata: $ targetClassMetadata ,
215
+ targetIdentifierReflection: $ targetIdentifierReflection ,
216
+ uninitializedSourceEntityIdsChunk: array_values ($ uninitializedSourceEntityIdsChunk ),
217
+ uninitializedCollections: $ uninitializedCollections ,
218
+ maxFetchJoinSameFieldCount: $ maxFetchJoinSameFieldCount ,
219
+ );
220
+
221
+ foreach ($ targetEntitiesChunk as $ targetEntityKey => $ targetEntity ) {
222
+ $ targetEntities [$ targetEntityKey ] = $ targetEntity ;
209
223
}
224
+ }
225
+
226
+ foreach ($ uninitializedCollections as $ sourceEntityCollection ) {
227
+ $ sourceEntityCollection ->setInitialized (true );
228
+ $ sourceEntityCollection ->takeSnapshot ();
229
+ }
230
+
231
+ return array_values ($ targetEntities );
232
+ }
233
+
234
+ /**
235
+ * @param ClassMetadata<S> $sourceClassMetadata
236
+ * @param ClassMetadata<T> $targetClassMetadata
237
+ * @param list<mixed> $uninitializedSourceEntityIdsChunk
238
+ * @param array<string, PersistentCollection<int, T>> $uninitializedCollections
239
+ * @param non-negative-int $maxFetchJoinSameFieldCount
240
+ * @return array<string, T>
241
+ * @template S of E
242
+ * @template T of E
243
+ */
244
+ private function preloadOneToManyInner (
245
+ ClassMetadata $ sourceClassMetadata ,
246
+ ReflectionProperty $ sourceIdentifierReflection ,
247
+ string $ sourcePropertyName ,
248
+ ClassMetadata $ targetClassMetadata ,
249
+ ReflectionProperty $ targetIdentifierReflection ,
250
+ array $ uninitializedSourceEntityIdsChunk ,
251
+ array $ uninitializedCollections ,
252
+ int $ maxFetchJoinSameFieldCount ,
253
+ ): array
254
+ {
255
+ $ targetPropertyName = $ sourceClassMetadata ->getAssociationMappedByTargetField ($ sourcePropertyName ); // e.g. 'order'
256
+ $ targetPropertyReflection = $ targetClassMetadata ->getReflectionProperty ($ targetPropertyName ); // e.g. Item::$order reflection
257
+ $ targetEntities = [];
258
+
259
+ if ($ targetPropertyReflection === null ) {
260
+ throw new LogicException ('Doctrine should use RuntimeReflectionService which never returns null. ' );
261
+ }
262
+
263
+ foreach ($ this ->loadEntitiesBy ($ targetClassMetadata , $ targetPropertyName , $ uninitializedSourceEntityIdsChunk , $ maxFetchJoinSameFieldCount ) as $ targetEntity ) {
264
+ $ sourceEntity = $ targetPropertyReflection ->getValue ($ targetEntity );
265
+ $ sourceEntityKey = (string ) $ sourceIdentifierReflection ->getValue ($ sourceEntity );
266
+ $ uninitializedCollections [$ sourceEntityKey ]->add ($ targetEntity );
267
+
268
+ $ targetEntityKey = (string ) $ targetIdentifierReflection ->getValue ($ targetEntity );
269
+ $ targetEntities [$ targetEntityKey ] = $ targetEntity ;
270
+ }
210
271
211
- foreach ($ chunk as $ sourceEntityCollection ) {
212
- $ sourceEntityCollection ->setInitialized (true );
213
- $ sourceEntityCollection ->takeSnapshot ();
272
+ return $ targetEntities ;
273
+ }
274
+
275
+ /**
276
+ * @param ClassMetadata<S> $sourceClassMetadata
277
+ * @param ClassMetadata<T> $targetClassMetadata
278
+ * @param list<mixed> $uninitializedSourceEntityIdsChunk
279
+ * @param array<string, PersistentCollection<int, T>> $uninitializedCollections
280
+ * @param non-negative-int $maxFetchJoinSameFieldCount
281
+ * @return array<string, T>
282
+ * @template S of E
283
+ * @template T of E
284
+ */
285
+ private function preloadManyToManyInner (
286
+ ClassMetadata $ sourceClassMetadata ,
287
+ ReflectionProperty $ sourceIdentifierReflection ,
288
+ string $ sourcePropertyName ,
289
+ ClassMetadata $ targetClassMetadata ,
290
+ ReflectionProperty $ targetIdentifierReflection ,
291
+ array $ uninitializedSourceEntityIdsChunk ,
292
+ array $ uninitializedCollections ,
293
+ int $ maxFetchJoinSameFieldCount ,
294
+ ): array
295
+ {
296
+ $ sourceIdentifierName = $ sourceClassMetadata ->getSingleIdentifierFieldName ();
297
+ $ targetIdentifierName = $ targetClassMetadata ->getSingleIdentifierFieldName ();
298
+
299
+ $ manyToManyRows = $ this ->entityManager ->createQueryBuilder ()
300
+ ->select ("source. {$ sourceIdentifierName } AS sourceId " , "target. {$ targetIdentifierName } AS targetId " )
301
+ ->from ($ sourceClassMetadata ->getName (), 'source ' )
302
+ ->join ("source. {$ sourcePropertyName }" , 'target ' )
303
+ ->andWhere ('source IN (:sourceEntityIds) ' )
304
+ ->setParameter ('sourceEntityIds ' , $ uninitializedSourceEntityIdsChunk )
305
+ ->getQuery ()
306
+ ->getResult ();
307
+
308
+ $ targetEntities = [];
309
+ $ uninitializedTargetEntityIds = [];
310
+
311
+ foreach ($ manyToManyRows as $ manyToManyRow ) {
312
+ $ targetEntityId = $ manyToManyRow ['targetId ' ];
313
+ $ targetEntityKey = (string ) $ targetEntityId ;
314
+
315
+ /** @var T|false $targetEntity */
316
+ $ targetEntity = $ this ->entityManager ->getUnitOfWork ()->tryGetById ($ targetEntityId , $ targetClassMetadata ->getName ());
317
+
318
+ if ($ targetEntity !== false && !$ this ->entityManager ->isUninitializedObject ($ targetEntity )) {
319
+ $ targetEntities [$ targetEntityKey ] = $ targetEntity ;
320
+ continue ;
214
321
}
322
+
323
+ $ uninitializedTargetEntityIds [$ targetEntityKey ] = $ targetEntityId ;
324
+ }
325
+
326
+ foreach ($ this ->loadEntitiesBy ($ targetClassMetadata , $ targetIdentifierName , array_values ($ uninitializedTargetEntityIds ), $ maxFetchJoinSameFieldCount ) as $ targetEntity ) {
327
+ $ targetEntityKey = (string ) $ targetIdentifierReflection ->getValue ($ targetEntity );
328
+ $ targetEntities [$ targetEntityKey ] = $ targetEntity ;
329
+ }
330
+
331
+ foreach ($ manyToManyRows as $ manyToManyRow ) {
332
+ $ sourceEntityKey = (string ) $ manyToManyRow ['sourceId ' ];
333
+ $ targetEntityKey = (string ) $ manyToManyRow ['targetId ' ];
334
+ $ uninitializedCollections [$ sourceEntityKey ]->add ($ targetEntities [$ targetEntityKey ]);
215
335
}
216
336
217
337
return $ targetEntities ;
@@ -237,12 +357,14 @@ private function preloadToOne(
237
357
): array
238
358
{
239
359
$ sourcePropertyReflection = $ sourceClassMetadata ->getReflectionProperty ($ sourcePropertyName ); // e.g. Item::$order reflection
240
- $ targetEntities = [];
241
360
242
361
if ($ sourcePropertyReflection === null ) {
243
362
throw new LogicException ('Doctrine should use RuntimeReflectionService which never returns null. ' );
244
363
}
245
364
365
+ $ batchSize ??= self ::PRELOAD_ENTITY_DEFAULT_BATCH_SIZE ;
366
+ $ targetEntities = [];
367
+
246
368
foreach ($ sourceEntities as $ sourceEntity ) {
247
369
$ targetEntity = $ sourcePropertyReflection ->getValue ($ sourceEntity );
248
370
@@ -253,7 +375,7 @@ private function preloadToOne(
253
375
$ targetEntities [] = $ targetEntity ;
254
376
}
255
377
256
- return $ this ->loadProxies ($ targetClassMetadata , $ targetEntities , $ batchSize ?? self :: BATCH_SIZE , $ maxFetchJoinSameFieldCount );
378
+ return $ this ->loadProxies ($ targetClassMetadata , $ targetEntities , $ batchSize , $ maxFetchJoinSameFieldCount );
257
379
}
258
380
259
381
/**
@@ -270,6 +392,10 @@ private function loadEntitiesBy(
270
392
int $ maxFetchJoinSameFieldCount ,
271
393
): array
272
394
{
395
+ if (count ($ fieldValues ) === 0 ) {
396
+ return [];
397
+ }
398
+
273
399
$ rootLevelAlias = 'e ' ;
274
400
275
401
$ queryBuilder = $ this ->entityManager ->createQueryBuilder ()
0 commit comments