Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like UnitOfWork often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use UnitOfWork, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
28 | class UnitOfWork implements PropertyChangedListener |
||
29 | { |
||
30 | /** |
||
31 | * An entity is in MANAGED state when its persistence is managed by an EntityManager. |
||
32 | */ |
||
33 | const STATE_MANAGED = 1; |
||
34 | /** |
||
35 | * An entity is new if it has just been instantiated (i.e. using the "new" operator) |
||
36 | * and is not (yet) managed by an EntityManager. |
||
37 | */ |
||
38 | const STATE_NEW = 2; |
||
39 | /** |
||
40 | * A detached entity is an instance with persistent state and identity that is not |
||
41 | * (or no longer) associated with an EntityManager (and a UnitOfWork). |
||
42 | */ |
||
43 | const STATE_DETACHED = 3; |
||
44 | /** |
||
45 | * A removed entity instance is an instance with a persistent identity, |
||
46 | * associated with an EntityManager, whose persistent state will be deleted |
||
47 | * on commit. |
||
48 | */ |
||
49 | const STATE_REMOVED = 4; |
||
50 | |||
51 | /** |
||
52 | * The (cached) states of any known entities. |
||
53 | * Keys are object ids (spl_object_hash). |
||
54 | * |
||
55 | * @var array |
||
56 | */ |
||
57 | private $entityStates = []; |
||
58 | |||
59 | /** @var EntityManager */ |
||
60 | private $manager; |
||
61 | /** @var EntityPersister[] */ |
||
62 | private $persisters = []; |
||
63 | /** @var CollectionPersister[] */ |
||
64 | private $collectionPersisters = []; |
||
65 | /** @var array */ |
||
66 | private $entityIdentifiers = []; |
||
67 | /** @var object[][] */ |
||
68 | private $identityMap = []; |
||
69 | /** @var IdentifierFlattener */ |
||
70 | private $identifierFlattener; |
||
71 | /** @var array */ |
||
72 | private $originalEntityData = []; |
||
73 | /** @var array */ |
||
74 | private $entityDeletions = []; |
||
75 | /** @var array */ |
||
76 | private $entityChangeSets = []; |
||
77 | /** @var array */ |
||
78 | private $entityInsertions = []; |
||
79 | /** @var array */ |
||
80 | private $entityUpdates = []; |
||
81 | /** @var array */ |
||
82 | private $readOnlyObjects = []; |
||
83 | /** @var array */ |
||
84 | private $scheduledForSynchronization = []; |
||
85 | /** @var array */ |
||
86 | private $orphanRemovals = []; |
||
87 | /** @var ApiCollection[] */ |
||
88 | private $collectionDeletions = []; |
||
89 | /** @var array */ |
||
90 | private $extraUpdates = []; |
||
91 | /** @var ApiCollection[] */ |
||
92 | private $collectionUpdates = []; |
||
93 | /** @var ApiCollection[] */ |
||
94 | private $visitedCollections = []; |
||
95 | /** @var ReflectionPropertiesGetter */ |
||
96 | private $reflectionPropertiesGetter; |
||
97 | |||
98 | /** |
||
99 | * UnitOfWork constructor. |
||
100 | * |
||
101 | * @param EntityManager $manager |
||
102 | */ |
||
103 | 18 | public function __construct(EntityManager $manager) |
|
109 | |||
110 | /** |
||
111 | * Helper method to show an object as string. |
||
112 | * |
||
113 | * @param object $obj |
||
114 | * |
||
115 | * @return string |
||
116 | */ |
||
117 | private static function objToStr($obj) |
||
118 | { |
||
119 | return method_exists($obj, '__toString') ? (string)$obj : get_class($obj).'@'.spl_object_hash($obj); |
||
120 | } |
||
121 | |||
122 | /** |
||
123 | * @param $className |
||
124 | * |
||
125 | * @return EntityPersister |
||
126 | */ |
||
127 | 17 | public function getEntityPersister($className) |
|
128 | { |
||
129 | 17 | if (!array_key_exists($className, $this->persisters)) { |
|
130 | /** @var ApiMetadata $classMetadata */ |
||
131 | 17 | $classMetadata = $this->manager->getClassMetadata($className); |
|
132 | |||
133 | 17 | $api = $this->createApi($classMetadata); |
|
134 | |||
135 | 17 | if ($api instanceof EntityCacheAwareInterface) { |
|
136 | 17 | $api->setEntityCache($this->createEntityCache($classMetadata)); |
|
137 | 17 | } |
|
138 | |||
139 | 17 | $this->persisters[$className] = new ApiPersister($this->manager, $api); |
|
140 | 17 | } |
|
141 | |||
142 | 17 | return $this->persisters[$className]; |
|
143 | } |
||
144 | |||
145 | /** |
||
146 | * Checks whether an entity is registered in the identity map of this UnitOfWork. |
||
147 | * |
||
148 | * @param object $entity |
||
149 | * |
||
150 | * @return boolean |
||
151 | */ |
||
152 | public function isInIdentityMap($entity) |
||
153 | { |
||
154 | $oid = spl_object_hash($entity); |
||
155 | |||
156 | if (!isset($this->entityIdentifiers[$oid])) { |
||
157 | return false; |
||
158 | } |
||
159 | |||
160 | /** @var EntityMetadata $classMetadata */ |
||
161 | $classMetadata = $this->manager->getClassMetadata(get_class($entity)); |
||
162 | $idHash = implode(' ', $this->entityIdentifiers[$oid]); |
||
163 | |||
164 | if ($idHash === '') { |
||
165 | return false; |
||
166 | } |
||
167 | |||
168 | return isset($this->identityMap[$classMetadata->rootEntityName][$idHash]); |
||
169 | } |
||
170 | |||
171 | /** |
||
172 | * Gets the identifier of an entity. |
||
173 | * The returned value is always an array of identifier values. If the entity |
||
174 | * has a composite identifier then the identifier values are in the same |
||
175 | * order as the identifier field names as returned by ClassMetadata#getIdentifierFieldNames(). |
||
176 | * |
||
177 | * @param object $entity |
||
178 | * |
||
179 | * @return array The identifier values. |
||
180 | */ |
||
181 | 1 | public function getEntityIdentifier($entity) |
|
182 | { |
||
183 | 1 | return $this->entityIdentifiers[spl_object_hash($entity)]; |
|
184 | } |
||
185 | |||
186 | /** |
||
187 | * @param $className |
||
188 | * @param \stdClass $data |
||
189 | * |
||
190 | * @return ObjectManagerAware|object |
||
191 | * @throws MappingException |
||
192 | */ |
||
193 | 12 | public function getOrCreateEntity($className, \stdClass $data) |
|
194 | { |
||
195 | /** @var EntityMetadata $class */ |
||
196 | 12 | $class = $this->manager->getClassMetadata($className); |
|
197 | 12 | $hydrator = new EntityHydrator($this->manager, $class); |
|
198 | |||
199 | 12 | $tmpEntity = $hydrator->hydarate($data); |
|
200 | |||
201 | 12 | $id = $this->identifierFlattener->flattenIdentifier($class, $class->getIdentifierValues($tmpEntity)); |
|
202 | 12 | $idHash = implode(' ', $id); |
|
203 | |||
204 | 12 | $overrideLocalValues = false; |
|
205 | 12 | if (isset($this->identityMap[$class->rootEntityName][$idHash])) { |
|
206 | 2 | $entity = $this->identityMap[$class->rootEntityName][$idHash]; |
|
207 | 2 | $oid = spl_object_hash($entity); |
|
208 | |||
209 | 2 | if ($entity instanceof Proxy && !$entity->__isInitialized()) { |
|
210 | 2 | $entity->__setInitialized(true); |
|
211 | |||
212 | 2 | $overrideLocalValues = true; |
|
213 | 2 | $this->originalEntityData[$oid] = $data; |
|
214 | |||
215 | 2 | if ($entity instanceof NotifyPropertyChanged) { |
|
216 | $entity->addPropertyChangedListener($this); |
||
217 | } |
||
218 | 2 | } |
|
219 | 2 | } else { |
|
220 | 11 | $entity = $this->newInstance($class); |
|
221 | 11 | $oid = spl_object_hash($entity); |
|
222 | 11 | $this->entityIdentifiers[$oid] = $id; |
|
223 | 11 | $this->entityStates[$oid] = self::STATE_MANAGED; |
|
224 | 11 | $this->originalEntityData[$oid] = $data; |
|
225 | 11 | $this->identityMap[$class->rootEntityName][$idHash] = $entity; |
|
226 | 11 | if ($entity instanceof NotifyPropertyChanged) { |
|
227 | $entity->addPropertyChangedListener($this); |
||
228 | } |
||
229 | 11 | $overrideLocalValues = true; |
|
230 | } |
||
231 | |||
232 | 12 | if (!$overrideLocalValues) { |
|
233 | return $entity; |
||
234 | } |
||
235 | |||
236 | 12 | $entity = $hydrator->hydarate($data, $entity); |
|
237 | |||
238 | 12 | return $entity; |
|
239 | } |
||
240 | |||
241 | /** |
||
242 | * INTERNAL: |
||
243 | * Registers an entity as managed. |
||
244 | * |
||
245 | * @param object $entity The entity. |
||
246 | * @param array $id The identifier values. |
||
247 | * @param \stdClass|null $data The original entity data. |
||
248 | * |
||
249 | * @return void |
||
250 | */ |
||
251 | 3 | public function registerManaged($entity, array $id, \stdClass $data = null) |
|
252 | { |
||
253 | 3 | $oid = spl_object_hash($entity); |
|
254 | |||
255 | 3 | $this->entityIdentifiers[$oid] = $id; |
|
256 | 3 | $this->entityStates[$oid] = self::STATE_MANAGED; |
|
257 | 3 | $this->originalEntityData[$oid] = $data; |
|
258 | |||
259 | 3 | $this->addToIdentityMap($entity); |
|
260 | |||
261 | 3 | if ($entity instanceof NotifyPropertyChanged && (!$entity instanceof Proxy || $entity->__isInitialized())) { |
|
262 | $entity->addPropertyChangedListener($this); |
||
263 | } |
||
264 | 3 | } |
|
265 | |||
266 | /** |
||
267 | * INTERNAL: |
||
268 | * Registers an entity in the identity map. |
||
269 | * Note that entities in a hierarchy are registered with the class name of |
||
270 | * the root entity. |
||
271 | * |
||
272 | * @ignore |
||
273 | * |
||
274 | * @param object $entity The entity to register. |
||
275 | * |
||
276 | * @return boolean TRUE if the registration was successful, FALSE if the identity of |
||
277 | * the entity in question is already managed. |
||
278 | * |
||
279 | */ |
||
280 | 7 | public function addToIdentityMap($entity) |
|
281 | { |
||
282 | /** @var EntityMetadata $classMetadata */ |
||
283 | 7 | $classMetadata = $this->manager->getClassMetadata(get_class($entity)); |
|
284 | 7 | $idHash = implode(' ', $this->entityIdentifiers[spl_object_hash($entity)]); |
|
285 | |||
286 | 7 | if ($idHash === '') { |
|
287 | throw new \InvalidArgumentException('Entitty does not have valid identifiers to be stored at identity map'); |
||
288 | } |
||
289 | |||
290 | 7 | $className = $classMetadata->rootEntityName; |
|
291 | |||
292 | 7 | if (isset($this->identityMap[$className][$idHash])) { |
|
293 | return false; |
||
294 | } |
||
295 | |||
296 | 7 | $this->identityMap[$className][$idHash] = $entity; |
|
297 | |||
298 | 7 | return true; |
|
299 | } |
||
300 | |||
301 | /** |
||
302 | * Gets the identity map of the UnitOfWork. |
||
303 | * |
||
304 | * @return array |
||
305 | */ |
||
306 | public function getIdentityMap() |
||
307 | { |
||
308 | return $this->identityMap; |
||
309 | } |
||
310 | |||
311 | /** |
||
312 | * Gets the original data of an entity. The original data is the data that was |
||
313 | * present at the time the entity was reconstituted from the database. |
||
314 | * |
||
315 | * @param object $entity |
||
316 | * |
||
317 | * @return array |
||
318 | */ |
||
319 | public function getOriginalEntityData($entity) |
||
320 | { |
||
321 | $oid = spl_object_hash($entity); |
||
322 | |||
323 | if (isset($this->originalEntityData[$oid])) { |
||
324 | return $this->originalEntityData[$oid]; |
||
325 | } |
||
326 | |||
327 | return []; |
||
328 | } |
||
329 | |||
330 | /** |
||
331 | * INTERNAL: |
||
332 | * Checks whether an identifier hash exists in the identity map. |
||
333 | * |
||
334 | * @ignore |
||
335 | * |
||
336 | * @param string $idHash |
||
337 | * @param string $rootClassName |
||
338 | * |
||
339 | * @return boolean |
||
340 | */ |
||
341 | public function containsIdHash($idHash, $rootClassName) |
||
342 | { |
||
343 | return isset($this->identityMap[$rootClassName][$idHash]); |
||
344 | } |
||
345 | |||
346 | /** |
||
347 | * INTERNAL: |
||
348 | * Gets an entity in the identity map by its identifier hash. |
||
349 | * |
||
350 | * @ignore |
||
351 | * |
||
352 | * @param string $idHash |
||
353 | * @param string $rootClassName |
||
354 | * |
||
355 | * @return object |
||
356 | */ |
||
357 | public function getByIdHash($idHash, $rootClassName) |
||
358 | { |
||
359 | return $this->identityMap[$rootClassName][$idHash]; |
||
360 | } |
||
361 | |||
362 | /** |
||
363 | * INTERNAL: |
||
364 | * Tries to get an entity by its identifier hash. If no entity is found for |
||
365 | * the given hash, FALSE is returned. |
||
366 | * |
||
367 | * @ignore |
||
368 | * |
||
369 | * @param mixed $idHash (must be possible to cast it to string) |
||
370 | * @param string $rootClassName |
||
371 | * |
||
372 | * @return object|bool The found entity or FALSE. |
||
373 | */ |
||
374 | public function tryGetByIdHash($idHash, $rootClassName) |
||
375 | { |
||
376 | $stringIdHash = (string)$idHash; |
||
377 | |||
378 | if (isset($this->identityMap[$rootClassName][$stringIdHash])) { |
||
379 | return $this->identityMap[$rootClassName][$stringIdHash]; |
||
380 | } |
||
381 | |||
382 | return false; |
||
383 | } |
||
384 | |||
385 | /** |
||
386 | * Gets the state of an entity with regard to the current unit of work. |
||
387 | * |
||
388 | * @param object $entity |
||
389 | * @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED). |
||
390 | * This parameter can be set to improve performance of entity state detection |
||
391 | * by potentially avoiding a database lookup if the distinction between NEW and DETACHED |
||
392 | * is either known or does not matter for the caller of the method. |
||
393 | * |
||
394 | * @return int The entity state. |
||
395 | */ |
||
396 | 4 | public function getEntityState($entity, $assume = null) |
|
397 | { |
||
398 | 4 | $oid = spl_object_hash($entity); |
|
399 | 4 | if (isset($this->entityStates[$oid])) { |
|
400 | 2 | return $this->entityStates[$oid]; |
|
401 | } |
||
402 | 4 | if ($assume !== null) { |
|
403 | 4 | return $assume; |
|
404 | } |
||
405 | // State can only be NEW or DETACHED, because MANAGED/REMOVED states are known. |
||
406 | // Note that you can not remember the NEW or DETACHED state in _entityStates since |
||
407 | // the UoW does not hold references to such objects and the object hash can be reused. |
||
408 | // More generally because the state may "change" between NEW/DETACHED without the UoW being aware of it. |
||
409 | $class = $this->manager->getClassMetadata(get_class($entity)); |
||
410 | $id = $class->getIdentifierValues($entity); |
||
411 | if (!$id) { |
||
|
|||
412 | return self::STATE_NEW; |
||
413 | } |
||
414 | |||
415 | return self::STATE_DETACHED; |
||
416 | } |
||
417 | |||
418 | /** |
||
419 | * Tries to find an entity with the given identifier in the identity map of |
||
420 | * this UnitOfWork. |
||
421 | * |
||
422 | * @param mixed $id The entity identifier to look for. |
||
423 | * @param string $rootClassName The name of the root class of the mapped entity hierarchy. |
||
424 | * |
||
425 | * @return object|bool Returns the entity with the specified identifier if it exists in |
||
426 | * this UnitOfWork, FALSE otherwise. |
||
427 | */ |
||
428 | 11 | public function tryGetById($id, $rootClassName) |
|
429 | { |
||
430 | /** @var EntityMetadata $metadata */ |
||
431 | 11 | $metadata = $this->manager->getClassMetadata($rootClassName); |
|
432 | 11 | $idHash = implode(' ', (array)$this->identifierFlattener->flattenIdentifier($metadata, $id)); |
|
433 | |||
434 | 11 | if (isset($this->identityMap[$rootClassName][$idHash])) { |
|
435 | 4 | return $this->identityMap[$rootClassName][$idHash]; |
|
436 | } |
||
437 | |||
438 | 11 | return false; |
|
439 | } |
||
440 | |||
441 | /** |
||
442 | * Notifies this UnitOfWork of a property change in an entity. |
||
443 | * |
||
444 | * @param object $entity The entity that owns the property. |
||
445 | * @param string $propertyName The name of the property that changed. |
||
446 | * @param mixed $oldValue The old value of the property. |
||
447 | * @param mixed $newValue The new value of the property. |
||
448 | * |
||
449 | * @return void |
||
450 | */ |
||
451 | public function propertyChanged($entity, $propertyName, $oldValue, $newValue) |
||
452 | { |
||
453 | $oid = spl_object_hash($entity); |
||
454 | $class = $this->manager->getClassMetadata(get_class($entity)); |
||
455 | $isAssocField = $class->hasAssociation($propertyName); |
||
456 | if (!$isAssocField && !$class->hasField($propertyName)) { |
||
457 | return; // ignore non-persistent fields |
||
458 | } |
||
459 | // Update changeset and mark entity for synchronization |
||
460 | $this->entityChangeSets[$oid][$propertyName] = [$oldValue, $newValue]; |
||
461 | if (!isset($this->scheduledForSynchronization[$class->getRootEntityName()][$oid])) { |
||
462 | $this->scheduleForDirtyCheck($entity); |
||
463 | } |
||
464 | } |
||
465 | |||
466 | /** |
||
467 | * Persists an entity as part of the current unit of work. |
||
468 | * |
||
469 | * @param object $entity The entity to persist. |
||
470 | * |
||
471 | * @return void |
||
472 | */ |
||
473 | 4 | public function persist($entity) |
|
474 | { |
||
475 | 4 | $visited = []; |
|
476 | 4 | $this->doPersist($entity, $visited); |
|
477 | 4 | } |
|
478 | |||
479 | /** |
||
480 | * @param ApiMetadata $class |
||
481 | * @param $entity |
||
482 | * |
||
483 | * @throws \InvalidArgumentException |
||
484 | * @throws \RuntimeException |
||
485 | */ |
||
486 | 1 | public function recomputeSingleEntityChangeSet(ApiMetadata $class, $entity) |
|
487 | { |
||
488 | 1 | $oid = spl_object_hash($entity); |
|
489 | 1 | if (!isset($this->entityStates[$oid]) || $this->entityStates[$oid] != self::STATE_MANAGED) { |
|
490 | throw new \InvalidArgumentException('Entity is not managed'); |
||
491 | } |
||
492 | |||
493 | 1 | $actualData = []; |
|
494 | 1 | foreach ($class->getReflectionProperties() as $name => $refProp) { |
|
495 | 1 | if (!$class->isIdentifier($name) && !$class->isCollectionValuedAssociation($name)) { |
|
496 | 1 | $actualData[$name] = $refProp->getValue($entity); |
|
497 | 1 | } |
|
498 | 1 | } |
|
499 | 1 | if (!isset($this->originalEntityData[$oid])) { |
|
500 | throw new \RuntimeException( |
||
501 | 'Cannot call recomputeSingleEntityChangeSet before computeChangeSet on an entity.' |
||
502 | ); |
||
503 | } |
||
504 | 1 | $originalData = $this->originalEntityData[$oid]; |
|
505 | 1 | $changeSet = []; |
|
506 | 1 | foreach ($actualData as $propName => $actualValue) { |
|
507 | 1 | $orgValue = isset($originalData[$propName]) ? $originalData[$propName] : null; |
|
508 | 1 | if ($orgValue !== $actualValue) { |
|
509 | $changeSet[$propName] = [$orgValue, $actualValue]; |
||
510 | } |
||
511 | 1 | } |
|
512 | 1 | if ($changeSet) { |
|
513 | if (isset($this->entityChangeSets[$oid])) { |
||
514 | $this->entityChangeSets[$oid] = array_merge($this->entityChangeSets[$oid], $changeSet); |
||
515 | } else { |
||
516 | if (!isset($this->entityInsertions[$oid])) { |
||
517 | $this->entityChangeSets[$oid] = $changeSet; |
||
518 | $this->entityUpdates[$oid] = $entity; |
||
519 | } |
||
520 | } |
||
521 | $this->originalEntityData[$oid] = $actualData; |
||
522 | } |
||
523 | 1 | } |
|
524 | |||
525 | /** |
||
526 | * Schedules an entity for insertion into the database. |
||
527 | * If the entity already has an identifier, it will be added to the identity map. |
||
528 | * |
||
529 | * @param object $entity The entity to schedule for insertion. |
||
530 | * |
||
531 | * @return void |
||
532 | * |
||
533 | * @throws \InvalidArgumentException |
||
534 | */ |
||
535 | 4 | public function scheduleForInsert($entity) |
|
536 | { |
||
537 | 4 | $oid = spl_object_hash($entity); |
|
538 | 4 | if (isset($this->entityUpdates[$oid])) { |
|
539 | throw new \InvalidArgumentException('Dirty entity cannot be scheduled for insertion'); |
||
540 | } |
||
541 | 4 | if (isset($this->entityDeletions[$oid])) { |
|
542 | throw new \InvalidArgumentException('Removed entity scheduled for insertion'); |
||
543 | } |
||
544 | 4 | if (isset($this->originalEntityData[$oid]) && !isset($this->entityInsertions[$oid])) { |
|
545 | throw new \InvalidArgumentException('Managed entity scheduled for insertion'); |
||
546 | } |
||
547 | 4 | if (isset($this->entityInsertions[$oid])) { |
|
548 | throw new \InvalidArgumentException('Entity scheduled for insertion twice'); |
||
549 | } |
||
550 | 4 | $this->entityInsertions[$oid] = $entity; |
|
551 | 4 | if (isset($this->entityIdentifiers[$oid])) { |
|
552 | $this->addToIdentityMap($entity); |
||
553 | } |
||
554 | 4 | if ($entity instanceof NotifyPropertyChanged) { |
|
555 | $entity->addPropertyChangedListener($this); |
||
556 | } |
||
557 | 4 | } |
|
558 | |||
559 | /** |
||
560 | * Checks whether an entity is scheduled for insertion. |
||
561 | * |
||
562 | * @param object $entity |
||
563 | * |
||
564 | * @return boolean |
||
565 | */ |
||
566 | 1 | public function isScheduledForInsert($entity) |
|
567 | { |
||
568 | 1 | return isset($this->entityInsertions[spl_object_hash($entity)]); |
|
569 | } |
||
570 | |||
571 | /** |
||
572 | * Schedules an entity for being updated. |
||
573 | * |
||
574 | * @param object $entity The entity to schedule for being updated. |
||
575 | * |
||
576 | * @return void |
||
577 | * |
||
578 | * @throws \InvalidArgumentException |
||
579 | */ |
||
580 | public function scheduleForUpdate($entity) |
||
581 | { |
||
582 | $oid = spl_object_hash($entity); |
||
583 | if (!isset($this->entityIdentifiers[$oid])) { |
||
584 | throw new \InvalidArgumentException('Entity has no identity'); |
||
585 | } |
||
586 | if (isset($this->entityDeletions[$oid])) { |
||
587 | throw new \InvalidArgumentException('Entity is removed'); |
||
588 | } |
||
589 | if (!isset($this->entityUpdates[$oid]) && !isset($this->entityInsertions[$oid])) { |
||
590 | $this->entityUpdates[$oid] = $entity; |
||
591 | } |
||
592 | } |
||
593 | |||
594 | /** |
||
595 | * Checks whether an entity is registered as dirty in the unit of work. |
||
596 | * Note: Is not very useful currently as dirty entities are only registered |
||
597 | * at commit time. |
||
598 | * |
||
599 | * @param object $entity |
||
600 | * |
||
601 | * @return boolean |
||
602 | */ |
||
603 | public function isScheduledForUpdate($entity) |
||
604 | { |
||
605 | return isset($this->entityUpdates[spl_object_hash($entity)]); |
||
606 | } |
||
607 | |||
608 | /** |
||
609 | * Checks whether an entity is registered to be checked in the unit of work. |
||
610 | * |
||
611 | * @param object $entity |
||
612 | * |
||
613 | * @return boolean |
||
614 | */ |
||
615 | public function isScheduledForDirtyCheck($entity) |
||
616 | { |
||
617 | $rootEntityName = $this->manager->getClassMetadata(get_class($entity))->getRootEntityName(); |
||
618 | |||
619 | return isset($this->scheduledForSynchronization[$rootEntityName][spl_object_hash($entity)]); |
||
620 | } |
||
621 | |||
622 | /** |
||
623 | * INTERNAL: |
||
624 | * Schedules an entity for deletion. |
||
625 | * |
||
626 | * @param object $entity |
||
627 | * |
||
628 | * @return void |
||
629 | */ |
||
630 | public function scheduleForDelete($entity) |
||
631 | { |
||
632 | $oid = spl_object_hash($entity); |
||
633 | if (isset($this->entityInsertions[$oid])) { |
||
634 | if ($this->isInIdentityMap($entity)) { |
||
635 | $this->removeFromIdentityMap($entity); |
||
636 | } |
||
637 | unset($this->entityInsertions[$oid], $this->entityStates[$oid]); |
||
638 | |||
639 | return; // entity has not been persisted yet, so nothing more to do. |
||
640 | } |
||
641 | if (!$this->isInIdentityMap($entity)) { |
||
642 | return; |
||
643 | } |
||
644 | $this->removeFromIdentityMap($entity); |
||
645 | unset($this->entityUpdates[$oid]); |
||
646 | if (!isset($this->entityDeletions[$oid])) { |
||
647 | $this->entityDeletions[$oid] = $entity; |
||
648 | $this->entityStates[$oid] = self::STATE_REMOVED; |
||
649 | } |
||
650 | } |
||
651 | |||
652 | /** |
||
653 | * Checks whether an entity is registered as removed/deleted with the unit |
||
654 | * of work. |
||
655 | * |
||
656 | * @param object $entity |
||
657 | * |
||
658 | * @return boolean |
||
659 | */ |
||
660 | public function isScheduledForDelete($entity) |
||
661 | { |
||
662 | return isset($this->entityDeletions[spl_object_hash($entity)]); |
||
663 | } |
||
664 | |||
665 | /** |
||
666 | * Checks whether an entity is scheduled for insertion, update or deletion. |
||
667 | * |
||
668 | * @param object $entity |
||
669 | * |
||
670 | * @return boolean |
||
671 | */ |
||
672 | public function isEntityScheduled($entity) |
||
673 | { |
||
674 | $oid = spl_object_hash($entity); |
||
675 | |||
676 | return isset($this->entityInsertions[$oid]) |
||
677 | || isset($this->entityUpdates[$oid]) |
||
678 | || isset($this->entityDeletions[$oid]); |
||
679 | } |
||
680 | |||
681 | /** |
||
682 | * INTERNAL: |
||
683 | * Removes an entity from the identity map. This effectively detaches the |
||
684 | * entity from the persistence management of Doctrine. |
||
685 | * |
||
686 | * @ignore |
||
687 | * |
||
688 | * @param object $entity |
||
689 | * |
||
690 | * @return boolean |
||
691 | * |
||
692 | * @throws \InvalidArgumentException |
||
693 | */ |
||
694 | public function removeFromIdentityMap($entity) |
||
695 | { |
||
696 | $oid = spl_object_hash($entity); |
||
697 | $classMetadata = $this->manager->getClassMetadata(get_class($entity)); |
||
698 | $idHash = implode(' ', $this->entityIdentifiers[$oid]); |
||
699 | if ($idHash === '') { |
||
700 | throw new \InvalidArgumentException('Entity has no identity'); |
||
701 | } |
||
702 | $className = $classMetadata->getRootEntityName(); |
||
703 | if (isset($this->identityMap[$className][$idHash])) { |
||
704 | unset($this->identityMap[$className][$idHash]); |
||
705 | unset($this->readOnlyObjects[$oid]); |
||
706 | |||
707 | //$this->entityStates[$oid] = self::STATE_DETACHED; |
||
708 | return true; |
||
709 | } |
||
710 | |||
711 | return false; |
||
712 | } |
||
713 | |||
714 | /** |
||
715 | * Commits the UnitOfWork, executing all operations that have been postponed |
||
716 | * up to this point. The state of all managed entities will be synchronized with |
||
717 | * the database. |
||
718 | * |
||
719 | * The operations are executed in the following order: |
||
720 | * |
||
721 | * 1) All entity insertions |
||
722 | * 2) All entity updates |
||
723 | * 3) All collection deletions |
||
724 | * 4) All collection updates |
||
725 | * 5) All entity deletions |
||
726 | * |
||
727 | * @param null|object|array $entity |
||
728 | * |
||
729 | * @return void |
||
730 | * |
||
731 | * @throws \Exception |
||
732 | */ |
||
733 | 4 | public function commit($entity = null) |
|
734 | { |
||
735 | // Compute changes done since last commit. |
||
736 | 4 | if ($entity === null) { |
|
737 | 4 | $this->computeChangeSets(); |
|
738 | 4 | } elseif (is_object($entity)) { |
|
739 | $this->computeSingleEntityChangeSet($entity); |
||
740 | } elseif (is_array($entity)) { |
||
741 | foreach ((array)$entity as $object) { |
||
742 | $this->computeSingleEntityChangeSet($object); |
||
743 | } |
||
744 | } |
||
745 | 4 | if (!($this->entityInsertions || |
|
746 | 1 | $this->entityDeletions || |
|
747 | 1 | $this->entityUpdates || |
|
748 | 1 | $this->collectionUpdates || |
|
749 | 1 | $this->collectionDeletions || |
|
750 | 1 | $this->orphanRemovals) |
|
751 | 4 | ) { |
|
752 | 1 | return; // Nothing to do. |
|
753 | } |
||
754 | 4 | if ($this->orphanRemovals) { |
|
755 | foreach ($this->orphanRemovals as $orphan) { |
||
756 | $this->remove($orphan); |
||
757 | } |
||
758 | } |
||
759 | // Now we need a commit order to maintain referential integrity |
||
760 | 4 | $commitOrder = $this->getCommitOrder(); |
|
761 | |||
762 | // Collection deletions (deletions of complete collections) |
||
763 | // foreach ($this->collectionDeletions as $collectionToDelete) { |
||
764 | // //fixme: collection mutations |
||
765 | // $this->getCollectionPersister($collectionToDelete->getMapping())->delete($collectionToDelete); |
||
766 | // } |
||
767 | 4 | if ($this->entityInsertions) { |
|
768 | 4 | foreach ($commitOrder as $class) { |
|
769 | 4 | $this->executeInserts($class); |
|
770 | 4 | } |
|
771 | 4 | } |
|
772 | 4 | if ($this->entityUpdates) { |
|
773 | 1 | foreach ($commitOrder as $class) { |
|
774 | 1 | $this->executeUpdates($class); |
|
775 | 1 | } |
|
776 | 1 | } |
|
777 | // Extra updates that were requested by persisters. |
||
778 | 4 | if ($this->extraUpdates) { |
|
779 | $this->executeExtraUpdates(); |
||
780 | } |
||
781 | // Collection updates (deleteRows, updateRows, insertRows) |
||
782 | 4 | foreach ($this->collectionUpdates as $collectionToUpdate) { |
|
783 | //fixme: decide what to do with collection mutation if API does not support this |
||
784 | //$this->getCollectionPersister($collectionToUpdate->getMapping())->update($collectionToUpdate); |
||
785 | 4 | } |
|
786 | // Entity deletions come last and need to be in reverse commit order |
||
787 | 4 | if ($this->entityDeletions) { |
|
788 | for ($count = count($commitOrder), $i = $count - 1; $i >= 0 && $this->entityDeletions; --$i) { |
||
789 | $this->executeDeletions($commitOrder[$i]); |
||
790 | } |
||
791 | } |
||
792 | |||
793 | // Take new snapshots from visited collections |
||
794 | 4 | foreach ($this->visitedCollections as $coll) { |
|
795 | 2 | $coll->takeSnapshot(); |
|
796 | 4 | } |
|
797 | |||
798 | // Clear up |
||
799 | 4 | $this->entityInsertions = |
|
800 | 4 | $this->entityUpdates = |
|
801 | 4 | $this->entityDeletions = |
|
802 | 4 | $this->extraUpdates = |
|
803 | 4 | $this->entityChangeSets = |
|
804 | 4 | $this->collectionUpdates = |
|
805 | 4 | $this->collectionDeletions = |
|
806 | 4 | $this->visitedCollections = |
|
807 | 4 | $this->scheduledForSynchronization = |
|
808 | 4 | $this->orphanRemovals = []; |
|
809 | 4 | } |
|
810 | |||
811 | /** |
||
812 | * Gets the changeset for an entity. |
||
813 | * |
||
814 | * @param object $entity |
||
815 | * |
||
816 | * @return array |
||
817 | */ |
||
818 | 1 | public function & getEntityChangeSet($entity) |
|
819 | { |
||
820 | 1 | $oid = spl_object_hash($entity); |
|
821 | 1 | $data = []; |
|
822 | 1 | if (!isset($this->entityChangeSets[$oid])) { |
|
823 | return $data; |
||
824 | } |
||
825 | |||
826 | 1 | return $this->entityChangeSets[$oid]; |
|
827 | } |
||
828 | |||
829 | /** |
||
830 | * Computes the changes that happened to a single entity. |
||
831 | * |
||
832 | * Modifies/populates the following properties: |
||
833 | * |
||
834 | * {@link _originalEntityData} |
||
835 | * If the entity is NEW or MANAGED but not yet fully persisted (only has an id) |
||
836 | * then it was not fetched from the database and therefore we have no original |
||
837 | * entity data yet. All of the current entity data is stored as the original entity data. |
||
838 | * |
||
839 | * {@link _entityChangeSets} |
||
840 | * The changes detected on all properties of the entity are stored there. |
||
841 | * A change is a tuple array where the first entry is the old value and the second |
||
842 | * entry is the new value of the property. Changesets are used by persisters |
||
843 | * to INSERT/UPDATE the persistent entity state. |
||
844 | * |
||
845 | * {@link _entityUpdates} |
||
846 | * If the entity is already fully MANAGED (has been fetched from the database before) |
||
847 | * and any changes to its properties are detected, then a reference to the entity is stored |
||
848 | * there to mark it for an update. |
||
849 | * |
||
850 | * {@link _collectionDeletions} |
||
851 | * If a PersistentCollection has been de-referenced in a fully MANAGED entity, |
||
852 | * then this collection is marked for deletion. |
||
853 | * |
||
854 | * @ignore |
||
855 | * |
||
856 | * @internal Don't call from the outside. |
||
857 | * |
||
858 | * @param ApiMetadata $class The class descriptor of the entity. |
||
859 | * @param object $entity The entity for which to compute the changes. |
||
860 | * |
||
861 | * @return void |
||
862 | */ |
||
863 | 4 | public function computeChangeSet(ApiMetadata $class, $entity) |
|
864 | { |
||
865 | 4 | $oid = spl_object_hash($entity); |
|
866 | 4 | if (isset($this->readOnlyObjects[$oid])) { |
|
867 | return; |
||
868 | } |
||
869 | // if ( ! $class->isInheritanceTypeNone()) { |
||
870 | // $class = $this->em->getClassMetadata(get_class($entity)); |
||
871 | // } |
||
872 | // $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preFlush) & ~ListenersInvoker::INVOKE_MANAGER; |
||
873 | // if ($invoke !== ListenersInvoker::INVOKE_NONE) { |
||
874 | // $this->listenersInvoker->invoke($class, Events::preFlush, $entity, new PreFlushEventArgs($this->em), $invoke); |
||
875 | // } |
||
876 | 4 | $actualData = []; |
|
877 | 4 | foreach ($class->getReflectionProperties() as $name => $refProp) { |
|
878 | 4 | $value = $refProp->getValue($entity); |
|
879 | 4 | if ($class->isCollectionValuedAssociation($name) && $value !== null) { |
|
880 | 2 | if ($value instanceof ApiCollection) { |
|
881 | 1 | if ($value->getOwner() === $entity) { |
|
882 | 1 | continue; |
|
883 | } |
||
884 | $value = new ArrayCollection($value->getValues()); |
||
885 | } |
||
886 | // If $value is not a Collection then use an ArrayCollection. |
||
887 | 2 | if (!$value instanceof Collection) { |
|
888 | $value = new ArrayCollection($value); |
||
889 | } |
||
890 | 2 | $assoc = $class->getAssociationMapping($name); |
|
891 | // Inject PersistentCollection |
||
892 | 2 | $value = new ApiCollection( |
|
893 | 2 | $this->manager, |
|
894 | 2 | $this->manager->getClassMetadata($assoc['target']), |
|
895 | $value |
||
896 | 2 | ); |
|
897 | 2 | $value->setOwner($entity, $assoc); |
|
898 | 2 | $value->setDirty(!$value->isEmpty()); |
|
899 | 2 | $class->getReflectionProperty($name)->setValue($entity, $value); |
|
900 | 2 | $actualData[$name] = $value; |
|
901 | 2 | continue; |
|
902 | } |
||
903 | 4 | if (!$class->isIdentifier($name)) { |
|
904 | 4 | $actualData[$name] = $value; |
|
905 | 4 | } |
|
906 | 4 | } |
|
907 | 4 | if (!isset($this->originalEntityData[$oid])) { |
|
908 | // Entity is either NEW or MANAGED but not yet fully persisted (only has an id). |
||
909 | // These result in an INSERT. |
||
910 | 4 | $this->originalEntityData[$oid] = $actualData; |
|
911 | 4 | $changeSet = []; |
|
912 | 4 | foreach ($actualData as $propName => $actualValue) { |
|
913 | 4 | View Code Duplication | if (!$class->hasAssociation($propName)) { |
914 | 4 | $changeSet[$propName] = [null, $actualValue]; |
|
915 | 4 | continue; |
|
916 | } |
||
917 | 4 | $assoc = $class->getAssociationMapping($propName); |
|
918 | 4 | if ($assoc['isOwningSide'] && $assoc['type'] & ApiMetadata::TO_ONE) { |
|
919 | 4 | $changeSet[$propName] = [null, $actualValue]; |
|
920 | 4 | } |
|
921 | 4 | } |
|
922 | 4 | $this->entityChangeSets[$oid] = $changeSet; |
|
923 | 4 | } else { |
|
924 | // Entity is "fully" MANAGED: it was already fully persisted before |
||
925 | // and we have a copy of the original data |
||
926 | 2 | $originalData = $this->originalEntityData[$oid]; |
|
927 | 2 | $isChangeTrackingNotify = false; |
|
928 | 2 | $changeSet = ($isChangeTrackingNotify && isset($this->entityChangeSets[$oid])) |
|
929 | 2 | ? $this->entityChangeSets[$oid] |
|
930 | 2 | : []; |
|
931 | 2 | foreach ($actualData as $propName => $actualValue) { |
|
932 | // skip field, its a partially omitted one! |
||
933 | 2 | if (!(isset($originalData[$propName]) || array_key_exists($propName, $originalData))) { |
|
934 | continue; |
||
935 | } |
||
936 | 2 | $orgValue = $originalData[$propName]; |
|
937 | // skip if value haven't changed |
||
938 | 2 | if ($orgValue === $actualValue) { |
|
939 | 2 | continue; |
|
940 | } |
||
941 | // if regular field |
||
942 | 1 | View Code Duplication | if (!$class->hasAssociation($propName)) { |
943 | if ($isChangeTrackingNotify) { |
||
944 | continue; |
||
945 | } |
||
946 | $changeSet[$propName] = [$orgValue, $actualValue]; |
||
947 | continue; |
||
948 | } |
||
949 | 1 | $assoc = $class->getAssociationMapping($propName); |
|
950 | // Persistent collection was exchanged with the "originally" |
||
951 | // created one. This can only mean it was cloned and replaced |
||
952 | // on another entity. |
||
953 | 1 | if ($actualValue instanceof ApiCollection) { |
|
954 | $owner = $actualValue->getOwner(); |
||
955 | if ($owner === null) { // cloned |
||
956 | $actualValue->setOwner($entity, $assoc); |
||
957 | } else { |
||
958 | if ($owner !== $entity) { // no clone, we have to fix |
||
959 | if (!$actualValue->isInitialized()) { |
||
960 | $actualValue->initialize(); // we have to do this otherwise the cols share state |
||
961 | } |
||
962 | $newValue = clone $actualValue; |
||
963 | $newValue->setOwner($entity, $assoc); |
||
964 | $class->getReflectionProperty($propName)->setValue($entity, $newValue); |
||
965 | } |
||
966 | } |
||
967 | } |
||
968 | 1 | if ($orgValue instanceof ApiCollection) { |
|
969 | // A PersistentCollection was de-referenced, so delete it. |
||
970 | $coid = spl_object_hash($orgValue); |
||
971 | if (isset($this->collectionDeletions[$coid])) { |
||
972 | continue; |
||
973 | } |
||
974 | $this->collectionDeletions[$coid] = $orgValue; |
||
975 | $changeSet[$propName] = $orgValue; // Signal changeset, to-many assocs will be ignored. |
||
976 | continue; |
||
977 | } |
||
978 | 1 | if ($assoc['type'] & ApiMetadata::TO_ONE) { |
|
979 | 1 | if ($assoc['isOwningSide']) { |
|
980 | 1 | $changeSet[$propName] = [$orgValue, $actualValue]; |
|
981 | 1 | } |
|
982 | 1 | if ($orgValue !== null && $assoc['orphanRemoval']) { |
|
983 | $this->scheduleOrphanRemoval($orgValue); |
||
984 | } |
||
985 | 1 | } |
|
986 | 2 | } |
|
987 | 2 | if ($changeSet) { |
|
988 | 1 | $this->entityChangeSets[$oid] = $changeSet; |
|
989 | 1 | $this->originalEntityData[$oid] = $actualData; |
|
990 | 1 | $this->entityUpdates[$oid] = $entity; |
|
991 | 1 | } |
|
992 | } |
||
993 | // Look for changes in associations of the entity |
||
994 | 4 | foreach ($class->getAssociationMappings() as $field => $assoc) { |
|
995 | 4 | if (($val = $class->getReflectionProperty($field)->getValue($entity)) === null) { |
|
996 | 4 | continue; |
|
997 | } |
||
998 | 2 | $this->computeAssociationChanges($assoc, $val); |
|
999 | 2 | if (!isset($this->entityChangeSets[$oid]) && |
|
1000 | 2 | $assoc['isOwningSide'] && |
|
1001 | 2 | $assoc['type'] == ApiMetadata::MANY_TO_MANY && |
|
1002 | 2 | $val instanceof ApiCollection && |
|
1003 | $val->isDirty() |
||
1004 | 2 | ) { |
|
1005 | $this->entityChangeSets[$oid] = []; |
||
1006 | $this->originalEntityData[$oid] = $actualData; |
||
1007 | $this->entityUpdates[$oid] = $entity; |
||
1008 | } |
||
1009 | 4 | } |
|
1010 | 4 | } |
|
1011 | |||
1012 | /** |
||
1013 | * Computes all the changes that have been done to entities and collections |
||
1014 | * since the last commit and stores these changes in the _entityChangeSet map |
||
1015 | * temporarily for access by the persisters, until the UoW commit is finished. |
||
1016 | * |
||
1017 | * @return void |
||
1018 | */ |
||
1019 | 4 | public function computeChangeSets() |
|
1020 | { |
||
1021 | // Compute changes for INSERTed entities first. This must always happen. |
||
1022 | 4 | $this->computeScheduleInsertsChangeSets(); |
|
1023 | // Compute changes for other MANAGED entities. Change tracking policies take effect here. |
||
1024 | 4 | foreach ($this->identityMap as $className => $entities) { |
|
1025 | 2 | $class = $this->manager->getClassMetadata($className); |
|
1026 | // Skip class if instances are read-only |
||
1027 | 2 | if ($class->isReadOnly()) { |
|
1028 | continue; |
||
1029 | } |
||
1030 | // If change tracking is explicit or happens through notification, then only compute |
||
1031 | // changes on entities of that type that are explicitly marked for synchronization. |
||
1032 | 2 | switch (true) { |
|
1033 | 2 | case ($class->isChangeTrackingDeferredImplicit()): |
|
1034 | 2 | $entitiesToProcess = $entities; |
|
1035 | 2 | break; |
|
1036 | case (isset($this->scheduledForSynchronization[$className])): |
||
1037 | $entitiesToProcess = $this->scheduledForSynchronization[$className]; |
||
1038 | break; |
||
1039 | default: |
||
1040 | $entitiesToProcess = []; |
||
1041 | } |
||
1042 | 2 | foreach ($entitiesToProcess as $entity) { |
|
1043 | // Ignore uninitialized proxy objects |
||
1044 | 2 | if ($entity instanceof Proxy && !$entity->__isInitialized__) { |
|
1045 | continue; |
||
1046 | } |
||
1047 | // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here. |
||
1048 | 2 | $oid = spl_object_hash($entity); |
|
1049 | 2 | View Code Duplication | if (!isset($this->entityInsertions[$oid]) && |
1050 | 2 | !isset($this->entityDeletions[$oid]) && |
|
1051 | 2 | isset($this->entityStates[$oid]) |
|
1052 | 2 | ) { |
|
1053 | 2 | $this->computeChangeSet($class, $entity); |
|
1054 | 2 | } |
|
1055 | 2 | } |
|
1056 | 4 | } |
|
1057 | 4 | } |
|
1058 | |||
1059 | /** |
||
1060 | * INTERNAL: |
||
1061 | * Schedules an orphaned entity for removal. The remove() operation will be |
||
1062 | * invoked on that entity at the beginning of the next commit of this |
||
1063 | * UnitOfWork. |
||
1064 | * |
||
1065 | * @ignore |
||
1066 | * |
||
1067 | * @param object $entity |
||
1068 | * |
||
1069 | * @return void |
||
1070 | */ |
||
1071 | public function scheduleOrphanRemoval($entity) |
||
1072 | { |
||
1073 | $this->orphanRemovals[spl_object_hash($entity)] = $entity; |
||
1074 | } |
||
1075 | |||
1076 | 2 | public function loadCollection(ApiCollection $collection) |
|
1077 | { |
||
1078 | 2 | $assoc = $collection->getMapping(); |
|
1079 | 2 | $persister = $this->getEntityPersister($assoc['target']); |
|
1080 | 2 | switch ($assoc['type']) { |
|
1081 | 2 | case ApiMetadata::ONE_TO_MANY: |
|
1082 | 2 | $persister->loadOneToManyCollection($assoc, $collection->getOwner(), $collection); |
|
1083 | 2 | break; |
|
1084 | 2 | } |
|
1085 | 2 | $collection->setInitialized(true); |
|
1086 | 2 | } |
|
1087 | |||
1088 | public function getCollectionPersister($association) |
||
1089 | { |
||
1090 | $role = isset($association['cache']) |
||
1091 | ? $association['sourceEntity'].'::'.$association['fieldName'] |
||
1092 | : $association['type']; |
||
1093 | if (array_key_exists($role, $this->collectionPersisters)) { |
||
1094 | return $this->collectionPersisters[$role]; |
||
1095 | } |
||
1096 | $this->collectionPersisters[$role] = new CollectionPersister($this->manager); |
||
1097 | |||
1098 | return $this->collectionPersisters[$role]; |
||
1099 | } |
||
1100 | |||
1101 | public function scheduleCollectionDeletion(Collection $collection) |
||
1102 | { |
||
1103 | } |
||
1104 | |||
1105 | 2 | public function cancelOrphanRemoval($value) |
|
1106 | { |
||
1107 | 2 | } |
|
1108 | |||
1109 | /** |
||
1110 | * INTERNAL: |
||
1111 | * Sets a property value of the original data array of an entity. |
||
1112 | * |
||
1113 | * @ignore |
||
1114 | * |
||
1115 | * @param string $oid |
||
1116 | * @param string $property |
||
1117 | * @param mixed $value |
||
1118 | * |
||
1119 | * @return void |
||
1120 | */ |
||
1121 | 9 | public function setOriginalEntityProperty($oid, $property, $value) |
|
1122 | { |
||
1123 | 9 | if (!array_key_exists($oid, $this->originalEntityData)) { |
|
1124 | 9 | $this->originalEntityData[$oid] = new \stdClass(); |
|
1125 | 9 | } |
|
1126 | |||
1127 | 9 | $this->originalEntityData[$oid]->$property = $value; |
|
1128 | 9 | } |
|
1129 | |||
1130 | public function scheduleExtraUpdate($entity, $changeset) |
||
1131 | { |
||
1132 | $oid = spl_object_hash($entity); |
||
1133 | $extraUpdate = [$entity, $changeset]; |
||
1134 | if (isset($this->extraUpdates[$oid])) { |
||
1135 | list(, $changeset2) = $this->extraUpdates[$oid]; |
||
1136 | $extraUpdate = [$entity, $changeset + $changeset2]; |
||
1137 | } |
||
1138 | $this->extraUpdates[$oid] = $extraUpdate; |
||
1139 | } |
||
1140 | |||
1141 | /** |
||
1142 | * Refreshes the state of the given entity from the database, overwriting |
||
1143 | * any local, unpersisted changes. |
||
1144 | * |
||
1145 | * @param object $entity The entity to refresh. |
||
1146 | * |
||
1147 | * @return void |
||
1148 | * |
||
1149 | * @throws InvalidArgumentException If the entity is not MANAGED. |
||
1150 | */ |
||
1151 | public function refresh($entity) |
||
1152 | { |
||
1153 | $visited = []; |
||
1154 | $this->doRefresh($entity, $visited); |
||
1155 | } |
||
1156 | |||
1157 | /** |
||
1158 | * Clears the UnitOfWork. |
||
1159 | * |
||
1160 | * @param string|null $entityName if given, only entities of this type will get detached. |
||
1161 | * |
||
1162 | * @return void |
||
1163 | */ |
||
1164 | public function clear($entityName = null) |
||
1165 | { |
||
1166 | if ($entityName === null) { |
||
1167 | $this->identityMap = |
||
1168 | $this->entityIdentifiers = |
||
1169 | $this->originalEntityData = |
||
1170 | $this->entityChangeSets = |
||
1171 | $this->entityStates = |
||
1172 | $this->scheduledForSynchronization = |
||
1173 | $this->entityInsertions = |
||
1174 | $this->entityUpdates = |
||
1175 | $this->entityDeletions = |
||
1176 | $this->collectionDeletions = |
||
1177 | $this->collectionUpdates = |
||
1178 | $this->extraUpdates = |
||
1179 | $this->readOnlyObjects = |
||
1180 | $this->visitedCollections = |
||
1181 | $this->orphanRemovals = []; |
||
1182 | } else { |
||
1183 | $this->clearIdentityMapForEntityName($entityName); |
||
1184 | $this->clearEntityInsertionsForEntityName($entityName); |
||
1185 | } |
||
1186 | } |
||
1187 | |||
1188 | /** |
||
1189 | * @param PersistentCollection $coll |
||
1190 | * |
||
1191 | * @return bool |
||
1192 | */ |
||
1193 | public function isCollectionScheduledForDeletion(PersistentCollection $coll) |
||
1194 | { |
||
1195 | return isset($this->collectionDeletions[spl_object_hash($coll)]); |
||
1196 | } |
||
1197 | |||
1198 | /** |
||
1199 | * Schedules an entity for dirty-checking at commit-time. |
||
1200 | * |
||
1201 | * @param object $entity The entity to schedule for dirty-checking. |
||
1202 | * |
||
1203 | * @return void |
||
1204 | * |
||
1205 | * @todo Rename: scheduleForSynchronization |
||
1206 | */ |
||
1207 | public function scheduleForDirtyCheck($entity) |
||
1208 | { |
||
1209 | $rootClassName = |
||
1210 | $this->manager->getClassMetadata(get_class($entity))->getRootEntityName(); |
||
1211 | $this->scheduledForSynchronization[$rootClassName][spl_object_hash($entity)] = $entity; |
||
1212 | } |
||
1213 | |||
1214 | /** |
||
1215 | * Deletes an entity as part of the current unit of work. |
||
1216 | * |
||
1217 | * @param object $entity The entity to remove. |
||
1218 | * |
||
1219 | * @return void |
||
1220 | */ |
||
1221 | public function remove($entity) |
||
1222 | { |
||
1223 | $visited = []; |
||
1224 | $this->doRemove($entity, $visited); |
||
1225 | } |
||
1226 | |||
1227 | /** |
||
1228 | * Merges the state of the given detached entity into this UnitOfWork. |
||
1229 | * |
||
1230 | * @param object $entity |
||
1231 | * |
||
1232 | * @return object The managed copy of the entity. |
||
1233 | */ |
||
1234 | public function merge($entity) |
||
1235 | { |
||
1236 | $visited = []; |
||
1237 | |||
1238 | return $this->doMerge($entity, $visited); |
||
1239 | } |
||
1240 | |||
1241 | /** |
||
1242 | * Detaches an entity from the persistence management. It's persistence will |
||
1243 | * no longer be managed by Doctrine. |
||
1244 | * |
||
1245 | * @param object $entity The entity to detach. |
||
1246 | * |
||
1247 | * @return void |
||
1248 | */ |
||
1249 | public function detach($entity) |
||
1250 | { |
||
1251 | $visited = []; |
||
1252 | $this->doDetach($entity, $visited); |
||
1253 | } |
||
1254 | |||
1255 | /** |
||
1256 | * @param ApiMetadata $class |
||
1257 | * |
||
1258 | * @return \Doctrine\Common\Persistence\ObjectManagerAware|object |
||
1259 | */ |
||
1260 | 11 | private function newInstance(ApiMetadata $class) |
|
1261 | { |
||
1262 | 11 | $entity = $class->newInstance(); |
|
1263 | |||
1264 | 11 | if ($entity instanceof ObjectManagerAware) { |
|
1265 | $entity->injectObjectManager($this->manager, $class); |
||
1266 | } |
||
1267 | |||
1268 | 11 | return $entity; |
|
1269 | } |
||
1270 | |||
1271 | /** |
||
1272 | * @param ApiMetadata $classMetadata |
||
1273 | * |
||
1274 | * @return EntityDataCacheInterface |
||
1275 | */ |
||
1276 | 17 | private function createEntityCache(ApiMetadata $classMetadata) |
|
1277 | { |
||
1278 | 17 | $configuration = $this->manager->getConfiguration()->getCacheConfiguration($classMetadata->getName()); |
|
1279 | 17 | $cache = new VoidEntityCache($classMetadata); |
|
1280 | 17 | if ($configuration->isEnabled() && $this->manager->getConfiguration()->getApiCache()) { |
|
1281 | $cache = |
||
1282 | 1 | new LoggingCache( |
|
1283 | 1 | new ApiEntityCache( |
|
1284 | 1 | $this->manager->getConfiguration()->getApiCache(), |
|
1285 | 1 | $classMetadata, |
|
1286 | $configuration |
||
1287 | 1 | ), |
|
1288 | 1 | $this->manager->getConfiguration()->getApiCacheLogger() |
|
1289 | 1 | ); |
|
1290 | |||
1291 | 1 | return $cache; |
|
1292 | } |
||
1293 | |||
1294 | 16 | return $cache; |
|
1295 | } |
||
1296 | |||
1297 | /** |
||
1298 | * @param ApiMetadata $classMetadata |
||
1299 | * |
||
1300 | * @return CrudsApiInterface |
||
1301 | */ |
||
1302 | 17 | private function createApi(ApiMetadata $classMetadata) |
|
1303 | { |
||
1304 | 17 | $client = $this->manager->getConfiguration()->getClientRegistry()->get($classMetadata->getClientName()); |
|
1305 | |||
1306 | 17 | $api = $this->manager |
|
1307 | 17 | ->getConfiguration() |
|
1308 | 17 | ->getFactoryRegistry() |
|
1309 | 17 | ->create( |
|
1310 | 17 | $classMetadata->getApiFactory(), |
|
1311 | 17 | $client, |
|
1312 | $classMetadata |
||
1313 | 17 | ); |
|
1314 | |||
1315 | 17 | return $api; |
|
1316 | } |
||
1317 | |||
1318 | 4 | private function doPersist($entity, $visited) |
|
1319 | { |
||
1320 | 4 | $oid = spl_object_hash($entity); |
|
1321 | 4 | if (isset($visited[$oid])) { |
|
1322 | return; // Prevent infinite recursion |
||
1323 | } |
||
1324 | 4 | $visited[$oid] = $entity; // Mark visited |
|
1325 | 4 | $class = $this->manager->getClassMetadata(get_class($entity)); |
|
1326 | // We assume NEW, so DETACHED entities result in an exception on flush (constraint violation). |
||
1327 | // If we would detect DETACHED here we would throw an exception anyway with the same |
||
1328 | // consequences (not recoverable/programming error), so just assuming NEW here |
||
1329 | // lets us avoid some database lookups for entities with natural identifiers. |
||
1330 | 4 | $entityState = $this->getEntityState($entity, self::STATE_NEW); |
|
1331 | switch ($entityState) { |
||
1332 | 4 | case self::STATE_MANAGED: |
|
1333 | $this->scheduleForDirtyCheck($entity); |
||
1334 | break; |
||
1335 | 4 | case self::STATE_NEW: |
|
1336 | 4 | $this->persistNew($class, $entity); |
|
1337 | 4 | break; |
|
1338 | case self::STATE_REMOVED: |
||
1339 | // Entity becomes managed again |
||
1340 | unset($this->entityDeletions[$oid]); |
||
1341 | $this->addToIdentityMap($entity); |
||
1342 | $this->entityStates[$oid] = self::STATE_MANAGED; |
||
1343 | break; |
||
1344 | case self::STATE_DETACHED: |
||
1345 | // Can actually not happen right now since we assume STATE_NEW. |
||
1346 | throw new \InvalidArgumentException('Detached entity cannot be persisted'); |
||
1347 | default: |
||
1348 | throw new \UnexpectedValueException("Unexpected entity state: $entityState.".self::objToStr($entity)); |
||
1349 | } |
||
1350 | 4 | $this->cascadePersist($entity, $visited); |
|
1351 | 4 | } |
|
1352 | |||
1353 | /** |
||
1354 | * Cascades the save operation to associated entities. |
||
1355 | * |
||
1356 | * @param object $entity |
||
1357 | * @param array $visited |
||
1358 | * |
||
1359 | * @return void |
||
1360 | * @throws \InvalidArgumentException |
||
1361 | * @throws MappingException |
||
1362 | */ |
||
1363 | 4 | private function cascadePersist($entity, array &$visited) |
|
1364 | { |
||
1365 | 4 | $class = $this->manager->getClassMetadata(get_class($entity)); |
|
1366 | 4 | $associationMappings = []; |
|
1367 | 4 | foreach ($class->getAssociationNames() as $name) { |
|
1368 | 4 | $assoc = $class->getAssociationMapping($name); |
|
1369 | 4 | if ($assoc['isCascadePersist']) { |
|
1370 | $associationMappings[$name] = $assoc; |
||
1371 | } |
||
1372 | 4 | } |
|
1373 | 4 | foreach ($associationMappings as $assoc) { |
|
1374 | $relatedEntities = $class->getReflectionProperty($assoc['field'])->getValue($entity); |
||
1375 | switch (true) { |
||
1376 | case ($relatedEntities instanceof ApiCollection): |
||
1377 | // Unwrap so that foreach() does not initialize |
||
1378 | $relatedEntities = $relatedEntities->unwrap(); |
||
1379 | // break; is commented intentionally! |
||
1380 | case ($relatedEntities instanceof Collection): |
||
1381 | case (is_array($relatedEntities)): |
||
1382 | if (($assoc['type'] & ApiMetadata::TO_MANY) <= 0) { |
||
1383 | throw new \InvalidArgumentException('Invalid association for cascade'); |
||
1384 | } |
||
1385 | foreach ($relatedEntities as $relatedEntity) { |
||
1386 | $this->doPersist($relatedEntity, $visited); |
||
1387 | } |
||
1388 | break; |
||
1389 | case ($relatedEntities !== null): |
||
1390 | if (!$relatedEntities instanceof $assoc['target']) { |
||
1391 | throw new \InvalidArgumentException('Invalid association for cascade'); |
||
1392 | } |
||
1393 | $this->doPersist($relatedEntities, $visited); |
||
1394 | break; |
||
1395 | default: |
||
1396 | // Do nothing |
||
1397 | } |
||
1398 | 4 | } |
|
1399 | 4 | } |
|
1400 | |||
1401 | /** |
||
1402 | * @param ApiMetadata $class |
||
1403 | * @param object $entity |
||
1404 | * |
||
1405 | * @return void |
||
1406 | */ |
||
1407 | 4 | private function persistNew($class, $entity) |
|
1408 | { |
||
1409 | 4 | $oid = spl_object_hash($entity); |
|
1410 | // $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::prePersist); |
||
1411 | // if ($invoke !== ListenersInvoker::INVOKE_NONE) { |
||
1412 | // $this->listenersInvoker->invoke($class, Events::prePersist, $entity, new LifecycleEventArgs($entity, $this->em), $invoke); |
||
1413 | // } |
||
1414 | // $idGen = $class->idGenerator; |
||
1415 | // if ( ! $idGen->isPostInsertGenerator()) { |
||
1416 | // $idValue = $idGen->generate($this->em, $entity); |
||
1417 | // if ( ! $idGen instanceof \Doctrine\ORM\Id\AssignedGenerator) { |
||
1418 | // $idValue = array($class->identifier[0] => $idValue); |
||
1419 | // $class->setIdentifierValues($entity, $idValue); |
||
1420 | // } |
||
1421 | // $this->entityIdentifiers[$oid] = $idValue; |
||
1422 | // } |
||
1423 | 4 | $this->entityStates[$oid] = self::STATE_MANAGED; |
|
1424 | 4 | $this->scheduleForInsert($entity); |
|
1425 | 4 | } |
|
1426 | |||
1427 | /** |
||
1428 | * Gets the commit order. |
||
1429 | * |
||
1430 | * @param array|null $entityChangeSet |
||
1431 | * |
||
1432 | * @return array |
||
1433 | */ |
||
1434 | 4 | private function getCommitOrder(array $entityChangeSet = null) |
|
1435 | { |
||
1436 | 4 | if ($entityChangeSet === null) { |
|
1437 | 4 | $entityChangeSet = array_merge($this->entityInsertions, $this->entityUpdates, $this->entityDeletions); |
|
1438 | 4 | } |
|
1439 | 4 | $calc = $this->getCommitOrderCalculator(); |
|
1440 | // See if there are any new classes in the changeset, that are not in the |
||
1441 | // commit order graph yet (don't have a node). |
||
1442 | // We have to inspect changeSet to be able to correctly build dependencies. |
||
1443 | // It is not possible to use IdentityMap here because post inserted ids |
||
1444 | // are not yet available. |
||
1445 | /** @var ApiMetadata[] $newNodes */ |
||
1446 | 4 | $newNodes = []; |
|
1447 | 4 | foreach ((array)$entityChangeSet as $entity) { |
|
1448 | 4 | $class = $this->manager->getClassMetadata(get_class($entity)); |
|
1449 | 4 | if ($calc->hasNode($class->getName())) { |
|
1450 | continue; |
||
1451 | } |
||
1452 | 4 | $calc->addNode($class->getName(), $class); |
|
1453 | 4 | $newNodes[] = $class; |
|
1454 | 4 | } |
|
1455 | // Calculate dependencies for new nodes |
||
1456 | 4 | while ($class = array_pop($newNodes)) { |
|
1457 | 4 | foreach ($class->getAssociationMappings() as $assoc) { |
|
1458 | 4 | if (!($assoc['isOwningSide'] && $assoc['type'] & ApiMetadata::TO_ONE)) { |
|
1459 | 4 | continue; |
|
1460 | } |
||
1461 | 4 | $targetClass = $this->manager->getClassMetadata($assoc['target']); |
|
1462 | 4 | if (!$calc->hasNode($targetClass->getName())) { |
|
1463 | 2 | $calc->addNode($targetClass->getName(), $targetClass); |
|
1464 | 2 | $newNodes[] = $targetClass; |
|
1465 | 2 | } |
|
1466 | 4 | $calc->addDependency($targetClass->getName(), $class->name, (int)empty($assoc['nullable'])); |
|
1467 | // If the target class has mapped subclasses, these share the same dependency. |
||
1468 | 4 | if (!$targetClass->getSubclasses()) { |
|
1469 | 4 | continue; |
|
1470 | } |
||
1471 | foreach ($targetClass->getSubclasses() as $subClassName) { |
||
1472 | $targetSubClass = $this->manager->getClassMetadata($subClassName); |
||
1473 | if (!$calc->hasNode($subClassName)) { |
||
1474 | $calc->addNode($targetSubClass->name, $targetSubClass); |
||
1475 | $newNodes[] = $targetSubClass; |
||
1476 | } |
||
1477 | $calc->addDependency($targetSubClass->name, $class->name, 1); |
||
1478 | } |
||
1479 | 4 | } |
|
1480 | 4 | } |
|
1481 | |||
1482 | 4 | return $calc->sort(); |
|
1483 | } |
||
1484 | |||
1485 | 4 | private function getCommitOrderCalculator() |
|
1489 | |||
1490 | /** |
||
1491 | * Only flushes the given entity according to a ruleset that keeps the UoW consistent. |
||
1492 | * |
||
1493 | * 1. All entities scheduled for insertion, (orphan) removals and changes in collections are processed as well! |
||
1494 | * 2. Read Only entities are skipped. |
||
1495 | * 3. Proxies are skipped. |
||
1496 | * 4. Only if entity is properly managed. |
||
1497 | * |
||
1498 | * @param object $entity |
||
1499 | * |
||
1500 | * @return void |
||
1501 | * |
||
1502 | * @throws \InvalidArgumentException |
||
1503 | */ |
||
1504 | private function computeSingleEntityChangeSet($entity) |
||
1531 | |||
1532 | /** |
||
1533 | * Computes the changesets of all entities scheduled for insertion. |
||
1534 | * |
||
1535 | * @return void |
||
1536 | */ |
||
1537 | 4 | private function computeScheduleInsertsChangeSets() |
|
1544 | |||
1545 | /** |
||
1546 | * Computes the changes of an association. |
||
1547 | * |
||
1548 | * @param array $assoc The association mapping. |
||
1549 | * @param mixed $value The value of the association. |
||
1550 | * |
||
1551 | * @throws \InvalidArgumentException |
||
1552 | * @throws \UnexpectedValueException |
||
1553 | * |
||
1554 | * @return void |
||
1555 | */ |
||
1556 | 2 | private function computeAssociationChanges($assoc, $value) |
|
1606 | |||
1607 | 4 | private function executeInserts(ApiMetadata $class) |
|
1634 | |||
1635 | 1 | private function executeUpdates($class) |
|
1651 | |||
1652 | /** |
||
1653 | * Executes a refresh operation on an entity. |
||
1654 | * |
||
1655 | * @param object $entity The entity to refresh. |
||
1656 | * @param array $visited The already visited entities during cascades. |
||
1657 | * |
||
1658 | * @return void |
||
1659 | * |
||
1660 | * @throws \InvalidArgumentException If the entity is not MANAGED. |
||
1661 | */ |
||
1662 | private function doRefresh($entity, array &$visited) |
||
1679 | |||
1680 | /** |
||
1681 | * Cascades a refresh operation to associated entities. |
||
1682 | * |
||
1683 | * @param object $entity |
||
1684 | * @param array $visited |
||
1685 | * |
||
1686 | * @return void |
||
1687 | */ |
||
1688 | View Code Duplication | private function cascadeRefresh($entity, array &$visited) |
|
1718 | |||
1719 | /** |
||
1720 | * Cascades a detach operation to associated entities. |
||
1721 | * |
||
1722 | * @param object $entity |
||
1723 | * @param array $visited |
||
1724 | * |
||
1725 | * @return void |
||
1726 | */ |
||
1727 | View Code Duplication | private function cascadeDetach($entity, array &$visited) |
|
1757 | |||
1758 | /** |
||
1759 | * Cascades a merge operation to associated entities. |
||
1760 | * |
||
1761 | * @param object $entity |
||
1762 | * @param object $managedCopy |
||
1763 | * @param array $visited |
||
1764 | * |
||
1765 | * @return void |
||
1766 | */ |
||
1767 | private function cascadeMerge($entity, $managedCopy, array &$visited) |
||
1796 | |||
1797 | /** |
||
1798 | * Cascades the delete operation to associated entities. |
||
1799 | * |
||
1800 | * @param object $entity |
||
1801 | * @param array $visited |
||
1802 | * |
||
1803 | * @return void |
||
1804 | */ |
||
1805 | private function cascadeRemove($entity, array &$visited) |
||
1839 | |||
1840 | /** |
||
1841 | * Executes any extra updates that have been scheduled. |
||
1842 | */ |
||
1843 | private function executeExtraUpdates() |
||
1852 | |||
1853 | private function executeDeletions(ApiMetadata $class) |
||
1876 | |||
1877 | /** |
||
1878 | * @param object $entity |
||
1879 | * @param object $managedCopy |
||
1880 | */ |
||
1881 | private function mergeEntityStateIntoManagedCopy($entity, $managedCopy) |
||
1958 | |||
1959 | /** |
||
1960 | * Deletes an entity as part of the current unit of work. |
||
1961 | * |
||
1962 | * This method is internally called during delete() cascades as it tracks |
||
1963 | * the already visited entities to prevent infinite recursions. |
||
1964 | * |
||
1965 | * @param object $entity The entity to delete. |
||
1966 | * @param array $visited The map of the already visited entities. |
||
1967 | * |
||
1968 | * @return void |
||
1969 | * |
||
1970 | * @throws \InvalidArgumentException If the instance is a detached entity. |
||
1971 | * @throws \UnexpectedValueException |
||
1972 | */ |
||
1973 | private function doRemove($entity, array &$visited) |
||
1999 | |||
2000 | /** |
||
2001 | * Tests if an entity is loaded - must either be a loaded proxy or not a proxy |
||
2002 | * |
||
2003 | * @param object $entity |
||
2004 | * |
||
2005 | * @return bool |
||
2006 | */ |
||
2007 | private function isLoaded($entity) |
||
2011 | |||
2012 | /** |
||
2013 | * Sets/adds associated managed copies into the previous entity's association field |
||
2014 | * |
||
2015 | * @param object $entity |
||
2016 | * @param array $association |
||
2017 | * @param object $previousManagedCopy |
||
2018 | * @param object $managedCopy |
||
2019 | * |
||
2020 | * @return void |
||
2021 | */ |
||
2022 | private function updateAssociationWithMergedEntity($entity, array $association, $previousManagedCopy, $managedCopy) |
||
2039 | |||
2040 | /** |
||
2041 | * Executes a merge operation on an entity. |
||
2042 | * |
||
2043 | * @param object $entity |
||
2044 | * @param array $visited |
||
2045 | * @param object|null $prevManagedCopy |
||
2046 | * @param array|null $assoc |
||
2047 | * |
||
2048 | * @return object The managed copy of the entity. |
||
2049 | * |
||
2050 | * @throws \InvalidArgumentException If the entity instance is NEW. |
||
2051 | * @throws \OutOfBoundsException |
||
2052 | */ |
||
2053 | private function doMerge($entity, array &$visited, $prevManagedCopy = null, array $assoc = []) |
||
2123 | |||
2124 | /** |
||
2125 | * Executes a detach operation on the given entity. |
||
2126 | * |
||
2127 | * @param object $entity |
||
2128 | * @param array $visited |
||
2129 | * @param boolean $noCascade if true, don't cascade detach operation. |
||
2130 | * |
||
2131 | * @return void |
||
2132 | */ |
||
2133 | private function doDetach($entity, array &$visited, $noCascade = false) |
||
2162 | |||
2163 | } |
||
2164 |
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.