1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Bankiru\Api\Doctrine; |
4
|
|
|
|
5
|
|
|
use Bankiru\Api\Doctrine\Cache\ApiEntityCache; |
6
|
|
|
use Bankiru\Api\Doctrine\Cache\EntityCacheAwareInterface; |
7
|
|
|
use Bankiru\Api\Doctrine\Cache\LoggingCache; |
8
|
|
|
use Bankiru\Api\Doctrine\Cache\VoidEntityCache; |
9
|
|
|
use Bankiru\Api\Doctrine\Exception\MappingException; |
10
|
|
|
use Bankiru\Api\Doctrine\Hydration\EntityHydrator; |
11
|
|
|
use Bankiru\Api\Doctrine\Hydration\Hydrator; |
12
|
|
|
use Bankiru\Api\Doctrine\Mapping\ApiMetadata; |
13
|
|
|
use Bankiru\Api\Doctrine\Mapping\EntityMetadata; |
14
|
|
|
use Bankiru\Api\Doctrine\Persister\ApiPersister; |
15
|
|
|
use Bankiru\Api\Doctrine\Persister\CollectionMatcher; |
16
|
|
|
use Bankiru\Api\Doctrine\Persister\CollectionPersister; |
17
|
|
|
use Bankiru\Api\Doctrine\Persister\EntityPersister; |
18
|
|
|
use Bankiru\Api\Doctrine\Proxy\ApiCollection; |
19
|
|
|
use Bankiru\Api\Doctrine\Rpc\CrudsApiInterface; |
20
|
|
|
use Bankiru\Api\Doctrine\Utility\IdentifierFlattener; |
21
|
|
|
use Bankiru\Api\Doctrine\Utility\ReflectionPropertiesGetter; |
22
|
|
|
use Doctrine\Common\Collections\ArrayCollection; |
23
|
|
|
use Doctrine\Common\Collections\Collection; |
24
|
|
|
use Doctrine\Common\NotifyPropertyChanged; |
25
|
|
|
use Doctrine\Common\Persistence\Mapping\RuntimeReflectionService; |
26
|
|
|
use Doctrine\Common\Persistence\ObjectManagerAware; |
27
|
|
|
use Doctrine\Common\PropertyChangedListener; |
28
|
|
|
use Doctrine\Common\Proxy\Proxy; |
29
|
|
|
|
30
|
|
|
class UnitOfWork implements PropertyChangedListener |
31
|
|
|
{ |
32
|
|
|
/** |
33
|
|
|
* An entity is in MANAGED state when its persistence is managed by an EntityManager. |
34
|
|
|
*/ |
35
|
|
|
const STATE_MANAGED = 1; |
36
|
|
|
/** |
37
|
|
|
* An entity is new if it has just been instantiated (i.e. using the "new" operator) |
38
|
|
|
* and is not (yet) managed by an EntityManager. |
39
|
|
|
*/ |
40
|
|
|
const STATE_NEW = 2; |
41
|
|
|
/** |
42
|
|
|
* A detached entity is an instance with persistent state and identity that is not |
43
|
|
|
* (or no longer) associated with an EntityManager (and a UnitOfWork). |
44
|
|
|
*/ |
45
|
|
|
const STATE_DETACHED = 3; |
46
|
|
|
/** |
47
|
|
|
* A removed entity instance is an instance with a persistent identity, |
48
|
|
|
* associated with an EntityManager, whose persistent state will be deleted |
49
|
|
|
* on commit. |
50
|
|
|
*/ |
51
|
|
|
const STATE_REMOVED = 4; |
52
|
|
|
|
53
|
|
|
/** |
54
|
|
|
* The (cached) states of any known entities. |
55
|
|
|
* Keys are object ids (spl_object_hash). |
56
|
|
|
* |
57
|
|
|
* @var array |
58
|
|
|
*/ |
59
|
|
|
private $entityStates = []; |
60
|
|
|
|
61
|
|
|
/** @var EntityManager */ |
62
|
|
|
private $manager; |
63
|
|
|
/** @var EntityPersister[] */ |
64
|
|
|
private $persisters = []; |
65
|
|
|
/** @var CollectionPersister[] */ |
66
|
|
|
private $collectionPersisters = []; |
67
|
|
|
/** @var array */ |
68
|
|
|
private $entityIdentifiers = []; |
69
|
|
|
/** @var object[][] */ |
70
|
|
|
private $identityMap = []; |
71
|
|
|
/** @var IdentifierFlattener */ |
72
|
|
|
private $identifierFlattener; |
73
|
|
|
/** @var array */ |
74
|
|
|
private $originalEntityData = []; |
75
|
|
|
/** @var array */ |
76
|
|
|
private $entityDeletions = []; |
77
|
|
|
/** @var array */ |
78
|
|
|
private $entityChangeSets = []; |
79
|
|
|
/** @var array */ |
80
|
|
|
private $entityInsertions = []; |
81
|
|
|
/** @var array */ |
82
|
|
|
private $entityUpdates = []; |
83
|
|
|
/** @var array */ |
84
|
|
|
private $readOnlyObjects = []; |
85
|
|
|
/** @var array */ |
86
|
|
|
private $scheduledForSynchronization = []; |
87
|
|
|
/** @var array */ |
88
|
|
|
private $orphanRemovals = []; |
89
|
|
|
/** @var ApiCollection[] */ |
90
|
|
|
private $collectionDeletions = []; |
91
|
|
|
/** @var array */ |
92
|
|
|
private $extraUpdates = []; |
93
|
|
|
/** @var ApiCollection[] */ |
94
|
|
|
private $collectionUpdates = []; |
95
|
|
|
/** @var ApiCollection[] */ |
96
|
|
|
private $visitedCollections = []; |
97
|
|
|
/** @var ReflectionPropertiesGetter */ |
98
|
|
|
private $reflectionPropertiesGetter; |
99
|
|
|
/** @var Hydrator[] */ |
100
|
|
|
private $hydrators = []; |
101
|
|
|
/** @var CrudsApiInterface[] */ |
102
|
|
|
private $apis = []; |
103
|
|
|
|
104
|
|
|
/** |
105
|
|
|
* UnitOfWork constructor. |
106
|
|
|
* |
107
|
|
|
* @param EntityManager $manager |
108
|
28 |
|
*/ |
109
|
|
|
public function __construct(EntityManager $manager) |
110
|
28 |
|
{ |
111
|
28 |
|
$this->manager = $manager; |
112
|
28 |
|
$this->identifierFlattener = new IdentifierFlattener($this->manager); |
113
|
28 |
|
$this->reflectionPropertiesGetter = new ReflectionPropertiesGetter(new RuntimeReflectionService()); |
114
|
|
|
} |
115
|
|
|
|
116
|
|
|
/** |
117
|
|
|
* @param $className |
118
|
|
|
* |
119
|
|
|
* @return EntityPersister |
120
|
26 |
|
*/ |
121
|
|
|
public function getEntityPersister($className) |
122
|
26 |
|
{ |
123
|
|
|
if (!array_key_exists($className, $this->persisters)) { |
124
|
26 |
|
/** @var ApiMetadata $classMetadata */ |
125
|
|
|
$classMetadata = $this->manager->getClassMetadata($className); |
126
|
26 |
|
|
127
|
|
|
$api = $this->getCrudsApi($classMetadata); |
128
|
26 |
|
|
129
|
26 |
|
if ($api instanceof EntityCacheAwareInterface) { |
130
|
26 |
|
$api->setEntityCache($this->createEntityCache($classMetadata)); |
131
|
|
|
} |
132
|
26 |
|
|
133
|
26 |
|
$this->persisters[$className] = new ApiPersister($this->manager, $api); |
134
|
|
|
} |
135
|
26 |
|
|
136
|
|
|
return $this->persisters[$className]; |
137
|
|
|
} |
138
|
|
|
|
139
|
|
|
/** |
140
|
|
|
* Checks whether an entity is registered in the identity map of this UnitOfWork. |
141
|
|
|
* |
142
|
|
|
* @param object $entity |
143
|
|
|
* |
144
|
|
|
* @return boolean |
145
|
|
|
*/ |
146
|
|
|
public function isInIdentityMap($entity) |
147
|
|
|
{ |
148
|
|
|
$oid = spl_object_hash($entity); |
149
|
|
|
|
150
|
|
|
if (!isset($this->entityIdentifiers[$oid])) { |
151
|
|
|
return false; |
152
|
|
|
} |
153
|
|
|
|
154
|
|
|
/** @var EntityMetadata $classMetadata */ |
155
|
|
|
$classMetadata = $this->manager->getClassMetadata(get_class($entity)); |
156
|
|
|
$idHash = implode(' ', $this->entityIdentifiers[$oid]); |
157
|
|
|
|
158
|
|
|
if ($idHash === '') { |
159
|
|
|
return false; |
160
|
|
|
} |
161
|
|
|
|
162
|
|
|
return isset($this->identityMap[$classMetadata->rootEntityName][$idHash]); |
163
|
|
|
} |
164
|
|
|
|
165
|
|
|
/** |
166
|
|
|
* Gets the identifier of an entity. |
167
|
|
|
* The returned value is always an array of identifier values. If the entity |
168
|
|
|
* has a composite identifier then the identifier values are in the same |
169
|
|
|
* order as the identifier field names as returned by ClassMetadata#getIdentifierFieldNames(). |
170
|
|
|
* |
171
|
|
|
* @param object $entity |
172
|
|
|
* |
173
|
|
|
* @return array The identifier values. |
174
|
1 |
|
*/ |
175
|
|
|
public function getEntityIdentifier($entity) |
176
|
1 |
|
{ |
177
|
|
|
return $this->entityIdentifiers[spl_object_hash($entity)]; |
178
|
|
|
} |
179
|
|
|
|
180
|
|
|
/** |
181
|
|
|
* @param $className |
182
|
|
|
* @param \stdClass $data |
183
|
|
|
* |
184
|
|
|
* @return ObjectManagerAware|object |
185
|
|
|
* @throws MappingException |
186
|
18 |
|
*/ |
187
|
|
|
public function getOrCreateEntity($className, \stdClass $data) |
188
|
|
|
{ |
189
|
18 |
|
/** @var EntityMetadata $class */ |
190
|
18 |
|
$class = $this->resolveSourceMetadataForClass($data, $className); |
191
|
|
|
$tmpEntity = $this->getHydratorForClass($class)->hydarate($data); |
192
|
18 |
|
|
193
|
18 |
|
$id = $this->identifierFlattener->flattenIdentifier($class, $class->getIdentifierValues($tmpEntity)); |
194
|
|
|
$idHash = implode(' ', $id); |
195
|
18 |
|
|
196
|
18 |
|
$overrideLocalValues = false; |
197
|
2 |
|
if (isset($this->identityMap[$class->rootEntityName][$idHash])) { |
198
|
2 |
|
$entity = $this->identityMap[$class->rootEntityName][$idHash]; |
199
|
|
|
$oid = spl_object_hash($entity); |
200
|
2 |
|
|
201
|
2 |
|
if ($entity instanceof Proxy && !$entity->__isInitialized()) { |
202
|
|
|
$entity->__setInitialized(true); |
203
|
2 |
|
|
204
|
2 |
|
$overrideLocalValues = true; |
205
|
|
|
$this->originalEntityData[$oid] = $data; |
206
|
2 |
|
|
207
|
|
|
if ($entity instanceof NotifyPropertyChanged) { |
208
|
|
|
$entity->addPropertyChangedListener($this); |
209
|
2 |
|
} |
210
|
2 |
|
} |
211
|
17 |
|
} else { |
212
|
17 |
|
$entity = $this->newInstance($class); |
213
|
17 |
|
$oid = spl_object_hash($entity); |
214
|
17 |
|
$this->entityIdentifiers[$oid] = $id; |
215
|
17 |
|
$this->entityStates[$oid] = self::STATE_MANAGED; |
216
|
17 |
|
$this->originalEntityData[$oid] = $data; |
217
|
17 |
|
$this->identityMap[$class->rootEntityName][$idHash] = $entity; |
218
|
|
|
if ($entity instanceof NotifyPropertyChanged) { |
219
|
|
|
$entity->addPropertyChangedListener($this); |
220
|
17 |
|
} |
221
|
|
|
$overrideLocalValues = true; |
222
|
|
|
} |
223
|
18 |
|
|
224
|
|
|
if (!$overrideLocalValues) { |
225
|
|
|
return $entity; |
226
|
|
|
} |
227
|
18 |
|
|
228
|
|
|
$entity = $this->getHydratorForClass($class)->hydarate($data, $entity); |
229
|
18 |
|
|
230
|
|
|
return $entity; |
231
|
|
|
} |
232
|
|
|
|
233
|
|
|
/** |
234
|
|
|
* INTERNAL: |
235
|
|
|
* Registers an entity as managed. |
236
|
|
|
* |
237
|
|
|
* @param object $entity The entity. |
238
|
|
|
* @param array $id The identifier values. |
239
|
|
|
* @param \stdClass|null $data The original entity data. |
240
|
|
|
* |
241
|
|
|
* @return void |
242
|
4 |
|
*/ |
243
|
|
|
public function registerManaged($entity, array $id, \stdClass $data = null) |
244
|
4 |
|
{ |
245
|
|
|
$oid = spl_object_hash($entity); |
246
|
4 |
|
|
247
|
4 |
|
$this->entityIdentifiers[$oid] = $id; |
248
|
4 |
|
$this->entityStates[$oid] = self::STATE_MANAGED; |
249
|
|
|
$this->originalEntityData[$oid] = $data; |
250
|
4 |
|
|
251
|
|
|
$this->addToIdentityMap($entity); |
252
|
4 |
|
|
253
|
|
|
if ($entity instanceof NotifyPropertyChanged && (!$entity instanceof Proxy || $entity->__isInitialized())) { |
254
|
|
|
$entity->addPropertyChangedListener($this); |
255
|
4 |
|
} |
256
|
|
|
} |
257
|
|
|
|
258
|
|
|
/** |
259
|
|
|
* INTERNAL: |
260
|
|
|
* Registers an entity in the identity map. |
261
|
|
|
* Note that entities in a hierarchy are registered with the class name of |
262
|
|
|
* the root entity. |
263
|
|
|
* |
264
|
|
|
* @ignore |
265
|
|
|
* |
266
|
|
|
* @param object $entity The entity to register. |
267
|
|
|
* |
268
|
|
|
* @return boolean TRUE if the registration was successful, FALSE if the identity of |
269
|
|
|
* the entity in question is already managed. |
270
|
|
|
* |
271
|
11 |
|
*/ |
272
|
|
|
public function addToIdentityMap($entity) |
273
|
|
|
{ |
274
|
11 |
|
/** @var EntityMetadata $classMetadata */ |
275
|
11 |
|
$classMetadata = $this->manager->getClassMetadata(get_class($entity)); |
276
|
|
|
$idHash = implode(' ', $this->entityIdentifiers[spl_object_hash($entity)]); |
277
|
11 |
|
|
278
|
|
|
if ($idHash === '') { |
279
|
|
|
throw new \InvalidArgumentException('Entitty does not have valid identifiers to be stored at identity map'); |
280
|
|
|
} |
281
|
11 |
|
|
282
|
|
|
$className = $classMetadata->rootEntityName; |
283
|
11 |
|
|
284
|
|
|
if (isset($this->identityMap[$className][$idHash])) { |
285
|
|
|
return false; |
286
|
|
|
} |
287
|
11 |
|
|
288
|
|
|
$this->identityMap[$className][$idHash] = $entity; |
289
|
11 |
|
|
290
|
|
|
return true; |
291
|
|
|
} |
292
|
|
|
|
293
|
|
|
/** |
294
|
|
|
* Gets the identity map of the UnitOfWork. |
295
|
|
|
* |
296
|
|
|
* @return array |
297
|
|
|
*/ |
298
|
|
|
public function getIdentityMap() |
299
|
|
|
{ |
300
|
|
|
return $this->identityMap; |
301
|
|
|
} |
302
|
|
|
|
303
|
|
|
/** |
304
|
|
|
* Gets the original data of an entity. The original data is the data that was |
305
|
|
|
* present at the time the entity was reconstituted from the database. |
306
|
|
|
* |
307
|
|
|
* @param object $entity |
308
|
|
|
* |
309
|
|
|
* @return array |
310
|
|
|
*/ |
311
|
|
|
public function getOriginalEntityData($entity) |
312
|
|
|
{ |
313
|
|
|
$oid = spl_object_hash($entity); |
314
|
|
|
|
315
|
|
|
if (isset($this->originalEntityData[$oid])) { |
316
|
|
|
return $this->originalEntityData[$oid]; |
317
|
|
|
} |
318
|
|
|
|
319
|
|
|
return []; |
320
|
|
|
} |
321
|
|
|
|
322
|
|
|
/** |
323
|
|
|
* INTERNAL: |
324
|
|
|
* Checks whether an identifier hash exists in the identity map. |
325
|
|
|
* |
326
|
|
|
* @ignore |
327
|
|
|
* |
328
|
|
|
* @param string $idHash |
329
|
|
|
* @param string $rootClassName |
330
|
|
|
* |
331
|
|
|
* @return boolean |
332
|
|
|
*/ |
333
|
|
|
public function containsIdHash($idHash, $rootClassName) |
334
|
|
|
{ |
335
|
|
|
return isset($this->identityMap[$rootClassName][$idHash]); |
336
|
|
|
} |
337
|
|
|
|
338
|
|
|
/** |
339
|
|
|
* INTERNAL: |
340
|
|
|
* Gets an entity in the identity map by its identifier hash. |
341
|
|
|
* |
342
|
|
|
* @ignore |
343
|
|
|
* |
344
|
|
|
* @param string $idHash |
345
|
|
|
* @param string $rootClassName |
346
|
|
|
* |
347
|
|
|
* @return object |
348
|
|
|
*/ |
349
|
|
|
public function getByIdHash($idHash, $rootClassName) |
350
|
|
|
{ |
351
|
|
|
return $this->identityMap[$rootClassName][$idHash]; |
352
|
|
|
} |
353
|
|
|
|
354
|
|
|
/** |
355
|
|
|
* INTERNAL: |
356
|
|
|
* Tries to get an entity by its identifier hash. If no entity is found for |
357
|
|
|
* the given hash, FALSE is returned. |
358
|
|
|
* |
359
|
|
|
* @ignore |
360
|
|
|
* |
361
|
|
|
* @param mixed $idHash (must be possible to cast it to string) |
362
|
|
|
* @param string $rootClassName |
363
|
|
|
* |
364
|
|
|
* @return object|bool The found entity or FALSE. |
365
|
|
|
*/ |
366
|
|
|
public function tryGetByIdHash($idHash, $rootClassName) |
367
|
|
|
{ |
368
|
|
|
$stringIdHash = (string)$idHash; |
369
|
|
|
|
370
|
|
|
if (isset($this->identityMap[$rootClassName][$stringIdHash])) { |
371
|
|
|
return $this->identityMap[$rootClassName][$stringIdHash]; |
372
|
|
|
} |
373
|
|
|
|
374
|
|
|
return false; |
375
|
|
|
} |
376
|
|
|
|
377
|
|
|
/** |
378
|
|
|
* Gets the state of an entity with regard to the current unit of work. |
379
|
|
|
* |
380
|
|
|
* @param object $entity |
381
|
|
|
* @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED). |
382
|
|
|
* This parameter can be set to improve performance of entity state detection |
383
|
|
|
* by potentially avoiding a database lookup if the distinction between NEW and DETACHED |
384
|
|
|
* is either known or does not matter for the caller of the method. |
385
|
|
|
* |
386
|
|
|
* @return int The entity state. |
387
|
7 |
|
*/ |
388
|
|
|
public function getEntityState($entity, $assume = null) |
389
|
7 |
|
{ |
390
|
7 |
|
$oid = spl_object_hash($entity); |
391
|
2 |
|
if (isset($this->entityStates[$oid])) { |
392
|
|
|
return $this->entityStates[$oid]; |
393
|
7 |
|
} |
394
|
7 |
|
if ($assume !== null) { |
395
|
|
|
return $assume; |
396
|
|
|
} |
397
|
|
|
// State can only be NEW or DETACHED, because MANAGED/REMOVED states are known. |
398
|
|
|
// Note that you can not remember the NEW or DETACHED state in _entityStates since |
399
|
|
|
// the UoW does not hold references to such objects and the object hash can be reused. |
400
|
|
|
// More generally because the state may "change" between NEW/DETACHED without the UoW being aware of it. |
401
|
|
|
$class = $this->manager->getClassMetadata(get_class($entity)); |
402
|
|
|
$id = $class->getIdentifierValues($entity); |
403
|
|
|
if (!$id) { |
|
|
|
|
404
|
|
|
return self::STATE_NEW; |
405
|
|
|
} |
406
|
|
|
|
407
|
|
|
return self::STATE_DETACHED; |
408
|
|
|
} |
409
|
|
|
|
410
|
|
|
/** |
411
|
|
|
* Tries to find an entity with the given identifier in the identity map of |
412
|
|
|
* this UnitOfWork. |
413
|
|
|
* |
414
|
|
|
* @param mixed $id The entity identifier to look for. |
415
|
|
|
* @param string $rootClassName The name of the root class of the mapped entity hierarchy. |
416
|
|
|
* |
417
|
|
|
* @return object|bool Returns the entity with the specified identifier if it exists in |
418
|
|
|
* this UnitOfWork, FALSE otherwise. |
419
|
15 |
|
*/ |
420
|
|
|
public function tryGetById($id, $rootClassName) |
421
|
|
|
{ |
422
|
15 |
|
/** @var EntityMetadata $metadata */ |
423
|
15 |
|
$metadata = $this->manager->getClassMetadata($rootClassName); |
424
|
|
|
$idHash = implode(' ', (array)$this->identifierFlattener->flattenIdentifier($metadata, $id)); |
425
|
15 |
|
|
426
|
5 |
|
if (isset($this->identityMap[$rootClassName][$idHash])) { |
427
|
|
|
return $this->identityMap[$rootClassName][$idHash]; |
428
|
|
|
} |
429
|
15 |
|
|
430
|
|
|
return false; |
431
|
|
|
} |
432
|
|
|
|
433
|
|
|
/** |
434
|
|
|
* Notifies this UnitOfWork of a property change in an entity. |
435
|
|
|
* |
436
|
|
|
* @param object $entity The entity that owns the property. |
437
|
|
|
* @param string $propertyName The name of the property that changed. |
438
|
|
|
* @param mixed $oldValue The old value of the property. |
439
|
|
|
* @param mixed $newValue The new value of the property. |
440
|
|
|
* |
441
|
|
|
* @return void |
442
|
|
|
*/ |
443
|
|
|
public function propertyChanged($entity, $propertyName, $oldValue, $newValue) |
444
|
|
|
{ |
445
|
|
|
$oid = spl_object_hash($entity); |
446
|
|
|
$class = $this->manager->getClassMetadata(get_class($entity)); |
447
|
|
|
$isAssocField = $class->hasAssociation($propertyName); |
448
|
|
|
if (!$isAssocField && !$class->hasField($propertyName)) { |
449
|
|
|
return; // ignore non-persistent fields |
450
|
|
|
} |
451
|
|
|
// Update changeset and mark entity for synchronization |
452
|
|
|
$this->entityChangeSets[$oid][$propertyName] = [$oldValue, $newValue]; |
453
|
|
|
if (!isset($this->scheduledForSynchronization[$class->getRootEntityName()][$oid])) { |
454
|
|
|
$this->scheduleForDirtyCheck($entity); |
455
|
|
|
} |
456
|
|
|
} |
457
|
|
|
|
458
|
|
|
/** |
459
|
|
|
* Persists an entity as part of the current unit of work. |
460
|
|
|
* |
461
|
|
|
* @param object $entity The entity to persist. |
462
|
|
|
* |
463
|
|
|
* @return void |
464
|
7 |
|
*/ |
465
|
|
|
public function persist($entity) |
466
|
7 |
|
{ |
467
|
7 |
|
$visited = []; |
468
|
7 |
|
$this->doPersist($entity, $visited); |
469
|
|
|
} |
470
|
|
|
|
471
|
|
|
/** |
472
|
|
|
* @param ApiMetadata $class |
473
|
|
|
* @param $entity |
474
|
|
|
* |
475
|
|
|
* @throws \InvalidArgumentException |
476
|
|
|
* @throws \RuntimeException |
477
|
2 |
|
*/ |
478
|
|
|
public function recomputeSingleEntityChangeSet(ApiMetadata $class, $entity) |
479
|
2 |
|
{ |
480
|
2 |
|
$oid = spl_object_hash($entity); |
481
|
|
|
if (!isset($this->entityStates[$oid]) || $this->entityStates[$oid] != self::STATE_MANAGED) { |
482
|
|
|
throw new \InvalidArgumentException('Entity is not managed'); |
483
|
|
|
} |
484
|
2 |
|
|
485
|
2 |
|
$actualData = []; |
486
|
2 |
|
foreach ($class->getReflectionProperties() as $name => $refProp) { |
487
|
2 |
|
if (!$class->isIdentifier($name) && !$class->isCollectionValuedAssociation($name)) { |
488
|
2 |
|
$actualData[$name] = $refProp->getValue($entity); |
489
|
2 |
|
} |
490
|
2 |
|
} |
491
|
|
|
if (!isset($this->originalEntityData[$oid])) { |
492
|
|
|
throw new \RuntimeException( |
493
|
|
|
'Cannot call recomputeSingleEntityChangeSet before computeChangeSet on an entity.' |
494
|
|
|
); |
495
|
2 |
|
} |
496
|
2 |
|
$originalData = $this->originalEntityData[$oid]; |
497
|
2 |
|
$changeSet = []; |
498
|
2 |
|
foreach ($actualData as $propName => $actualValue) { |
499
|
2 |
|
$orgValue = isset($originalData->$propName) ? $originalData->$propName : null; |
500
|
|
|
if ($orgValue !== $actualValue) { |
501
|
|
|
$changeSet[$propName] = [$orgValue, $actualValue]; |
502
|
2 |
|
} |
503
|
2 |
|
} |
504
|
|
|
if ($changeSet) { |
|
|
|
|
505
|
|
|
if (isset($this->entityChangeSets[$oid])) { |
506
|
|
|
$this->entityChangeSets[$oid] = array_merge($this->entityChangeSets[$oid], $changeSet); |
507
|
|
|
} else { |
508
|
|
|
if (!isset($this->entityInsertions[$oid])) { |
509
|
|
|
$this->entityChangeSets[$oid] = $changeSet; |
510
|
|
|
$this->entityUpdates[$oid] = $entity; |
511
|
|
|
} |
512
|
|
|
} |
513
|
|
|
$this->originalEntityData[$oid] = (object)$actualData; |
514
|
2 |
|
} |
515
|
|
|
} |
516
|
|
|
|
517
|
|
|
/** |
518
|
|
|
* Schedules an entity for insertion into the database. |
519
|
|
|
* If the entity already has an identifier, it will be added to the identity map. |
520
|
|
|
* |
521
|
|
|
* @param object $entity The entity to schedule for insertion. |
522
|
|
|
* |
523
|
|
|
* @return void |
524
|
|
|
* |
525
|
|
|
* @throws \InvalidArgumentException |
526
|
7 |
|
*/ |
527
|
|
|
public function scheduleForInsert($entity) |
528
|
7 |
|
{ |
529
|
7 |
|
$oid = spl_object_hash($entity); |
530
|
|
|
if (isset($this->entityUpdates[$oid])) { |
531
|
|
|
throw new \InvalidArgumentException('Dirty entity cannot be scheduled for insertion'); |
532
|
7 |
|
} |
533
|
|
|
if (isset($this->entityDeletions[$oid])) { |
534
|
|
|
throw new \InvalidArgumentException('Removed entity scheduled for insertion'); |
535
|
7 |
|
} |
536
|
|
|
if (isset($this->originalEntityData[$oid]) && !isset($this->entityInsertions[$oid])) { |
537
|
|
|
throw new \InvalidArgumentException('Managed entity scheduled for insertion'); |
538
|
7 |
|
} |
539
|
|
|
if (isset($this->entityInsertions[$oid])) { |
540
|
|
|
throw new \InvalidArgumentException('Entity scheduled for insertion twice'); |
541
|
7 |
|
} |
542
|
7 |
|
$this->entityInsertions[$oid] = $entity; |
543
|
|
|
if (isset($this->entityIdentifiers[$oid])) { |
544
|
|
|
$this->addToIdentityMap($entity); |
545
|
7 |
|
} |
546
|
|
|
if ($entity instanceof NotifyPropertyChanged) { |
547
|
|
|
$entity->addPropertyChangedListener($this); |
548
|
7 |
|
} |
549
|
|
|
} |
550
|
|
|
|
551
|
|
|
/** |
552
|
|
|
* Checks whether an entity is scheduled for insertion. |
553
|
|
|
* |
554
|
|
|
* @param object $entity |
555
|
|
|
* |
556
|
|
|
* @return boolean |
557
|
1 |
|
*/ |
558
|
|
|
public function isScheduledForInsert($entity) |
559
|
1 |
|
{ |
560
|
|
|
return isset($this->entityInsertions[spl_object_hash($entity)]); |
561
|
|
|
} |
562
|
|
|
|
563
|
|
|
/** |
564
|
|
|
* Schedules an entity for being updated. |
565
|
|
|
* |
566
|
|
|
* @param object $entity The entity to schedule for being updated. |
567
|
|
|
* |
568
|
|
|
* @return void |
569
|
|
|
* |
570
|
|
|
* @throws \InvalidArgumentException |
571
|
|
|
*/ |
572
|
|
|
public function scheduleForUpdate($entity) |
573
|
|
|
{ |
574
|
|
|
$oid = spl_object_hash($entity); |
575
|
|
|
if (!isset($this->entityIdentifiers[$oid])) { |
576
|
|
|
throw new \InvalidArgumentException('Entity has no identity'); |
577
|
|
|
} |
578
|
|
|
if (isset($this->entityDeletions[$oid])) { |
579
|
|
|
throw new \InvalidArgumentException('Entity is removed'); |
580
|
|
|
} |
581
|
|
|
if (!isset($this->entityUpdates[$oid]) && !isset($this->entityInsertions[$oid])) { |
582
|
|
|
$this->entityUpdates[$oid] = $entity; |
583
|
|
|
} |
584
|
|
|
} |
585
|
|
|
|
586
|
|
|
/** |
587
|
|
|
* Checks whether an entity is registered as dirty in the unit of work. |
588
|
|
|
* Note: Is not very useful currently as dirty entities are only registered |
589
|
|
|
* at commit time. |
590
|
|
|
* |
591
|
|
|
* @param object $entity |
592
|
|
|
* |
593
|
|
|
* @return boolean |
594
|
|
|
*/ |
595
|
|
|
public function isScheduledForUpdate($entity) |
596
|
|
|
{ |
597
|
|
|
return isset($this->entityUpdates[spl_object_hash($entity)]); |
598
|
|
|
} |
599
|
|
|
|
600
|
|
|
/** |
601
|
|
|
* Checks whether an entity is registered to be checked in the unit of work. |
602
|
|
|
* |
603
|
|
|
* @param object $entity |
604
|
|
|
* |
605
|
|
|
* @return boolean |
606
|
|
|
*/ |
607
|
|
|
public function isScheduledForDirtyCheck($entity) |
608
|
|
|
{ |
609
|
|
|
$rootEntityName = $this->manager->getClassMetadata(get_class($entity))->getRootEntityName(); |
610
|
|
|
|
611
|
|
|
return isset($this->scheduledForSynchronization[$rootEntityName][spl_object_hash($entity)]); |
612
|
|
|
} |
613
|
|
|
|
614
|
|
|
/** |
615
|
|
|
* INTERNAL: |
616
|
|
|
* Schedules an entity for deletion. |
617
|
|
|
* |
618
|
|
|
* @param object $entity |
619
|
|
|
* |
620
|
|
|
* @return void |
621
|
|
|
*/ |
622
|
|
|
public function scheduleForDelete($entity) |
623
|
|
|
{ |
624
|
|
|
$oid = spl_object_hash($entity); |
625
|
|
|
if (isset($this->entityInsertions[$oid])) { |
626
|
|
|
if ($this->isInIdentityMap($entity)) { |
627
|
|
|
$this->removeFromIdentityMap($entity); |
628
|
|
|
} |
629
|
|
|
unset($this->entityInsertions[$oid], $this->entityStates[$oid]); |
630
|
|
|
|
631
|
|
|
return; // entity has not been persisted yet, so nothing more to do. |
632
|
|
|
} |
633
|
|
|
if (!$this->isInIdentityMap($entity)) { |
634
|
|
|
return; |
635
|
|
|
} |
636
|
|
|
$this->removeFromIdentityMap($entity); |
637
|
|
|
unset($this->entityUpdates[$oid]); |
638
|
|
|
if (!isset($this->entityDeletions[$oid])) { |
639
|
|
|
$this->entityDeletions[$oid] = $entity; |
640
|
|
|
$this->entityStates[$oid] = self::STATE_REMOVED; |
641
|
|
|
} |
642
|
|
|
} |
643
|
|
|
|
644
|
|
|
/** |
645
|
|
|
* Checks whether an entity is registered as removed/deleted with the unit |
646
|
|
|
* of work. |
647
|
|
|
* |
648
|
|
|
* @param object $entity |
649
|
|
|
* |
650
|
|
|
* @return boolean |
651
|
|
|
*/ |
652
|
|
|
public function isScheduledForDelete($entity) |
653
|
|
|
{ |
654
|
|
|
return isset($this->entityDeletions[spl_object_hash($entity)]); |
655
|
|
|
} |
656
|
|
|
|
657
|
|
|
/** |
658
|
|
|
* Checks whether an entity is scheduled for insertion, update or deletion. |
659
|
|
|
* |
660
|
|
|
* @param object $entity |
661
|
|
|
* |
662
|
|
|
* @return boolean |
663
|
|
|
*/ |
664
|
|
|
public function isEntityScheduled($entity) |
665
|
|
|
{ |
666
|
|
|
$oid = spl_object_hash($entity); |
667
|
|
|
|
668
|
|
|
return isset($this->entityInsertions[$oid]) |
669
|
|
|
|| isset($this->entityUpdates[$oid]) |
670
|
|
|
|| isset($this->entityDeletions[$oid]); |
671
|
|
|
} |
672
|
|
|
|
673
|
|
|
/** |
674
|
|
|
* INTERNAL: |
675
|
|
|
* Removes an entity from the identity map. This effectively detaches the |
676
|
|
|
* entity from the persistence management of Doctrine. |
677
|
|
|
* |
678
|
|
|
* @ignore |
679
|
|
|
* |
680
|
|
|
* @param object $entity |
681
|
|
|
* |
682
|
|
|
* @return boolean |
683
|
|
|
* |
684
|
|
|
* @throws \InvalidArgumentException |
685
|
|
|
*/ |
686
|
|
|
public function removeFromIdentityMap($entity) |
687
|
|
|
{ |
688
|
|
|
$oid = spl_object_hash($entity); |
689
|
|
|
$classMetadata = $this->manager->getClassMetadata(get_class($entity)); |
690
|
|
|
$idHash = implode(' ', $this->entityIdentifiers[$oid]); |
691
|
|
|
if ($idHash === '') { |
692
|
|
|
throw new \InvalidArgumentException('Entity has no identity'); |
693
|
|
|
} |
694
|
|
|
$className = $classMetadata->getRootEntityName(); |
695
|
|
|
if (isset($this->identityMap[$className][$idHash])) { |
696
|
|
|
unset($this->identityMap[$className][$idHash]); |
697
|
|
|
unset($this->readOnlyObjects[$oid]); |
698
|
|
|
|
699
|
|
|
//$this->entityStates[$oid] = self::STATE_DETACHED; |
|
|
|
|
700
|
|
|
return true; |
701
|
|
|
} |
702
|
|
|
|
703
|
|
|
return false; |
704
|
|
|
} |
705
|
|
|
|
706
|
|
|
/** |
707
|
|
|
* Commits the UnitOfWork, executing all operations that have been postponed |
708
|
|
|
* up to this point. The state of all managed entities will be synchronized with |
709
|
|
|
* the database. |
710
|
|
|
* |
711
|
|
|
* The operations are executed in the following order: |
712
|
|
|
* |
713
|
|
|
* 1) All entity insertions |
714
|
|
|
* 2) All entity updates |
715
|
|
|
* 3) All collection deletions |
716
|
|
|
* 4) All collection updates |
717
|
|
|
* 5) All entity deletions |
718
|
|
|
* |
719
|
|
|
* @param null|object|array $entity |
720
|
|
|
* |
721
|
|
|
* @return void |
722
|
|
|
* |
723
|
|
|
* @throws \Exception |
724
|
7 |
|
*/ |
725
|
|
|
public function commit($entity = null) |
726
|
|
|
{ |
727
|
7 |
|
// Compute changes done since last commit. |
728
|
7 |
|
if ($entity === null) { |
729
|
7 |
|
$this->computeChangeSets(); |
730
|
|
|
} elseif (is_object($entity)) { |
731
|
|
|
$this->computeSingleEntityChangeSet($entity); |
732
|
|
|
} elseif (is_array($entity)) { |
733
|
|
|
foreach ((array)$entity as $object) { |
734
|
|
|
$this->computeSingleEntityChangeSet($object); |
735
|
|
|
} |
736
|
7 |
|
} |
737
|
2 |
|
if (!($this->entityInsertions || |
|
|
|
|
738
|
2 |
|
$this->entityDeletions || |
|
|
|
|
739
|
1 |
|
$this->entityUpdates || |
|
|
|
|
740
|
1 |
|
$this->collectionUpdates || |
|
|
|
|
741
|
1 |
|
$this->collectionDeletions || |
|
|
|
|
742
|
7 |
|
$this->orphanRemovals) |
|
|
|
|
743
|
1 |
|
) { |
744
|
|
|
return; // Nothing to do. |
745
|
7 |
|
} |
746
|
|
|
if ($this->orphanRemovals) { |
|
|
|
|
747
|
|
|
foreach ($this->orphanRemovals as $orphan) { |
748
|
|
|
$this->remove($orphan); |
749
|
|
|
} |
750
|
|
|
} |
751
|
7 |
|
// Now we need a commit order to maintain referential integrity |
752
|
|
|
$commitOrder = $this->getCommitOrder(); |
753
|
|
|
|
754
|
|
|
// Collection deletions (deletions of complete collections) |
755
|
|
|
// foreach ($this->collectionDeletions as $collectionToDelete) { |
|
|
|
|
756
|
|
|
// //fixme: collection mutations |
757
|
|
|
// $this->getCollectionPersister($collectionToDelete->getMapping())->delete($collectionToDelete); |
|
|
|
|
758
|
7 |
|
// } |
759
|
7 |
|
if ($this->entityInsertions) { |
|
|
|
|
760
|
7 |
|
foreach ($commitOrder as $class) { |
761
|
7 |
|
$this->executeInserts($class); |
762
|
7 |
|
} |
763
|
7 |
|
} |
764
|
2 |
|
if ($this->entityUpdates) { |
|
|
|
|
765
|
2 |
|
foreach ($commitOrder as $class) { |
766
|
2 |
|
$this->executeUpdates($class); |
767
|
2 |
|
} |
768
|
|
|
} |
769
|
7 |
|
// Extra updates that were requested by persisters. |
770
|
|
|
if ($this->extraUpdates) { |
|
|
|
|
771
|
|
|
$this->executeExtraUpdates(); |
772
|
|
|
} |
773
|
7 |
|
// Collection updates (deleteRows, updateRows, insertRows) |
774
|
|
|
foreach ($this->collectionUpdates as $collectionToUpdate) { |
|
|
|
|
775
|
|
|
//fixme: decide what to do with collection mutation if API does not support this |
776
|
7 |
|
//$this->getCollectionPersister($collectionToUpdate->getMapping())->update($collectionToUpdate); |
|
|
|
|
777
|
|
|
} |
778
|
7 |
|
// Entity deletions come last and need to be in reverse commit order |
779
|
|
|
if ($this->entityDeletions) { |
|
|
|
|
780
|
|
|
for ($count = count($commitOrder), $i = $count - 1; $i >= 0 && $this->entityDeletions; --$i) { |
|
|
|
|
781
|
|
|
$this->executeDeletions($commitOrder[$i]); |
782
|
|
|
} |
783
|
|
|
} |
784
|
|
|
|
785
|
7 |
|
// Take new snapshots from visited collections |
786
|
2 |
|
foreach ($this->visitedCollections as $coll) { |
787
|
7 |
|
$coll->takeSnapshot(); |
788
|
|
|
} |
789
|
|
|
|
790
|
7 |
|
// Clear up |
791
|
7 |
|
$this->entityInsertions = |
792
|
7 |
|
$this->entityUpdates = |
793
|
7 |
|
$this->entityDeletions = |
794
|
7 |
|
$this->extraUpdates = |
795
|
7 |
|
$this->entityChangeSets = |
796
|
7 |
|
$this->collectionUpdates = |
797
|
7 |
|
$this->collectionDeletions = |
798
|
7 |
|
$this->visitedCollections = |
799
|
7 |
|
$this->scheduledForSynchronization = |
800
|
7 |
|
$this->orphanRemovals = []; |
801
|
|
|
} |
802
|
|
|
|
803
|
|
|
/** |
804
|
|
|
* Gets the changeset for an entity. |
805
|
|
|
* |
806
|
|
|
* @param object $entity |
807
|
|
|
* |
808
|
|
|
* @return array |
809
|
2 |
|
*/ |
810
|
|
|
public function & getEntityChangeSet($entity) |
811
|
2 |
|
{ |
812
|
2 |
|
$oid = spl_object_hash($entity); |
813
|
2 |
|
$data = []; |
814
|
|
|
if (!isset($this->entityChangeSets[$oid])) { |
815
|
|
|
return $data; |
816
|
|
|
} |
817
|
2 |
|
|
818
|
|
|
return $this->entityChangeSets[$oid]; |
819
|
|
|
} |
820
|
|
|
|
821
|
|
|
/** |
822
|
|
|
* Computes the changes that happened to a single entity. |
823
|
|
|
* |
824
|
|
|
* Modifies/populates the following properties: |
825
|
|
|
* |
826
|
|
|
* {@link _originalEntityData} |
827
|
|
|
* If the entity is NEW or MANAGED but not yet fully persisted (only has an id) |
828
|
|
|
* then it was not fetched from the database and therefore we have no original |
829
|
|
|
* entity data yet. All of the current entity data is stored as the original entity data. |
830
|
|
|
* |
831
|
|
|
* {@link _entityChangeSets} |
832
|
|
|
* The changes detected on all properties of the entity are stored there. |
833
|
|
|
* A change is a tuple array where the first entry is the old value and the second |
834
|
|
|
* entry is the new value of the property. Changesets are used by persisters |
835
|
|
|
* to INSERT/UPDATE the persistent entity state. |
836
|
|
|
* |
837
|
|
|
* {@link _entityUpdates} |
838
|
|
|
* If the entity is already fully MANAGED (has been fetched from the database before) |
839
|
|
|
* and any changes to its properties are detected, then a reference to the entity is stored |
840
|
|
|
* there to mark it for an update. |
841
|
|
|
* |
842
|
|
|
* {@link _collectionDeletions} |
843
|
|
|
* If a PersistentCollection has been de-referenced in a fully MANAGED entity, |
844
|
|
|
* then this collection is marked for deletion. |
845
|
|
|
* |
846
|
|
|
* @ignore |
847
|
|
|
* |
848
|
|
|
* @internal Don't call from the outside. |
849
|
|
|
* |
850
|
|
|
* @param ApiMetadata $class The class descriptor of the entity. |
851
|
|
|
* @param object $entity The entity for which to compute the changes. |
852
|
|
|
* |
853
|
|
|
* @return void |
854
|
7 |
|
*/ |
855
|
|
|
public function computeChangeSet(ApiMetadata $class, $entity) |
856
|
7 |
|
{ |
857
|
7 |
|
$oid = spl_object_hash($entity); |
858
|
|
|
if (isset($this->readOnlyObjects[$oid])) { |
859
|
|
|
return; |
860
|
|
|
} |
861
|
7 |
|
|
862
|
7 |
|
$actualData = []; |
863
|
7 |
|
foreach ($class->getReflectionProperties() as $name => $refProp) { |
864
|
7 |
|
$value = $refProp->getValue($entity); |
865
|
2 |
|
if (null !== $value && $class->isCollectionValuedAssociation($name)) { |
866
|
1 |
|
if ($value instanceof ApiCollection) { |
867
|
1 |
|
if ($value->getOwner() === $entity) { |
868
|
|
|
continue; |
869
|
|
|
} |
870
|
|
|
$value = new ArrayCollection($value->getValues()); |
871
|
|
|
} |
872
|
2 |
|
// If $value is not a Collection then use an ArrayCollection. |
873
|
|
|
if (!$value instanceof Collection) { |
874
|
|
|
$value = new ArrayCollection($value); |
875
|
2 |
|
} |
876
|
|
|
$assoc = $class->getAssociationMapping($name); |
877
|
2 |
|
// Inject PersistentCollection |
878
|
2 |
|
$value = new ApiCollection( |
879
|
2 |
|
$this->manager, |
880
|
|
|
$this->manager->getClassMetadata($assoc['target']), |
881
|
2 |
|
$value |
882
|
2 |
|
); |
883
|
2 |
|
$value->setOwner($entity, $assoc); |
884
|
2 |
|
$value->setDirty(!$value->isEmpty()); |
885
|
2 |
|
$class->getReflectionProperty($name)->setValue($entity, $value); |
886
|
2 |
|
$actualData[$name] = $value; |
887
|
|
|
continue; |
888
|
7 |
|
} |
889
|
7 |
|
if (!$class->isIdentifier($name)) { |
890
|
7 |
|
$actualData[$name] = $value; |
891
|
7 |
|
} |
892
|
7 |
|
} |
893
|
|
|
if (!isset($this->originalEntityData[$oid])) { |
894
|
|
|
// Entity is either NEW or MANAGED but not yet fully persisted (only has an id). |
895
|
7 |
|
// These result in an INSERT. |
896
|
7 |
|
$this->originalEntityData[$oid] = (object)$actualData; |
897
|
7 |
|
$changeSet = []; |
898
|
7 |
|
foreach ($actualData as $propName => $actualValue) { |
899
|
7 |
View Code Duplication |
if (!$class->hasAssociation($propName)) { |
|
|
|
|
900
|
7 |
|
$changeSet[$propName] = [null, $actualValue]; |
901
|
|
|
continue; |
902
|
5 |
|
} |
903
|
5 |
|
$assoc = $class->getAssociationMapping($propName); |
904
|
5 |
|
if ($assoc['isOwningSide'] && $assoc['type'] & ApiMetadata::TO_ONE) { |
905
|
5 |
|
$changeSet[$propName] = [null, $actualValue]; |
906
|
7 |
|
} |
907
|
7 |
|
} |
908
|
7 |
|
$this->entityChangeSets[$oid] = $changeSet; |
909
|
|
|
} else { |
910
|
|
|
|
911
|
|
|
// Entity is "fully" MANAGED: it was already fully persisted before |
912
|
3 |
|
// and we have a copy of the original data |
913
|
3 |
|
$originalData = $this->originalEntityData[$oid]; |
914
|
3 |
|
$isChangeTrackingNotify = false; |
915
|
3 |
|
$changeSet = ($isChangeTrackingNotify && isset($this->entityChangeSets[$oid])) |
916
|
3 |
|
? $this->entityChangeSets[$oid] |
917
|
|
|
: []; |
918
|
3 |
|
|
919
|
|
|
foreach ($actualData as $propName => $actualValue) { |
920
|
3 |
|
// skip field, its a partially omitted one! |
921
|
|
|
if (!property_exists($originalData, $propName)) { |
922
|
|
|
continue; |
923
|
3 |
|
} |
924
|
|
|
$orgValue = $originalData->$propName; |
925
|
3 |
|
// skip if value haven't changed |
926
|
|
|
if ($orgValue === $actualValue) { |
927
|
3 |
|
|
928
|
|
|
continue; |
929
|
|
|
} |
930
|
2 |
|
// if regular field |
931
|
1 |
View Code Duplication |
if (!$class->hasAssociation($propName)) { |
|
|
|
|
932
|
|
|
if ($isChangeTrackingNotify) { |
933
|
|
|
continue; |
934
|
1 |
|
} |
935
|
1 |
|
$changeSet[$propName] = [$orgValue, $actualValue]; |
936
|
|
|
continue; |
937
|
|
|
} |
938
|
1 |
|
|
939
|
|
|
$assoc = $class->getAssociationMapping($propName); |
940
|
|
|
// Persistent collection was exchanged with the "originally" |
941
|
|
|
// created one. This can only mean it was cloned and replaced |
942
|
1 |
|
// on another entity. |
943
|
|
|
if ($actualValue instanceof ApiCollection) { |
944
|
|
|
$owner = $actualValue->getOwner(); |
945
|
|
|
if ($owner === null) { // cloned |
946
|
|
|
$actualValue->setOwner($entity, $assoc); |
947
|
|
|
} else { |
948
|
|
|
if ($owner !== $entity) { // no clone, we have to fix |
949
|
|
|
if (!$actualValue->isInitialized()) { |
950
|
|
|
$actualValue->initialize(); // we have to do this otherwise the cols share state |
951
|
|
|
} |
952
|
|
|
$newValue = clone $actualValue; |
953
|
|
|
$newValue->setOwner($entity, $assoc); |
954
|
|
|
$class->getReflectionProperty($propName)->setValue($entity, $newValue); |
955
|
|
|
} |
956
|
|
|
} |
957
|
1 |
|
} |
958
|
|
|
if ($orgValue instanceof ApiCollection) { |
959
|
|
|
// A PersistentCollection was de-referenced, so delete it. |
960
|
|
|
$coid = spl_object_hash($orgValue); |
961
|
|
|
if (isset($this->collectionDeletions[$coid])) { |
962
|
|
|
continue; |
963
|
|
|
} |
964
|
|
|
$this->collectionDeletions[$coid] = $orgValue; |
965
|
|
|
$changeSet[$propName] = $orgValue; // Signal changeset, to-many assocs will be ignored. |
966
|
|
|
continue; |
967
|
1 |
|
} |
968
|
1 |
|
if ($assoc['type'] & ApiMetadata::TO_ONE) { |
969
|
1 |
|
if ($assoc['isOwningSide']) { |
970
|
1 |
|
$changeSet[$propName] = [$orgValue, $actualValue]; |
971
|
1 |
|
} |
972
|
|
|
if ($orgValue !== null && $assoc['orphanRemoval']) { |
973
|
|
|
$this->scheduleOrphanRemoval($orgValue); |
974
|
1 |
|
} |
975
|
3 |
|
} |
976
|
3 |
|
} |
977
|
2 |
|
if ($changeSet) { |
978
|
2 |
|
$this->entityChangeSets[$oid] = $changeSet; |
979
|
2 |
|
$this->originalEntityData[$oid] = (object)$actualData; |
980
|
2 |
|
$this->entityUpdates[$oid] = $entity; |
981
|
|
|
} |
982
|
|
|
} |
983
|
7 |
|
// Look for changes in associations of the entity |
984
|
5 |
|
foreach ($class->getAssociationMappings() as $field => $assoc) { |
985
|
5 |
|
if (($val = $class->getReflectionProperty($field)->getValue($entity)) === null) { |
986
|
|
|
continue; |
987
|
2 |
|
} |
988
|
2 |
|
$this->computeAssociationChanges($assoc, $val); |
989
|
2 |
|
if (!isset($this->entityChangeSets[$oid]) && |
990
|
2 |
|
$assoc['isOwningSide'] && |
991
|
2 |
|
$assoc['type'] == ApiMetadata::MANY_TO_MANY && |
992
|
|
|
$val instanceof ApiCollection && |
993
|
2 |
|
$val->isDirty() |
994
|
|
|
) { |
995
|
|
|
$this->entityChangeSets[$oid] = []; |
996
|
|
|
$this->originalEntityData[$oid] = (object)$actualData; |
997
|
|
|
$this->entityUpdates[$oid] = $entity; |
998
|
7 |
|
} |
999
|
7 |
|
} |
1000
|
|
|
} |
1001
|
|
|
|
1002
|
|
|
/** |
1003
|
|
|
* Computes all the changes that have been done to entities and collections |
1004
|
|
|
* since the last commit and stores these changes in the _entityChangeSet map |
1005
|
|
|
* temporarily for access by the persisters, until the UoW commit is finished. |
1006
|
|
|
* |
1007
|
|
|
* @return void |
1008
|
7 |
|
*/ |
1009
|
|
|
public function computeChangeSets() |
1010
|
|
|
{ |
1011
|
7 |
|
// Compute changes for INSERTed entities first. This must always happen. |
1012
|
|
|
$this->computeScheduleInsertsChangeSets(); |
1013
|
7 |
|
// Compute changes for other MANAGED entities. Change tracking policies take effect here. |
1014
|
3 |
|
foreach ($this->identityMap as $className => $entities) { |
1015
|
|
|
$class = $this->manager->getClassMetadata($className); |
1016
|
3 |
|
// Skip class if instances are read-only |
1017
|
|
|
if ($class->isReadOnly()) { |
1018
|
|
|
continue; |
1019
|
|
|
} |
1020
|
|
|
// If change tracking is explicit or happens through notification, then only compute |
1021
|
3 |
|
// changes on entities of that type that are explicitly marked for synchronization. |
1022
|
3 |
|
switch (true) { |
1023
|
3 |
|
case ($class->isChangeTrackingDeferredImplicit()): |
1024
|
3 |
|
$entitiesToProcess = $entities; |
1025
|
|
|
break; |
1026
|
|
|
case (isset($this->scheduledForSynchronization[$className])): |
1027
|
|
|
$entitiesToProcess = $this->scheduledForSynchronization[$className]; |
1028
|
|
|
break; |
1029
|
|
|
default: |
1030
|
|
|
$entitiesToProcess = []; |
1031
|
3 |
|
} |
1032
|
|
|
foreach ($entitiesToProcess as $entity) { |
1033
|
3 |
|
// Ignore uninitialized proxy objects |
1034
|
|
|
if ($entity instanceof Proxy && !$entity->__isInitialized__) { |
|
|
|
|
1035
|
|
|
continue; |
1036
|
|
|
} |
1037
|
3 |
|
// Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here. |
1038
|
3 |
|
$oid = spl_object_hash($entity); |
1039
|
3 |
View Code Duplication |
if (!isset($this->entityInsertions[$oid]) && |
|
|
|
|
1040
|
3 |
|
!isset($this->entityDeletions[$oid]) && |
1041
|
3 |
|
isset($this->entityStates[$oid]) |
1042
|
3 |
|
) { |
1043
|
3 |
|
$this->computeChangeSet($class, $entity); |
1044
|
3 |
|
} |
1045
|
7 |
|
} |
1046
|
7 |
|
} |
1047
|
|
|
} |
1048
|
|
|
|
1049
|
|
|
/** |
1050
|
|
|
* INTERNAL: |
1051
|
|
|
* Schedules an orphaned entity for removal. The remove() operation will be |
1052
|
|
|
* invoked on that entity at the beginning of the next commit of this |
1053
|
|
|
* UnitOfWork. |
1054
|
|
|
* |
1055
|
|
|
* @ignore |
1056
|
|
|
* |
1057
|
|
|
* @param object $entity |
1058
|
|
|
* |
1059
|
|
|
* @return void |
1060
|
|
|
*/ |
1061
|
|
|
public function scheduleOrphanRemoval($entity) |
1062
|
|
|
{ |
1063
|
|
|
$this->orphanRemovals[spl_object_hash($entity)] = $entity; |
1064
|
|
|
} |
1065
|
2 |
|
|
1066
|
|
|
public function loadCollection(ApiCollection $collection) |
1067
|
2 |
|
{ |
1068
|
2 |
|
$assoc = $collection->getMapping(); |
1069
|
2 |
|
$persister = $this->getEntityPersister($assoc['target']); |
1070
|
2 |
|
switch ($assoc['type']) { |
1071
|
2 |
|
case ApiMetadata::ONE_TO_MANY: |
1072
|
2 |
|
$persister->loadOneToManyCollection($assoc, $collection->getOwner(), $collection); |
1073
|
2 |
|
break; |
1074
|
2 |
|
} |
1075
|
2 |
|
$collection->setInitialized(true); |
1076
|
|
|
} |
1077
|
1 |
|
|
1078
|
|
|
public function getCollectionPersister($association) |
1079
|
1 |
|
{ |
1080
|
1 |
|
$targetMetadata = $this->manager->getClassMetadata($association['target']); |
1081
|
|
|
$role = $association['sourceEntity'] . '::' . $association['field']; |
1082
|
1 |
|
|
1083
|
1 |
|
if (!array_key_exists($role, $this->collectionPersisters)) { |
1084
|
1 |
|
$this->collectionPersisters[$role] = new CollectionPersister( |
1085
|
1 |
|
$targetMetadata, |
1086
|
|
|
new CollectionMatcher($this->manager, $this->getCrudsApi($targetMetadata)), |
1087
|
1 |
|
$association |
1088
|
1 |
|
); |
1089
|
|
|
} |
1090
|
1 |
|
|
1091
|
|
|
return $this->collectionPersisters[$role]; |
1092
|
|
|
} |
1093
|
|
|
|
1094
|
|
|
public function scheduleCollectionDeletion(Collection $collection) |
|
|
|
|
1095
|
|
|
{ |
1096
|
|
|
} |
1097
|
|
|
|
1098
|
|
|
public function cancelOrphanRemoval($value) |
|
|
|
|
1099
|
|
|
{ |
1100
|
|
|
} |
1101
|
|
|
|
1102
|
|
|
/** |
1103
|
|
|
* INTERNAL: |
1104
|
|
|
* Sets a property value of the original data array of an entity. |
1105
|
|
|
* |
1106
|
|
|
* @ignore |
1107
|
2 |
|
* |
1108
|
|
|
* @param string $oid |
1109
|
2 |
|
* @param string $property |
1110
|
|
|
* @param mixed $value |
1111
|
|
|
* |
1112
|
|
|
* @return void |
1113
|
|
|
*/ |
1114
|
|
|
public function setOriginalEntityProperty($oid, $property, $value) |
1115
|
|
|
{ |
1116
|
|
|
if (!array_key_exists($oid, $this->originalEntityData)) { |
1117
|
|
|
$this->originalEntityData[$oid] = new \stdClass(); |
1118
|
|
|
} |
1119
|
|
|
|
1120
|
|
|
$this->originalEntityData[$oid]->$property = $value; |
1121
|
|
|
} |
1122
|
|
|
|
1123
|
11 |
|
public function scheduleExtraUpdate($entity, $changeset) |
1124
|
|
|
{ |
1125
|
11 |
|
$oid = spl_object_hash($entity); |
1126
|
11 |
|
$extraUpdate = [$entity, $changeset]; |
1127
|
11 |
|
if (isset($this->extraUpdates[$oid])) { |
1128
|
|
|
list(, $changeset2) = $this->extraUpdates[$oid]; |
1129
|
11 |
|
$extraUpdate = [$entity, $changeset + $changeset2]; |
1130
|
11 |
|
} |
1131
|
|
|
$this->extraUpdates[$oid] = $extraUpdate; |
1132
|
|
|
} |
1133
|
|
|
|
1134
|
|
|
/** |
1135
|
|
|
* Refreshes the state of the given entity from the database, overwriting |
1136
|
|
|
* any local, unpersisted changes. |
1137
|
|
|
* |
1138
|
|
|
* @param object $entity The entity to refresh. |
1139
|
|
|
* |
1140
|
|
|
* @return void |
1141
|
|
|
* |
1142
|
|
|
* @throws InvalidArgumentException If the entity is not MANAGED. |
1143
|
|
|
*/ |
1144
|
|
|
public function refresh($entity) |
1145
|
|
|
{ |
1146
|
|
|
$visited = []; |
1147
|
|
|
$this->doRefresh($entity, $visited); |
1148
|
|
|
} |
1149
|
|
|
|
1150
|
|
|
/** |
1151
|
|
|
* Clears the UnitOfWork. |
1152
|
|
|
* |
1153
|
|
|
* @param string|null $entityName if given, only entities of this type will get detached. |
1154
|
|
|
* |
1155
|
|
|
* @return void |
1156
|
|
|
*/ |
1157
|
|
|
public function clear($entityName = null) |
1158
|
|
|
{ |
1159
|
|
|
if ($entityName === null) { |
1160
|
|
|
$this->identityMap = |
1161
|
|
|
$this->entityIdentifiers = |
1162
|
|
|
$this->originalEntityData = |
1163
|
|
|
$this->entityChangeSets = |
1164
|
|
|
$this->entityStates = |
1165
|
|
|
$this->scheduledForSynchronization = |
1166
|
|
|
$this->entityInsertions = |
1167
|
|
|
$this->entityUpdates = |
1168
|
|
|
$this->entityDeletions = |
1169
|
|
|
$this->collectionDeletions = |
1170
|
|
|
$this->collectionUpdates = |
1171
|
|
|
$this->extraUpdates = |
1172
|
|
|
$this->readOnlyObjects = |
1173
|
|
|
$this->visitedCollections = |
1174
|
|
|
$this->orphanRemovals = []; |
1175
|
|
|
} else { |
1176
|
|
|
$this->clearIdentityMapForEntityName($entityName); |
|
|
|
|
1177
|
|
|
$this->clearEntityInsertionsForEntityName($entityName); |
|
|
|
|
1178
|
|
|
} |
1179
|
|
|
} |
1180
|
|
|
|
1181
|
|
|
/** |
1182
|
|
|
* @param PersistentCollection $coll |
1183
|
|
|
* |
1184
|
|
|
* @return bool |
1185
|
|
|
*/ |
1186
|
|
|
public function isCollectionScheduledForDeletion(PersistentCollection $coll) |
1187
|
|
|
{ |
1188
|
|
|
return isset($this->collectionDeletions[spl_object_hash($coll)]); |
1189
|
|
|
} |
1190
|
|
|
|
1191
|
|
|
/** |
1192
|
|
|
* Schedules an entity for dirty-checking at commit-time. |
1193
|
|
|
* |
1194
|
|
|
* @param object $entity The entity to schedule for dirty-checking. |
1195
|
|
|
* |
1196
|
|
|
* @return void |
1197
|
|
|
* |
1198
|
|
|
* @todo Rename: scheduleForSynchronization |
1199
|
|
|
*/ |
1200
|
|
|
public function scheduleForDirtyCheck($entity) |
1201
|
|
|
{ |
1202
|
|
|
$rootClassName = |
1203
|
|
|
$this->manager->getClassMetadata(get_class($entity))->getRootEntityName(); |
1204
|
|
|
$this->scheduledForSynchronization[$rootClassName][spl_object_hash($entity)] = $entity; |
1205
|
|
|
} |
1206
|
|
|
|
1207
|
|
|
/** |
1208
|
|
|
* Deletes an entity as part of the current unit of work. |
1209
|
|
|
* |
1210
|
|
|
* @param object $entity The entity to remove. |
1211
|
|
|
* |
1212
|
|
|
* @return void |
1213
|
|
|
*/ |
1214
|
|
|
public function remove($entity) |
1215
|
|
|
{ |
1216
|
|
|
$visited = []; |
1217
|
|
|
$this->doRemove($entity, $visited); |
1218
|
|
|
} |
1219
|
|
|
|
1220
|
|
|
/** |
1221
|
|
|
* Merges the state of the given detached entity into this UnitOfWork. |
1222
|
|
|
* |
1223
|
|
|
* @param object $entity |
1224
|
|
|
* |
1225
|
|
|
* @return object The managed copy of the entity. |
1226
|
|
|
*/ |
1227
|
|
|
public function merge($entity) |
1228
|
|
|
{ |
1229
|
|
|
$visited = []; |
1230
|
|
|
|
1231
|
|
|
return $this->doMerge($entity, $visited); |
1232
|
|
|
} |
1233
|
|
|
|
1234
|
|
|
/** |
1235
|
|
|
* Detaches an entity from the persistence management. It's persistence will |
1236
|
|
|
* no longer be managed by Doctrine. |
1237
|
|
|
* |
1238
|
|
|
* @param object $entity The entity to detach. |
1239
|
|
|
* |
1240
|
|
|
* @return void |
1241
|
|
|
*/ |
1242
|
|
|
public function detach($entity) |
1243
|
|
|
{ |
1244
|
|
|
$visited = []; |
1245
|
|
|
$this->doDetach($entity, $visited); |
1246
|
|
|
} |
1247
|
|
|
|
1248
|
|
|
/** |
1249
|
|
|
* Resolve metadata against source data and root class |
1250
|
|
|
* |
1251
|
|
|
* @param \stdClass $data |
1252
|
|
|
* @param string $class |
1253
|
|
|
* |
1254
|
|
|
* @return ApiMetadata |
1255
|
|
|
* @throws MappingException |
1256
|
|
|
*/ |
1257
|
|
|
private function resolveSourceMetadataForClass(\stdClass $data, $class) |
1258
|
|
|
{ |
1259
|
|
|
$metadata = $this->manager->getClassMetadata($class); |
1260
|
|
|
$discriminatorValue = $metadata->getDiscriminatorValue(); |
1261
|
|
|
if ($metadata->getDiscriminatorField()) { |
1262
|
|
|
$property = $metadata->getDiscriminatorField()['fieldName']; |
1263
|
|
|
if (isset($data->$property)) { |
1264
|
|
|
$discriminatorValue = $data->$property; |
1265
|
|
|
} |
1266
|
18 |
|
} |
1267
|
|
|
|
1268
|
18 |
|
$map = $metadata->getDiscriminatorMap(); |
1269
|
18 |
|
|
1270
|
18 |
|
if (!array_key_exists($discriminatorValue, $map)) { |
1271
|
2 |
|
throw MappingException::unknownDiscriminatorValue($discriminatorValue, $class); |
1272
|
2 |
|
} |
1273
|
2 |
|
|
1274
|
2 |
|
$realClass = $map[$discriminatorValue]; |
1275
|
2 |
|
|
1276
|
|
|
return $this->manager->getClassMetadata($realClass); |
1277
|
18 |
|
} |
1278
|
|
|
|
1279
|
18 |
|
/** |
1280
|
|
|
* Helper method to show an object as string. |
1281
|
|
|
* |
1282
|
|
|
* @param object $obj |
1283
|
18 |
|
* |
1284
|
|
|
* @return string |
1285
|
18 |
|
*/ |
1286
|
|
|
private static function objToStr($obj) |
1287
|
|
|
{ |
1288
|
|
|
return method_exists($obj, '__toString') ? (string)$obj : get_class($obj) . '@' . spl_object_hash($obj); |
1289
|
|
|
} |
1290
|
|
|
|
1291
|
|
|
/** |
1292
|
|
|
* @param ApiMetadata $class |
1293
|
|
|
* |
1294
|
|
|
* @return \Doctrine\Common\Persistence\ObjectManagerAware|object |
1295
|
|
|
*/ |
1296
|
|
|
private function newInstance(ApiMetadata $class) |
1297
|
|
|
{ |
1298
|
|
|
$entity = $class->newInstance(); |
1299
|
|
|
|
1300
|
|
|
if ($entity instanceof ObjectManagerAware) { |
1301
|
|
|
$entity->injectObjectManager($this->manager, $class); |
1302
|
|
|
} |
1303
|
|
|
|
1304
|
|
|
return $entity; |
1305
|
17 |
|
} |
1306
|
|
|
|
1307
|
17 |
|
/** |
1308
|
|
|
* @param ApiMetadata $classMetadata |
1309
|
17 |
|
* |
1310
|
|
|
* @return EntityDataCacheInterface |
1311
|
|
|
*/ |
1312
|
|
|
private function createEntityCache(ApiMetadata $classMetadata) |
1313
|
17 |
|
{ |
1314
|
|
|
$configuration = $this->manager->getConfiguration()->getCacheConfiguration($classMetadata->getName()); |
1315
|
|
|
$cache = new VoidEntityCache($classMetadata); |
1316
|
|
|
if ($configuration->isEnabled() && $this->manager->getConfiguration()->getApiCache()) { |
1317
|
|
|
$cache = |
1318
|
|
|
new LoggingCache( |
1319
|
|
|
new ApiEntityCache( |
1320
|
|
|
$this->manager->getConfiguration()->getApiCache(), |
1321
|
26 |
|
$classMetadata, |
1322
|
|
|
$configuration |
1323
|
26 |
|
), |
1324
|
26 |
|
$this->manager->getConfiguration()->getApiCacheLogger() |
1325
|
26 |
|
); |
1326
|
|
|
|
1327
|
1 |
|
return $cache; |
1328
|
1 |
|
} |
1329
|
1 |
|
|
1330
|
1 |
|
return $cache; |
1331
|
|
|
} |
1332
|
1 |
|
|
1333
|
1 |
|
/** |
1334
|
1 |
|
* @param ApiMetadata $classMetadata |
1335
|
|
|
* |
1336
|
1 |
|
* @return CrudsApiInterface |
1337
|
|
|
*/ |
1338
|
|
|
private function getCrudsApi(ApiMetadata $classMetadata) |
1339
|
25 |
|
{ |
1340
|
|
|
if (!array_key_exists($classMetadata->getName(), $this->apis)) { |
1341
|
|
|
$client = $this->manager->getConfiguration()->getClientRegistry()->get($classMetadata->getClientName()); |
1342
|
|
|
|
1343
|
|
|
$api = $this->manager |
1344
|
|
|
->getConfiguration() |
1345
|
|
|
->getFactoryRegistry() |
1346
|
|
|
->create( |
1347
|
26 |
|
$classMetadata->getApiFactory(), |
1348
|
|
|
$client, |
1349
|
26 |
|
$classMetadata |
1350
|
26 |
|
); |
1351
|
|
|
|
1352
|
26 |
|
$this->apis[$classMetadata->getName()] = $api; |
1353
|
26 |
|
} |
1354
|
26 |
|
|
1355
|
26 |
|
return $this->apis[$classMetadata->getName()]; |
1356
|
26 |
|
} |
1357
|
26 |
|
|
1358
|
|
|
private function doPersist($entity, $visited) |
1359
|
26 |
|
{ |
1360
|
|
|
$oid = spl_object_hash($entity); |
1361
|
26 |
|
if (isset($visited[$oid])) { |
1362
|
26 |
|
return; // Prevent infinite recursion |
1363
|
|
|
} |
1364
|
26 |
|
$visited[$oid] = $entity; // Mark visited |
1365
|
|
|
$class = $this->manager->getClassMetadata(get_class($entity)); |
1366
|
|
|
// We assume NEW, so DETACHED entities result in an exception on flush (constraint violation). |
1367
|
7 |
|
// If we would detect DETACHED here we would throw an exception anyway with the same |
1368
|
|
|
// consequences (not recoverable/programming error), so just assuming NEW here |
1369
|
7 |
|
// lets us avoid some database lookups for entities with natural identifiers. |
1370
|
7 |
|
$entityState = $this->getEntityState($entity, self::STATE_NEW); |
1371
|
|
|
switch ($entityState) { |
1372
|
|
|
case self::STATE_MANAGED: |
1373
|
7 |
|
$this->scheduleForDirtyCheck($entity); |
1374
|
7 |
|
break; |
1375
|
|
|
case self::STATE_NEW: |
1376
|
|
|
$this->persistNew($class, $entity); |
1377
|
|
|
break; |
1378
|
|
|
case self::STATE_REMOVED: |
1379
|
7 |
|
// Entity becomes managed again |
1380
|
|
|
unset($this->entityDeletions[$oid]); |
1381
|
7 |
|
$this->addToIdentityMap($entity); |
1382
|
|
|
$this->entityStates[$oid] = self::STATE_MANAGED; |
1383
|
|
|
break; |
1384
|
7 |
|
case self::STATE_DETACHED: |
1385
|
7 |
|
// Can actually not happen right now since we assume STATE_NEW. |
1386
|
7 |
|
throw new \InvalidArgumentException('Detached entity cannot be persisted'); |
1387
|
|
|
default: |
1388
|
|
|
throw new \UnexpectedValueException("Unexpected entity state: $entityState." . self::objToStr($entity)); |
1389
|
|
|
} |
1390
|
|
|
$this->cascadePersist($entity, $visited); |
1391
|
|
|
} |
1392
|
|
|
|
1393
|
|
|
/** |
1394
|
|
|
* Cascades the save operation to associated entities. |
1395
|
|
|
* |
1396
|
|
|
* @param object $entity |
1397
|
|
|
* @param array $visited |
1398
|
|
|
* |
1399
|
7 |
|
* @return void |
1400
|
7 |
|
* @throws \InvalidArgumentException |
1401
|
|
|
* @throws MappingException |
1402
|
|
|
*/ |
1403
|
|
|
private function cascadePersist($entity, array &$visited) |
1404
|
|
|
{ |
1405
|
|
|
$class = $this->manager->getClassMetadata(get_class($entity)); |
1406
|
|
|
$associationMappings = []; |
1407
|
|
|
foreach ($class->getAssociationNames() as $name) { |
1408
|
|
|
$assoc = $class->getAssociationMapping($name); |
1409
|
|
|
if ($assoc['isCascadePersist']) { |
1410
|
|
|
$associationMappings[$name] = $assoc; |
1411
|
|
|
} |
1412
|
7 |
|
} |
1413
|
|
|
foreach ($associationMappings as $assoc) { |
1414
|
7 |
|
$relatedEntities = $class->getReflectionProperty($assoc['field'])->getValue($entity); |
1415
|
7 |
|
switch (true) { |
1416
|
7 |
|
case ($relatedEntities instanceof ApiCollection): |
1417
|
5 |
|
// Unwrap so that foreach() does not initialize |
1418
|
5 |
|
$relatedEntities = $relatedEntities->unwrap(); |
1419
|
|
|
// break; is commented intentionally! |
1420
|
|
|
case ($relatedEntities instanceof Collection): |
1421
|
7 |
|
case (is_array($relatedEntities)): |
1422
|
7 |
|
if (($assoc['type'] & ApiMetadata::TO_MANY) <= 0) { |
1423
|
|
|
throw new \InvalidArgumentException('Invalid association for cascade'); |
1424
|
|
|
} |
1425
|
|
|
foreach ($relatedEntities as $relatedEntity) { |
1426
|
|
|
$this->doPersist($relatedEntity, $visited); |
1427
|
|
|
} |
1428
|
|
|
break; |
1429
|
|
|
case ($relatedEntities !== null): |
1430
|
|
|
if (!$relatedEntities instanceof $assoc['target']) { |
1431
|
|
|
throw new \InvalidArgumentException('Invalid association for cascade'); |
1432
|
|
|
} |
1433
|
|
|
$this->doPersist($relatedEntities, $visited); |
1434
|
|
|
break; |
1435
|
|
|
default: |
1436
|
|
|
// Do nothing |
1437
|
|
|
} |
1438
|
|
|
} |
1439
|
|
|
} |
1440
|
|
|
|
1441
|
|
|
/** |
1442
|
|
|
* @param ApiMetadata $class |
1443
|
|
|
* @param object $entity |
1444
|
|
|
* |
1445
|
|
|
* @return void |
1446
|
|
|
*/ |
1447
|
7 |
|
private function persistNew($class, $entity) |
|
|
|
|
1448
|
7 |
|
{ |
1449
|
|
|
$oid = spl_object_hash($entity); |
1450
|
|
|
// $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::prePersist); |
|
|
|
|
1451
|
|
|
// if ($invoke !== ListenersInvoker::INVOKE_NONE) { |
|
|
|
|
1452
|
|
|
// $this->listenersInvoker->invoke($class, Events::prePersist, $entity, new LifecycleEventArgs($entity, $this->em), $invoke); |
|
|
|
|
1453
|
|
|
// } |
1454
|
|
|
// $idGen = $class->idGenerator; |
|
|
|
|
1455
|
|
|
// if ( ! $idGen->isPostInsertGenerator()) { |
|
|
|
|
1456
|
7 |
|
// $idValue = $idGen->generate($this->em, $entity); |
|
|
|
|
1457
|
|
|
// if ( ! $idGen instanceof \Doctrine\ORM\Id\AssignedGenerator) { |
|
|
|
|
1458
|
7 |
|
// $idValue = array($class->identifier[0] => $idValue); |
|
|
|
|
1459
|
|
|
// $class->setIdentifierValues($entity, $idValue); |
|
|
|
|
1460
|
|
|
// } |
1461
|
|
|
// $this->entityIdentifiers[$oid] = $idValue; |
|
|
|
|
1462
|
|
|
// } |
1463
|
|
|
$this->entityStates[$oid] = self::STATE_MANAGED; |
1464
|
|
|
$this->scheduleForInsert($entity); |
1465
|
|
|
} |
1466
|
|
|
|
1467
|
|
|
/** |
1468
|
|
|
* Gets the commit order. |
1469
|
|
|
* |
1470
|
|
|
* @param array|null $entityChangeSet |
1471
|
|
|
* |
1472
|
7 |
|
* @return array |
1473
|
7 |
|
*/ |
1474
|
7 |
|
private function getCommitOrder(array $entityChangeSet = null) |
1475
|
|
|
{ |
1476
|
|
|
if ($entityChangeSet === null) { |
1477
|
|
|
$entityChangeSet = array_merge($this->entityInsertions, $this->entityUpdates, $this->entityDeletions); |
1478
|
|
|
} |
1479
|
|
|
$calc = $this->getCommitOrderCalculator(); |
1480
|
|
|
// See if there are any new classes in the changeset, that are not in the |
1481
|
|
|
// commit order graph yet (don't have a node). |
1482
|
|
|
// We have to inspect changeSet to be able to correctly build dependencies. |
1483
|
7 |
|
// It is not possible to use IdentityMap here because post inserted ids |
1484
|
|
|
// are not yet available. |
1485
|
7 |
|
/** @var ApiMetadata[] $newNodes */ |
1486
|
7 |
|
$newNodes = []; |
1487
|
7 |
|
foreach ((array)$entityChangeSet as $entity) { |
1488
|
7 |
|
$class = $this->manager->getClassMetadata(get_class($entity)); |
1489
|
|
|
if ($calc->hasNode($class->getName())) { |
1490
|
|
|
continue; |
1491
|
|
|
} |
1492
|
|
|
$calc->addNode($class->getName(), $class); |
1493
|
|
|
$newNodes[] = $class; |
1494
|
|
|
} |
1495
|
7 |
|
// Calculate dependencies for new nodes |
1496
|
7 |
|
while ($class = array_pop($newNodes)) { |
1497
|
7 |
|
foreach ($class->getAssociationMappings() as $assoc) { |
1498
|
7 |
|
if (!($assoc['isOwningSide'] && $assoc['type'] & ApiMetadata::TO_ONE)) { |
1499
|
|
|
continue; |
1500
|
|
|
} |
1501
|
7 |
|
$targetClass = $this->manager->getClassMetadata($assoc['target']); |
1502
|
7 |
|
if (!$calc->hasNode($targetClass->getName())) { |
1503
|
7 |
|
$calc->addNode($targetClass->getName(), $targetClass); |
1504
|
|
|
$newNodes[] = $targetClass; |
1505
|
7 |
|
} |
1506
|
7 |
|
$calc->addDependency($targetClass->getName(), $class->name, (int)empty($assoc['nullable'])); |
1507
|
5 |
|
// If the target class has mapped subclasses, these share the same dependency. |
1508
|
5 |
|
if (!$targetClass->getSubclasses()) { |
1509
|
|
|
continue; |
1510
|
5 |
|
} |
1511
|
5 |
|
foreach ($targetClass->getSubclasses() as $subClassName) { |
1512
|
3 |
|
$targetSubClass = $this->manager->getClassMetadata($subClassName); |
1513
|
3 |
|
if (!$calc->hasNode($subClassName)) { |
1514
|
3 |
|
$calc->addNode($targetSubClass->name, $targetSubClass); |
|
|
|
|
1515
|
5 |
|
$newNodes[] = $targetSubClass; |
1516
|
|
|
} |
1517
|
5 |
|
$calc->addDependency($targetSubClass->name, $class->name, 1); |
|
|
|
|
1518
|
|
|
} |
1519
|
|
|
} |
1520
|
5 |
|
} |
1521
|
5 |
|
|
1522
|
5 |
|
return $calc->sort(); |
1523
|
5 |
|
} |
1524
|
5 |
|
|
1525
|
5 |
|
private function getCommitOrderCalculator() |
1526
|
5 |
|
{ |
1527
|
5 |
|
return new Utility\CommitOrderCalculator(); |
1528
|
7 |
|
} |
1529
|
7 |
|
|
1530
|
|
|
/** |
1531
|
7 |
|
* Only flushes the given entity according to a ruleset that keeps the UoW consistent. |
1532
|
|
|
* |
1533
|
|
|
* 1. All entities scheduled for insertion, (orphan) removals and changes in collections are processed as well! |
1534
|
7 |
|
* 2. Read Only entities are skipped. |
1535
|
|
|
* 3. Proxies are skipped. |
1536
|
7 |
|
* 4. Only if entity is properly managed. |
1537
|
|
|
* |
1538
|
|
|
* @param object $entity |
1539
|
|
|
* |
1540
|
|
|
* @return void |
1541
|
|
|
* |
1542
|
|
|
* @throws \InvalidArgumentException |
1543
|
|
|
*/ |
1544
|
|
|
private function computeSingleEntityChangeSet($entity) |
1545
|
|
|
{ |
1546
|
|
|
$state = $this->getEntityState($entity); |
1547
|
|
|
if ($state !== self::STATE_MANAGED && $state !== self::STATE_REMOVED) { |
1548
|
|
|
throw new \InvalidArgumentException( |
1549
|
|
|
"Entity has to be managed or scheduled for removal for single computation " . self::objToStr($entity) |
1550
|
|
|
); |
1551
|
|
|
} |
1552
|
|
|
$class = $this->manager->getClassMetadata(get_class($entity)); |
1553
|
|
|
// Compute changes for INSERTed entities first. This must always happen even in this case. |
1554
|
|
|
$this->computeScheduleInsertsChangeSets(); |
1555
|
|
|
if ($class->isReadOnly()) { |
1556
|
|
|
return; |
1557
|
|
|
} |
1558
|
|
|
// Ignore uninitialized proxy objects |
1559
|
|
|
if ($entity instanceof Proxy && !$entity->__isInitialized__) { |
|
|
|
|
1560
|
|
|
return; |
1561
|
|
|
} |
1562
|
|
|
// Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here. |
1563
|
|
|
$oid = spl_object_hash($entity); |
1564
|
|
View Code Duplication |
if (!isset($this->entityInsertions[$oid]) && |
|
|
|
|
1565
|
|
|
!isset($this->entityDeletions[$oid]) && |
1566
|
|
|
isset($this->entityStates[$oid]) |
1567
|
|
|
) { |
1568
|
|
|
$this->computeChangeSet($class, $entity); |
1569
|
|
|
} |
1570
|
|
|
} |
1571
|
|
|
|
1572
|
|
|
/** |
1573
|
|
|
* Computes the changesets of all entities scheduled for insertion. |
1574
|
|
|
* |
1575
|
|
|
* @return void |
1576
|
|
|
*/ |
1577
|
|
|
private function computeScheduleInsertsChangeSets() |
1578
|
|
|
{ |
1579
|
|
|
foreach ($this->entityInsertions as $entity) { |
1580
|
|
|
$class = $this->manager->getClassMetadata(get_class($entity)); |
1581
|
|
|
$this->computeChangeSet($class, $entity); |
1582
|
|
|
} |
1583
|
|
|
} |
1584
|
|
|
|
1585
|
|
|
/** |
1586
|
7 |
|
* Computes the changes of an association. |
1587
|
|
|
* |
1588
|
7 |
|
* @param array $assoc The association mapping. |
1589
|
7 |
|
* @param mixed $value The value of the association. |
1590
|
7 |
|
* |
1591
|
7 |
|
* @throws \InvalidArgumentException |
1592
|
7 |
|
* @throws \UnexpectedValueException |
1593
|
|
|
* |
1594
|
|
|
* @return void |
1595
|
|
|
*/ |
1596
|
|
|
private function computeAssociationChanges($assoc, $value) |
1597
|
|
|
{ |
1598
|
|
|
if ($value instanceof Proxy && !$value->__isInitialized__) { |
|
|
|
|
1599
|
|
|
return; |
1600
|
|
|
} |
1601
|
|
|
if ($value instanceof ApiCollection && $value->isDirty()) { |
1602
|
|
|
$coid = spl_object_hash($value); |
1603
|
|
|
$this->collectionUpdates[$coid] = $value; |
1604
|
|
|
$this->visitedCollections[$coid] = $value; |
1605
|
2 |
|
} |
1606
|
|
|
// Look through the entities, and in any of their associations, |
1607
|
2 |
|
// for transient (new) entities, recursively. ("Persistence by reachability") |
|
|
|
|
1608
|
|
|
// Unwrap. Uninitialized collections will simply be empty. |
1609
|
|
|
$unwrappedValue = ($assoc['type'] & ApiMetadata::TO_ONE) ? [$value] : $value->unwrap(); |
1610
|
2 |
|
$targetClass = $this->manager->getClassMetadata($assoc['target']); |
1611
|
2 |
|
$targetClassName = $targetClass->getName(); |
1612
|
2 |
|
foreach ($unwrappedValue as $key => $entry) { |
1613
|
2 |
|
if (!($entry instanceof $targetClassName)) { |
1614
|
2 |
|
throw new \InvalidArgumentException('Invalid association'); |
1615
|
|
|
} |
1616
|
|
|
$state = $this->getEntityState($entry, self::STATE_NEW); |
1617
|
|
|
if (!($entry instanceof $assoc['target'])) { |
1618
|
2 |
|
throw new \UnexpectedValueException('Unexpected association'); |
1619
|
2 |
|
} |
1620
|
2 |
|
switch ($state) { |
1621
|
2 |
|
case self::STATE_NEW: |
1622
|
2 |
|
if (!$assoc['isCascadePersist']) { |
1623
|
|
|
throw new \InvalidArgumentException('New entity through relationship'); |
1624
|
|
|
} |
1625
|
2 |
|
$this->persistNew($targetClass, $entry); |
1626
|
2 |
|
$this->computeChangeSet($targetClass, $entry); |
1627
|
|
|
break; |
1628
|
|
|
case self::STATE_REMOVED: |
1629
|
|
|
// Consume the $value as array (it's either an array or an ArrayAccess) |
1630
|
2 |
|
// and remove the element from Collection. |
1631
|
|
|
if ($assoc['type'] & ApiMetadata::TO_MANY) { |
1632
|
|
|
unset($value[$key]); |
1633
|
|
|
} |
1634
|
|
|
break; |
1635
|
|
|
case self::STATE_DETACHED: |
1636
|
|
|
// Can actually not happen right now as we assume STATE_NEW, |
1637
|
2 |
|
// so the exception will be raised from the DBAL layer (constraint violation). |
1638
|
|
|
throw new \InvalidArgumentException('Detached entity through relationship'); |
1639
|
|
|
break; |
|
|
|
|
1640
|
|
|
default: |
1641
|
|
|
// MANAGED associated entities are already taken into account |
1642
|
|
|
// during changeset calculation anyway, since they are in the identity map. |
1643
|
|
|
} |
1644
|
2 |
|
} |
1645
|
|
|
} |
1646
|
|
|
|
1647
|
|
|
private function executeInserts(ApiMetadata $class) |
1648
|
|
|
{ |
1649
|
2 |
|
$className = $class->getName(); |
1650
|
|
|
$persister = $this->getEntityPersister($className); |
1651
|
|
|
foreach ($this->entityInsertions as $oid => $entity) { |
1652
|
2 |
|
if ($this->manager->getClassMetadata(get_class($entity))->getName() !== $className) { |
1653
|
2 |
|
continue; |
1654
|
2 |
|
} |
1655
|
|
|
$persister->pushNewEntity($entity); |
1656
|
7 |
|
unset($this->entityInsertions[$oid]); |
1657
|
|
|
} |
1658
|
7 |
|
$postInsertIds = $persister->flushNewEntities(); |
1659
|
7 |
|
if ($postInsertIds) { |
1660
|
7 |
|
// Persister returned post-insert IDs |
1661
|
7 |
|
foreach ($postInsertIds as $postInsertId) { |
1662
|
5 |
|
$id = $postInsertId['generatedId']; |
1663
|
|
|
$entity = $postInsertId['entity']; |
1664
|
7 |
|
$oid = spl_object_hash($entity); |
1665
|
7 |
|
|
1666
|
7 |
|
if ($id instanceof \stdClass) { |
1667
|
7 |
|
$id = (array)$id; |
1668
|
7 |
|
} |
1669
|
|
|
if (!is_array($id)) { |
1670
|
7 |
|
$id = [$class->getApiFieldName($class->getIdentifierFieldNames()[0]) => $id]; |
1671
|
7 |
|
} |
1672
|
7 |
|
|
1673
|
7 |
|
if (!array_key_exists($oid, $this->originalEntityData)) { |
1674
|
|
|
$this->originalEntityData[$oid] = new \stdClass(); |
1675
|
7 |
|
} |
1676
|
5 |
|
|
1677
|
5 |
|
$idValues = []; |
1678
|
7 |
|
foreach ((array)$id as $apiIdField => $idValue) { |
1679
|
2 |
|
$idName = $class->getFieldName($apiIdField); |
1680
|
2 |
|
$typeName = $class->getTypeOfField($idName); |
1681
|
|
|
$type = $this->manager->getConfiguration()->getTypeRegistry()->get($typeName); |
1682
|
7 |
|
$idValue = $type->toApiValue($idValue, $class->getFieldOptions($idName)); |
1683
|
|
|
$class->getReflectionProperty($idName)->setValue($entity, $idValue); |
1684
|
|
|
$idValues[$idName] = $idValue; |
1685
|
|
|
$this->originalEntityData[$oid]->$idName = $idValue; |
1686
|
7 |
|
} |
1687
|
7 |
|
|
1688
|
7 |
|
$this->entityIdentifiers[$oid] = $idValues; |
1689
|
7 |
|
$this->entityStates[$oid] = self::STATE_MANAGED; |
1690
|
7 |
|
$this->addToIdentityMap($entity); |
1691
|
7 |
|
} |
1692
|
7 |
|
} |
1693
|
7 |
|
} |
1694
|
7 |
|
|
1695
|
7 |
|
private function executeUpdates($class) |
1696
|
|
|
{ |
1697
|
7 |
|
$className = $class->name; |
1698
|
7 |
|
$persister = $this->getEntityPersister($className); |
1699
|
7 |
|
foreach ($this->entityUpdates as $oid => $entity) { |
1700
|
7 |
|
if ($this->manager->getClassMetadata(get_class($entity))->getName() !== $className) { |
1701
|
7 |
|
continue; |
1702
|
7 |
|
} |
1703
|
|
|
$this->recomputeSingleEntityChangeSet($class, $entity); |
1704
|
2 |
|
|
1705
|
|
|
if (!empty($this->entityChangeSets[$oid])) { |
1706
|
2 |
|
$persister->update($entity); |
1707
|
2 |
|
} |
1708
|
2 |
|
unset($this->entityUpdates[$oid]); |
1709
|
2 |
|
} |
1710
|
1 |
|
} |
1711
|
|
|
|
1712
|
2 |
|
/** |
1713
|
|
|
* Executes a refresh operation on an entity. |
1714
|
2 |
|
* |
1715
|
2 |
|
* @param object $entity The entity to refresh. |
1716
|
2 |
|
* @param array $visited The already visited entities during cascades. |
1717
|
2 |
|
* |
1718
|
2 |
|
* @return void |
1719
|
2 |
|
* |
1720
|
|
|
* @throws \InvalidArgumentException If the entity is not MANAGED. |
1721
|
|
|
*/ |
1722
|
|
|
private function doRefresh($entity, array &$visited) |
1723
|
|
|
{ |
1724
|
|
|
$oid = spl_object_hash($entity); |
1725
|
|
|
if (isset($visited[$oid])) { |
1726
|
|
|
return; // Prevent infinite recursion |
1727
|
|
|
} |
1728
|
|
|
$visited[$oid] = $entity; // mark visited |
1729
|
|
|
$class = $this->manager->getClassMetadata(get_class($entity)); |
1730
|
|
|
if ($this->getEntityState($entity) !== self::STATE_MANAGED) { |
1731
|
|
|
throw new \InvalidArgumentException('Entity not managed'); |
1732
|
|
|
} |
1733
|
|
|
$this->getEntityPersister($class->getName())->refresh( |
1734
|
|
|
array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]), |
1735
|
|
|
$entity |
1736
|
|
|
); |
1737
|
|
|
$this->cascadeRefresh($entity, $visited); |
1738
|
|
|
} |
1739
|
|
|
|
1740
|
|
|
/** |
1741
|
|
|
* Cascades a refresh operation to associated entities. |
1742
|
|
|
* |
1743
|
|
|
* @param object $entity |
1744
|
|
|
* @param array $visited |
1745
|
|
|
* |
1746
|
|
|
* @return void |
1747
|
|
|
*/ |
1748
|
|
View Code Duplication |
private function cascadeRefresh($entity, array &$visited) |
|
|
|
|
1749
|
|
|
{ |
1750
|
|
|
$class = $this->manager->getClassMetadata(get_class($entity)); |
1751
|
|
|
$associationMappings = array_filter( |
1752
|
|
|
$class->getAssociationMappings(), |
1753
|
|
|
function ($assoc) { |
1754
|
|
|
return $assoc['isCascadeRefresh']; |
1755
|
|
|
} |
1756
|
|
|
); |
1757
|
|
|
foreach ($associationMappings as $assoc) { |
1758
|
|
|
$relatedEntities = $class->getReflectionProperty($assoc['fieldName'])->getValue($entity); |
1759
|
|
|
switch (true) { |
1760
|
|
|
case ($relatedEntities instanceof ApiCollection): |
1761
|
|
|
// Unwrap so that foreach() does not initialize |
1762
|
|
|
$relatedEntities = $relatedEntities->unwrap(); |
1763
|
|
|
// break; is commented intentionally! |
1764
|
|
|
case ($relatedEntities instanceof Collection): |
1765
|
|
|
case (is_array($relatedEntities)): |
1766
|
|
|
foreach ($relatedEntities as $relatedEntity) { |
1767
|
|
|
$this->doRefresh($relatedEntity, $visited); |
1768
|
|
|
} |
1769
|
|
|
break; |
1770
|
|
|
case ($relatedEntities !== null): |
1771
|
|
|
$this->doRefresh($relatedEntities, $visited); |
1772
|
|
|
break; |
1773
|
|
|
default: |
1774
|
|
|
// Do nothing |
1775
|
|
|
} |
1776
|
|
|
} |
1777
|
|
|
} |
1778
|
|
|
|
1779
|
|
|
/** |
1780
|
|
|
* Cascades a detach operation to associated entities. |
1781
|
|
|
* |
1782
|
|
|
* @param object $entity |
1783
|
|
|
* @param array $visited |
1784
|
|
|
* |
1785
|
|
|
* @return void |
1786
|
|
|
*/ |
1787
|
|
View Code Duplication |
private function cascadeDetach($entity, array &$visited) |
|
|
|
|
1788
|
|
|
{ |
1789
|
|
|
$class = $this->manager->getClassMetadata(get_class($entity)); |
1790
|
|
|
$associationMappings = array_filter( |
1791
|
|
|
$class->getAssociationMappings(), |
1792
|
|
|
function ($assoc) { |
1793
|
|
|
return $assoc['isCascadeDetach']; |
1794
|
|
|
} |
1795
|
|
|
); |
1796
|
|
|
foreach ($associationMappings as $assoc) { |
1797
|
|
|
$relatedEntities = $class->getReflectionProperty($assoc['fieldName'])->getValue($entity); |
1798
|
|
|
switch (true) { |
1799
|
|
|
case ($relatedEntities instanceof ApiCollection): |
1800
|
|
|
// Unwrap so that foreach() does not initialize |
1801
|
|
|
$relatedEntities = $relatedEntities->unwrap(); |
1802
|
|
|
// break; is commented intentionally! |
1803
|
|
|
case ($relatedEntities instanceof Collection): |
1804
|
|
|
case (is_array($relatedEntities)): |
1805
|
|
|
foreach ($relatedEntities as $relatedEntity) { |
1806
|
|
|
$this->doDetach($relatedEntity, $visited); |
1807
|
|
|
} |
1808
|
|
|
break; |
1809
|
|
|
case ($relatedEntities !== null): |
1810
|
|
|
$this->doDetach($relatedEntities, $visited); |
1811
|
|
|
break; |
1812
|
|
|
default: |
1813
|
|
|
// Do nothing |
1814
|
|
|
} |
1815
|
|
|
} |
1816
|
|
|
} |
1817
|
|
|
|
1818
|
|
|
/** |
1819
|
|
|
* Cascades a merge operation to associated entities. |
1820
|
|
|
* |
1821
|
|
|
* @param object $entity |
1822
|
|
|
* @param object $managedCopy |
1823
|
|
|
* @param array $visited |
1824
|
|
|
* |
1825
|
|
|
* @return void |
1826
|
|
|
*/ |
1827
|
|
|
private function cascadeMerge($entity, $managedCopy, array &$visited) |
1828
|
|
|
{ |
1829
|
|
|
$class = $this->manager->getClassMetadata(get_class($entity)); |
1830
|
|
|
$associationMappings = array_filter( |
1831
|
|
|
$class->getAssociationMappings(), |
1832
|
|
|
function ($assoc) { |
1833
|
|
|
return $assoc['isCascadeMerge']; |
1834
|
|
|
} |
1835
|
|
|
); |
1836
|
|
|
foreach ($associationMappings as $assoc) { |
1837
|
|
|
$relatedEntities = $class->getReflectionProperty($assoc['field'])->getValue($entity); |
1838
|
|
|
if ($relatedEntities instanceof Collection) { |
1839
|
|
|
if ($relatedEntities === $class->getReflectionProperty($assoc['field'])->getValue($managedCopy)) { |
1840
|
|
|
continue; |
1841
|
|
|
} |
1842
|
|
|
if ($relatedEntities instanceof ApiCollection) { |
1843
|
|
|
// Unwrap so that foreach() does not initialize |
1844
|
|
|
$relatedEntities = $relatedEntities->unwrap(); |
1845
|
|
|
} |
1846
|
|
|
foreach ($relatedEntities as $relatedEntity) { |
1847
|
|
|
$this->doMerge($relatedEntity, $visited, $managedCopy, $assoc); |
1848
|
|
|
} |
1849
|
|
|
} else { |
1850
|
|
|
if ($relatedEntities !== null) { |
1851
|
|
|
$this->doMerge($relatedEntities, $visited, $managedCopy, $assoc); |
1852
|
|
|
} |
1853
|
|
|
} |
1854
|
|
|
} |
1855
|
|
|
} |
1856
|
|
|
|
1857
|
|
|
/** |
1858
|
|
|
* Cascades the delete operation to associated entities. |
1859
|
|
|
* |
1860
|
|
|
* @param object $entity |
1861
|
|
|
* @param array $visited |
1862
|
|
|
* |
1863
|
|
|
* @return void |
1864
|
|
|
*/ |
1865
|
|
|
private function cascadeRemove($entity, array &$visited) |
1866
|
|
|
{ |
1867
|
|
|
$class = $this->manager->getClassMetadata(get_class($entity)); |
1868
|
|
|
$associationMappings = array_filter( |
1869
|
|
|
$class->getAssociationMappings(), |
1870
|
|
|
function ($assoc) { |
1871
|
|
|
return $assoc['isCascadeRemove']; |
1872
|
|
|
} |
1873
|
|
|
); |
1874
|
|
|
$entitiesToCascade = []; |
1875
|
|
|
foreach ($associationMappings as $assoc) { |
1876
|
|
|
if ($entity instanceof Proxy && !$entity->__isInitialized__) { |
|
|
|
|
1877
|
|
|
$entity->__load(); |
1878
|
|
|
} |
1879
|
|
|
$relatedEntities = $class->getReflectionProperty($assoc['fieldName'])->getValue($entity); |
1880
|
|
|
switch (true) { |
1881
|
|
|
case ($relatedEntities instanceof Collection): |
1882
|
|
|
case (is_array($relatedEntities)): |
1883
|
|
|
// If its a PersistentCollection initialization is intended! No unwrap! |
1884
|
|
|
foreach ($relatedEntities as $relatedEntity) { |
1885
|
|
|
$entitiesToCascade[] = $relatedEntity; |
1886
|
|
|
} |
1887
|
|
|
break; |
1888
|
|
|
case ($relatedEntities !== null): |
1889
|
|
|
$entitiesToCascade[] = $relatedEntities; |
1890
|
|
|
break; |
1891
|
|
|
default: |
1892
|
|
|
// Do nothing |
1893
|
|
|
} |
1894
|
|
|
} |
1895
|
|
|
foreach ($entitiesToCascade as $relatedEntity) { |
1896
|
|
|
$this->doRemove($relatedEntity, $visited); |
1897
|
|
|
} |
1898
|
|
|
} |
1899
|
|
|
|
1900
|
|
|
/** |
1901
|
|
|
* Executes any extra updates that have been scheduled. |
1902
|
|
|
*/ |
1903
|
|
|
private function executeExtraUpdates() |
1904
|
|
|
{ |
1905
|
|
|
foreach ($this->extraUpdates as $oid => $update) { |
1906
|
|
|
list ($entity, $changeset) = $update; |
1907
|
|
|
$this->entityChangeSets[$oid] = $changeset; |
1908
|
|
|
$this->getEntityPersister(get_class($entity))->update($entity); |
1909
|
|
|
} |
1910
|
|
|
$this->extraUpdates = []; |
1911
|
|
|
} |
1912
|
|
|
|
1913
|
|
|
private function executeDeletions(ApiMetadata $class) |
1914
|
|
|
{ |
1915
|
|
|
$className = $class->getName(); |
1916
|
|
|
$persister = $this->getEntityPersister($className); |
1917
|
|
|
foreach ($this->entityDeletions as $oid => $entity) { |
1918
|
|
|
if ($this->manager->getClassMetadata(get_class($entity))->getName() !== $className) { |
1919
|
|
|
continue; |
1920
|
|
|
} |
1921
|
|
|
$persister->delete($entity); |
1922
|
|
|
unset( |
1923
|
|
|
$this->entityDeletions[$oid], |
1924
|
|
|
$this->entityIdentifiers[$oid], |
1925
|
|
|
$this->originalEntityData[$oid], |
1926
|
|
|
$this->entityStates[$oid] |
1927
|
|
|
); |
1928
|
|
|
// Entity with this $oid after deletion treated as NEW, even if the $oid |
1929
|
|
|
// is obtained by a new entity because the old one went out of scope. |
1930
|
|
|
//$this->entityStates[$oid] = self::STATE_NEW; |
|
|
|
|
1931
|
|
|
// if ( ! $class->isIdentifierNatural()) { |
|
|
|
|
1932
|
|
|
// $class->getReflectionProperty($class->getIdentifierFieldNames()[0])->setValue($entity, null); |
|
|
|
|
1933
|
|
|
// } |
1934
|
|
|
} |
1935
|
|
|
} |
1936
|
|
|
|
1937
|
|
|
/** |
1938
|
|
|
* @param object $entity |
1939
|
|
|
* @param object $managedCopy |
1940
|
|
|
*/ |
1941
|
|
|
private function mergeEntityStateIntoManagedCopy($entity, $managedCopy) |
1942
|
|
|
{ |
1943
|
|
|
$class = $this->manager->getClassMetadata(get_class($entity)); |
1944
|
|
|
foreach ($this->reflectionPropertiesGetter->getProperties($class->getName()) as $prop) { |
1945
|
|
|
$name = $prop->name; |
1946
|
|
|
$prop->setAccessible(true); |
1947
|
|
|
if ($class->hasAssociation($name)) { |
1948
|
|
|
if (!$class->isIdentifier($name)) { |
1949
|
|
|
$prop->setValue($managedCopy, $prop->getValue($entity)); |
1950
|
|
|
} |
1951
|
|
|
} else { |
1952
|
|
|
$assoc2 = $class->getAssociationMapping($name); |
1953
|
|
|
if ($assoc2['type'] & ApiMetadata::TO_ONE) { |
1954
|
|
|
$other = $prop->getValue($entity); |
1955
|
|
|
if ($other === null) { |
1956
|
|
|
$prop->setValue($managedCopy, null); |
1957
|
|
|
} else { |
1958
|
|
|
if ($other instanceof Proxy && !$other->__isInitialized()) { |
1959
|
|
|
// do not merge fields marked lazy that have not been fetched. |
1960
|
|
|
continue; |
1961
|
|
|
} |
1962
|
|
|
if (!$assoc2['isCascadeMerge']) { |
1963
|
|
|
if ($this->getEntityState($other) === self::STATE_DETACHED) { |
1964
|
|
|
$targetClass = $this->manager->getClassMetadata($assoc2['targetEntity']); |
1965
|
|
|
$relatedId = $targetClass->getIdentifierValues($other); |
1966
|
|
|
if ($targetClass->getSubclasses()) { |
1967
|
|
|
$other = $this->manager->find($targetClass->getName(), $relatedId); |
1968
|
|
|
} else { |
1969
|
|
|
$other = $this->manager->getProxyFactory()->getProxy( |
1970
|
|
|
$assoc2['targetEntity'], |
1971
|
|
|
$relatedId |
1972
|
|
|
); |
1973
|
|
|
$this->registerManaged($other, $relatedId, []); |
|
|
|
|
1974
|
|
|
} |
1975
|
|
|
} |
1976
|
|
|
$prop->setValue($managedCopy, $other); |
1977
|
|
|
} |
1978
|
|
|
} |
1979
|
|
|
} else { |
1980
|
|
|
$mergeCol = $prop->getValue($entity); |
1981
|
|
|
if ($mergeCol instanceof ApiCollection && !$mergeCol->isInitialized()) { |
1982
|
|
|
// do not merge fields marked lazy that have not been fetched. |
1983
|
|
|
// keep the lazy persistent collection of the managed copy. |
1984
|
|
|
continue; |
1985
|
|
|
} |
1986
|
|
|
$managedCol = $prop->getValue($managedCopy); |
1987
|
|
|
if (!$managedCol) { |
1988
|
|
|
$managedCol = new ApiCollection( |
1989
|
|
|
$this->manager, |
1990
|
|
|
$this->manager->getClassMetadata($assoc2['target']), |
1991
|
|
|
new ArrayCollection |
1992
|
|
|
); |
1993
|
|
|
$managedCol->setOwner($managedCopy, $assoc2); |
1994
|
|
|
$prop->setValue($managedCopy, $managedCol); |
1995
|
|
|
} |
1996
|
|
|
if ($assoc2['isCascadeMerge']) { |
1997
|
|
|
$managedCol->initialize(); |
1998
|
|
|
// clear and set dirty a managed collection if its not also the same collection to merge from. |
1999
|
|
|
if (!$managedCol->isEmpty() && $managedCol !== $mergeCol) { |
2000
|
|
|
$managedCol->unwrap()->clear(); |
2001
|
|
|
$managedCol->setDirty(true); |
2002
|
|
|
if ($assoc2['isOwningSide'] |
2003
|
|
|
&& $assoc2['type'] == ApiMetadata::MANY_TO_MANY |
2004
|
|
|
&& $class->isChangeTrackingNotify() |
2005
|
|
|
) { |
2006
|
|
|
$this->scheduleForDirtyCheck($managedCopy); |
2007
|
|
|
} |
2008
|
|
|
} |
2009
|
|
|
} |
2010
|
|
|
} |
2011
|
|
|
} |
2012
|
|
|
if ($class->isChangeTrackingNotify()) { |
2013
|
|
|
// Just treat all properties as changed, there is no other choice. |
2014
|
|
|
$this->propertyChanged($managedCopy, $name, null, $prop->getValue($managedCopy)); |
2015
|
|
|
} |
2016
|
|
|
} |
2017
|
|
|
} |
2018
|
|
|
|
2019
|
|
|
/** |
2020
|
|
|
* Deletes an entity as part of the current unit of work. |
2021
|
|
|
* |
2022
|
|
|
* This method is internally called during delete() cascades as it tracks |
2023
|
|
|
* the already visited entities to prevent infinite recursions. |
2024
|
|
|
* |
2025
|
|
|
* @param object $entity The entity to delete. |
2026
|
|
|
* @param array $visited The map of the already visited entities. |
2027
|
|
|
* |
2028
|
|
|
* @return void |
2029
|
|
|
* |
2030
|
|
|
* @throws \InvalidArgumentException If the instance is a detached entity. |
2031
|
|
|
* @throws \UnexpectedValueException |
2032
|
|
|
*/ |
2033
|
|
|
private function doRemove($entity, array &$visited) |
2034
|
|
|
{ |
2035
|
|
|
$oid = spl_object_hash($entity); |
2036
|
|
|
if (isset($visited[$oid])) { |
2037
|
|
|
return; // Prevent infinite recursion |
2038
|
|
|
} |
2039
|
|
|
$visited[$oid] = $entity; // mark visited |
2040
|
|
|
// Cascade first, because scheduleForDelete() removes the entity from the identity map, which |
2041
|
|
|
// can cause problems when a lazy proxy has to be initialized for the cascade operation. |
2042
|
|
|
$this->cascadeRemove($entity, $visited); |
2043
|
|
|
$class = $this->manager->getClassMetadata(get_class($entity)); |
|
|
|
|
2044
|
|
|
$entityState = $this->getEntityState($entity); |
2045
|
|
|
switch ($entityState) { |
2046
|
|
|
case self::STATE_NEW: |
2047
|
|
|
case self::STATE_REMOVED: |
2048
|
|
|
// nothing to do |
2049
|
|
|
break; |
2050
|
|
|
case self::STATE_MANAGED: |
2051
|
|
|
$this->scheduleForDelete($entity); |
2052
|
|
|
break; |
2053
|
|
|
case self::STATE_DETACHED: |
2054
|
|
|
throw new \InvalidArgumentException('Detached entity cannot be removed'); |
2055
|
|
|
default: |
2056
|
|
|
throw new \UnexpectedValueException("Unexpected entity state: $entityState." . self::objToStr($entity)); |
2057
|
|
|
} |
2058
|
|
|
} |
2059
|
|
|
|
2060
|
|
|
/** |
2061
|
|
|
* Tests if an entity is loaded - must either be a loaded proxy or not a proxy |
2062
|
|
|
* |
2063
|
|
|
* @param object $entity |
2064
|
|
|
* |
2065
|
|
|
* @return bool |
2066
|
|
|
*/ |
2067
|
|
|
private function isLoaded($entity) |
2068
|
|
|
{ |
2069
|
|
|
return !($entity instanceof Proxy) || $entity->__isInitialized(); |
2070
|
|
|
} |
2071
|
|
|
|
2072
|
|
|
/** |
2073
|
|
|
* Sets/adds associated managed copies into the previous entity's association field |
2074
|
|
|
* |
2075
|
|
|
* @param object $entity |
2076
|
|
|
* @param array $association |
2077
|
|
|
* @param object $previousManagedCopy |
2078
|
|
|
* @param object $managedCopy |
2079
|
|
|
* |
2080
|
|
|
* @return void |
2081
|
|
|
*/ |
2082
|
|
|
private function updateAssociationWithMergedEntity($entity, array $association, $previousManagedCopy, $managedCopy) |
2083
|
|
|
{ |
2084
|
|
|
$assocField = $association['fieldName']; |
2085
|
|
|
$prevClass = $this->manager->getClassMetadata(get_class($previousManagedCopy)); |
2086
|
|
|
if ($association['type'] & ApiMetadata::TO_ONE) { |
2087
|
|
|
$prevClass->getReflectionProperty($assocField)->setValue($previousManagedCopy, $managedCopy); |
2088
|
|
|
|
2089
|
|
|
return; |
2090
|
|
|
} |
2091
|
|
|
/** @var array $value */ |
2092
|
|
|
$value = $prevClass->getReflectionProperty($assocField)->getValue($previousManagedCopy); |
2093
|
|
|
$value[] = $managedCopy; |
2094
|
|
|
if ($association['type'] == ApiMetadata::ONE_TO_MANY) { |
2095
|
|
|
$class = $this->manager->getClassMetadata(get_class($entity)); |
2096
|
|
|
$class->getReflectionProperty($association['mappedBy'])->setValue($managedCopy, $previousManagedCopy); |
2097
|
|
|
} |
2098
|
|
|
} |
2099
|
|
|
|
2100
|
|
|
/** |
2101
|
|
|
* Executes a merge operation on an entity. |
2102
|
|
|
* |
2103
|
|
|
* @param object $entity |
2104
|
|
|
* @param array $visited |
2105
|
|
|
* @param object|null $prevManagedCopy |
2106
|
|
|
* @param array|null $assoc |
2107
|
|
|
* |
2108
|
|
|
* @return object The managed copy of the entity. |
2109
|
|
|
* |
2110
|
|
|
* @throws \InvalidArgumentException If the entity instance is NEW. |
2111
|
|
|
* @throws \OutOfBoundsException |
2112
|
|
|
*/ |
2113
|
|
|
private function doMerge($entity, array &$visited, $prevManagedCopy = null, array $assoc = []) |
2114
|
|
|
{ |
2115
|
|
|
$oid = spl_object_hash($entity); |
2116
|
|
|
if (isset($visited[$oid])) { |
2117
|
|
|
$managedCopy = $visited[$oid]; |
2118
|
|
|
if ($prevManagedCopy !== null) { |
2119
|
|
|
$this->updateAssociationWithMergedEntity($entity, $assoc, $prevManagedCopy, $managedCopy); |
2120
|
|
|
} |
2121
|
|
|
|
2122
|
|
|
return $managedCopy; |
2123
|
|
|
} |
2124
|
|
|
$class = $this->manager->getClassMetadata(get_class($entity)); |
2125
|
|
|
// First we assume DETACHED, although it can still be NEW but we can avoid |
2126
|
|
|
// an extra db-roundtrip this way. If it is not MANAGED but has an identity, |
2127
|
|
|
// we need to fetch it from the db anyway in order to merge. |
2128
|
|
|
// MANAGED entities are ignored by the merge operation. |
2129
|
|
|
$managedCopy = $entity; |
2130
|
|
|
if ($this->getEntityState($entity, self::STATE_DETACHED) !== self::STATE_MANAGED) { |
2131
|
|
|
// Try to look the entity up in the identity map. |
2132
|
|
|
$id = $class->getIdentifierValues($entity); |
2133
|
|
|
// If there is no ID, it is actually NEW. |
2134
|
|
|
if (!$id) { |
|
|
|
|
2135
|
|
|
$managedCopy = $this->newInstance($class); |
2136
|
|
|
$this->persistNew($class, $managedCopy); |
2137
|
|
|
} else { |
2138
|
|
|
$flatId = ($class->containsForeignIdentifier()) |
2139
|
|
|
? $this->identifierFlattener->flattenIdentifier($class, $id) |
2140
|
|
|
: $id; |
2141
|
|
|
$managedCopy = $this->tryGetById($flatId, $class->getRootEntityName()); |
2142
|
|
|
if ($managedCopy) { |
2143
|
|
|
// We have the entity in-memory already, just make sure its not removed. |
2144
|
|
|
if ($this->getEntityState($managedCopy) == self::STATE_REMOVED) { |
2145
|
|
|
throw new \InvalidArgumentException('Removed entity cannot be merged'); |
2146
|
|
|
} |
2147
|
|
|
} else { |
2148
|
|
|
// We need to fetch the managed copy in order to merge. |
2149
|
|
|
$managedCopy = $this->manager->find($class->getName(), $flatId); |
2150
|
|
|
} |
2151
|
|
|
if ($managedCopy === null) { |
2152
|
|
|
// If the identifier is ASSIGNED, it is NEW, otherwise an error |
2153
|
|
|
// since the managed entity was not found. |
2154
|
|
|
if (!$class->isIdentifierNatural()) { |
2155
|
|
|
throw new \OutOfBoundsException('Entity not found'); |
2156
|
|
|
} |
2157
|
|
|
$managedCopy = $this->newInstance($class); |
2158
|
|
|
$class->setIdentifierValues($managedCopy, $id); |
2159
|
|
|
$this->persistNew($class, $managedCopy); |
2160
|
|
|
} |
2161
|
|
|
} |
2162
|
|
|
|
2163
|
|
|
$visited[$oid] = $managedCopy; // mark visited |
2164
|
|
|
if ($this->isLoaded($entity)) { |
2165
|
|
|
if ($managedCopy instanceof Proxy && !$managedCopy->__isInitialized()) { |
2166
|
|
|
$managedCopy->__load(); |
2167
|
|
|
} |
2168
|
|
|
$this->mergeEntityStateIntoManagedCopy($entity, $managedCopy); |
2169
|
|
|
} |
2170
|
|
|
if ($class->isChangeTrackingDeferredExplicit()) { |
2171
|
|
|
$this->scheduleForDirtyCheck($entity); |
2172
|
|
|
} |
2173
|
|
|
} |
2174
|
|
|
if ($prevManagedCopy !== null) { |
2175
|
|
|
$this->updateAssociationWithMergedEntity($entity, $assoc, $prevManagedCopy, $managedCopy); |
2176
|
|
|
} |
2177
|
|
|
// Mark the managed copy visited as well |
2178
|
|
|
$visited[spl_object_hash($managedCopy)] = $managedCopy; |
2179
|
|
|
$this->cascadeMerge($entity, $managedCopy, $visited); |
2180
|
|
|
|
2181
|
|
|
return $managedCopy; |
2182
|
|
|
} |
2183
|
|
|
|
2184
|
|
|
/** |
2185
|
|
|
* Executes a detach operation on the given entity. |
2186
|
|
|
* |
2187
|
|
|
* @param object $entity |
2188
|
|
|
* @param array $visited |
2189
|
|
|
* @param boolean $noCascade if true, don't cascade detach operation. |
2190
|
|
|
* |
2191
|
|
|
* @return void |
2192
|
|
|
*/ |
2193
|
|
|
private function doDetach($entity, array &$visited, $noCascade = false) |
2194
|
|
|
{ |
2195
|
|
|
$oid = spl_object_hash($entity); |
2196
|
|
|
if (isset($visited[$oid])) { |
2197
|
|
|
return; // Prevent infinite recursion |
2198
|
|
|
} |
2199
|
|
|
$visited[$oid] = $entity; // mark visited |
2200
|
|
|
switch ($this->getEntityState($entity, self::STATE_DETACHED)) { |
2201
|
|
|
case self::STATE_MANAGED: |
2202
|
|
|
if ($this->isInIdentityMap($entity)) { |
2203
|
|
|
$this->removeFromIdentityMap($entity); |
2204
|
|
|
} |
2205
|
|
|
unset( |
2206
|
|
|
$this->entityInsertions[$oid], |
2207
|
|
|
$this->entityUpdates[$oid], |
2208
|
|
|
$this->entityDeletions[$oid], |
2209
|
|
|
$this->entityIdentifiers[$oid], |
2210
|
|
|
$this->entityStates[$oid], |
2211
|
|
|
$this->originalEntityData[$oid] |
2212
|
|
|
); |
2213
|
|
|
break; |
2214
|
|
|
case self::STATE_NEW: |
2215
|
|
|
case self::STATE_DETACHED: |
2216
|
|
|
return; |
2217
|
|
|
} |
2218
|
|
|
if (!$noCascade) { |
2219
|
|
|
$this->cascadeDetach($entity, $visited); |
2220
|
|
|
} |
2221
|
|
|
} |
2222
|
|
|
|
2223
|
|
|
/** |
2224
|
|
|
* @param ApiMetadata $class |
2225
|
|
|
* |
2226
|
|
|
* @return EntityHydrator |
2227
|
|
|
*/ |
2228
|
|
|
private function getHydratorForClass(ApiMetadata $class) |
2229
|
|
|
{ |
2230
|
|
|
if (!array_key_exists($class->getName(), $this->hydrators)) { |
2231
|
|
|
$this->hydrators[$class->getName()] = new EntityHydrator($this->manager, $class); |
2232
|
|
|
} |
2233
|
|
|
|
2234
|
|
|
return $this->hydrators[$class->getName()]; |
2235
|
|
|
} |
2236
|
|
|
} |
2237
|
|
|
|
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.