1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
namespace Doctrine\ORM; |
6
|
|
|
|
7
|
|
|
use Doctrine\Common\Collections\Collection; |
8
|
|
|
use Doctrine\Common\NotifyPropertyChanged; |
9
|
|
|
use Doctrine\Common\PropertyChangedListener; |
10
|
|
|
use Doctrine\DBAL\LockMode; |
11
|
|
|
use Doctrine\Instantiator\Instantiator; |
12
|
|
|
use Doctrine\ORM\Cache\Persister\CachedPersister; |
13
|
|
|
use Doctrine\ORM\Event\LifecycleEventArgs; |
14
|
|
|
use Doctrine\ORM\Event\ListenersInvoker; |
15
|
|
|
use Doctrine\ORM\Event\OnFlushEventArgs; |
16
|
|
|
use Doctrine\ORM\Event\PostFlushEventArgs; |
17
|
|
|
use Doctrine\ORM\Event\PreFlushEventArgs; |
18
|
|
|
use Doctrine\ORM\Event\PreUpdateEventArgs; |
19
|
|
|
use Doctrine\ORM\Internal\HydrationCompleteHandler; |
20
|
|
|
use Doctrine\ORM\Mapping\AssociationMetadata; |
21
|
|
|
use Doctrine\ORM\Mapping\ChangeTrackingPolicy; |
22
|
|
|
use Doctrine\ORM\Mapping\ClassMetadata; |
23
|
|
|
use Doctrine\ORM\Mapping\FetchMode; |
24
|
|
|
use Doctrine\ORM\Mapping\FieldMetadata; |
25
|
|
|
use Doctrine\ORM\Mapping\GeneratorType; |
26
|
|
|
use Doctrine\ORM\Mapping\InheritanceType; |
27
|
|
|
use Doctrine\ORM\Mapping\JoinColumnMetadata; |
28
|
|
|
use Doctrine\ORM\Mapping\ManyToManyAssociationMetadata; |
29
|
|
|
use Doctrine\ORM\Mapping\OneToManyAssociationMetadata; |
30
|
|
|
use Doctrine\ORM\Mapping\OneToOneAssociationMetadata; |
31
|
|
|
use Doctrine\ORM\Mapping\ToManyAssociationMetadata; |
32
|
|
|
use Doctrine\ORM\Mapping\ToOneAssociationMetadata; |
33
|
|
|
use Doctrine\ORM\Mapping\VersionFieldMetadata; |
34
|
|
|
use Doctrine\ORM\Persisters\Collection\CollectionPersister; |
35
|
|
|
use Doctrine\ORM\Persisters\Collection\ManyToManyPersister; |
36
|
|
|
use Doctrine\ORM\Persisters\Collection\OneToManyPersister; |
37
|
|
|
use Doctrine\ORM\Persisters\Entity\BasicEntityPersister; |
38
|
|
|
use Doctrine\ORM\Persisters\Entity\EntityPersister; |
39
|
|
|
use Doctrine\ORM\Persisters\Entity\JoinedSubclassPersister; |
40
|
|
|
use Doctrine\ORM\Persisters\Entity\SingleTablePersister; |
41
|
|
|
use Doctrine\ORM\Utility\NormalizeIdentifier; |
42
|
|
|
use InvalidArgumentException; |
43
|
|
|
use ProxyManager\Proxy\GhostObjectInterface; |
44
|
|
|
use UnexpectedValueException; |
45
|
|
|
|
46
|
|
|
/** |
47
|
|
|
* The UnitOfWork is responsible for tracking changes to objects during an |
48
|
|
|
* "object-level" transaction and for writing out changes to the database |
49
|
|
|
* in the correct order. |
50
|
|
|
* |
51
|
|
|
* Internal note: This class contains highly performance-sensitive code. |
52
|
|
|
*/ |
53
|
|
|
class UnitOfWork implements PropertyChangedListener |
54
|
|
|
{ |
55
|
|
|
/** |
56
|
|
|
* An entity is in MANAGED state when its persistence is managed by an EntityManager. |
57
|
|
|
*/ |
58
|
|
|
public const STATE_MANAGED = 1; |
59
|
|
|
|
60
|
|
|
/** |
61
|
|
|
* An entity is new if it has just been instantiated (i.e. using the "new" operator) |
62
|
|
|
* and is not (yet) managed by an EntityManager. |
63
|
|
|
*/ |
64
|
|
|
public const STATE_NEW = 2; |
65
|
|
|
|
66
|
|
|
/** |
67
|
|
|
* A detached entity is an instance with persistent state and identity that is not |
68
|
|
|
* (or no longer) associated with an EntityManager (and a UnitOfWork). |
69
|
|
|
*/ |
70
|
|
|
public const STATE_DETACHED = 3; |
71
|
|
|
|
72
|
|
|
/** |
73
|
|
|
* A removed entity instance is an instance with a persistent identity, |
74
|
|
|
* associated with an EntityManager, whose persistent state will be deleted |
75
|
|
|
* on commit. |
76
|
|
|
*/ |
77
|
|
|
public const STATE_REMOVED = 4; |
78
|
|
|
|
79
|
|
|
/** |
80
|
|
|
* Hint used to collect all primary keys of associated entities during hydration |
81
|
|
|
* and execute it in a dedicated query afterwards |
82
|
|
|
* @see https://doctrine-orm.readthedocs.org/en/latest/reference/dql-doctrine-query-language.html?highlight=eager#temporarily-change-fetch-mode-in-dql |
83
|
|
|
*/ |
84
|
|
|
public const HINT_DEFEREAGERLOAD = 'deferEagerLoad'; |
85
|
|
|
|
86
|
|
|
/** |
87
|
|
|
* The identity map that holds references to all managed entities that have |
88
|
|
|
* an identity. The entities are grouped by their class name. |
89
|
|
|
* Since all classes in a hierarchy must share the same identifier set, |
90
|
|
|
* we always take the root class name of the hierarchy. |
91
|
|
|
* |
92
|
|
|
* @var object[] |
93
|
|
|
*/ |
94
|
|
|
private $identityMap = []; |
95
|
|
|
|
96
|
|
|
/** |
97
|
|
|
* Map of all identifiers of managed entities. |
98
|
|
|
* This is a 2-dimensional data structure (map of maps). Keys are object ids (spl_object_id). |
99
|
|
|
* Values are maps of entity identifiers, where its key is the column name and the value is the raw value. |
100
|
|
|
* |
101
|
|
|
* @var mixed[][] |
102
|
|
|
*/ |
103
|
|
|
private $entityIdentifiers = []; |
104
|
|
|
|
105
|
|
|
/** |
106
|
|
|
* Map of the original entity data of managed entities. |
107
|
|
|
* This is a 2-dimensional data structure (map of maps). Keys are object ids (spl_object_id). |
108
|
|
|
* Values are maps of entity data, where its key is the field name and the value is the converted |
109
|
|
|
* (convertToPHPValue) value. |
110
|
|
|
* This structure is used for calculating changesets at commit time. |
111
|
|
|
* |
112
|
|
|
* Internal: Note that PHPs "copy-on-write" behavior helps a lot with memory usage. |
113
|
|
|
* A value will only really be copied if the value in the entity is modified by the user. |
114
|
|
|
* |
115
|
|
|
* @var mixed[][] |
116
|
|
|
*/ |
117
|
|
|
private $originalEntityData = []; |
118
|
|
|
|
119
|
|
|
/** |
120
|
|
|
* Map of entity changes. Keys are object ids (spl_object_id). |
121
|
|
|
* Filled at the beginning of a commit of the UnitOfWork and cleaned at the end. |
122
|
|
|
* |
123
|
|
|
* @var mixed[][] |
124
|
|
|
*/ |
125
|
|
|
private $entityChangeSets = []; |
126
|
|
|
|
127
|
|
|
/** |
128
|
|
|
* The (cached) states of any known entities. |
129
|
|
|
* Keys are object ids (spl_object_id). |
130
|
|
|
* |
131
|
|
|
* @var int[] |
132
|
|
|
*/ |
133
|
|
|
private $entityStates = []; |
134
|
|
|
|
135
|
|
|
/** |
136
|
|
|
* Map of entities that are scheduled for dirty checking at commit time. |
137
|
|
|
* This is only used for entities with a change tracking policy of DEFERRED_EXPLICIT. |
138
|
|
|
* Keys are object ids (spl_object_id). |
139
|
|
|
* |
140
|
|
|
* @var object[] |
141
|
|
|
*/ |
142
|
|
|
private $scheduledForSynchronization = []; |
143
|
|
|
|
144
|
|
|
/** |
145
|
|
|
* A list of all pending entity insertions. |
146
|
|
|
* |
147
|
|
|
* @var object[] |
148
|
|
|
*/ |
149
|
|
|
private $entityInsertions = []; |
150
|
|
|
|
151
|
|
|
/** |
152
|
|
|
* A list of all pending entity updates. |
153
|
|
|
* |
154
|
|
|
* @var object[] |
155
|
|
|
*/ |
156
|
|
|
private $entityUpdates = []; |
157
|
|
|
|
158
|
|
|
/** |
159
|
|
|
* Any pending extra updates that have been scheduled by persisters. |
160
|
|
|
* |
161
|
|
|
* @var object[] |
162
|
|
|
*/ |
163
|
|
|
private $extraUpdates = []; |
164
|
|
|
|
165
|
|
|
/** |
166
|
|
|
* A list of all pending entity deletions. |
167
|
|
|
* |
168
|
|
|
* @var object[] |
169
|
|
|
*/ |
170
|
|
|
private $entityDeletions = []; |
171
|
|
|
|
172
|
|
|
/** |
173
|
|
|
* New entities that were discovered through relationships that were not |
174
|
|
|
* marked as cascade-persist. During flush, this array is populated and |
175
|
|
|
* then pruned of any entities that were discovered through a valid |
176
|
|
|
* cascade-persist path. (Leftovers cause an error.) |
177
|
|
|
* |
178
|
|
|
* Keys are OIDs, payload is a two-item array describing the association |
179
|
|
|
* and the entity. |
180
|
|
|
* |
181
|
|
|
* @var object[][]|array[][] indexed by respective object spl_object_id() |
182
|
|
|
*/ |
183
|
|
|
private $nonCascadedNewDetectedEntities = []; |
184
|
|
|
|
185
|
|
|
/** |
186
|
|
|
* All pending collection deletions. |
187
|
|
|
* |
188
|
|
|
* @var Collection[]|object[][] |
189
|
|
|
*/ |
190
|
|
|
private $collectionDeletions = []; |
191
|
|
|
|
192
|
|
|
/** |
193
|
|
|
* All pending collection updates. |
194
|
|
|
* |
195
|
|
|
* @var Collection[]|object[][] |
196
|
|
|
*/ |
197
|
|
|
private $collectionUpdates = []; |
198
|
|
|
|
199
|
|
|
/** |
200
|
|
|
* List of collections visited during changeset calculation on a commit-phase of a UnitOfWork. |
201
|
|
|
* At the end of the UnitOfWork all these collections will make new snapshots |
202
|
|
|
* of their data. |
203
|
|
|
* |
204
|
|
|
* @var Collection[]|object[][] |
205
|
|
|
*/ |
206
|
|
|
private $visitedCollections = []; |
207
|
|
|
|
208
|
|
|
/** |
209
|
|
|
* The EntityManager that "owns" this UnitOfWork instance. |
210
|
|
|
* |
211
|
|
|
* @var EntityManagerInterface |
212
|
|
|
*/ |
213
|
|
|
private $em; |
214
|
|
|
|
215
|
|
|
/** |
216
|
|
|
* The entity persister instances used to persist entity instances. |
217
|
|
|
* |
218
|
|
|
* @var EntityPersister[] |
219
|
|
|
*/ |
220
|
|
|
private $entityPersisters = []; |
221
|
|
|
|
222
|
|
|
/** |
223
|
|
|
* The collection persister instances used to persist collections. |
224
|
|
|
* |
225
|
|
|
* @var CollectionPersister[] |
226
|
|
|
*/ |
227
|
|
|
private $collectionPersisters = []; |
228
|
|
|
|
229
|
|
|
/** |
230
|
|
|
* The EventManager used for dispatching events. |
231
|
|
|
* |
232
|
|
|
* @var \Doctrine\Common\EventManager |
233
|
|
|
*/ |
234
|
|
|
private $eventManager; |
235
|
|
|
|
236
|
|
|
/** |
237
|
|
|
* The ListenersInvoker used for dispatching events. |
238
|
|
|
* |
239
|
|
|
* @var \Doctrine\ORM\Event\ListenersInvoker |
240
|
|
|
*/ |
241
|
|
|
private $listenersInvoker; |
242
|
|
|
|
243
|
|
|
/** |
244
|
|
|
* @var Instantiator |
245
|
|
|
*/ |
246
|
|
|
private $instantiator; |
247
|
|
|
|
248
|
|
|
/** |
249
|
|
|
* Orphaned entities that are scheduled for removal. |
250
|
|
|
* |
251
|
|
|
* @var object[] |
252
|
|
|
*/ |
253
|
|
|
private $orphanRemovals = []; |
254
|
|
|
|
255
|
|
|
/** |
256
|
|
|
* Read-Only objects are never evaluated |
257
|
|
|
* |
258
|
|
|
* @var object[] |
259
|
|
|
*/ |
260
|
|
|
private $readOnlyObjects = []; |
261
|
|
|
|
262
|
|
|
/** |
263
|
|
|
* Map of Entity Class-Names and corresponding IDs that should eager loaded when requested. |
264
|
|
|
* |
265
|
|
|
* @var mixed[][][] |
266
|
|
|
*/ |
267
|
|
|
private $eagerLoadingEntities = []; |
268
|
|
|
|
269
|
|
|
/** |
270
|
|
|
* @var bool |
271
|
|
|
*/ |
272
|
|
|
protected $hasCache = false; |
273
|
|
|
|
274
|
|
|
/** |
275
|
|
|
* Helper for handling completion of hydration |
276
|
|
|
* |
277
|
|
|
* @var HydrationCompleteHandler |
278
|
|
|
*/ |
279
|
|
|
private $hydrationCompleteHandler; |
280
|
|
|
|
281
|
|
|
/** |
282
|
|
|
* @var NormalizeIdentifier |
283
|
|
|
*/ |
284
|
|
|
private $normalizeIdentifier; |
285
|
|
|
|
286
|
|
|
/** |
287
|
|
|
* Initializes a new UnitOfWork instance, bound to the given EntityManager. |
288
|
|
|
*/ |
289
|
2273 |
|
public function __construct(EntityManagerInterface $em) |
290
|
|
|
{ |
291
|
2273 |
|
$this->em = $em; |
292
|
2273 |
|
$this->eventManager = $em->getEventManager(); |
293
|
2273 |
|
$this->listenersInvoker = new ListenersInvoker($em); |
294
|
2273 |
|
$this->hasCache = $em->getConfiguration()->isSecondLevelCacheEnabled(); |
295
|
2273 |
|
$this->instantiator = new Instantiator(); |
296
|
2273 |
|
$this->hydrationCompleteHandler = new HydrationCompleteHandler($this->listenersInvoker, $em); |
297
|
2273 |
|
$this->normalizeIdentifier = new NormalizeIdentifier(); |
298
|
2273 |
|
} |
299
|
|
|
|
300
|
|
|
/** |
301
|
|
|
* Commits the UnitOfWork, executing all operations that have been postponed |
302
|
|
|
* up to this point. The state of all managed entities will be synchronized with |
303
|
|
|
* the database. |
304
|
|
|
* |
305
|
|
|
* The operations are executed in the following order: |
306
|
|
|
* |
307
|
|
|
* 1) All entity insertions |
308
|
|
|
* 2) All entity updates |
309
|
|
|
* 3) All collection deletions |
310
|
|
|
* 4) All collection updates |
311
|
|
|
* 5) All entity deletions |
312
|
|
|
* |
313
|
|
|
* @throws \Exception |
314
|
|
|
*/ |
315
|
1016 |
|
public function commit() |
316
|
|
|
{ |
317
|
|
|
// Raise preFlush |
318
|
1016 |
|
if ($this->eventManager->hasListeners(Events::preFlush)) { |
319
|
2 |
|
$this->eventManager->dispatchEvent(Events::preFlush, new PreFlushEventArgs($this->em)); |
320
|
|
|
} |
321
|
|
|
|
322
|
1016 |
|
$this->computeChangeSets(); |
323
|
|
|
|
324
|
1014 |
|
if (! ($this->entityInsertions || |
325
|
154 |
|
$this->entityDeletions || |
326
|
119 |
|
$this->entityUpdates || |
|
|
|
|
327
|
36 |
|
$this->collectionUpdates || |
328
|
33 |
|
$this->collectionDeletions || |
329
|
1014 |
|
$this->orphanRemovals)) { |
|
|
|
|
330
|
21 |
|
$this->dispatchOnFlushEvent(); |
331
|
21 |
|
$this->dispatchPostFlushEvent(); |
332
|
|
|
|
333
|
21 |
|
return; // Nothing to do. |
334
|
|
|
} |
335
|
|
|
|
336
|
1009 |
|
$this->assertThatThereAreNoUnintentionallyNonPersistedAssociations(); |
337
|
|
|
|
338
|
1007 |
|
if ($this->orphanRemovals) { |
|
|
|
|
339
|
15 |
|
foreach ($this->orphanRemovals as $orphan) { |
340
|
15 |
|
$this->remove($orphan); |
341
|
|
|
} |
342
|
|
|
} |
343
|
|
|
|
344
|
1007 |
|
$this->dispatchOnFlushEvent(); |
345
|
|
|
|
346
|
|
|
// Now we need a commit order to maintain referential integrity |
347
|
1007 |
|
$commitOrder = $this->getCommitOrder(); |
348
|
|
|
|
349
|
1007 |
|
$conn = $this->em->getConnection(); |
350
|
1007 |
|
$conn->beginTransaction(); |
351
|
|
|
|
352
|
|
|
try { |
353
|
|
|
// Collection deletions (deletions of complete collections) |
354
|
1007 |
|
foreach ($this->collectionDeletions as $collectionToDelete) { |
355
|
19 |
|
$this->getCollectionPersister($collectionToDelete->getMapping())->delete($collectionToDelete); |
356
|
|
|
} |
357
|
|
|
|
358
|
1007 |
|
if ($this->entityInsertions) { |
359
|
1003 |
|
foreach ($commitOrder as $class) { |
360
|
1003 |
|
$this->executeInserts($class); |
361
|
|
|
} |
362
|
|
|
} |
363
|
|
|
|
364
|
1006 |
|
if ($this->entityUpdates) { |
|
|
|
|
365
|
108 |
|
foreach ($commitOrder as $class) { |
366
|
108 |
|
$this->executeUpdates($class); |
367
|
|
|
} |
368
|
|
|
} |
369
|
|
|
|
370
|
|
|
// Extra updates that were requested by persisters. |
371
|
1002 |
|
if ($this->extraUpdates) { |
|
|
|
|
372
|
33 |
|
$this->executeExtraUpdates(); |
373
|
|
|
} |
374
|
|
|
|
375
|
|
|
// Collection updates (deleteRows, updateRows, insertRows) |
376
|
1002 |
|
foreach ($this->collectionUpdates as $collectionToUpdate) { |
377
|
528 |
|
$this->getCollectionPersister($collectionToUpdate->getMapping())->update($collectionToUpdate); |
378
|
|
|
} |
379
|
|
|
|
380
|
|
|
// Entity deletions come last and need to be in reverse commit order |
381
|
1002 |
|
if ($this->entityDeletions) { |
382
|
60 |
|
foreach (array_reverse($commitOrder) as $committedEntityName) { |
383
|
60 |
|
if (! $this->entityDeletions) { |
384
|
34 |
|
break; // just a performance optimisation |
385
|
|
|
} |
386
|
|
|
|
387
|
60 |
|
$this->executeDeletions($committedEntityName); |
388
|
|
|
} |
389
|
|
|
} |
390
|
|
|
|
391
|
1002 |
|
$conn->commit(); |
392
|
10 |
|
} catch (\Throwable $e) { |
393
|
10 |
|
$this->em->close(); |
394
|
10 |
|
$conn->rollBack(); |
395
|
|
|
|
396
|
10 |
|
$this->afterTransactionRolledBack(); |
397
|
|
|
|
398
|
10 |
|
throw $e; |
399
|
|
|
} |
400
|
|
|
|
401
|
1002 |
|
$this->afterTransactionComplete(); |
402
|
|
|
|
403
|
|
|
// Take new snapshots from visited collections |
404
|
1002 |
|
foreach ($this->visitedCollections as $coll) { |
405
|
527 |
|
$coll->takeSnapshot(); |
406
|
|
|
} |
407
|
|
|
|
408
|
1002 |
|
$this->dispatchPostFlushEvent(); |
409
|
|
|
|
410
|
|
|
// Clean up |
411
|
1001 |
|
$this->entityInsertions = |
412
|
1001 |
|
$this->entityUpdates = |
413
|
1001 |
|
$this->entityDeletions = |
414
|
1001 |
|
$this->extraUpdates = |
415
|
1001 |
|
$this->entityChangeSets = |
416
|
1001 |
|
$this->collectionUpdates = |
417
|
1001 |
|
$this->collectionDeletions = |
418
|
1001 |
|
$this->visitedCollections = |
419
|
1001 |
|
$this->scheduledForSynchronization = |
420
|
1001 |
|
$this->orphanRemovals = []; |
421
|
1001 |
|
} |
422
|
|
|
|
423
|
|
|
/** |
424
|
|
|
* Computes the changesets of all entities scheduled for insertion. |
425
|
|
|
*/ |
426
|
1016 |
|
private function computeScheduleInsertsChangeSets() |
427
|
|
|
{ |
428
|
1016 |
|
foreach ($this->entityInsertions as $entity) { |
429
|
1007 |
|
$class = $this->em->getClassMetadata(get_class($entity)); |
430
|
|
|
|
431
|
1007 |
|
$this->computeChangeSet($class, $entity); |
|
|
|
|
432
|
|
|
} |
433
|
1014 |
|
} |
434
|
|
|
|
435
|
|
|
/** |
436
|
|
|
* Executes any extra updates that have been scheduled. |
437
|
|
|
*/ |
438
|
33 |
|
private function executeExtraUpdates() |
439
|
|
|
{ |
440
|
33 |
|
foreach ($this->extraUpdates as $oid => $update) { |
441
|
33 |
|
list ($entity, $changeset) = $update; |
442
|
|
|
|
443
|
33 |
|
$this->entityChangeSets[$oid] = $changeset; |
444
|
|
|
|
445
|
|
|
// echo 'Extra update: '; |
|
|
|
|
446
|
|
|
// \Doctrine\Common\Util\Debug::dump($changeset, 3); |
447
|
|
|
|
448
|
33 |
|
$this->getEntityPersister(get_class($entity))->update($entity); |
449
|
|
|
} |
450
|
|
|
|
451
|
33 |
|
$this->extraUpdates = []; |
452
|
33 |
|
} |
453
|
|
|
|
454
|
|
|
/** |
455
|
|
|
* Gets the changeset for an entity. |
456
|
|
|
* |
457
|
|
|
* @param object $entity |
458
|
|
|
* |
459
|
|
|
* @return mixed[] |
460
|
|
|
*/ |
461
|
1002 |
|
public function & getEntityChangeSet($entity) |
462
|
|
|
{ |
463
|
1002 |
|
$oid = spl_object_id($entity); |
|
|
|
|
464
|
1002 |
|
$data = []; |
465
|
|
|
|
466
|
1002 |
|
if (! isset($this->entityChangeSets[$oid])) { |
467
|
2 |
|
return $data; |
468
|
|
|
} |
469
|
|
|
|
470
|
1002 |
|
return $this->entityChangeSets[$oid]; |
471
|
|
|
} |
472
|
|
|
|
473
|
|
|
/** |
474
|
|
|
* Computes the changes that happened to a single entity. |
475
|
|
|
* |
476
|
|
|
* Modifies/populates the following properties: |
477
|
|
|
* |
478
|
|
|
* {@link originalEntityData} |
479
|
|
|
* If the entity is NEW or MANAGED but not yet fully persisted (only has an id) |
480
|
|
|
* then it was not fetched from the database and therefore we have no original |
481
|
|
|
* entity data yet. All of the current entity data is stored as the original entity data. |
482
|
|
|
* |
483
|
|
|
* {@link entityChangeSets} |
484
|
|
|
* The changes detected on all properties of the entity are stored there. |
485
|
|
|
* A change is a tuple array where the first entry is the old value and the second |
486
|
|
|
* entry is the new value of the property. Changesets are used by persisters |
487
|
|
|
* to INSERT/UPDATE the persistent entity state. |
488
|
|
|
* |
489
|
|
|
* {@link entityUpdates} |
490
|
|
|
* If the entity is already fully MANAGED (has been fetched from the database before) |
491
|
|
|
* and any changes to its properties are detected, then a reference to the entity is stored |
492
|
|
|
* there to mark it for an update. |
493
|
|
|
* |
494
|
|
|
* {@link collectionDeletions} |
495
|
|
|
* If a PersistentCollection has been de-referenced in a fully MANAGED entity, |
496
|
|
|
* then this collection is marked for deletion. |
497
|
|
|
* |
498
|
|
|
* @ignore |
499
|
|
|
* |
500
|
|
|
* @internal Don't call from the outside. |
501
|
|
|
* |
502
|
|
|
* @param ClassMetadata $class The class descriptor of the entity. |
503
|
|
|
* @param object $entity The entity for which to compute the changes. |
504
|
|
|
* |
505
|
|
|
*/ |
506
|
1017 |
|
public function computeChangeSet(ClassMetadata $class, $entity) |
507
|
|
|
{ |
508
|
1017 |
|
$oid = spl_object_id($entity); |
|
|
|
|
509
|
|
|
|
510
|
1017 |
|
if (isset($this->readOnlyObjects[$oid])) { |
511
|
2 |
|
return; |
512
|
|
|
} |
513
|
|
|
|
514
|
1017 |
|
if ($class->inheritanceType !== InheritanceType::NONE) { |
515
|
330 |
|
$class = $this->em->getClassMetadata(get_class($entity)); |
516
|
|
|
} |
517
|
|
|
|
518
|
1017 |
|
$invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preFlush) & ~ListenersInvoker::INVOKE_MANAGER; |
|
|
|
|
519
|
|
|
|
520
|
1017 |
|
if ($invoke !== ListenersInvoker::INVOKE_NONE) { |
521
|
135 |
|
$this->listenersInvoker->invoke($class, Events::preFlush, $entity, new PreFlushEventArgs($this->em), $invoke); |
|
|
|
|
522
|
|
|
} |
523
|
|
|
|
524
|
1017 |
|
$actualData = []; |
525
|
|
|
|
526
|
1017 |
|
foreach ($class->getDeclaredPropertiesIterator() as $name => $property) { |
|
|
|
|
527
|
1017 |
|
$value = $property->getValue($entity); |
528
|
|
|
|
529
|
1017 |
|
if ($property instanceof ToManyAssociationMetadata && $value !== null) { |
530
|
778 |
|
if ($value instanceof PersistentCollection && $value->getOwner() === $entity) { |
531
|
184 |
|
continue; |
532
|
|
|
} |
533
|
|
|
|
534
|
775 |
|
$value = $property->wrap($entity, $value, $this->em); |
535
|
|
|
|
536
|
775 |
|
$property->setValue($entity, $value); |
537
|
|
|
|
538
|
775 |
|
$actualData[$name] = $value; |
539
|
|
|
|
540
|
775 |
|
continue; |
541
|
|
|
} |
542
|
|
|
|
543
|
1017 |
|
if (( ! $class->isIdentifier($name) |
544
|
1017 |
|
|| ! $class->getProperty($name) instanceof FieldMetadata |
|
|
|
|
545
|
1017 |
|
|| ! $class->getProperty($name)->hasValueGenerator() |
|
|
|
|
546
|
1017 |
|
|| $class->getProperty($name)->getValueGenerator()->getType() !== GeneratorType::IDENTITY |
|
|
|
|
547
|
1017 |
|
) && (! $class->isVersioned() || $name !== $class->versionProperty->getName())) { |
|
|
|
|
548
|
1017 |
|
$actualData[$name] = $value; |
549
|
|
|
} |
550
|
|
|
} |
551
|
|
|
|
552
|
1017 |
|
if (! isset($this->originalEntityData[$oid])) { |
553
|
|
|
// Entity is either NEW or MANAGED but not yet fully persisted (only has an id). |
554
|
|
|
// These result in an INSERT. |
555
|
1013 |
|
$this->originalEntityData[$oid] = $actualData; |
556
|
1013 |
|
$changeSet = []; |
557
|
|
|
|
558
|
1013 |
|
foreach ($actualData as $propName => $actualValue) { |
559
|
993 |
|
$property = $class->getProperty($propName); |
560
|
|
|
|
561
|
993 |
|
if (($property instanceof FieldMetadata) || |
562
|
993 |
|
($property instanceof ToOneAssociationMetadata && $property->isOwningSide())) { |
563
|
993 |
|
$changeSet[$propName] = [null, $actualValue]; |
564
|
|
|
} |
565
|
|
|
} |
566
|
|
|
|
567
|
1013 |
|
$this->entityChangeSets[$oid] = $changeSet; |
568
|
|
|
} else { |
569
|
|
|
// Entity is "fully" MANAGED: it was already fully persisted before |
570
|
|
|
// and we have a copy of the original data |
571
|
250 |
|
$originalData = $this->originalEntityData[$oid]; |
572
|
250 |
|
$isChangeTrackingNotify = $class->changeTrackingPolicy === ChangeTrackingPolicy::NOTIFY; |
573
|
250 |
|
$changeSet = ($isChangeTrackingNotify && isset($this->entityChangeSets[$oid])) |
574
|
|
|
? $this->entityChangeSets[$oid] |
575
|
250 |
|
: []; |
576
|
|
|
|
577
|
250 |
|
foreach ($actualData as $propName => $actualValue) { |
578
|
|
|
// skip field, its a partially omitted one! |
579
|
240 |
|
if (! (isset($originalData[$propName]) || array_key_exists($propName, $originalData))) { |
580
|
36 |
|
continue; |
581
|
|
|
} |
582
|
|
|
|
583
|
240 |
|
$orgValue = $originalData[$propName]; |
584
|
|
|
|
585
|
|
|
// skip if value haven't changed |
586
|
240 |
|
if ($orgValue === $actualValue) { |
587
|
224 |
|
continue; |
588
|
|
|
} |
589
|
|
|
|
590
|
106 |
|
$property = $class->getProperty($propName); |
591
|
|
|
|
592
|
|
|
// Persistent collection was exchanged with the "originally" |
593
|
|
|
// created one. This can only mean it was cloned and replaced |
594
|
|
|
// on another entity. |
595
|
106 |
|
if ($actualValue instanceof PersistentCollection) { |
596
|
8 |
|
$owner = $actualValue->getOwner(); |
597
|
|
|
|
598
|
8 |
|
if ($owner === null) { // cloned |
599
|
|
|
$actualValue->setOwner($entity, $property); |
600
|
8 |
|
} elseif ($owner !== $entity) { // no clone, we have to fix |
601
|
|
|
if (! $actualValue->isInitialized()) { |
602
|
|
|
$actualValue->initialize(); // we have to do this otherwise the cols share state |
603
|
|
|
} |
604
|
|
|
|
605
|
|
|
$newValue = clone $actualValue; |
606
|
|
|
|
607
|
|
|
$newValue->setOwner($entity, $property); |
608
|
|
|
|
609
|
|
|
$property->setValue($entity, $newValue); |
610
|
|
|
} |
611
|
|
|
} |
612
|
|
|
|
613
|
|
|
switch (true) { |
614
|
106 |
|
case ($property instanceof FieldMetadata): |
615
|
55 |
|
if ($isChangeTrackingNotify) { |
616
|
|
|
// Continue inside switch behaves as break. |
617
|
|
|
// We are required to use continue 2, since we need to continue to next $actualData item |
618
|
|
|
continue 2; |
619
|
|
|
} |
620
|
|
|
|
621
|
55 |
|
$changeSet[$propName] = [$orgValue, $actualValue]; |
622
|
55 |
|
break; |
623
|
|
|
|
624
|
57 |
|
case ($property instanceof ToOneAssociationMetadata): |
625
|
46 |
|
if ($property->isOwningSide()) { |
626
|
20 |
|
$changeSet[$propName] = [$orgValue, $actualValue]; |
627
|
|
|
} |
628
|
|
|
|
629
|
46 |
|
if ($orgValue !== null && $property->isOrphanRemoval()) { |
630
|
4 |
|
$this->scheduleOrphanRemoval($orgValue); |
631
|
|
|
} |
632
|
|
|
|
633
|
46 |
|
break; |
634
|
|
|
|
635
|
12 |
|
case ($property instanceof ToManyAssociationMetadata): |
636
|
|
|
// Check if original value exists |
637
|
9 |
|
if ($orgValue instanceof PersistentCollection) { |
638
|
|
|
// A PersistentCollection was de-referenced, so delete it. |
639
|
8 |
|
if (! $this->isCollectionScheduledForDeletion($orgValue)) { |
640
|
8 |
|
$this->scheduleCollectionDeletion($orgValue); |
641
|
|
|
|
642
|
8 |
|
$changeSet[$propName] = $orgValue; // Signal changeset, to-many associations will be ignored |
643
|
|
|
} |
644
|
|
|
} |
645
|
|
|
|
646
|
9 |
|
break; |
647
|
|
|
|
648
|
106 |
|
default: |
649
|
|
|
// Do nothing |
650
|
|
|
} |
651
|
|
|
} |
652
|
|
|
|
653
|
250 |
|
if ($changeSet) { |
654
|
81 |
|
$this->entityChangeSets[$oid] = $changeSet; |
655
|
81 |
|
$this->originalEntityData[$oid] = $actualData; |
656
|
81 |
|
$this->entityUpdates[$oid] = $entity; |
657
|
|
|
} |
658
|
|
|
} |
659
|
|
|
|
660
|
|
|
// Look for changes in associations of the entity |
661
|
1017 |
|
foreach ($class->getDeclaredPropertiesIterator() as $property) { |
662
|
1017 |
|
if (! $property instanceof AssociationMetadata) { |
663
|
1017 |
|
continue; |
664
|
|
|
} |
665
|
|
|
|
666
|
889 |
|
$value = $property->getValue($entity); |
667
|
|
|
|
668
|
889 |
|
if ($value === null) { |
669
|
628 |
|
continue; |
670
|
|
|
} |
671
|
|
|
|
672
|
865 |
|
$this->computeAssociationChanges($property, $value); |
673
|
|
|
|
674
|
857 |
|
if ($property instanceof ManyToManyAssociationMetadata && |
675
|
857 |
|
$value instanceof PersistentCollection && |
676
|
857 |
|
! isset($this->entityChangeSets[$oid]) && |
677
|
857 |
|
$property->isOwningSide() && |
678
|
857 |
|
$value->isDirty()) { |
679
|
31 |
|
$this->entityChangeSets[$oid] = []; |
680
|
31 |
|
$this->originalEntityData[$oid] = $actualData; |
681
|
857 |
|
$this->entityUpdates[$oid] = $entity; |
682
|
|
|
} |
683
|
|
|
} |
684
|
1009 |
|
} |
685
|
|
|
|
686
|
|
|
/** |
687
|
|
|
* Computes all the changes that have been done to entities and collections |
688
|
|
|
* since the last commit and stores these changes in the _entityChangeSet map |
689
|
|
|
* temporarily for access by the persisters, until the UoW commit is finished. |
690
|
|
|
*/ |
691
|
1016 |
|
public function computeChangeSets() |
692
|
|
|
{ |
693
|
|
|
// Compute changes for INSERTed entities first. This must always happen. |
694
|
1016 |
|
$this->computeScheduleInsertsChangeSets(); |
695
|
|
|
|
696
|
|
|
// Compute changes for other MANAGED entities. Change tracking policies take effect here. |
697
|
1014 |
|
foreach ($this->identityMap as $className => $entities) { |
698
|
447 |
|
$class = $this->em->getClassMetadata($className); |
699
|
|
|
|
700
|
|
|
// Skip class if instances are read-only |
701
|
447 |
|
if ($class->isReadOnly()) { |
|
|
|
|
702
|
1 |
|
continue; |
703
|
|
|
} |
704
|
|
|
|
705
|
|
|
// If change tracking is explicit or happens through notification, then only compute |
706
|
|
|
// changes on entities of that type that are explicitly marked for synchronization. |
707
|
|
|
switch (true) { |
708
|
446 |
|
case ($class->changeTrackingPolicy === ChangeTrackingPolicy::DEFERRED_IMPLICIT): |
|
|
|
|
709
|
444 |
|
$entitiesToProcess = $entities; |
710
|
444 |
|
break; |
711
|
|
|
|
712
|
3 |
|
case (isset($this->scheduledForSynchronization[$className])): |
713
|
3 |
|
$entitiesToProcess = $this->scheduledForSynchronization[$className]; |
714
|
3 |
|
break; |
715
|
|
|
|
716
|
|
|
default: |
717
|
1 |
|
$entitiesToProcess = []; |
718
|
|
|
} |
719
|
|
|
|
720
|
446 |
|
foreach ($entitiesToProcess as $entity) { |
721
|
|
|
// Ignore uninitialized proxy objects |
722
|
426 |
|
if ($entity instanceof GhostObjectInterface && ! $entity->isProxyInitialized()) { |
723
|
37 |
|
continue; |
724
|
|
|
} |
725
|
|
|
|
726
|
|
|
// Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here. |
727
|
424 |
|
$oid = spl_object_id($entity); |
|
|
|
|
728
|
|
|
|
729
|
424 |
|
if (! isset($this->entityInsertions[$oid]) && ! isset($this->entityDeletions[$oid]) && isset($this->entityStates[$oid])) { |
730
|
446 |
|
$this->computeChangeSet($class, $entity); |
|
|
|
|
731
|
|
|
} |
732
|
|
|
} |
733
|
|
|
} |
734
|
1014 |
|
} |
735
|
|
|
|
736
|
|
|
/** |
737
|
|
|
* Computes the changes of an association. |
738
|
|
|
* |
739
|
|
|
* @param AssociationMetadata $association The association mapping. |
740
|
|
|
* @param mixed $value The value of the association. |
741
|
|
|
* |
742
|
|
|
* @throws ORMInvalidArgumentException |
743
|
|
|
* @throws ORMException |
744
|
|
|
*/ |
745
|
865 |
|
private function computeAssociationChanges(AssociationMetadata $association, $value) |
746
|
|
|
{ |
747
|
865 |
|
if ($value instanceof GhostObjectInterface && ! $value->isProxyInitialized()) { |
748
|
30 |
|
return; |
749
|
|
|
} |
750
|
|
|
|
751
|
864 |
|
if ($value instanceof PersistentCollection && $value->isDirty()) { |
752
|
531 |
|
$coid = spl_object_id($value); |
|
|
|
|
753
|
|
|
|
754
|
531 |
|
$this->collectionUpdates[$coid] = $value; |
755
|
531 |
|
$this->visitedCollections[$coid] = $value; |
756
|
|
|
} |
757
|
|
|
|
758
|
|
|
// Look through the entities, and in any of their associations, |
759
|
|
|
// for transient (new) entities, recursively. ("Persistence by reachability") |
|
|
|
|
760
|
|
|
// Unwrap. Uninitialized collections will simply be empty. |
761
|
864 |
|
$unwrappedValue = ($association instanceof ToOneAssociationMetadata) ? [$value] : $value->unwrap(); |
762
|
864 |
|
$targetEntity = $association->getTargetEntity(); |
763
|
864 |
|
$targetClass = $this->em->getClassMetadata($targetEntity); |
764
|
|
|
|
765
|
864 |
|
foreach ($unwrappedValue as $key => $entry) { |
766
|
721 |
|
if (! ($entry instanceof $targetEntity)) { |
767
|
8 |
|
throw ORMInvalidArgumentException::invalidAssociation($targetClass, $association, $entry); |
|
|
|
|
768
|
|
|
} |
769
|
|
|
|
770
|
713 |
|
$state = $this->getEntityState($entry, self::STATE_NEW); |
771
|
|
|
|
772
|
713 |
|
if (! ($entry instanceof $targetEntity)) { |
773
|
|
|
throw ORMException::unexpectedAssociationValue( |
774
|
|
|
$association->getSourceEntity(), |
775
|
|
|
$association->getName(), |
776
|
|
|
get_class($entry), |
777
|
|
|
$targetEntity |
778
|
|
|
); |
779
|
|
|
} |
780
|
|
|
|
781
|
|
|
switch ($state) { |
782
|
713 |
|
case self::STATE_NEW: |
783
|
41 |
|
if (! in_array('persist', $association->getCascade())) { |
784
|
5 |
|
$this->nonCascadedNewDetectedEntities[\spl_object_id($entry)] = [$association, $entry]; |
785
|
|
|
|
786
|
5 |
|
break; |
787
|
|
|
} |
788
|
|
|
|
789
|
37 |
|
$this->persistNew($targetClass, $entry); |
|
|
|
|
790
|
37 |
|
$this->computeChangeSet($targetClass, $entry); |
|
|
|
|
791
|
|
|
|
792
|
37 |
|
break; |
793
|
|
|
|
794
|
706 |
|
case self::STATE_REMOVED: |
795
|
|
|
// Consume the $value as array (it's either an array or an ArrayAccess) |
796
|
|
|
// and remove the element from Collection. |
797
|
4 |
|
if ($association instanceof ToManyAssociationMetadata) { |
798
|
3 |
|
unset($value[$key]); |
799
|
|
|
} |
800
|
4 |
|
break; |
801
|
|
|
|
802
|
706 |
|
case self::STATE_DETACHED: |
803
|
|
|
// Can actually not happen right now as we assume STATE_NEW, |
804
|
|
|
// so the exception will be raised from the DBAL layer (constraint violation). |
805
|
|
|
throw ORMInvalidArgumentException::detachedEntityFoundThroughRelationship($association, $entry); |
806
|
|
|
break; |
807
|
|
|
|
808
|
713 |
|
default: |
809
|
|
|
// MANAGED associated entities are already taken into account |
810
|
|
|
// during changeset calculation anyway, since they are in the identity map. |
811
|
|
|
} |
812
|
|
|
} |
813
|
856 |
|
} |
814
|
|
|
|
815
|
|
|
/** |
816
|
|
|
* @param \Doctrine\ORM\Mapping\ClassMetadata $class |
817
|
|
|
* @param object $entity |
818
|
|
|
*/ |
819
|
1028 |
|
private function persistNew($class, $entity) |
820
|
|
|
{ |
821
|
1028 |
|
$oid = spl_object_id($entity); |
|
|
|
|
822
|
1028 |
|
$invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::prePersist); |
823
|
|
|
|
824
|
1028 |
|
if ($invoke !== ListenersInvoker::INVOKE_NONE) { |
825
|
137 |
|
$this->listenersInvoker->invoke($class, Events::prePersist, $entity, new LifecycleEventArgs($entity, $this->em), $invoke); |
826
|
|
|
} |
827
|
|
|
|
828
|
1028 |
|
$generationPlan = $class->getValueGenerationPlan(); |
829
|
1028 |
|
$persister = $this->getEntityPersister($class->getClassName()); |
830
|
1028 |
|
$generationPlan->executeImmediate($this->em, $entity); |
831
|
|
|
|
832
|
1028 |
|
if (! $generationPlan->containsDeferred()) { |
833
|
269 |
|
$id = $this->em->getIdentifierFlattener()->flattenIdentifier($class, $persister->getIdentifier($entity)); |
834
|
269 |
|
$this->entityIdentifiers[$oid] = $id; |
835
|
|
|
} |
836
|
|
|
|
837
|
1028 |
|
$this->entityStates[$oid] = self::STATE_MANAGED; |
838
|
|
|
|
839
|
1028 |
|
$this->scheduleForInsert($entity); |
840
|
1028 |
|
} |
841
|
|
|
|
842
|
|
|
/** |
843
|
|
|
* INTERNAL: |
844
|
|
|
* Computes the changeset of an individual entity, independently of the |
845
|
|
|
* computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit(). |
846
|
|
|
* |
847
|
|
|
* The passed entity must be a managed entity. If the entity already has a change set |
848
|
|
|
* because this method is invoked during a commit cycle then the change sets are added. |
849
|
|
|
* whereby changes detected in this method prevail. |
850
|
|
|
* |
851
|
|
|
* @ignore |
852
|
|
|
* |
853
|
|
|
* @param ClassMetadata $class The class descriptor of the entity. |
854
|
|
|
* @param object $entity The entity for which to (re)calculate the change set. |
855
|
|
|
* |
856
|
|
|
* @throws ORMInvalidArgumentException If the passed entity is not MANAGED. |
857
|
|
|
* @throws \RuntimeException |
858
|
|
|
*/ |
859
|
15 |
|
public function recomputeSingleEntityChangeSet(ClassMetadata $class, $entity) : void |
860
|
|
|
{ |
861
|
15 |
|
$oid = spl_object_id($entity); |
|
|
|
|
862
|
|
|
|
863
|
15 |
|
if (! isset($this->entityStates[$oid]) || $this->entityStates[$oid] !== self::STATE_MANAGED) { |
864
|
|
|
throw ORMInvalidArgumentException::entityNotManaged($entity); |
865
|
|
|
} |
866
|
|
|
|
867
|
|
|
// skip if change tracking is "NOTIFY" |
868
|
15 |
|
if ($class->changeTrackingPolicy === ChangeTrackingPolicy::NOTIFY) { |
869
|
|
|
return; |
870
|
|
|
} |
871
|
|
|
|
872
|
15 |
|
if ($class->inheritanceType !== InheritanceType::NONE) { |
873
|
3 |
|
$class = $this->em->getClassMetadata(get_class($entity)); |
874
|
|
|
} |
875
|
|
|
|
876
|
15 |
|
$actualData = []; |
877
|
|
|
|
878
|
15 |
|
foreach ($class->getDeclaredPropertiesIterator() as $name => $property) { |
879
|
|
|
switch (true) { |
880
|
15 |
|
case ($property instanceof VersionFieldMetadata): |
881
|
|
|
// Ignore version field |
882
|
|
|
break; |
883
|
|
|
|
884
|
15 |
|
case ($property instanceof FieldMetadata): |
885
|
15 |
|
if (! $property->isPrimaryKey() |
886
|
15 |
|
|| ! $property->getValueGenerator() |
887
|
15 |
|
|| $property->getValueGenerator()->getType() !== GeneratorType::IDENTITY) { |
888
|
15 |
|
$actualData[$name] = $property->getValue($entity); |
889
|
|
|
} |
890
|
|
|
|
891
|
15 |
|
break; |
892
|
|
|
|
893
|
11 |
|
case ($property instanceof ToOneAssociationMetadata): |
894
|
9 |
|
$actualData[$name] = $property->getValue($entity); |
895
|
15 |
|
break; |
896
|
|
|
} |
897
|
|
|
} |
898
|
|
|
|
899
|
15 |
|
if (! isset($this->originalEntityData[$oid])) { |
900
|
|
|
throw new \RuntimeException('Cannot call recomputeSingleEntityChangeSet before computeChangeSet on an entity.'); |
901
|
|
|
} |
902
|
|
|
|
903
|
15 |
|
$originalData = $this->originalEntityData[$oid]; |
904
|
15 |
|
$changeSet = []; |
905
|
|
|
|
906
|
15 |
|
foreach ($actualData as $propName => $actualValue) { |
907
|
15 |
|
$orgValue = $originalData[$propName] ?? null; |
908
|
|
|
|
909
|
15 |
|
if ($orgValue !== $actualValue) { |
910
|
15 |
|
$changeSet[$propName] = [$orgValue, $actualValue]; |
911
|
|
|
} |
912
|
|
|
} |
913
|
|
|
|
914
|
15 |
|
if ($changeSet) { |
915
|
7 |
|
if (isset($this->entityChangeSets[$oid])) { |
916
|
6 |
|
$this->entityChangeSets[$oid] = array_merge($this->entityChangeSets[$oid], $changeSet); |
917
|
1 |
|
} elseif (! isset($this->entityInsertions[$oid])) { |
918
|
1 |
|
$this->entityChangeSets[$oid] = $changeSet; |
919
|
1 |
|
$this->entityUpdates[$oid] = $entity; |
920
|
|
|
} |
921
|
7 |
|
$this->originalEntityData[$oid] = $actualData; |
922
|
|
|
} |
923
|
15 |
|
} |
924
|
|
|
|
925
|
|
|
/** |
926
|
|
|
* Executes all entity insertions for entities of the specified type. |
927
|
|
|
*/ |
928
|
1003 |
|
private function executeInserts(ClassMetadata $class) : void |
929
|
|
|
{ |
930
|
1003 |
|
$className = $class->getClassName(); |
931
|
1003 |
|
$persister = $this->getEntityPersister($className); |
932
|
1003 |
|
$invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postPersist); |
933
|
1003 |
|
$generationPlan = $class->getValueGenerationPlan(); |
934
|
|
|
|
935
|
1003 |
|
foreach ($this->entityInsertions as $oid => $entity) { |
936
|
1003 |
|
if ($this->em->getClassMetadata(get_class($entity))->getClassName() !== $className) { |
|
|
|
|
937
|
857 |
|
continue; |
938
|
|
|
} |
939
|
|
|
|
940
|
1003 |
|
$persister->insert($entity); |
941
|
|
|
|
942
|
1002 |
|
if ($generationPlan->containsDeferred()) { |
943
|
|
|
// Entity has post-insert IDs |
944
|
910 |
|
$oid = spl_object_id($entity); |
|
|
|
|
945
|
910 |
|
$id = $this->em->getIdentifierFlattener()->flattenIdentifier($class, $persister->getIdentifier($entity)); |
946
|
|
|
|
947
|
910 |
|
$this->entityIdentifiers[$oid] = $id; |
948
|
910 |
|
$this->entityStates[$oid] = self::STATE_MANAGED; |
949
|
910 |
|
$this->originalEntityData[$oid] = $id + $this->originalEntityData[$oid]; |
950
|
|
|
|
951
|
910 |
|
$this->addToIdentityMap($entity); |
952
|
|
|
} |
953
|
|
|
|
954
|
1002 |
|
unset($this->entityInsertions[$oid]); |
955
|
|
|
|
956
|
1002 |
|
if ($invoke !== ListenersInvoker::INVOKE_NONE) { |
957
|
133 |
|
$eventArgs = new LifecycleEventArgs($entity, $this->em); |
958
|
|
|
|
959
|
1002 |
|
$this->listenersInvoker->invoke($class, Events::postPersist, $entity, $eventArgs, $invoke); |
960
|
|
|
} |
961
|
|
|
} |
962
|
1003 |
|
} |
963
|
|
|
|
964
|
|
|
/** |
965
|
|
|
* Executes all entity updates for entities of the specified type. |
966
|
|
|
* |
967
|
|
|
* @param \Doctrine\ORM\Mapping\ClassMetadata $class |
968
|
|
|
*/ |
969
|
108 |
|
private function executeUpdates($class) |
970
|
|
|
{ |
971
|
108 |
|
$className = $class->getClassName(); |
972
|
108 |
|
$persister = $this->getEntityPersister($className); |
973
|
108 |
|
$preUpdateInvoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preUpdate); |
974
|
108 |
|
$postUpdateInvoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postUpdate); |
975
|
|
|
|
976
|
108 |
|
foreach ($this->entityUpdates as $oid => $entity) { |
977
|
108 |
|
if ($this->em->getClassMetadata(get_class($entity))->getClassName() !== $className) { |
978
|
70 |
|
continue; |
979
|
|
|
} |
980
|
|
|
|
981
|
108 |
|
if ($preUpdateInvoke !== ListenersInvoker::INVOKE_NONE) { |
982
|
12 |
|
$this->listenersInvoker->invoke($class, Events::preUpdate, $entity, new PreUpdateEventArgs($entity, $this->em, $this->getEntityChangeSet($entity)), $preUpdateInvoke); |
983
|
|
|
|
984
|
12 |
|
$this->recomputeSingleEntityChangeSet($class, $entity); |
985
|
|
|
} |
986
|
|
|
|
987
|
108 |
|
if (! empty($this->entityChangeSets[$oid])) { |
988
|
|
|
// echo 'Update: '; |
|
|
|
|
989
|
|
|
// \Doctrine\Common\Util\Debug::dump($this->entityChangeSets[$oid], 3); |
990
|
|
|
|
991
|
78 |
|
$persister->update($entity); |
992
|
|
|
} |
993
|
|
|
|
994
|
104 |
|
unset($this->entityUpdates[$oid]); |
995
|
|
|
|
996
|
104 |
|
if ($postUpdateInvoke !== ListenersInvoker::INVOKE_NONE) { |
997
|
104 |
|
$this->listenersInvoker->invoke($class, Events::postUpdate, $entity, new LifecycleEventArgs($entity, $this->em), $postUpdateInvoke); |
998
|
|
|
} |
999
|
|
|
} |
1000
|
104 |
|
} |
1001
|
|
|
|
1002
|
|
|
/** |
1003
|
|
|
* Executes all entity deletions for entities of the specified type. |
1004
|
|
|
* |
1005
|
|
|
* @param \Doctrine\ORM\Mapping\ClassMetadata $class |
1006
|
|
|
*/ |
1007
|
60 |
|
private function executeDeletions($class) |
1008
|
|
|
{ |
1009
|
60 |
|
$className = $class->getClassName(); |
1010
|
60 |
|
$persister = $this->getEntityPersister($className); |
1011
|
60 |
|
$invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postRemove); |
1012
|
|
|
|
1013
|
60 |
|
foreach ($this->entityDeletions as $oid => $entity) { |
1014
|
60 |
|
if ($this->em->getClassMetadata(get_class($entity))->getClassName() !== $className) { |
1015
|
24 |
|
continue; |
1016
|
|
|
} |
1017
|
|
|
|
1018
|
60 |
|
$persister->delete($entity); |
1019
|
|
|
|
1020
|
|
|
unset( |
1021
|
60 |
|
$this->entityDeletions[$oid], |
1022
|
60 |
|
$this->entityIdentifiers[$oid], |
1023
|
60 |
|
$this->originalEntityData[$oid], |
1024
|
60 |
|
$this->entityStates[$oid] |
1025
|
|
|
); |
1026
|
|
|
|
1027
|
|
|
// Entity with this $oid after deletion treated as NEW, even if the $oid |
1028
|
|
|
// is obtained by a new entity because the old one went out of scope. |
1029
|
|
|
//$this->entityStates[$oid] = self::STATE_NEW; |
|
|
|
|
1030
|
60 |
|
if (! $class->isIdentifierComposite()) { |
1031
|
57 |
|
$property = $class->getProperty($class->getSingleIdentifierFieldName()); |
1032
|
|
|
|
1033
|
57 |
|
if ($property instanceof FieldMetadata && $property->hasValueGenerator()) { |
1034
|
50 |
|
$property->setValue($entity, null); |
1035
|
|
|
} |
1036
|
|
|
} |
1037
|
|
|
|
1038
|
60 |
|
if ($invoke !== ListenersInvoker::INVOKE_NONE) { |
1039
|
9 |
|
$eventArgs = new LifecycleEventArgs($entity, $this->em); |
1040
|
|
|
|
1041
|
60 |
|
$this->listenersInvoker->invoke($class, Events::postRemove, $entity, $eventArgs, $invoke); |
1042
|
|
|
} |
1043
|
|
|
} |
1044
|
59 |
|
} |
1045
|
|
|
|
1046
|
|
|
/** |
1047
|
|
|
* Gets the commit order. |
1048
|
|
|
* |
1049
|
|
|
* @return ClassMetadata[] |
1050
|
|
|
*/ |
1051
|
1007 |
|
private function getCommitOrder() |
1052
|
|
|
{ |
1053
|
1007 |
|
$calc = new Internal\CommitOrderCalculator(); |
1054
|
|
|
|
1055
|
|
|
// See if there are any new classes in the changeset, that are not in the |
1056
|
|
|
// commit order graph yet (don't have a node). |
1057
|
|
|
// We have to inspect changeSet to be able to correctly build dependencies. |
1058
|
|
|
// It is not possible to use IdentityMap here because post inserted ids |
1059
|
|
|
// are not yet available. |
1060
|
1007 |
|
$newNodes = []; |
1061
|
|
|
|
1062
|
1007 |
|
foreach (\array_merge($this->entityInsertions, $this->entityUpdates, $this->entityDeletions) as $entity) { |
1063
|
1007 |
|
$class = $this->em->getClassMetadata(get_class($entity)); |
1064
|
|
|
|
1065
|
1007 |
|
if ($calc->hasNode($class->getClassName())) { |
1066
|
636 |
|
continue; |
1067
|
|
|
} |
1068
|
|
|
|
1069
|
1007 |
|
$calc->addNode($class->getClassName(), $class); |
1070
|
|
|
|
1071
|
1007 |
|
$newNodes[] = $class; |
1072
|
|
|
} |
1073
|
|
|
|
1074
|
|
|
// Calculate dependencies for new nodes |
1075
|
1007 |
|
while ($class = array_pop($newNodes)) { |
1076
|
1007 |
|
foreach ($class->getDeclaredPropertiesIterator() as $property) { |
1077
|
1007 |
|
if (! ($property instanceof ToOneAssociationMetadata && $property->isOwningSide())) { |
1078
|
1007 |
|
continue; |
1079
|
|
|
} |
1080
|
|
|
|
1081
|
837 |
|
$targetClass = $this->em->getClassMetadata($property->getTargetEntity()); |
1082
|
|
|
|
1083
|
837 |
|
if (! $calc->hasNode($targetClass->getClassName())) { |
1084
|
645 |
|
$calc->addNode($targetClass->getClassName(), $targetClass); |
1085
|
|
|
|
1086
|
645 |
|
$newNodes[] = $targetClass; |
1087
|
|
|
} |
1088
|
|
|
|
1089
|
837 |
|
$weight = ! array_filter( |
1090
|
837 |
|
$property->getJoinColumns(), |
1091
|
837 |
|
function (JoinColumnMetadata $joinColumn) { |
1092
|
837 |
|
return $joinColumn->isNullable(); |
1093
|
837 |
|
} |
1094
|
|
|
); |
1095
|
|
|
|
1096
|
837 |
|
$calc->addDependency($targetClass->getClassName(), $class->getClassName(), $weight); |
|
|
|
|
1097
|
|
|
|
1098
|
|
|
// If the target class has mapped subclasses, these share the same dependency. |
1099
|
837 |
|
if (! $targetClass->getSubClasses()) { |
|
|
|
|
1100
|
832 |
|
continue; |
1101
|
|
|
} |
1102
|
|
|
|
1103
|
233 |
|
foreach ($targetClass->getSubClasses() as $subClassName) { |
1104
|
233 |
|
$targetSubClass = $this->em->getClassMetadata($subClassName); |
1105
|
|
|
|
1106
|
233 |
|
if (! $calc->hasNode($subClassName)) { |
1107
|
205 |
|
$calc->addNode($targetSubClass->getClassName(), $targetSubClass); |
1108
|
|
|
|
1109
|
205 |
|
$newNodes[] = $targetSubClass; |
1110
|
|
|
} |
1111
|
|
|
|
1112
|
233 |
|
$calc->addDependency($targetSubClass->getClassName(), $class->getClassName(), 1); |
1113
|
|
|
} |
1114
|
|
|
} |
1115
|
|
|
} |
1116
|
|
|
|
1117
|
1007 |
|
return $calc->sort(); |
1118
|
|
|
} |
1119
|
|
|
|
1120
|
|
|
/** |
1121
|
|
|
* Schedules an entity for insertion into the database. |
1122
|
|
|
* If the entity already has an identifier, it will be added to the identity map. |
1123
|
|
|
* |
1124
|
|
|
* @param object $entity The entity to schedule for insertion. |
1125
|
|
|
* |
1126
|
|
|
* @throws ORMInvalidArgumentException |
1127
|
|
|
* @throws \InvalidArgumentException |
1128
|
|
|
*/ |
1129
|
1029 |
|
public function scheduleForInsert($entity) |
1130
|
|
|
{ |
1131
|
1029 |
|
$oid = spl_object_id($entity); |
|
|
|
|
1132
|
|
|
|
1133
|
1029 |
|
if (isset($this->entityUpdates[$oid])) { |
1134
|
|
|
throw new InvalidArgumentException('Dirty entity can not be scheduled for insertion.'); |
1135
|
|
|
} |
1136
|
|
|
|
1137
|
1029 |
|
if (isset($this->entityDeletions[$oid])) { |
1138
|
1 |
|
throw ORMInvalidArgumentException::scheduleInsertForRemovedEntity($entity); |
1139
|
|
|
} |
1140
|
1029 |
|
if (isset($this->originalEntityData[$oid]) && ! isset($this->entityInsertions[$oid])) { |
1141
|
1 |
|
throw ORMInvalidArgumentException::scheduleInsertForManagedEntity($entity); |
1142
|
|
|
} |
1143
|
|
|
|
1144
|
1029 |
|
if (isset($this->entityInsertions[$oid])) { |
1145
|
1 |
|
throw ORMInvalidArgumentException::scheduleInsertTwice($entity); |
1146
|
|
|
} |
1147
|
|
|
|
1148
|
1029 |
|
$this->entityInsertions[$oid] = $entity; |
1149
|
|
|
|
1150
|
1029 |
|
if (isset($this->entityIdentifiers[$oid])) { |
1151
|
269 |
|
$this->addToIdentityMap($entity); |
1152
|
|
|
} |
1153
|
|
|
|
1154
|
1029 |
|
if ($entity instanceof NotifyPropertyChanged) { |
1155
|
5 |
|
$entity->addPropertyChangedListener($this); |
1156
|
|
|
} |
1157
|
1029 |
|
} |
1158
|
|
|
|
1159
|
|
|
/** |
1160
|
|
|
* Checks whether an entity is scheduled for insertion. |
1161
|
|
|
* |
1162
|
|
|
* @param object $entity |
1163
|
|
|
* |
1164
|
|
|
* @return bool |
1165
|
|
|
*/ |
1166
|
624 |
|
public function isScheduledForInsert($entity) |
1167
|
|
|
{ |
1168
|
624 |
|
return isset($this->entityInsertions[spl_object_id($entity)]); |
|
|
|
|
1169
|
|
|
} |
1170
|
|
|
|
1171
|
|
|
/** |
1172
|
|
|
* Schedules an entity for being updated. |
1173
|
|
|
* |
1174
|
|
|
* @param object $entity The entity to schedule for being updated. |
1175
|
|
|
* |
1176
|
|
|
* @throws ORMInvalidArgumentException |
1177
|
|
|
*/ |
1178
|
1 |
|
public function scheduleForUpdate($entity) : void |
1179
|
|
|
{ |
1180
|
1 |
|
$oid = spl_object_id($entity); |
|
|
|
|
1181
|
|
|
|
1182
|
1 |
|
if (! isset($this->entityIdentifiers[$oid])) { |
1183
|
|
|
throw ORMInvalidArgumentException::entityHasNoIdentity($entity, 'scheduling for update'); |
1184
|
|
|
} |
1185
|
|
|
|
1186
|
1 |
|
if (isset($this->entityDeletions[$oid])) { |
1187
|
|
|
throw ORMInvalidArgumentException::entityIsRemoved($entity, 'schedule for update'); |
1188
|
|
|
} |
1189
|
|
|
|
1190
|
1 |
|
if (! isset($this->entityUpdates[$oid]) && ! isset($this->entityInsertions[$oid])) { |
1191
|
1 |
|
$this->entityUpdates[$oid] = $entity; |
1192
|
|
|
} |
1193
|
1 |
|
} |
1194
|
|
|
|
1195
|
|
|
/** |
1196
|
|
|
* INTERNAL: |
1197
|
|
|
* Schedules an extra update that will be executed immediately after the |
1198
|
|
|
* regular entity updates within the currently running commit cycle. |
1199
|
|
|
* |
1200
|
|
|
* Extra updates for entities are stored as (entity, changeset) tuples. |
1201
|
|
|
* |
1202
|
|
|
* @ignore |
1203
|
|
|
* |
1204
|
|
|
* @param object $entity The entity for which to schedule an extra update. |
1205
|
|
|
* @param mixed[] $changeset The changeset of the entity (what to update). |
1206
|
|
|
* |
1207
|
|
|
*/ |
1208
|
33 |
|
public function scheduleExtraUpdate($entity, array $changeset) : void |
1209
|
|
|
{ |
1210
|
33 |
|
$oid = spl_object_id($entity); |
|
|
|
|
1211
|
33 |
|
$extraUpdate = [$entity, $changeset]; |
1212
|
|
|
|
1213
|
33 |
|
if (isset($this->extraUpdates[$oid])) { |
1214
|
1 |
|
[$unused, $changeset2] = $this->extraUpdates[$oid]; |
1215
|
|
|
|
1216
|
1 |
|
$extraUpdate = [$entity, $changeset + $changeset2]; |
1217
|
|
|
} |
1218
|
|
|
|
1219
|
33 |
|
$this->extraUpdates[$oid] = $extraUpdate; |
1220
|
33 |
|
} |
1221
|
|
|
|
1222
|
|
|
/** |
1223
|
|
|
* Checks whether an entity is registered as dirty in the unit of work. |
1224
|
|
|
* Note: Is not very useful currently as dirty entities are only registered |
1225
|
|
|
* at commit time. |
1226
|
|
|
* |
1227
|
|
|
* @param object $entity |
1228
|
|
|
*/ |
1229
|
|
|
public function isScheduledForUpdate($entity) : bool |
1230
|
|
|
{ |
1231
|
|
|
return isset($this->entityUpdates[spl_object_id($entity)]); |
|
|
|
|
1232
|
|
|
} |
1233
|
|
|
|
1234
|
|
|
/** |
1235
|
|
|
* Checks whether an entity is registered to be checked in the unit of work. |
1236
|
|
|
* |
1237
|
|
|
* @param object $entity |
1238
|
|
|
*/ |
1239
|
1 |
|
public function isScheduledForDirtyCheck($entity) : bool |
1240
|
|
|
{ |
1241
|
1 |
|
$rootEntityName = $this->em->getClassMetadata(get_class($entity))->getRootClassName(); |
|
|
|
|
1242
|
|
|
|
1243
|
1 |
|
return isset($this->scheduledForSynchronization[$rootEntityName][spl_object_id($entity)]); |
|
|
|
|
1244
|
|
|
} |
1245
|
|
|
|
1246
|
|
|
/** |
1247
|
|
|
* INTERNAL: |
1248
|
|
|
* Schedules an entity for deletion. |
1249
|
|
|
* |
1250
|
|
|
* @param object $entity |
1251
|
|
|
*/ |
1252
|
63 |
|
public function scheduleForDelete($entity) |
1253
|
|
|
{ |
1254
|
63 |
|
$oid = spl_object_id($entity); |
|
|
|
|
1255
|
|
|
|
1256
|
63 |
|
if (isset($this->entityInsertions[$oid])) { |
1257
|
1 |
|
if ($this->isInIdentityMap($entity)) { |
1258
|
|
|
$this->removeFromIdentityMap($entity); |
1259
|
|
|
} |
1260
|
|
|
|
1261
|
1 |
|
unset($this->entityInsertions[$oid], $this->entityStates[$oid]); |
1262
|
|
|
|
1263
|
1 |
|
return; // entity has not been persisted yet, so nothing more to do. |
1264
|
|
|
} |
1265
|
|
|
|
1266
|
63 |
|
if (! $this->isInIdentityMap($entity)) { |
1267
|
1 |
|
return; |
1268
|
|
|
} |
1269
|
|
|
|
1270
|
62 |
|
$this->removeFromIdentityMap($entity); |
1271
|
|
|
|
1272
|
62 |
|
unset($this->entityUpdates[$oid]); |
1273
|
|
|
|
1274
|
62 |
|
if (! isset($this->entityDeletions[$oid])) { |
1275
|
62 |
|
$this->entityDeletions[$oid] = $entity; |
1276
|
62 |
|
$this->entityStates[$oid] = self::STATE_REMOVED; |
1277
|
|
|
} |
1278
|
62 |
|
} |
1279
|
|
|
|
1280
|
|
|
/** |
1281
|
|
|
* Checks whether an entity is registered as removed/deleted with the unit |
1282
|
|
|
* of work. |
1283
|
|
|
* |
1284
|
|
|
* @param object $entity |
1285
|
|
|
* |
1286
|
|
|
* @return bool |
1287
|
|
|
*/ |
1288
|
13 |
|
public function isScheduledForDelete($entity) |
1289
|
|
|
{ |
1290
|
13 |
|
return isset($this->entityDeletions[spl_object_id($entity)]); |
|
|
|
|
1291
|
|
|
} |
1292
|
|
|
|
1293
|
|
|
/** |
1294
|
|
|
* Checks whether an entity is scheduled for insertion, update or deletion. |
1295
|
|
|
* |
1296
|
|
|
* @param object $entity |
1297
|
|
|
* |
1298
|
|
|
* @return bool |
1299
|
|
|
*/ |
1300
|
|
|
public function isEntityScheduled($entity) |
1301
|
|
|
{ |
1302
|
|
|
$oid = spl_object_id($entity); |
|
|
|
|
1303
|
|
|
|
1304
|
|
|
return isset($this->entityInsertions[$oid]) |
1305
|
|
|
|| isset($this->entityUpdates[$oid]) |
1306
|
|
|
|| isset($this->entityDeletions[$oid]); |
1307
|
|
|
} |
1308
|
|
|
|
1309
|
|
|
/** |
1310
|
|
|
* INTERNAL: |
1311
|
|
|
* Registers an entity in the identity map. |
1312
|
|
|
* Note that entities in a hierarchy are registered with the class name of |
1313
|
|
|
* the root entity. |
1314
|
|
|
* |
1315
|
|
|
* @ignore |
1316
|
|
|
* |
1317
|
|
|
* @param object $entity The entity to register. |
1318
|
|
|
* |
1319
|
|
|
* @return bool TRUE if the registration was successful, FALSE if the identity of |
1320
|
|
|
* the entity in question is already managed. |
1321
|
|
|
* |
1322
|
|
|
* @throws ORMInvalidArgumentException |
1323
|
|
|
*/ |
1324
|
1097 |
|
public function addToIdentityMap($entity) |
1325
|
|
|
{ |
1326
|
1097 |
|
$classMetadata = $this->em->getClassMetadata(get_class($entity)); |
1327
|
1097 |
|
$identifier = $this->entityIdentifiers[spl_object_id($entity)]; |
|
|
|
|
1328
|
|
|
|
1329
|
1097 |
|
if (empty($identifier) || in_array(null, $identifier, true)) { |
1330
|
6 |
|
throw ORMInvalidArgumentException::entityWithoutIdentity($classMetadata->getClassName(), $entity); |
1331
|
|
|
} |
1332
|
|
|
|
1333
|
1091 |
|
$idHash = implode(' ', $identifier); |
1334
|
1091 |
|
$className = $classMetadata->getRootClassName(); |
1335
|
|
|
|
1336
|
1091 |
|
if (isset($this->identityMap[$className][$idHash])) { |
1337
|
32 |
|
return false; |
1338
|
|
|
} |
1339
|
|
|
|
1340
|
1091 |
|
$this->identityMap[$className][$idHash] = $entity; |
1341
|
|
|
|
1342
|
1091 |
|
return true; |
1343
|
|
|
} |
1344
|
|
|
|
1345
|
|
|
/** |
1346
|
|
|
* Gets the state of an entity with regard to the current unit of work. |
1347
|
|
|
* |
1348
|
|
|
* @param object $entity |
1349
|
|
|
* @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED). |
1350
|
|
|
* This parameter can be set to improve performance of entity state detection |
1351
|
|
|
* by potentially avoiding a database lookup if the distinction between NEW and DETACHED |
1352
|
|
|
* is either known or does not matter for the caller of the method. |
1353
|
|
|
* |
1354
|
|
|
* @return int The entity state. |
1355
|
|
|
*/ |
1356
|
1037 |
|
public function getEntityState($entity, $assume = null) |
1357
|
|
|
{ |
1358
|
1037 |
|
$oid = spl_object_id($entity); |
|
|
|
|
1359
|
|
|
|
1360
|
1037 |
|
if (isset($this->entityStates[$oid])) { |
1361
|
753 |
|
return $this->entityStates[$oid]; |
1362
|
|
|
} |
1363
|
|
|
|
1364
|
1032 |
|
if ($assume !== null) { |
1365
|
1029 |
|
return $assume; |
1366
|
|
|
} |
1367
|
|
|
|
1368
|
|
|
// State can only be NEW or DETACHED, because MANAGED/REMOVED states are known. |
1369
|
|
|
// Note that you can not remember the NEW or DETACHED state in _entityStates since |
1370
|
|
|
// the UoW does not hold references to such objects and the object hash can be reused. |
1371
|
|
|
// More generally because the state may "change" between NEW/DETACHED without the UoW being aware of it. |
1372
|
8 |
|
$class = $this->em->getClassMetadata(get_class($entity)); |
1373
|
8 |
|
$persister = $this->getEntityPersister($class->getClassName()); |
1374
|
8 |
|
$id = $persister->getIdentifier($entity); |
1375
|
|
|
|
1376
|
8 |
|
if (! $id) { |
|
|
|
|
1377
|
3 |
|
return self::STATE_NEW; |
1378
|
|
|
} |
1379
|
|
|
|
1380
|
6 |
|
$flatId = $this->em->getIdentifierFlattener()->flattenIdentifier($class, $id); |
1381
|
|
|
|
1382
|
6 |
|
if ($class->isIdentifierComposite() |
1383
|
5 |
|
|| ! $class->getProperty($class->getSingleIdentifierFieldName()) instanceof FieldMetadata |
1384
|
6 |
|
|| ! $class->getProperty($class->getSingleIdentifierFieldName())->hasValueGenerator() |
1385
|
|
|
) { |
1386
|
|
|
// Check for a version field, if available, to avoid a db lookup. |
1387
|
5 |
|
if ($class->isVersioned()) { |
1388
|
1 |
|
return $class->versionProperty->getValue($entity) |
1389
|
|
|
? self::STATE_DETACHED |
1390
|
1 |
|
: self::STATE_NEW; |
1391
|
|
|
} |
1392
|
|
|
|
1393
|
|
|
// Last try before db lookup: check the identity map. |
1394
|
4 |
|
if ($this->tryGetById($flatId, $class->getRootClassName())) { |
1395
|
1 |
|
return self::STATE_DETACHED; |
1396
|
|
|
} |
1397
|
|
|
|
1398
|
|
|
// db lookup |
1399
|
4 |
|
if ($this->getEntityPersister($class->getClassName())->exists($entity)) { |
1400
|
|
|
return self::STATE_DETACHED; |
1401
|
|
|
} |
1402
|
|
|
|
1403
|
4 |
|
return self::STATE_NEW; |
1404
|
|
|
} |
1405
|
|
|
|
1406
|
1 |
|
if ($class->isIdentifierComposite() |
1407
|
1 |
|
|| ! $class->getProperty($class->getSingleIdentifierFieldName()) instanceof FieldMetadata |
1408
|
1 |
|
|| ! $class->getValueGenerationPlan()->containsDeferred()) { |
1409
|
|
|
// if we have a pre insert generator we can't be sure that having an id |
1410
|
|
|
// really means that the entity exists. We have to verify this through |
1411
|
|
|
// the last resort: a db lookup |
1412
|
|
|
|
1413
|
|
|
// Last try before db lookup: check the identity map. |
1414
|
|
|
if ($this->tryGetById($flatId, $class->getRootClassName())) { |
1415
|
|
|
return self::STATE_DETACHED; |
1416
|
|
|
} |
1417
|
|
|
|
1418
|
|
|
// db lookup |
1419
|
|
|
if ($this->getEntityPersister($class->getClassName())->exists($entity)) { |
1420
|
|
|
return self::STATE_DETACHED; |
1421
|
|
|
} |
1422
|
|
|
|
1423
|
|
|
return self::STATE_NEW; |
1424
|
|
|
} |
1425
|
|
|
|
1426
|
1 |
|
return self::STATE_DETACHED; |
1427
|
|
|
} |
1428
|
|
|
|
1429
|
|
|
/** |
1430
|
|
|
* INTERNAL: |
1431
|
|
|
* Removes an entity from the identity map. This effectively detaches the |
1432
|
|
|
* entity from the persistence management of Doctrine. |
1433
|
|
|
* |
1434
|
|
|
* @ignore |
1435
|
|
|
* |
1436
|
|
|
* @param object $entity |
1437
|
|
|
* |
1438
|
|
|
* @return bool |
1439
|
|
|
* |
1440
|
|
|
* @throws ORMInvalidArgumentException |
1441
|
|
|
*/ |
1442
|
62 |
|
public function removeFromIdentityMap($entity) |
1443
|
|
|
{ |
1444
|
62 |
|
$oid = spl_object_id($entity); |
|
|
|
|
1445
|
62 |
|
$classMetadata = $this->em->getClassMetadata(get_class($entity)); |
1446
|
62 |
|
$idHash = implode(' ', $this->entityIdentifiers[$oid]); |
1447
|
|
|
|
1448
|
62 |
|
if ($idHash === '') { |
1449
|
|
|
throw ORMInvalidArgumentException::entityHasNoIdentity($entity, 'remove from identity map'); |
1450
|
|
|
} |
1451
|
|
|
|
1452
|
62 |
|
$className = $classMetadata->getRootClassName(); |
1453
|
|
|
|
1454
|
62 |
|
if (isset($this->identityMap[$className][$idHash])) { |
1455
|
62 |
|
unset($this->identityMap[$className][$idHash], $this->readOnlyObjects[$oid]); |
1456
|
|
|
|
1457
|
|
|
//$this->entityStates[$oid] = self::STATE_DETACHED; |
|
|
|
|
1458
|
|
|
|
1459
|
62 |
|
return true; |
1460
|
|
|
} |
1461
|
|
|
|
1462
|
|
|
return false; |
1463
|
|
|
} |
1464
|
|
|
|
1465
|
|
|
/** |
1466
|
|
|
* INTERNAL: |
1467
|
|
|
* Gets an entity in the identity map by its identifier hash. |
1468
|
|
|
* |
1469
|
|
|
* @ignore |
1470
|
|
|
* |
1471
|
|
|
* @param string $idHash |
1472
|
|
|
* @param string $rootClassName |
1473
|
|
|
* |
1474
|
|
|
* @return object |
1475
|
|
|
*/ |
1476
|
6 |
|
public function getByIdHash($idHash, $rootClassName) |
1477
|
|
|
{ |
1478
|
6 |
|
return $this->identityMap[$rootClassName][$idHash]; |
1479
|
|
|
} |
1480
|
|
|
|
1481
|
|
|
/** |
1482
|
|
|
* INTERNAL: |
1483
|
|
|
* Tries to get an entity by its identifier hash. If no entity is found for |
1484
|
|
|
* the given hash, FALSE is returned. |
1485
|
|
|
* |
1486
|
|
|
* @ignore |
1487
|
|
|
* |
1488
|
|
|
* @param mixed $idHash (must be possible to cast it to string) |
1489
|
|
|
* @param string $rootClassName |
1490
|
|
|
* |
1491
|
|
|
* @return object|bool The found entity or FALSE. |
1492
|
|
|
*/ |
1493
|
|
|
public function tryGetByIdHash($idHash, $rootClassName) |
1494
|
|
|
{ |
1495
|
|
|
$stringIdHash = (string) $idHash; |
1496
|
|
|
|
1497
|
|
|
return $this->identityMap[$rootClassName][$stringIdHash] ?? false; |
1498
|
|
|
} |
1499
|
|
|
|
1500
|
|
|
/** |
1501
|
|
|
* Checks whether an entity is registered in the identity map of this UnitOfWork. |
1502
|
|
|
* |
1503
|
|
|
* @param object $entity |
1504
|
|
|
* |
1505
|
|
|
* @return bool |
1506
|
|
|
*/ |
1507
|
144 |
|
public function isInIdentityMap($entity) |
1508
|
|
|
{ |
1509
|
144 |
|
$oid = spl_object_id($entity); |
|
|
|
|
1510
|
|
|
|
1511
|
144 |
|
if (empty($this->entityIdentifiers[$oid])) { |
1512
|
23 |
|
return false; |
1513
|
|
|
} |
1514
|
|
|
|
1515
|
130 |
|
$classMetadata = $this->em->getClassMetadata(get_class($entity)); |
1516
|
130 |
|
$idHash = implode(' ', $this->entityIdentifiers[$oid]); |
1517
|
|
|
|
1518
|
130 |
|
return isset($this->identityMap[$classMetadata->getRootClassName()][$idHash]); |
1519
|
|
|
} |
1520
|
|
|
|
1521
|
|
|
/** |
1522
|
|
|
* INTERNAL: |
1523
|
|
|
* Checks whether an identifier hash exists in the identity map. |
1524
|
|
|
* |
1525
|
|
|
* @ignore |
1526
|
|
|
* |
1527
|
|
|
* @param string $idHash |
1528
|
|
|
* @param string $rootClassName |
1529
|
|
|
* |
1530
|
|
|
* @return bool |
1531
|
|
|
*/ |
1532
|
|
|
public function containsIdHash($idHash, $rootClassName) |
1533
|
|
|
{ |
1534
|
|
|
return isset($this->identityMap[$rootClassName][$idHash]); |
1535
|
|
|
} |
1536
|
|
|
|
1537
|
|
|
/** |
1538
|
|
|
* Persists an entity as part of the current unit of work. |
1539
|
|
|
* |
1540
|
|
|
* @param object $entity The entity to persist. |
1541
|
|
|
*/ |
1542
|
1029 |
|
public function persist($entity) |
1543
|
|
|
{ |
1544
|
1029 |
|
$visited = []; |
1545
|
|
|
|
1546
|
1029 |
|
$this->doPersist($entity, $visited); |
1547
|
1022 |
|
} |
1548
|
|
|
|
1549
|
|
|
/** |
1550
|
|
|
* Persists an entity as part of the current unit of work. |
1551
|
|
|
* |
1552
|
|
|
* This method is internally called during persist() cascades as it tracks |
1553
|
|
|
* the already visited entities to prevent infinite recursions. |
1554
|
|
|
* |
1555
|
|
|
* @param object $entity The entity to persist. |
1556
|
|
|
* @param object[] $visited The already visited entities. |
1557
|
|
|
* |
1558
|
|
|
* @throws ORMInvalidArgumentException |
1559
|
|
|
* @throws UnexpectedValueException |
1560
|
|
|
*/ |
1561
|
1029 |
|
private function doPersist($entity, array &$visited) |
1562
|
|
|
{ |
1563
|
1029 |
|
$oid = spl_object_id($entity); |
|
|
|
|
1564
|
|
|
|
1565
|
1029 |
|
if (isset($visited[$oid])) { |
1566
|
109 |
|
return; // Prevent infinite recursion |
1567
|
|
|
} |
1568
|
|
|
|
1569
|
1029 |
|
$visited[$oid] = $entity; // Mark visited |
1570
|
|
|
|
1571
|
1029 |
|
$class = $this->em->getClassMetadata(get_class($entity)); |
1572
|
|
|
|
1573
|
|
|
// We assume NEW, so DETACHED entities result in an exception on flush (constraint violation). |
1574
|
|
|
// If we would detect DETACHED here we would throw an exception anyway with the same |
1575
|
|
|
// consequences (not recoverable/programming error), so just assuming NEW here |
1576
|
|
|
// lets us avoid some database lookups for entities with natural identifiers. |
1577
|
1029 |
|
$entityState = $this->getEntityState($entity, self::STATE_NEW); |
1578
|
|
|
|
1579
|
|
|
switch ($entityState) { |
1580
|
1029 |
|
case self::STATE_MANAGED: |
|
|
|
|
1581
|
|
|
// Nothing to do, except if policy is "deferred explicit" |
1582
|
220 |
|
if ($class->changeTrackingPolicy === ChangeTrackingPolicy::DEFERRED_EXPLICIT) { |
|
|
|
|
1583
|
2 |
|
$this->scheduleForSynchronization($entity); |
1584
|
|
|
} |
1585
|
220 |
|
break; |
1586
|
|
|
|
1587
|
1029 |
|
case self::STATE_NEW: |
|
|
|
|
1588
|
1028 |
|
$this->persistNew($class, $entity); |
|
|
|
|
1589
|
1028 |
|
break; |
1590
|
|
|
|
1591
|
1 |
|
case self::STATE_REMOVED: |
1592
|
|
|
// Entity becomes managed again |
1593
|
1 |
|
unset($this->entityDeletions[$oid]); |
1594
|
1 |
|
$this->addToIdentityMap($entity); |
1595
|
|
|
|
1596
|
1 |
|
$this->entityStates[$oid] = self::STATE_MANAGED; |
1597
|
1 |
|
break; |
1598
|
|
|
|
1599
|
|
|
case self::STATE_DETACHED: |
1600
|
|
|
// Can actually not happen right now since we assume STATE_NEW. |
1601
|
|
|
throw ORMInvalidArgumentException::detachedEntityCannot($entity, 'persisted'); |
1602
|
|
|
|
1603
|
|
|
default: |
1604
|
|
|
throw new UnexpectedValueException( |
1605
|
|
|
sprintf('Unexpected entity state: %d.%s', $entityState, self::objToStr($entity)) |
1606
|
|
|
); |
1607
|
|
|
} |
1608
|
|
|
|
1609
|
1029 |
|
$this->cascadePersist($entity, $visited); |
1610
|
1022 |
|
} |
1611
|
|
|
|
1612
|
|
|
/** |
1613
|
|
|
* Deletes an entity as part of the current unit of work. |
1614
|
|
|
* |
1615
|
|
|
* @param object $entity The entity to remove. |
1616
|
|
|
*/ |
1617
|
62 |
|
public function remove($entity) |
1618
|
|
|
{ |
1619
|
62 |
|
$visited = []; |
1620
|
|
|
|
1621
|
62 |
|
$this->doRemove($entity, $visited); |
1622
|
62 |
|
} |
1623
|
|
|
|
1624
|
|
|
/** |
1625
|
|
|
* Deletes an entity as part of the current unit of work. |
1626
|
|
|
* |
1627
|
|
|
* This method is internally called during delete() cascades as it tracks |
1628
|
|
|
* the already visited entities to prevent infinite recursions. |
1629
|
|
|
* |
1630
|
|
|
* @param object $entity The entity to delete. |
1631
|
|
|
* @param object[] $visited The map of the already visited entities. |
1632
|
|
|
* |
1633
|
|
|
* @throws ORMInvalidArgumentException If the instance is a detached entity. |
1634
|
|
|
* @throws UnexpectedValueException |
1635
|
|
|
*/ |
1636
|
62 |
|
private function doRemove($entity, array &$visited) |
1637
|
|
|
{ |
1638
|
62 |
|
$oid = spl_object_id($entity); |
|
|
|
|
1639
|
|
|
|
1640
|
62 |
|
if (isset($visited[$oid])) { |
1641
|
1 |
|
return; // Prevent infinite recursion |
1642
|
|
|
} |
1643
|
|
|
|
1644
|
62 |
|
$visited[$oid] = $entity; // mark visited |
1645
|
|
|
|
1646
|
|
|
// Cascade first, because scheduleForDelete() removes the entity from the identity map, which |
1647
|
|
|
// can cause problems when a lazy proxy has to be initialized for the cascade operation. |
1648
|
62 |
|
$this->cascadeRemove($entity, $visited); |
1649
|
|
|
|
1650
|
62 |
|
$class = $this->em->getClassMetadata(get_class($entity)); |
1651
|
62 |
|
$entityState = $this->getEntityState($entity); |
1652
|
|
|
|
1653
|
|
|
switch ($entityState) { |
1654
|
62 |
|
case self::STATE_NEW: |
1655
|
62 |
|
case self::STATE_REMOVED: |
1656
|
|
|
// nothing to do |
1657
|
2 |
|
break; |
1658
|
|
|
|
1659
|
62 |
|
case self::STATE_MANAGED: |
1660
|
62 |
|
$invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preRemove); |
|
|
|
|
1661
|
|
|
|
1662
|
62 |
|
if ($invoke !== ListenersInvoker::INVOKE_NONE) { |
1663
|
8 |
|
$this->listenersInvoker->invoke($class, Events::preRemove, $entity, new LifecycleEventArgs($entity, $this->em), $invoke); |
|
|
|
|
1664
|
|
|
} |
1665
|
|
|
|
1666
|
62 |
|
$this->scheduleForDelete($entity); |
1667
|
62 |
|
break; |
1668
|
|
|
|
1669
|
|
|
case self::STATE_DETACHED: |
1670
|
|
|
throw ORMInvalidArgumentException::detachedEntityCannot($entity, 'removed'); |
1671
|
|
|
default: |
1672
|
|
|
throw new UnexpectedValueException( |
1673
|
|
|
sprintf('Unexpected entity state: %d.%s', $entityState, self::objToStr($entity)) |
1674
|
|
|
); |
1675
|
|
|
} |
1676
|
62 |
|
} |
1677
|
|
|
|
1678
|
|
|
/** |
1679
|
|
|
* Refreshes the state of the given entity from the database, overwriting |
1680
|
|
|
* any local, unpersisted changes. |
1681
|
|
|
* |
1682
|
|
|
* @param object $entity The entity to refresh. |
1683
|
|
|
* |
1684
|
|
|
* @throws InvalidArgumentException If the entity is not MANAGED. |
1685
|
|
|
*/ |
1686
|
15 |
|
public function refresh($entity) |
1687
|
|
|
{ |
1688
|
15 |
|
$visited = []; |
1689
|
|
|
|
1690
|
15 |
|
$this->doRefresh($entity, $visited); |
1691
|
15 |
|
} |
1692
|
|
|
|
1693
|
|
|
/** |
1694
|
|
|
* Executes a refresh operation on an entity. |
1695
|
|
|
* |
1696
|
|
|
* @param object $entity The entity to refresh. |
1697
|
|
|
* @param object[] $visited The already visited entities during cascades. |
1698
|
|
|
* |
1699
|
|
|
* @throws ORMInvalidArgumentException If the entity is not MANAGED. |
1700
|
|
|
*/ |
1701
|
15 |
|
private function doRefresh($entity, array &$visited) |
1702
|
|
|
{ |
1703
|
15 |
|
$oid = spl_object_id($entity); |
|
|
|
|
1704
|
|
|
|
1705
|
15 |
|
if (isset($visited[$oid])) { |
1706
|
|
|
return; // Prevent infinite recursion |
1707
|
|
|
} |
1708
|
|
|
|
1709
|
15 |
|
$visited[$oid] = $entity; // mark visited |
1710
|
|
|
|
1711
|
15 |
|
$class = $this->em->getClassMetadata(get_class($entity)); |
1712
|
|
|
|
1713
|
15 |
|
if ($this->getEntityState($entity) !== self::STATE_MANAGED) { |
1714
|
|
|
throw ORMInvalidArgumentException::entityNotManaged($entity); |
1715
|
|
|
} |
1716
|
|
|
|
1717
|
15 |
|
$this->getEntityPersister($class->getClassName())->refresh( |
1718
|
15 |
|
array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]), |
1719
|
15 |
|
$entity |
1720
|
|
|
); |
1721
|
|
|
|
1722
|
15 |
|
$this->cascadeRefresh($entity, $visited); |
1723
|
15 |
|
} |
1724
|
|
|
|
1725
|
|
|
/** |
1726
|
|
|
* Cascades a refresh operation to associated entities. |
1727
|
|
|
* |
1728
|
|
|
* @param object $entity |
1729
|
|
|
* @param object[] $visited |
1730
|
|
|
*/ |
1731
|
15 |
|
private function cascadeRefresh($entity, array &$visited) |
1732
|
|
|
{ |
1733
|
15 |
|
$class = $this->em->getClassMetadata(get_class($entity)); |
1734
|
|
|
|
1735
|
15 |
|
foreach ($class->getDeclaredPropertiesIterator() as $association) { |
1736
|
15 |
|
if (! ($association instanceof AssociationMetadata && in_array('refresh', $association->getCascade()))) { |
1737
|
15 |
|
continue; |
1738
|
|
|
} |
1739
|
|
|
|
1740
|
4 |
|
$relatedEntities = $association->getValue($entity); |
1741
|
|
|
|
1742
|
|
|
switch (true) { |
1743
|
4 |
|
case ($relatedEntities instanceof PersistentCollection): |
1744
|
|
|
// Unwrap so that foreach() does not initialize |
1745
|
4 |
|
$relatedEntities = $relatedEntities->unwrap(); |
1746
|
|
|
// break; is commented intentionally! |
1747
|
|
|
|
1748
|
|
|
case ($relatedEntities instanceof Collection): |
1749
|
|
|
case (is_array($relatedEntities)): |
1750
|
4 |
|
foreach ($relatedEntities as $relatedEntity) { |
1751
|
|
|
$this->doRefresh($relatedEntity, $visited); |
1752
|
|
|
} |
1753
|
4 |
|
break; |
1754
|
|
|
|
1755
|
|
|
case ($relatedEntities !== null): |
1756
|
|
|
$this->doRefresh($relatedEntities, $visited); |
1757
|
|
|
break; |
1758
|
|
|
|
1759
|
4 |
|
default: |
1760
|
|
|
// Do nothing |
1761
|
|
|
} |
1762
|
|
|
} |
1763
|
15 |
|
} |
1764
|
|
|
|
1765
|
|
|
/** |
1766
|
|
|
* Cascades the save operation to associated entities. |
1767
|
|
|
* |
1768
|
|
|
* @param object $entity |
1769
|
|
|
* @param object[] $visited |
1770
|
|
|
* |
1771
|
|
|
* @throws ORMInvalidArgumentException |
1772
|
|
|
*/ |
1773
|
1029 |
|
private function cascadePersist($entity, array &$visited) |
1774
|
|
|
{ |
1775
|
1029 |
|
$class = $this->em->getClassMetadata(get_class($entity)); |
1776
|
|
|
|
1777
|
1029 |
|
if ($entity instanceof GhostObjectInterface && ! $entity->isProxyInitialized()) { |
1778
|
|
|
// nothing to do - proxy is not initialized, therefore we don't do anything with it |
1779
|
1 |
|
return; |
1780
|
|
|
} |
1781
|
|
|
|
1782
|
1029 |
|
foreach ($class->getDeclaredPropertiesIterator() as $association) { |
1783
|
1029 |
|
if (! ($association instanceof AssociationMetadata && in_array('persist', $association->getCascade()))) { |
1784
|
1029 |
|
continue; |
1785
|
|
|
} |
1786
|
|
|
|
1787
|
|
|
/** @var AssociationMetadata $association */ |
1788
|
651 |
|
$relatedEntities = $association->getValue($entity); |
1789
|
651 |
|
$targetEntity = $association->getTargetEntity(); |
1790
|
|
|
|
1791
|
|
|
switch (true) { |
1792
|
651 |
|
case ($relatedEntities instanceof PersistentCollection): |
1793
|
|
|
// Unwrap so that foreach() does not initialize |
1794
|
13 |
|
$relatedEntities = $relatedEntities->unwrap(); |
1795
|
|
|
// break; is commented intentionally! |
1796
|
|
|
|
1797
|
651 |
|
case ($relatedEntities instanceof Collection): |
1798
|
589 |
|
case (is_array($relatedEntities)): |
1799
|
547 |
|
if (! ($association instanceof ToManyAssociationMetadata)) { |
1800
|
3 |
|
throw ORMInvalidArgumentException::invalidAssociation( |
1801
|
3 |
|
$this->em->getClassMetadata($targetEntity), |
|
|
|
|
1802
|
3 |
|
$association, |
1803
|
3 |
|
$relatedEntities |
1804
|
|
|
); |
1805
|
|
|
} |
1806
|
|
|
|
1807
|
544 |
|
foreach ($relatedEntities as $relatedEntity) { |
1808
|
285 |
|
$this->doPersist($relatedEntity, $visited); |
1809
|
|
|
} |
1810
|
|
|
|
1811
|
544 |
|
break; |
1812
|
|
|
|
1813
|
579 |
|
case ($relatedEntities !== null): |
1814
|
247 |
|
if (! $relatedEntities instanceof $targetEntity) { |
1815
|
4 |
|
throw ORMInvalidArgumentException::invalidAssociation( |
1816
|
4 |
|
$this->em->getClassMetadata($targetEntity), |
1817
|
4 |
|
$association, |
1818
|
4 |
|
$relatedEntities |
1819
|
|
|
); |
1820
|
|
|
} |
1821
|
|
|
|
1822
|
243 |
|
$this->doPersist($relatedEntities, $visited); |
1823
|
243 |
|
break; |
1824
|
|
|
|
1825
|
645 |
|
default: |
1826
|
|
|
// Do nothing |
1827
|
|
|
} |
1828
|
|
|
} |
1829
|
1022 |
|
} |
1830
|
|
|
|
1831
|
|
|
/** |
1832
|
|
|
* Cascades the delete operation to associated entities. |
1833
|
|
|
* |
1834
|
|
|
* @param object $entity |
1835
|
|
|
* @param object[] $visited |
1836
|
|
|
*/ |
1837
|
62 |
|
private function cascadeRemove($entity, array &$visited) |
1838
|
|
|
{ |
1839
|
62 |
|
$entitiesToCascade = []; |
1840
|
62 |
|
$class = $this->em->getClassMetadata(get_class($entity)); |
1841
|
|
|
|
1842
|
62 |
|
foreach ($class->getDeclaredPropertiesIterator() as $association) { |
1843
|
62 |
|
if (! ($association instanceof AssociationMetadata && in_array('remove', $association->getCascade()))) { |
1844
|
62 |
|
continue; |
1845
|
|
|
} |
1846
|
|
|
|
1847
|
25 |
|
if ($entity instanceof GhostObjectInterface && ! $entity->isProxyInitialized()) { |
1848
|
6 |
|
$entity->initializeProxy(); |
1849
|
|
|
} |
1850
|
|
|
|
1851
|
25 |
|
$relatedEntities = $association->getValue($entity); |
1852
|
|
|
|
1853
|
|
|
switch (true) { |
1854
|
25 |
|
case ($relatedEntities instanceof Collection): |
1855
|
18 |
|
case (\is_array($relatedEntities)): |
1856
|
|
|
// If its a PersistentCollection initialization is intended! No unwrap! |
1857
|
20 |
|
foreach ($relatedEntities as $relatedEntity) { |
1858
|
10 |
|
$entitiesToCascade[] = $relatedEntity; |
1859
|
|
|
} |
1860
|
20 |
|
break; |
1861
|
|
|
|
1862
|
18 |
|
case ($relatedEntities !== null): |
1863
|
7 |
|
$entitiesToCascade[] = $relatedEntities; |
1864
|
7 |
|
break; |
1865
|
|
|
|
1866
|
25 |
|
default: |
1867
|
|
|
// Do nothing |
1868
|
|
|
} |
1869
|
|
|
} |
1870
|
|
|
|
1871
|
62 |
|
foreach ($entitiesToCascade as $relatedEntity) { |
1872
|
16 |
|
$this->doRemove($relatedEntity, $visited); |
1873
|
|
|
} |
1874
|
62 |
|
} |
1875
|
|
|
|
1876
|
|
|
/** |
1877
|
|
|
* Acquire a lock on the given entity. |
1878
|
|
|
* |
1879
|
|
|
* @param object $entity |
1880
|
|
|
* @param int $lockMode |
1881
|
|
|
* @param int $lockVersion |
1882
|
|
|
* |
1883
|
|
|
* @throws ORMInvalidArgumentException |
1884
|
|
|
* @throws TransactionRequiredException |
1885
|
|
|
* @throws OptimisticLockException |
1886
|
|
|
* @throws \InvalidArgumentException |
1887
|
|
|
*/ |
1888
|
10 |
|
public function lock($entity, $lockMode, $lockVersion = null) |
1889
|
|
|
{ |
1890
|
10 |
|
if ($entity === null) { |
1891
|
1 |
|
throw new \InvalidArgumentException('No entity passed to UnitOfWork#lock().'); |
1892
|
|
|
} |
1893
|
|
|
|
1894
|
9 |
|
if ($this->getEntityState($entity, self::STATE_DETACHED) !== self::STATE_MANAGED) { |
1895
|
1 |
|
throw ORMInvalidArgumentException::entityNotManaged($entity); |
1896
|
|
|
} |
1897
|
|
|
|
1898
|
8 |
|
$class = $this->em->getClassMetadata(get_class($entity)); |
1899
|
|
|
|
1900
|
|
|
switch (true) { |
1901
|
8 |
|
case $lockMode === LockMode::OPTIMISTIC: |
1902
|
6 |
|
if (! $class->isVersioned()) { |
1903
|
2 |
|
throw OptimisticLockException::notVersioned($class->getClassName()); |
1904
|
|
|
} |
1905
|
|
|
|
1906
|
4 |
|
if ($lockVersion === null) { |
1907
|
|
|
return; |
1908
|
|
|
} |
1909
|
|
|
|
1910
|
4 |
|
if ($entity instanceof GhostObjectInterface && ! $entity->isProxyInitialized()) { |
1911
|
1 |
|
$entity->initializeProxy(); |
1912
|
|
|
} |
1913
|
|
|
|
1914
|
4 |
|
$entityVersion = $class->versionProperty->getValue($entity); |
|
|
|
|
1915
|
|
|
|
1916
|
4 |
|
if ($entityVersion !== $lockVersion) { |
1917
|
2 |
|
throw OptimisticLockException::lockFailedVersionMismatch($entity, $lockVersion, $entityVersion); |
1918
|
|
|
} |
1919
|
|
|
|
1920
|
2 |
|
break; |
1921
|
|
|
|
1922
|
2 |
|
case $lockMode === LockMode::NONE: |
1923
|
2 |
|
case $lockMode === LockMode::PESSIMISTIC_READ: |
1924
|
1 |
|
case $lockMode === LockMode::PESSIMISTIC_WRITE: |
1925
|
2 |
|
if (! $this->em->getConnection()->isTransactionActive()) { |
1926
|
2 |
|
throw TransactionRequiredException::transactionRequired(); |
1927
|
|
|
} |
1928
|
|
|
|
1929
|
|
|
$oid = spl_object_id($entity); |
|
|
|
|
1930
|
|
|
|
1931
|
|
|
$this->getEntityPersister($class->getClassName())->lock( |
1932
|
|
|
array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]), |
1933
|
|
|
$lockMode |
1934
|
|
|
); |
1935
|
|
|
break; |
1936
|
|
|
|
1937
|
|
|
default: |
1938
|
|
|
// Do nothing |
1939
|
|
|
} |
1940
|
2 |
|
} |
1941
|
|
|
|
1942
|
|
|
/** |
1943
|
|
|
* Clears the UnitOfWork. |
1944
|
|
|
*/ |
1945
|
1213 |
|
public function clear() |
1946
|
|
|
{ |
1947
|
1213 |
|
$this->entityPersisters = |
1948
|
1213 |
|
$this->collectionPersisters = |
1949
|
1213 |
|
$this->eagerLoadingEntities = |
1950
|
1213 |
|
$this->identityMap = |
1951
|
1213 |
|
$this->entityIdentifiers = |
1952
|
1213 |
|
$this->originalEntityData = |
1953
|
1213 |
|
$this->entityChangeSets = |
1954
|
1213 |
|
$this->entityStates = |
1955
|
1213 |
|
$this->scheduledForSynchronization = |
1956
|
1213 |
|
$this->entityInsertions = |
1957
|
1213 |
|
$this->entityUpdates = |
1958
|
1213 |
|
$this->entityDeletions = |
1959
|
1213 |
|
$this->collectionDeletions = |
1960
|
1213 |
|
$this->collectionUpdates = |
1961
|
1213 |
|
$this->extraUpdates = |
1962
|
1213 |
|
$this->readOnlyObjects = |
1963
|
1213 |
|
$this->visitedCollections = |
1964
|
1213 |
|
$this->nonCascadedNewDetectedEntities = |
1965
|
1213 |
|
$this->orphanRemovals = []; |
1966
|
1213 |
|
} |
1967
|
|
|
|
1968
|
|
|
/** |
1969
|
|
|
* INTERNAL: |
1970
|
|
|
* Schedules an orphaned entity for removal. The remove() operation will be |
1971
|
|
|
* invoked on that entity at the beginning of the next commit of this |
1972
|
|
|
* UnitOfWork. |
1973
|
|
|
* |
1974
|
|
|
* @ignore |
1975
|
|
|
* |
1976
|
|
|
* @param object $entity |
1977
|
|
|
*/ |
1978
|
16 |
|
public function scheduleOrphanRemoval($entity) |
1979
|
|
|
{ |
1980
|
16 |
|
$this->orphanRemovals[spl_object_id($entity)] = $entity; |
|
|
|
|
1981
|
16 |
|
} |
1982
|
|
|
|
1983
|
|
|
/** |
1984
|
|
|
* INTERNAL: |
1985
|
|
|
* Cancels a previously scheduled orphan removal. |
1986
|
|
|
* |
1987
|
|
|
* @ignore |
1988
|
|
|
* |
1989
|
|
|
* @param object $entity |
1990
|
|
|
*/ |
1991
|
111 |
|
public function cancelOrphanRemoval($entity) |
1992
|
|
|
{ |
1993
|
111 |
|
unset($this->orphanRemovals[spl_object_id($entity)]); |
|
|
|
|
1994
|
111 |
|
} |
1995
|
|
|
|
1996
|
|
|
/** |
1997
|
|
|
* INTERNAL: |
1998
|
|
|
* Schedules a complete collection for removal when this UnitOfWork commits. |
1999
|
|
|
*/ |
2000
|
22 |
|
public function scheduleCollectionDeletion(PersistentCollection $coll) |
2001
|
|
|
{ |
2002
|
22 |
|
$coid = spl_object_id($coll); |
|
|
|
|
2003
|
|
|
|
2004
|
|
|
// TODO: if $coll is already scheduled for recreation ... what to do? |
2005
|
|
|
// Just remove $coll from the scheduled recreations? |
2006
|
22 |
|
unset($this->collectionUpdates[$coid]); |
2007
|
|
|
|
2008
|
22 |
|
$this->collectionDeletions[$coid] = $coll; |
2009
|
22 |
|
} |
2010
|
|
|
|
2011
|
|
|
/** |
2012
|
|
|
* @return bool |
2013
|
|
|
*/ |
2014
|
8 |
|
public function isCollectionScheduledForDeletion(PersistentCollection $coll) |
2015
|
|
|
{ |
2016
|
8 |
|
return isset($this->collectionDeletions[spl_object_id($coll)]); |
|
|
|
|
2017
|
|
|
} |
2018
|
|
|
|
2019
|
|
|
/** |
2020
|
|
|
* INTERNAL: |
2021
|
|
|
* Creates a new instance of the mapped class, without invoking the constructor. |
2022
|
|
|
* This is only meant to be used internally, and should not be consumed by end users. |
2023
|
|
|
* |
2024
|
|
|
* @ignore |
2025
|
|
|
* |
2026
|
|
|
* @return EntityManagerAware|object |
2027
|
|
|
*/ |
2028
|
669 |
|
public function newInstance(ClassMetadata $class) |
2029
|
|
|
{ |
2030
|
669 |
|
$entity = $this->instantiator->instantiate($class->getClassName()); |
2031
|
|
|
|
2032
|
669 |
|
if ($entity instanceof EntityManagerAware) { |
2033
|
5 |
|
$entity->injectEntityManager($this->em, $class); |
2034
|
|
|
} |
2035
|
|
|
|
2036
|
669 |
|
return $entity; |
2037
|
|
|
} |
2038
|
|
|
|
2039
|
|
|
/** |
2040
|
|
|
* INTERNAL: |
2041
|
|
|
* Creates an entity. Used for reconstitution of persistent entities. |
2042
|
|
|
* |
2043
|
|
|
* Internal note: Highly performance-sensitive method. |
2044
|
|
|
* |
2045
|
|
|
* @ignore |
2046
|
|
|
* |
2047
|
|
|
* @param string $className The name of the entity class. |
2048
|
|
|
* @param mixed[] $data The data for the entity. |
2049
|
|
|
* @param mixed[] $hints Any hints to account for during reconstitution/lookup of the entity. |
2050
|
|
|
* |
2051
|
|
|
* @return object The managed entity instance. |
2052
|
|
|
* |
2053
|
|
|
* @todo Rename: getOrCreateEntity |
2054
|
|
|
*/ |
2055
|
807 |
|
public function createEntity($className, array $data, &$hints = []) |
2056
|
|
|
{ |
2057
|
807 |
|
$class = $this->em->getClassMetadata($className); |
2058
|
807 |
|
$id = $this->em->getIdentifierFlattener()->flattenIdentifier($class, $data); |
|
|
|
|
2059
|
807 |
|
$idHash = implode(' ', $id); |
2060
|
|
|
|
2061
|
807 |
|
if (isset($this->identityMap[$class->getRootClassName()][$idHash])) { |
2062
|
307 |
|
$entity = $this->identityMap[$class->getRootClassName()][$idHash]; |
2063
|
307 |
|
$oid = spl_object_id($entity); |
|
|
|
|
2064
|
|
|
|
2065
|
307 |
|
if (isset($hints[Query::HINT_REFRESH], $hints[Query::HINT_REFRESH_ENTITY])) { |
2066
|
66 |
|
$unmanagedProxy = $hints[Query::HINT_REFRESH_ENTITY]; |
2067
|
66 |
|
if ($unmanagedProxy !== $entity |
2068
|
66 |
|
&& $unmanagedProxy instanceof GhostObjectInterface |
2069
|
66 |
|
&& $this->isIdentifierEquals($unmanagedProxy, $entity) |
2070
|
|
|
) { |
2071
|
|
|
// We will hydrate the given un-managed proxy anyway: |
2072
|
|
|
// continue work, but consider it the entity from now on |
2073
|
5 |
|
$entity = $unmanagedProxy; |
2074
|
|
|
} |
2075
|
|
|
} |
2076
|
|
|
|
2077
|
307 |
|
if ($entity instanceof GhostObjectInterface && ! $entity->isProxyInitialized()) { |
2078
|
21 |
|
$entity->setProxyInitializer(null); |
2079
|
|
|
|
2080
|
21 |
|
if ($entity instanceof NotifyPropertyChanged) { |
2081
|
21 |
|
$entity->addPropertyChangedListener($this); |
2082
|
|
|
} |
2083
|
|
|
} else { |
2084
|
293 |
|
if (! isset($hints[Query::HINT_REFRESH]) |
2085
|
293 |
|
|| (isset($hints[Query::HINT_REFRESH_ENTITY]) && $hints[Query::HINT_REFRESH_ENTITY] !== $entity)) { |
2086
|
230 |
|
return $entity; |
2087
|
|
|
} |
2088
|
|
|
} |
2089
|
|
|
|
2090
|
|
|
// inject EntityManager upon refresh. |
2091
|
104 |
|
if ($entity instanceof EntityManagerAware) { |
2092
|
3 |
|
$entity->injectEntityManager($this->em, $class); |
|
|
|
|
2093
|
|
|
} |
2094
|
|
|
|
2095
|
104 |
|
$this->originalEntityData[$oid] = $data; |
2096
|
|
|
} else { |
2097
|
666 |
|
$entity = $this->newInstance($class); |
|
|
|
|
2098
|
666 |
|
$oid = spl_object_id($entity); |
2099
|
|
|
|
2100
|
666 |
|
$this->entityIdentifiers[$oid] = $id; |
2101
|
666 |
|
$this->entityStates[$oid] = self::STATE_MANAGED; |
2102
|
666 |
|
$this->originalEntityData[$oid] = $data; |
2103
|
|
|
|
2104
|
666 |
|
$this->identityMap[$class->getRootClassName()][$idHash] = $entity; |
2105
|
|
|
} |
2106
|
|
|
|
2107
|
699 |
|
if ($entity instanceof NotifyPropertyChanged) { |
2108
|
3 |
|
$entity->addPropertyChangedListener($this); |
2109
|
|
|
} |
2110
|
|
|
|
2111
|
699 |
|
foreach ($data as $field => $value) { |
2112
|
699 |
|
$property = $class->getProperty($field); |
2113
|
|
|
|
2114
|
699 |
|
if ($property instanceof FieldMetadata) { |
2115
|
699 |
|
$property->setValue($entity, $value); |
2116
|
|
|
} |
2117
|
|
|
} |
2118
|
|
|
|
2119
|
|
|
// Loading the entity right here, if its in the eager loading map get rid of it there. |
2120
|
699 |
|
unset($this->eagerLoadingEntities[$class->getRootClassName()][$idHash]); |
2121
|
|
|
|
2122
|
699 |
|
if (isset($this->eagerLoadingEntities[$class->getRootClassName()]) && ! $this->eagerLoadingEntities[$class->getRootClassName()]) { |
2123
|
|
|
unset($this->eagerLoadingEntities[$class->getRootClassName()]); |
2124
|
|
|
} |
2125
|
|
|
|
2126
|
|
|
// Properly initialize any unfetched associations, if partial objects are not allowed. |
2127
|
699 |
|
if (isset($hints[Query::HINT_FORCE_PARTIAL_LOAD])) { |
2128
|
34 |
|
return $entity; |
2129
|
|
|
} |
2130
|
|
|
|
2131
|
665 |
|
foreach ($class->getDeclaredPropertiesIterator() as $field => $association) { |
2132
|
665 |
|
if (! ($association instanceof AssociationMetadata)) { |
2133
|
665 |
|
continue; |
2134
|
|
|
} |
2135
|
|
|
|
2136
|
|
|
// Check if the association is not among the fetch-joined associations already. |
2137
|
574 |
|
if (isset($hints['fetchAlias'], $hints['fetched'][$hints['fetchAlias']][$field])) { |
2138
|
247 |
|
continue; |
2139
|
|
|
} |
2140
|
|
|
|
2141
|
551 |
|
$targetEntity = $association->getTargetEntity(); |
2142
|
551 |
|
$targetClass = $this->em->getClassMetadata($targetEntity); |
2143
|
|
|
|
2144
|
551 |
|
if ($association instanceof ToManyAssociationMetadata) { |
2145
|
|
|
// Ignore if its a cached collection |
2146
|
471 |
|
if (isset($hints[Query::HINT_CACHE_ENABLED]) && |
2147
|
471 |
|
$association->getValue($entity) instanceof PersistentCollection) { |
2148
|
|
|
continue; |
2149
|
|
|
} |
2150
|
|
|
|
2151
|
471 |
|
$hasDataField = isset($data[$field]); |
2152
|
|
|
|
2153
|
|
|
// use the given collection |
2154
|
471 |
|
if ($hasDataField && $data[$field] instanceof PersistentCollection) { |
2155
|
|
|
$data[$field]->setOwner($entity, $association); |
2156
|
|
|
|
2157
|
|
|
$association->setValue($entity, $data[$field]); |
2158
|
|
|
|
2159
|
|
|
$this->originalEntityData[$oid][$field] = $data[$field]; |
2160
|
|
|
|
2161
|
|
|
continue; |
2162
|
|
|
} |
2163
|
|
|
|
2164
|
|
|
// Inject collection |
2165
|
471 |
|
$pColl = $association->wrap($entity, $hasDataField ? $data[$field] : [], $this->em); |
2166
|
|
|
|
2167
|
471 |
|
$pColl->setInitialized($hasDataField); |
2168
|
|
|
|
2169
|
471 |
|
$association->setValue($entity, $pColl); |
2170
|
|
|
|
2171
|
471 |
|
if ($association->getFetchMode() === FetchMode::EAGER) { |
2172
|
4 |
|
$this->loadCollection($pColl); |
2173
|
4 |
|
$pColl->takeSnapshot(); |
2174
|
|
|
} |
2175
|
|
|
|
2176
|
471 |
|
$this->originalEntityData[$oid][$field] = $pColl; |
2177
|
|
|
|
2178
|
471 |
|
continue; |
2179
|
|
|
} |
2180
|
|
|
|
2181
|
477 |
|
if (! $association->isOwningSide()) { |
2182
|
|
|
// use the given entity association |
2183
|
65 |
|
if (isset($data[$field]) && is_object($data[$field]) && |
2184
|
65 |
|
isset($this->entityStates[spl_object_id($data[$field])])) { |
2185
|
3 |
|
$inverseAssociation = $targetClass->getProperty($association->getMappedBy()); |
2186
|
|
|
|
2187
|
3 |
|
$association->setValue($entity, $data[$field]); |
2188
|
3 |
|
$inverseAssociation->setValue($data[$field], $entity); |
2189
|
|
|
|
2190
|
3 |
|
$this->originalEntityData[$oid][$field] = $data[$field]; |
2191
|
|
|
|
2192
|
3 |
|
continue; |
2193
|
|
|
} |
2194
|
|
|
|
2195
|
|
|
// Inverse side of x-to-one can never be lazy |
2196
|
62 |
|
$persister = $this->getEntityPersister($targetEntity); |
2197
|
|
|
|
2198
|
62 |
|
$association->setValue($entity, $persister->loadToOneEntity($association, $entity)); |
2199
|
|
|
|
2200
|
62 |
|
continue; |
2201
|
|
|
} |
2202
|
|
|
|
2203
|
|
|
// use the entity association |
2204
|
477 |
|
if (isset($data[$field]) && is_object($data[$field]) && isset($this->entityStates[spl_object_id($data[$field])])) { |
2205
|
38 |
|
$association->setValue($entity, $data[$field]); |
2206
|
|
|
|
2207
|
38 |
|
$this->originalEntityData[$oid][$field] = $data[$field]; |
2208
|
|
|
|
2209
|
38 |
|
continue; |
2210
|
|
|
} |
2211
|
|
|
|
2212
|
470 |
|
$associatedId = []; |
2213
|
|
|
|
2214
|
|
|
// TODO: Is this even computed right in all cases of composite keys? |
2215
|
470 |
|
foreach ($association->getJoinColumns() as $joinColumn) { |
|
|
|
|
2216
|
|
|
/** @var JoinColumnMetadata $joinColumn */ |
2217
|
470 |
|
$joinColumnName = $joinColumn->getColumnName(); |
2218
|
470 |
|
$joinColumnValue = $data[$joinColumnName] ?? null; |
2219
|
470 |
|
$targetField = $targetClass->fieldNames[$joinColumn->getReferencedColumnName()]; |
|
|
|
|
2220
|
|
|
|
2221
|
470 |
|
if ($joinColumnValue === null && in_array($targetField, $targetClass->identifier, true)) { |
|
|
|
|
2222
|
|
|
// the missing key is part of target's entity primary key |
2223
|
275 |
|
$associatedId = []; |
2224
|
|
|
|
2225
|
275 |
|
continue; |
2226
|
|
|
} |
2227
|
|
|
|
2228
|
284 |
|
$associatedId[$targetField] = $joinColumnValue; |
2229
|
|
|
} |
2230
|
|
|
|
2231
|
470 |
|
if (! $associatedId) { |
2232
|
|
|
// Foreign key is NULL |
2233
|
275 |
|
$association->setValue($entity, null); |
2234
|
275 |
|
$this->originalEntityData[$oid][$field] = null; |
2235
|
|
|
|
2236
|
275 |
|
continue; |
2237
|
|
|
} |
2238
|
|
|
|
2239
|
|
|
// @todo guilhermeblanco Can we remove the need of this somehow? |
2240
|
284 |
|
if (! isset($hints['fetchMode'][$class->getClassName()][$field])) { |
2241
|
281 |
|
$hints['fetchMode'][$class->getClassName()][$field] = $association->getFetchMode(); |
2242
|
|
|
} |
2243
|
|
|
|
2244
|
|
|
// Foreign key is set |
2245
|
|
|
// Check identity map first |
2246
|
|
|
// FIXME: Can break easily with composite keys if join column values are in |
2247
|
|
|
// wrong order. The correct order is the one in ClassMetadata#identifier. |
2248
|
284 |
|
$relatedIdHash = implode(' ', $associatedId); |
2249
|
|
|
|
2250
|
|
|
switch (true) { |
2251
|
284 |
|
case (isset($this->identityMap[$targetClass->getRootClassName()][$relatedIdHash])): |
|
|
|
|
2252
|
166 |
|
$newValue = $this->identityMap[$targetClass->getRootClassName()][$relatedIdHash]; |
2253
|
|
|
|
2254
|
|
|
// If this is an uninitialized proxy, we are deferring eager loads, |
2255
|
|
|
// this association is marked as eager fetch, and its an uninitialized proxy (wtf!) |
2256
|
|
|
// then we can append this entity for eager loading! |
2257
|
166 |
|
if (! $targetClass->isIdentifierComposite() && |
2258
|
166 |
|
$newValue instanceof GhostObjectInterface && |
2259
|
166 |
|
isset($hints[self::HINT_DEFEREAGERLOAD]) && |
2260
|
166 |
|
$hints['fetchMode'][$class->getClassName()][$field] === FetchMode::EAGER && |
2261
|
166 |
|
! $newValue->isProxyInitialized() |
2262
|
|
|
) { |
2263
|
|
|
$this->eagerLoadingEntities[$targetClass->getRootClassName()][$relatedIdHash] = current($associatedId); |
2264
|
|
|
} |
2265
|
|
|
|
2266
|
166 |
|
break; |
2267
|
|
|
|
2268
|
192 |
|
case ($targetClass->getSubClasses()): |
|
|
|
|
2269
|
|
|
// If it might be a subtype, it can not be lazy. There isn't even |
2270
|
|
|
// a way to solve this with deferred eager loading, which means putting |
2271
|
|
|
// an entity with subclasses at a *-to-one location is really bad! (performance-wise) |
2272
|
30 |
|
$persister = $this->getEntityPersister($targetEntity); |
2273
|
30 |
|
$newValue = $persister->loadToOneEntity($association, $entity, $associatedId); |
2274
|
30 |
|
break; |
2275
|
|
|
|
2276
|
|
|
default: |
2277
|
|
|
// Proxies do not carry any kind of original entity data until they're fully loaded/initialized |
2278
|
164 |
|
$managedData = []; |
2279
|
|
|
|
2280
|
164 |
|
$normalizedAssociatedId = $this->normalizeIdentifier->__invoke( |
2281
|
164 |
|
$this->em, |
2282
|
164 |
|
$targetClass, |
2283
|
164 |
|
$associatedId |
2284
|
|
|
); |
2285
|
|
|
|
2286
|
|
|
switch (true) { |
2287
|
|
|
// We are negating the condition here. Other cases will assume it is valid! |
2288
|
164 |
|
case ($hints['fetchMode'][$class->getClassName()][$field] !== FetchMode::EAGER): |
2289
|
157 |
|
$newValue = $this->em->getProxyFactory()->getProxy($targetClass, $normalizedAssociatedId); |
2290
|
157 |
|
break; |
2291
|
|
|
|
2292
|
|
|
// Deferred eager load only works for single identifier classes |
2293
|
7 |
|
case (isset($hints[self::HINT_DEFEREAGERLOAD]) && ! $targetClass->isIdentifierComposite()): |
2294
|
|
|
// TODO: Is there a faster approach? |
2295
|
7 |
|
$this->eagerLoadingEntities[$targetClass->getRootClassName()][$relatedIdHash] = current($normalizedAssociatedId); |
2296
|
|
|
|
2297
|
7 |
|
$newValue = $this->em->getProxyFactory()->getProxy($targetClass, $normalizedAssociatedId); |
2298
|
7 |
|
break; |
2299
|
|
|
|
2300
|
|
|
default: |
2301
|
|
|
// TODO: This is very imperformant, ignore it? |
2302
|
|
|
$newValue = $this->em->find($targetEntity, $normalizedAssociatedId); |
2303
|
|
|
// Needed to re-assign original entity data for freshly loaded entity |
2304
|
|
|
$managedData = $this->originalEntityData[spl_object_id($newValue)]; |
2305
|
|
|
break; |
2306
|
|
|
} |
2307
|
|
|
|
2308
|
|
|
// @TODO using `$associatedId` here seems to be risky. |
2309
|
164 |
|
$this->registerManaged($newValue, $associatedId, $managedData); |
2310
|
|
|
|
2311
|
164 |
|
break; |
2312
|
|
|
} |
2313
|
|
|
|
2314
|
284 |
|
$this->originalEntityData[$oid][$field] = $newValue; |
2315
|
284 |
|
$association->setValue($entity, $newValue); |
2316
|
|
|
|
2317
|
284 |
|
if ($association->getInversedBy() |
2318
|
284 |
|
&& $association instanceof OneToOneAssociationMetadata |
2319
|
|
|
// @TODO refactor this |
2320
|
|
|
// we don't want to set any values in un-initialized proxies |
2321
|
|
|
&& ! ( |
2322
|
56 |
|
$newValue instanceof GhostObjectInterface |
2323
|
284 |
|
&& ! $newValue->isProxyInitialized() |
2324
|
|
|
) |
2325
|
|
|
) { |
2326
|
19 |
|
$inverseAssociation = $targetClass->getProperty($association->getInversedBy()); |
2327
|
|
|
|
2328
|
284 |
|
$inverseAssociation->setValue($newValue, $entity); |
2329
|
|
|
} |
2330
|
|
|
} |
2331
|
|
|
|
2332
|
|
|
// defer invoking of postLoad event to hydration complete step |
2333
|
665 |
|
$this->hydrationCompleteHandler->deferPostLoadInvoking($class, $entity); |
|
|
|
|
2334
|
|
|
|
2335
|
665 |
|
return $entity; |
2336
|
|
|
} |
2337
|
|
|
|
2338
|
868 |
|
public function triggerEagerLoads() |
2339
|
|
|
{ |
2340
|
868 |
|
if (! $this->eagerLoadingEntities) { |
|
|
|
|
2341
|
868 |
|
return; |
2342
|
|
|
} |
2343
|
|
|
|
2344
|
|
|
// avoid infinite recursion |
2345
|
7 |
|
$eagerLoadingEntities = $this->eagerLoadingEntities; |
2346
|
7 |
|
$this->eagerLoadingEntities = []; |
2347
|
|
|
|
2348
|
7 |
|
foreach ($eagerLoadingEntities as $entityName => $ids) { |
2349
|
7 |
|
if (! $ids) { |
|
|
|
|
2350
|
|
|
continue; |
2351
|
|
|
} |
2352
|
|
|
|
2353
|
7 |
|
$class = $this->em->getClassMetadata($entityName); |
2354
|
|
|
|
2355
|
7 |
|
$this->getEntityPersister($entityName)->loadAll( |
2356
|
7 |
|
array_combine($class->identifier, [array_values($ids)]) |
2357
|
|
|
); |
2358
|
|
|
} |
2359
|
7 |
|
} |
2360
|
|
|
|
2361
|
|
|
/** |
2362
|
|
|
* Initializes (loads) an uninitialized persistent collection of an entity. |
2363
|
|
|
* |
2364
|
|
|
* @param \Doctrine\ORM\PersistentCollection $collection The collection to initialize. |
2365
|
|
|
* |
2366
|
|
|
* @todo Maybe later move to EntityManager#initialize($proxyOrCollection). See DDC-733. |
2367
|
|
|
*/ |
2368
|
138 |
|
public function loadCollection(PersistentCollection $collection) |
2369
|
|
|
{ |
2370
|
138 |
|
$association = $collection->getMapping(); |
2371
|
138 |
|
$persister = $this->getEntityPersister($association->getTargetEntity()); |
2372
|
|
|
|
2373
|
138 |
|
if ($association instanceof OneToManyAssociationMetadata) { |
2374
|
73 |
|
$persister->loadOneToManyCollection($association, $collection->getOwner(), $collection); |
2375
|
|
|
} else { |
2376
|
75 |
|
$persister->loadManyToManyCollection($association, $collection->getOwner(), $collection); |
2377
|
|
|
} |
2378
|
|
|
|
2379
|
138 |
|
$collection->setInitialized(true); |
2380
|
138 |
|
} |
2381
|
|
|
|
2382
|
|
|
/** |
2383
|
|
|
* Gets the identity map of the UnitOfWork. |
2384
|
|
|
* |
2385
|
|
|
* @return object[] |
2386
|
|
|
*/ |
2387
|
1 |
|
public function getIdentityMap() |
2388
|
|
|
{ |
2389
|
1 |
|
return $this->identityMap; |
2390
|
|
|
} |
2391
|
|
|
|
2392
|
|
|
/** |
2393
|
|
|
* Gets the original data of an entity. The original data is the data that was |
2394
|
|
|
* present at the time the entity was reconstituted from the database. |
2395
|
|
|
* |
2396
|
|
|
* @param object $entity |
2397
|
|
|
* |
2398
|
|
|
* @return mixed[] |
2399
|
|
|
*/ |
2400
|
120 |
|
public function getOriginalEntityData($entity) |
2401
|
|
|
{ |
2402
|
120 |
|
$oid = spl_object_id($entity); |
|
|
|
|
2403
|
|
|
|
2404
|
120 |
|
return $this->originalEntityData[$oid] ?? []; |
2405
|
|
|
} |
2406
|
|
|
|
2407
|
|
|
/** |
2408
|
|
|
* @ignore |
2409
|
|
|
* |
2410
|
|
|
* @param object $entity |
2411
|
|
|
* @param mixed[] $data |
2412
|
|
|
*/ |
2413
|
|
|
public function setOriginalEntityData($entity, array $data) |
2414
|
|
|
{ |
2415
|
|
|
$this->originalEntityData[spl_object_id($entity)] = $data; |
|
|
|
|
2416
|
|
|
} |
2417
|
|
|
|
2418
|
|
|
/** |
2419
|
|
|
* INTERNAL: |
2420
|
|
|
* Sets a property value of the original data array of an entity. |
2421
|
|
|
* |
2422
|
|
|
* @ignore |
2423
|
|
|
* |
2424
|
|
|
* @param string $oid |
2425
|
|
|
* @param string $property |
2426
|
|
|
* @param mixed $value |
2427
|
|
|
*/ |
2428
|
305 |
|
public function setOriginalEntityProperty($oid, $property, $value) |
2429
|
|
|
{ |
2430
|
305 |
|
$this->originalEntityData[$oid][$property] = $value; |
2431
|
305 |
|
} |
2432
|
|
|
|
2433
|
|
|
/** |
2434
|
|
|
* Gets the identifier of an entity. |
2435
|
|
|
* The returned value is always an array of identifier values. If the entity |
2436
|
|
|
* has a composite identifier then the identifier values are in the same |
2437
|
|
|
* order as the identifier field names as returned by ClassMetadata#getIdentifierFieldNames(). |
2438
|
|
|
* |
2439
|
|
|
* @param object $entity |
2440
|
|
|
* |
2441
|
|
|
* @return mixed[] The identifier values. |
2442
|
|
|
*/ |
2443
|
563 |
|
public function getEntityIdentifier($entity) |
2444
|
|
|
{ |
2445
|
563 |
|
return $this->entityIdentifiers[spl_object_id($entity)]; |
|
|
|
|
2446
|
|
|
} |
2447
|
|
|
|
2448
|
|
|
/** |
2449
|
|
|
* Processes an entity instance to extract their identifier values. |
2450
|
|
|
* |
2451
|
|
|
* @param object $entity The entity instance. |
2452
|
|
|
* |
2453
|
|
|
* @return mixed A scalar value. |
2454
|
|
|
* |
2455
|
|
|
* @throws \Doctrine\ORM\ORMInvalidArgumentException |
2456
|
|
|
*/ |
2457
|
67 |
|
public function getSingleIdentifierValue($entity) |
2458
|
|
|
{ |
2459
|
67 |
|
$class = $this->em->getClassMetadata(get_class($entity)); |
2460
|
67 |
|
$persister = $this->getEntityPersister($class->getClassName()); |
2461
|
|
|
|
2462
|
67 |
|
if ($class->isIdentifierComposite()) { |
|
|
|
|
2463
|
|
|
throw ORMInvalidArgumentException::invalidCompositeIdentifier(); |
2464
|
|
|
} |
2465
|
|
|
|
2466
|
67 |
|
$values = $this->isInIdentityMap($entity) |
2467
|
55 |
|
? $this->getEntityIdentifier($entity) |
2468
|
67 |
|
: $persister->getIdentifier($entity); |
2469
|
|
|
|
2470
|
67 |
|
return $values[$class->identifier[0]] ?? null; |
|
|
|
|
2471
|
|
|
} |
2472
|
|
|
|
2473
|
|
|
/** |
2474
|
|
|
* Tries to find an entity with the given identifier in the identity map of |
2475
|
|
|
* this UnitOfWork. |
2476
|
|
|
* |
2477
|
|
|
* @param mixed|mixed[] $id The entity identifier to look for. |
2478
|
|
|
* @param string $rootClassName The name of the root class of the mapped entity hierarchy. |
2479
|
|
|
* |
2480
|
|
|
* @return object|bool Returns the entity with the specified identifier if it exists in |
2481
|
|
|
* this UnitOfWork, FALSE otherwise. |
2482
|
|
|
*/ |
2483
|
537 |
|
public function tryGetById($id, $rootClassName) |
2484
|
|
|
{ |
2485
|
537 |
|
$idHash = implode(' ', (array) $id); |
2486
|
|
|
|
2487
|
537 |
|
return $this->identityMap[$rootClassName][$idHash] ?? false; |
2488
|
|
|
} |
2489
|
|
|
|
2490
|
|
|
/** |
2491
|
|
|
* Schedules an entity for dirty-checking at commit-time. |
2492
|
|
|
* |
2493
|
|
|
* @param object $entity The entity to schedule for dirty-checking. |
2494
|
|
|
*/ |
2495
|
5 |
|
public function scheduleForSynchronization($entity) |
2496
|
|
|
{ |
2497
|
5 |
|
$rootClassName = $this->em->getClassMetadata(get_class($entity))->getRootClassName(); |
2498
|
|
|
|
2499
|
5 |
|
$this->scheduledForSynchronization[$rootClassName][spl_object_id($entity)] = $entity; |
|
|
|
|
2500
|
5 |
|
} |
2501
|
|
|
|
2502
|
|
|
/** |
2503
|
|
|
* Checks whether the UnitOfWork has any pending insertions. |
2504
|
|
|
* |
2505
|
|
|
* @return bool TRUE if this UnitOfWork has pending insertions, FALSE otherwise. |
2506
|
|
|
*/ |
2507
|
|
|
public function hasPendingInsertions() |
2508
|
|
|
{ |
2509
|
|
|
return ! empty($this->entityInsertions); |
2510
|
|
|
} |
2511
|
|
|
|
2512
|
|
|
/** |
2513
|
|
|
* Calculates the size of the UnitOfWork. The size of the UnitOfWork is the |
2514
|
|
|
* number of entities in the identity map. |
2515
|
|
|
* |
2516
|
|
|
* @return int |
2517
|
|
|
*/ |
2518
|
1 |
|
public function size() |
2519
|
|
|
{ |
2520
|
1 |
|
return \array_sum(\array_map('count', $this->identityMap)); |
2521
|
|
|
} |
2522
|
|
|
|
2523
|
|
|
/** |
2524
|
|
|
* Gets the EntityPersister for an Entity. |
2525
|
|
|
* |
2526
|
|
|
* @param string $entityName The name of the Entity. |
2527
|
|
|
* |
2528
|
|
|
* @return \Doctrine\ORM\Persisters\Entity\EntityPersister |
2529
|
|
|
*/ |
2530
|
1086 |
|
public function getEntityPersister($entityName) |
2531
|
|
|
{ |
2532
|
1086 |
|
if (isset($this->entityPersisters[$entityName])) { |
2533
|
1029 |
|
return $this->entityPersisters[$entityName]; |
2534
|
|
|
} |
2535
|
|
|
|
2536
|
1086 |
|
$class = $this->em->getClassMetadata($entityName); |
2537
|
|
|
|
2538
|
|
|
switch (true) { |
2539
|
1086 |
|
case ($class->inheritanceType === InheritanceType::NONE): |
|
|
|
|
2540
|
1045 |
|
$persister = new BasicEntityPersister($this->em, $class); |
|
|
|
|
2541
|
1045 |
|
break; |
2542
|
|
|
|
2543
|
382 |
|
case ($class->inheritanceType === InheritanceType::SINGLE_TABLE): |
|
|
|
|
2544
|
223 |
|
$persister = new SingleTablePersister($this->em, $class); |
|
|
|
|
2545
|
223 |
|
break; |
2546
|
|
|
|
2547
|
352 |
|
case ($class->inheritanceType === InheritanceType::JOINED): |
|
|
|
|
2548
|
352 |
|
$persister = new JoinedSubclassPersister($this->em, $class); |
|
|
|
|
2549
|
352 |
|
break; |
2550
|
|
|
|
2551
|
|
|
default: |
|
|
|
|
2552
|
|
|
throw new \RuntimeException('No persister found for entity.'); |
2553
|
|
|
} |
2554
|
|
|
|
2555
|
1086 |
|
if ($this->hasCache && $class->getCache()) { |
|
|
|
|
2556
|
130 |
|
$persister = $this->em->getConfiguration() |
2557
|
130 |
|
->getSecondLevelCacheConfiguration() |
2558
|
130 |
|
->getCacheFactory() |
2559
|
130 |
|
->buildCachedEntityPersister($this->em, $persister, $class); |
|
|
|
|
2560
|
|
|
} |
2561
|
|
|
|
2562
|
1086 |
|
$this->entityPersisters[$entityName] = $persister; |
2563
|
|
|
|
2564
|
1086 |
|
return $this->entityPersisters[$entityName]; |
2565
|
|
|
} |
2566
|
|
|
|
2567
|
|
|
/** |
2568
|
|
|
* Gets a collection persister for a collection-valued association. |
2569
|
|
|
* |
2570
|
|
|
* @return \Doctrine\ORM\Persisters\Collection\CollectionPersister |
2571
|
|
|
*/ |
2572
|
567 |
|
public function getCollectionPersister(ToManyAssociationMetadata $association) |
2573
|
|
|
{ |
2574
|
567 |
|
$role = $association->getCache() |
2575
|
78 |
|
? sprintf('%s::%s', $association->getSourceEntity(), $association->getName()) |
2576
|
567 |
|
: get_class($association); |
2577
|
|
|
|
2578
|
567 |
|
if (isset($this->collectionPersisters[$role])) { |
2579
|
434 |
|
return $this->collectionPersisters[$role]; |
2580
|
|
|
} |
2581
|
|
|
|
2582
|
567 |
|
$persister = $association instanceof OneToManyAssociationMetadata |
2583
|
404 |
|
? new OneToManyPersister($this->em) |
2584
|
567 |
|
: new ManyToManyPersister($this->em); |
2585
|
|
|
|
2586
|
567 |
|
if ($this->hasCache && $association->getCache()) { |
2587
|
77 |
|
$persister = $this->em->getConfiguration() |
2588
|
77 |
|
->getSecondLevelCacheConfiguration() |
2589
|
77 |
|
->getCacheFactory() |
2590
|
77 |
|
->buildCachedCollectionPersister($this->em, $persister, $association); |
2591
|
|
|
} |
2592
|
|
|
|
2593
|
567 |
|
$this->collectionPersisters[$role] = $persister; |
2594
|
|
|
|
2595
|
567 |
|
return $this->collectionPersisters[$role]; |
2596
|
|
|
} |
2597
|
|
|
|
2598
|
|
|
/** |
2599
|
|
|
* INTERNAL: |
2600
|
|
|
* Registers an entity as managed. |
2601
|
|
|
* |
2602
|
|
|
* @param object $entity The entity. |
2603
|
|
|
* @param mixed[] $id Map containing identifier field names as key and its associated values. |
2604
|
|
|
* @param mixed[] $data The original entity data. |
2605
|
|
|
*/ |
2606
|
292 |
|
public function registerManaged($entity, array $id, array $data) |
2607
|
|
|
{ |
2608
|
292 |
|
$isProxy = $entity instanceof GhostObjectInterface && ! $entity->isProxyInitialized(); |
2609
|
292 |
|
$oid = spl_object_id($entity); |
|
|
|
|
2610
|
|
|
|
2611
|
292 |
|
$this->entityIdentifiers[$oid] = $id; |
2612
|
292 |
|
$this->entityStates[$oid] = self::STATE_MANAGED; |
2613
|
292 |
|
$this->originalEntityData[$oid] = $data; |
2614
|
|
|
|
2615
|
292 |
|
$this->addToIdentityMap($entity); |
2616
|
|
|
|
2617
|
286 |
|
if ($entity instanceof NotifyPropertyChanged && ! $isProxy) { |
2618
|
1 |
|
$entity->addPropertyChangedListener($this); |
2619
|
|
|
} |
2620
|
286 |
|
} |
2621
|
|
|
|
2622
|
|
|
/** |
2623
|
|
|
* INTERNAL: |
2624
|
|
|
* Clears the property changeset of the entity with the given OID. |
2625
|
|
|
* |
2626
|
|
|
* @param string $oid The entity's OID. |
2627
|
|
|
*/ |
2628
|
|
|
public function clearEntityChangeSet($oid) |
2629
|
|
|
{ |
2630
|
|
|
unset($this->entityChangeSets[$oid]); |
2631
|
|
|
} |
2632
|
|
|
|
2633
|
|
|
/* PropertyChangedListener implementation */ |
2634
|
|
|
|
2635
|
|
|
/** |
2636
|
|
|
* Notifies this UnitOfWork of a property change in an entity. |
2637
|
|
|
* |
2638
|
|
|
* @param object $entity The entity that owns the property. |
2639
|
|
|
* @param string $propertyName The name of the property that changed. |
2640
|
|
|
* @param mixed $oldValue The old value of the property. |
2641
|
|
|
* @param mixed $newValue The new value of the property. |
2642
|
|
|
*/ |
2643
|
3 |
|
public function propertyChanged($entity, $propertyName, $oldValue, $newValue) |
2644
|
|
|
{ |
2645
|
3 |
|
$class = $this->em->getClassMetadata(get_class($entity)); |
2646
|
|
|
|
2647
|
3 |
|
if (! $class->getProperty($propertyName)) { |
2648
|
|
|
return; // ignore non-persistent fields |
2649
|
|
|
} |
2650
|
|
|
|
2651
|
3 |
|
$oid = spl_object_id($entity); |
|
|
|
|
2652
|
|
|
|
2653
|
|
|
// Update changeset and mark entity for synchronization |
2654
|
3 |
|
$this->entityChangeSets[$oid][$propertyName] = [$oldValue, $newValue]; |
2655
|
|
|
|
2656
|
3 |
|
if (! isset($this->scheduledForSynchronization[$class->getRootClassName()][$oid])) { |
2657
|
3 |
|
$this->scheduleForSynchronization($entity); |
2658
|
|
|
} |
2659
|
3 |
|
} |
2660
|
|
|
|
2661
|
|
|
/** |
2662
|
|
|
* Gets the currently scheduled entity insertions in this UnitOfWork. |
2663
|
|
|
* |
2664
|
|
|
* @return object[] |
2665
|
|
|
*/ |
2666
|
2 |
|
public function getScheduledEntityInsertions() |
2667
|
|
|
{ |
2668
|
2 |
|
return $this->entityInsertions; |
2669
|
|
|
} |
2670
|
|
|
|
2671
|
|
|
/** |
2672
|
|
|
* Gets the currently scheduled entity updates in this UnitOfWork. |
2673
|
|
|
* |
2674
|
|
|
* @return object[] |
2675
|
|
|
*/ |
2676
|
3 |
|
public function getScheduledEntityUpdates() |
2677
|
|
|
{ |
2678
|
3 |
|
return $this->entityUpdates; |
2679
|
|
|
} |
2680
|
|
|
|
2681
|
|
|
/** |
2682
|
|
|
* Gets the currently scheduled entity deletions in this UnitOfWork. |
2683
|
|
|
* |
2684
|
|
|
* @return object[] |
2685
|
|
|
*/ |
2686
|
1 |
|
public function getScheduledEntityDeletions() |
2687
|
|
|
{ |
2688
|
1 |
|
return $this->entityDeletions; |
2689
|
|
|
} |
2690
|
|
|
|
2691
|
|
|
/** |
2692
|
|
|
* Gets the currently scheduled complete collection deletions |
2693
|
|
|
* |
2694
|
|
|
* @return Collection[]|object[][] |
2695
|
|
|
*/ |
2696
|
1 |
|
public function getScheduledCollectionDeletions() |
2697
|
|
|
{ |
2698
|
1 |
|
return $this->collectionDeletions; |
2699
|
|
|
} |
2700
|
|
|
|
2701
|
|
|
/** |
2702
|
|
|
* Gets the currently scheduled collection inserts, updates and deletes. |
2703
|
|
|
* |
2704
|
|
|
* @return Collection[]|object[][] |
2705
|
|
|
*/ |
2706
|
|
|
public function getScheduledCollectionUpdates() |
2707
|
|
|
{ |
2708
|
|
|
return $this->collectionUpdates; |
2709
|
|
|
} |
2710
|
|
|
|
2711
|
|
|
/** |
2712
|
|
|
* Helper method to initialize a lazy loading proxy or persistent collection. |
2713
|
|
|
* |
2714
|
|
|
* @param object $obj |
2715
|
|
|
*/ |
2716
|
2 |
|
public function initializeObject($obj) |
2717
|
|
|
{ |
2718
|
2 |
|
if ($obj instanceof GhostObjectInterface) { |
2719
|
1 |
|
$obj->initializeProxy(); |
2720
|
|
|
|
2721
|
1 |
|
return; |
2722
|
|
|
} |
2723
|
|
|
|
2724
|
1 |
|
if ($obj instanceof PersistentCollection) { |
2725
|
1 |
|
$obj->initialize(); |
2726
|
|
|
} |
2727
|
1 |
|
} |
2728
|
|
|
|
2729
|
|
|
/** |
2730
|
|
|
* Helper method to show an object as string. |
2731
|
|
|
* |
2732
|
|
|
* @param object $obj |
2733
|
|
|
* |
2734
|
|
|
* @return string |
2735
|
|
|
*/ |
2736
|
|
|
private static function objToStr($obj) |
2737
|
|
|
{ |
2738
|
|
|
return method_exists($obj, '__toString') ? (string) $obj : get_class($obj) . '@' . spl_object_id($obj); |
|
|
|
|
2739
|
|
|
} |
2740
|
|
|
|
2741
|
|
|
/** |
2742
|
|
|
* Marks an entity as read-only so that it will not be considered for updates during UnitOfWork#commit(). |
2743
|
|
|
* |
2744
|
|
|
* This operation cannot be undone as some parts of the UnitOfWork now keep gathering information |
2745
|
|
|
* on this object that might be necessary to perform a correct update. |
2746
|
|
|
* |
2747
|
|
|
* @param object $object |
2748
|
|
|
* |
2749
|
|
|
* @throws ORMInvalidArgumentException |
2750
|
|
|
*/ |
2751
|
6 |
|
public function markReadOnly($object) |
2752
|
|
|
{ |
2753
|
6 |
|
if (! is_object($object) || ! $this->isInIdentityMap($object)) { |
2754
|
1 |
|
throw ORMInvalidArgumentException::readOnlyRequiresManagedEntity($object); |
2755
|
|
|
} |
2756
|
|
|
|
2757
|
5 |
|
$this->readOnlyObjects[spl_object_id($object)] = true; |
|
|
|
|
2758
|
5 |
|
} |
2759
|
|
|
|
2760
|
|
|
/** |
2761
|
|
|
* Is this entity read only? |
2762
|
|
|
* |
2763
|
|
|
* @param object $object |
2764
|
|
|
* |
2765
|
|
|
* @return bool |
2766
|
|
|
* |
2767
|
|
|
* @throws ORMInvalidArgumentException |
2768
|
|
|
*/ |
2769
|
3 |
|
public function isReadOnly($object) |
2770
|
|
|
{ |
2771
|
3 |
|
if (! is_object($object)) { |
2772
|
|
|
throw ORMInvalidArgumentException::readOnlyRequiresManagedEntity($object); |
2773
|
|
|
} |
2774
|
|
|
|
2775
|
3 |
|
return isset($this->readOnlyObjects[spl_object_id($object)]); |
|
|
|
|
2776
|
|
|
} |
2777
|
|
|
|
2778
|
|
|
/** |
2779
|
|
|
* Perform whatever processing is encapsulated here after completion of the transaction. |
2780
|
|
|
*/ |
2781
|
|
|
private function afterTransactionComplete() |
2782
|
|
|
{ |
2783
|
1002 |
|
$this->performCallbackOnCachedPersister(function (CachedPersister $persister) { |
2784
|
94 |
|
$persister->afterTransactionComplete(); |
2785
|
1002 |
|
}); |
2786
|
1002 |
|
} |
2787
|
|
|
|
2788
|
|
|
/** |
2789
|
|
|
* Perform whatever processing is encapsulated here after completion of the rolled-back. |
2790
|
|
|
*/ |
2791
|
|
|
private function afterTransactionRolledBack() |
2792
|
|
|
{ |
2793
|
10 |
|
$this->performCallbackOnCachedPersister(function (CachedPersister $persister) { |
2794
|
|
|
$persister->afterTransactionRolledBack(); |
2795
|
10 |
|
}); |
2796
|
10 |
|
} |
2797
|
|
|
|
2798
|
|
|
/** |
2799
|
|
|
* Performs an action after the transaction. |
2800
|
|
|
*/ |
2801
|
1007 |
|
private function performCallbackOnCachedPersister(callable $callback) |
2802
|
|
|
{ |
2803
|
1007 |
|
if (! $this->hasCache) { |
2804
|
913 |
|
return; |
2805
|
|
|
} |
2806
|
|
|
|
2807
|
94 |
|
foreach (array_merge($this->entityPersisters, $this->collectionPersisters) as $persister) { |
2808
|
94 |
|
if ($persister instanceof CachedPersister) { |
2809
|
94 |
|
$callback($persister); |
2810
|
|
|
} |
2811
|
|
|
} |
2812
|
94 |
|
} |
2813
|
|
|
|
2814
|
1012 |
|
private function dispatchOnFlushEvent() |
2815
|
|
|
{ |
2816
|
1012 |
|
if ($this->eventManager->hasListeners(Events::onFlush)) { |
2817
|
4 |
|
$this->eventManager->dispatchEvent(Events::onFlush, new OnFlushEventArgs($this->em)); |
2818
|
|
|
} |
2819
|
1012 |
|
} |
2820
|
|
|
|
2821
|
1007 |
|
private function dispatchPostFlushEvent() |
2822
|
|
|
{ |
2823
|
1007 |
|
if ($this->eventManager->hasListeners(Events::postFlush)) { |
2824
|
5 |
|
$this->eventManager->dispatchEvent(Events::postFlush, new PostFlushEventArgs($this->em)); |
2825
|
|
|
} |
2826
|
1006 |
|
} |
2827
|
|
|
|
2828
|
|
|
/** |
2829
|
|
|
* Verifies if two given entities actually are the same based on identifier comparison |
2830
|
|
|
* |
2831
|
|
|
* @param object $entity1 |
2832
|
|
|
* @param object $entity2 |
2833
|
|
|
* |
2834
|
|
|
* @return bool |
2835
|
|
|
*/ |
2836
|
17 |
|
private function isIdentifierEquals($entity1, $entity2) |
2837
|
|
|
{ |
2838
|
17 |
|
if ($entity1 === $entity2) { |
2839
|
|
|
return true; |
2840
|
|
|
} |
2841
|
|
|
|
2842
|
17 |
|
$class = $this->em->getClassMetadata(get_class($entity1)); |
2843
|
17 |
|
$persister = $this->getEntityPersister($class->getClassName()); |
2844
|
|
|
|
2845
|
17 |
|
if ($class !== $this->em->getClassMetadata(get_class($entity2))) { |
2846
|
11 |
|
return false; |
2847
|
|
|
} |
2848
|
|
|
|
2849
|
6 |
|
$identifierFlattener = $this->em->getIdentifierFlattener(); |
2850
|
|
|
|
2851
|
6 |
|
$oid1 = spl_object_id($entity1); |
|
|
|
|
2852
|
6 |
|
$oid2 = spl_object_id($entity2); |
2853
|
|
|
|
2854
|
6 |
|
$id1 = $this->entityIdentifiers[$oid1] |
2855
|
6 |
|
?? $identifierFlattener->flattenIdentifier($class, $persister->getIdentifier($entity1)); |
|
|
|
|
2856
|
6 |
|
$id2 = $this->entityIdentifiers[$oid2] |
2857
|
6 |
|
?? $identifierFlattener->flattenIdentifier($class, $persister->getIdentifier($entity2)); |
2858
|
|
|
|
2859
|
6 |
|
return $id1 === $id2 || implode(' ', $id1) === implode(' ', $id2); |
2860
|
|
|
} |
2861
|
|
|
|
2862
|
|
|
/** |
2863
|
|
|
* @throws ORMInvalidArgumentException |
2864
|
|
|
*/ |
2865
|
1009 |
|
private function assertThatThereAreNoUnintentionallyNonPersistedAssociations() : void |
2866
|
|
|
{ |
2867
|
1009 |
|
$entitiesNeedingCascadePersist = \array_diff_key($this->nonCascadedNewDetectedEntities, $this->entityInsertions); |
2868
|
|
|
|
2869
|
1009 |
|
$this->nonCascadedNewDetectedEntities = []; |
2870
|
|
|
|
2871
|
1009 |
|
if ($entitiesNeedingCascadePersist) { |
|
|
|
|
2872
|
4 |
|
throw ORMInvalidArgumentException::newEntitiesFoundThroughRelationships( |
2873
|
4 |
|
\array_values($entitiesNeedingCascadePersist) |
2874
|
|
|
); |
2875
|
|
|
} |
2876
|
1007 |
|
} |
2877
|
|
|
|
2878
|
|
|
/** |
2879
|
|
|
* This method called by hydrators, and indicates that hydrator totally completed current hydration cycle. |
2880
|
|
|
* Unit of work able to fire deferred events, related to loading events here. |
2881
|
|
|
* |
2882
|
|
|
* @internal should be called internally from object hydrators |
2883
|
|
|
*/ |
2884
|
883 |
|
public function hydrationComplete() |
2885
|
|
|
{ |
2886
|
883 |
|
$this->hydrationCompleteHandler->hydrationComplete(); |
2887
|
883 |
|
} |
2888
|
|
|
} |
2889
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.