UnitOfWork::callParentPrivateMethod()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 12
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 9
nc 1
nop 1
1
<?php
2
3
namespace steevanb\DoctrineEvents\Doctrine\ORM;
4
5
use Doctrine\Common\Collections\ArrayCollection;
6
use Doctrine\Common\NotifyPropertyChanged;
7
use Doctrine\Common\Persistence\ObjectManagerAware;
8
use Doctrine\ORM\EntityManagerInterface;
9
use Doctrine\ORM\Mapping\ClassMetadata;
10
use Doctrine\ORM\PersistentCollection;
11
use Doctrine\ORM\Proxy\Proxy;
12
use Doctrine\ORM\Query;
13
use Doctrine\ORM\UnitOfWork as DoctrineUnitOfWork;
14
use Doctrine\ORM\Utility\IdentifierFlattener;
15
use steevanb\DoctrineEvents\Behavior\ReflectionTrait;
16
use steevanb\DoctrineEvents\Doctrine\ORM\Event\OnCreateEntityDefineFieldValuesEventArgs;
17
use steevanb\DoctrineEvents\Doctrine\ORM\Event\OnCreateEntityOverrideLocalValuesEventArgs;
18
use steevanb\DoctrineEvents\Doctrine\ORM\Event\OnNewEntityInstanceEventArgs;
19
20
class UnitOfWork extends DoctrineUnitOfWork
21
{
22
    use ReflectionTrait;
23
24
    /** @var EntityManagerInterface */
25
    protected $em;
26
27
    /** @var IdentifierFlattener */
28
    protected $identifierFlattener;
29
30
    /**
31
     * @param EntityManagerInterface $em
32
     */
33
    public function __construct(EntityManagerInterface $em)
34
    {
35
        parent::__construct($em);
36
37
        $this->em = $em;
38
        $this->identifierFlattener = $this->getParentPrivatePropertyValue('identifierFlattener');
39
    }
40
41
    /**
42
     * Mostly copied from Doctrine\ORM\UnitOfWork, cause everything is on a single method
43
     * @param string $className
44
     * @param array $data
45
     * @param array $hints
46
     * @return object
47
     */
48
    public function createEntity($className, array $data, &$hints = array())
49
    {
50
        $class = $this->em->getClassMetadata($className);
51
52
        $id = $this->identifierFlattener->flattenIdentifier($class, $data);
53
        $idHash = implode(' ', $id);
54
55
        $identityMap = $this->getParentPrivatePropertyValue('identityMap');
56
        if (isset($identityMap[$class->rootEntityName][$idHash])) {
57
            $entity = $identityMap[$class->rootEntityName][$idHash];
58
            $oid = spl_object_hash($entity);
59
60
            if (
61
                isset($hints[Query::HINT_REFRESH])
62
                && isset($hints[Query::HINT_REFRESH_ENTITY])
63
                && ($unmanagedProxy = $hints[Query::HINT_REFRESH_ENTITY]) !== $entity
64
                && $unmanagedProxy instanceof Proxy
65
                && $this->callParentPrivateMethod('isIdentifierEquals', $unmanagedProxy, $entity)
66
            ) {
67
                // DDC-1238 - we have a managed instance, but it isn't the provided one.
68
                // Therefore we clear its identifier. Also, we must re-fetch metadata since the
69
                // refreshed object may be anything
70
71
                foreach ($class->identifier as $fieldName) {
72
                    $class->reflFields[$fieldName]->setValue($unmanagedProxy, null);
73
                }
74
75
                return $unmanagedProxy;
76
            }
77
78
            if ($entity instanceof Proxy && ! $entity->__isInitialized()) {
79
                $entity->__setInitialized(true);
80
81
                $overrideLocalValues = true;
82
83
                if ($entity instanceof NotifyPropertyChanged) {
84
                    $entity->addPropertyChangedListener($this);
85
                }
86
            } else {
87
                $overrideLocalValues = isset($hints[Query::HINT_REFRESH]);
88
89
                // If only a specific entity is set to refresh, check that it's the one
90
                if (isset($hints[Query::HINT_REFRESH_ENTITY])) {
91
                    $overrideLocalValues = $hints[Query::HINT_REFRESH_ENTITY] === $entity;
92
                }
93
            }
94
95
            if ($overrideLocalValues) {
96
                // inject ObjectManager upon refresh.
97
                if ($entity instanceof ObjectManagerAware) {
98
                    $entity->injectObjectManager($this->em, $class);
99
                }
100
101
                $this->setParentOriginalEntityData($oid, $data);
102
            }
103
        } else {
104
            $entity = $this->newInstance($class);
105
            $oid    = spl_object_hash($entity);
106
107
            $this->setParentEntityIdentifiers($oid, $id);
108
            $this->setParentEntityStates($oid, static::STATE_MANAGED);
109
            $this->setParentOriginalEntityData($oid, $data);
110
111
            $this->setParentIdentityMap($class->rootEntityName, $idHash, $entity);
112
113
            if ($entity instanceof NotifyPropertyChanged) {
114
                $entity->addPropertyChangedListener($this);
115
            }
116
117
            $overrideLocalValues = true;
118
        }
119
120
        $overrideLocalValues = $this->dispatchOnCreateEntityOverrideLocalValues($className, $data, $hints, $overrideLocalValues);
121
        if ($overrideLocalValues === false) {
122
            return $entity;
123
        }
124
125
        $eventArgs = $this->dispatchOnCreateEntityDefineFieldValues($className, $data, $hints, $entity);
126
        foreach ($data as $field => $value) {
127
            if (isset($class->fieldMappings[$field]) && $eventArgs->isValueDefined($field) === false) {
128
                $class->reflFields[$field]->setValue($entity, $value);
129
            }
130
        }
131
        unset($eventArgs);
132
133
        // Loading the entity right here, if its in the eager loading map get rid of it there.
134
        $this->unsetParentEagerLoadingEntities($class->rootEntityName, $idHash);
135
136
        $eagerLoadingEntities = $this->getParentPrivatePropertyValue('eagerLoadingEntities');
137
        if (isset($eagerLoadingEntities[$class->rootEntityName]) && ! $eagerLoadingEntities[$class->rootEntityName]) {
138
            $this->unsetParentEagerLoadingEntities($class->rootEntityName);
139
        }
140
141
        // Properly initialize any unfetched associations, if partial objects are not allowed.
142
        if (isset($hints[Query::HINT_FORCE_PARTIAL_LOAD])) {
143
            return $entity;
144
        }
145
146
        foreach ($class->associationMappings as $field => $assoc) {
147
            // Check if the association is not among the fetch-joined associations already.
148
            if (isset($hints['fetchAlias']) && isset($hints['fetched'][$hints['fetchAlias']][$field])) {
149
                continue;
150
            }
151
152
            $targetClass = $this->em->getClassMetadata($assoc['targetEntity']);
153
154
            switch (true) {
155
                case ($assoc['type'] & ClassMetadata::TO_ONE):
156
                    if ( ! $assoc['isOwningSide']) {
157
158
                        // use the given entity association
159
                        if (
160
                            isset($data[$field]) && is_object($data[$field])
161
                            && isset($this->getParentPrivatePropertyValue('entityStates')[spl_object_hash($data[$field])])
162
                        ) {
163
164
                            $this->setParentOriginalEntityDataField($oid, $field, $data[$field]);
165
166
                            $class->reflFields[$field]->setValue($entity, $data[$field]);
167
                            $targetClass->reflFields[$assoc['mappedBy']]->setValue($data[$field], $entity);
168
169
                            continue 2;
170
                        }
171
172
                        // Inverse side of x-to-one can never be lazy
173
                        $class
174
                            ->reflFields[$field]
175
                            ->setValue(
176
                                $entity,
177
                                $this->getEntityPersister($assoc['targetEntity'])->loadOneToOneEntity($assoc, $entity)
178
                            );
179
180
                        continue 2;
181
                    }
182
183
                    // use the entity association
184
                    if (
185
                        isset($data[$field]) && is_object($data[$field])
186
                        && isset($this->getParentPrivatePropertyValue('entityStates')[spl_object_hash($data[$field])])
187
                    ) {
188
                        $class->reflFields[$field]->setValue($entity, $data[$field]);
189
                        $this->setParentOriginalEntityDataField($oid, $field, $data[$field]);
190
191
                        continue;
192
                    }
193
194
                    $associatedId = array();
195
196
                    foreach ($assoc['targetToSourceKeyColumns'] as $targetColumn => $srcColumn) {
197
                        $joinColumnValue = isset($data[$srcColumn]) ? $data[$srcColumn] : null;
198
199
                        if ($joinColumnValue !== null) {
200
                            if ($targetClass->containsForeignIdentifier) {
201
                                $associatedId[$targetClass->getFieldForColumn($targetColumn)] = $joinColumnValue;
202
                            } else {
203
                                $associatedId[$targetClass->fieldNames[$targetColumn]] = $joinColumnValue;
204
                            }
205
                        } elseif ($targetClass->containsForeignIdentifier
206
                            && in_array($targetClass->getFieldForColumn($targetColumn), $targetClass->identifier, true)
207
                        ) {
208
                            // the missing key is part of target's entity primary key
209
                            $associatedId = array();
210
                            break;
211
                        }
212
                    }
213
214
                    if ( ! $associatedId) {
215
                        // Foreign key is NULL
216
                        $class->reflFields[$field]->setValue($entity, null);
217
                        $this->setParentOriginalEntityDataField($oid, $field, null);
218
219
                        continue;
220
                    }
221
222
                    if ( ! isset($hints['fetchMode'][$class->name][$field])) {
223
                        $hints['fetchMode'][$class->name][$field] = $assoc['fetch'];
224
                    }
225
226
                    // Foreign key is set
227
                    // Check identity map first
228
                    $relatedIdHash = implode(' ', $associatedId);
229
230
                    $identityMap = $this->getParentPrivatePropertyValue('identityMap');
231
                    switch (true) {
232
                        case (isset($identityMap[$targetClass->rootEntityName][$relatedIdHash])):
233
                            $newValue = $identityMap[$targetClass->rootEntityName][$relatedIdHash];
234
235
                            // If this is an uninitialized proxy, we are deferring eager loads,
236
                            // this association is marked as eager fetch, and its an uninitialized proxy (wtf!)
237
                            // then we can append this entity for eager loading!
238
                            if ($hints['fetchMode'][$class->name][$field] == ClassMetadata::FETCH_EAGER &&
239
                                isset($hints[self::HINT_DEFEREAGERLOAD]) &&
240
                                !$targetClass->isIdentifierComposite &&
241
                                $newValue instanceof Proxy &&
242
                                $newValue->__isInitialized__ === false) {
243
244
                                $this->setParentEagerLoadingEntities(
245
                                    $targetClass->rootEntityName,
246
                                    $relatedIdHash,
247
                                    current($associatedId)
248
                                );
249
                            }
250
251
                            break;
252
253
                        case ($targetClass->subClasses):
254
                            // If it might be a subtype, it can not be lazy. There isn't even
255
                            // a way to solve this with deferred eager loading, which means putting
256
                            // an entity with subclasses at a *-to-one location is really bad! (performance-wise)
257
                            $newValue = $this
258
                                ->getEntityPersister($assoc['targetEntity'])
259
                                ->loadOneToOneEntity($assoc, $entity, $associatedId);
260
                            break;
261
262
                        default:
263
                            switch (true) {
264
                                // We are negating the condition here. Other cases will assume it is valid!
265
                                case ($hints['fetchMode'][$class->name][$field] !== ClassMetadata::FETCH_EAGER):
266
                                    $newValue = $this
267
                                        ->em
268
                                        ->getProxyFactory()
269
                                        ->getProxy($assoc['targetEntity'], $associatedId);
270
                                    break;
271
272
                                // Deferred eager load only works for single identifier classes
273
                                case (isset($hints[self::HINT_DEFEREAGERLOAD]) && ! $targetClass->isIdentifierComposite):
274
                                    $this->setParentEagerLoadingEntities(
275
                                        $targetClass->rootEntityName,
276
                                        $relatedIdHash,
277
                                        current($associatedId)
278
                                    );
279
280
                                    $newValue = $this
281
                                        ->em
282
                                        ->getProxyFactory()
283
                                        ->getProxy($assoc['targetEntity'], $associatedId);
284
                                    break;
285
286
                                default:
287
                                    $newValue = $this->em->find($assoc['targetEntity'], $associatedId);
288
                                    break;
289
                            }
290
291
                            // PERF: Inlined & optimized code from UnitOfWork#registerManaged()
292
                            $newValueOid = spl_object_hash($newValue);
293
                            $this->setParentEntityIdentifiers($newValueOid, $associatedId);
294
                            $this->setParentIdentityMap($targetClass->rootEntityName, $relatedIdHash, $newValue);
295
296
                            if (
297
                                $newValue instanceof NotifyPropertyChanged &&
298
                                ( ! $newValue instanceof Proxy || $newValue->__isInitialized())
299
                            ) {
300
                                $newValue->addPropertyChangedListener($this);
301
                            }
302
                            $this->setParentEntityStates($newValueOid, static::STATE_MANAGED);
303
                            // make sure that when an proxy is then finally loaded, $this->originalEntityData is set also!
304
                            break;
305
                    }
306
307
                    $this->setParentOriginalEntityDataField($oid, $field, $newValue);
308
                    $class->reflFields[$field]->setValue($entity, $newValue);
309
310
                    if ($assoc['inversedBy'] && $assoc['type'] & ClassMetadata::ONE_TO_ONE) {
311
                        $inverseAssoc = $targetClass->associationMappings[$assoc['inversedBy']];
312
                        $targetClass->reflFields[$inverseAssoc['fieldName']]->setValue($newValue, $entity);
313
                    }
314
315
                    break;
316
317
                default:
318
                    // Ignore if its a cached collection
319
                    if (
320
                        isset($hints[Query::HINT_CACHE_ENABLED])
321
                        && $class->getFieldValue($entity, $field) instanceof PersistentCollection
322
                    ) {
323
                        break;
324
                    }
325
326
                    // use the given collection
327
                    if (isset($data[$field]) && $data[$field] instanceof PersistentCollection) {
328
329
                        $data[$field]->setOwner($entity, $assoc);
330
331
                        $class->reflFields[$field]->setValue($entity, $data[$field]);
332
                        $this->setParentOriginalEntityDataField($oid, $field, $data[$field]);
333
334
                        break;
335
                    }
336
337
                    // Inject collection
338
                    $pColl = new PersistentCollection($this->em, $targetClass, new ArrayCollection());
339
                    $pColl->setOwner($entity, $assoc);
340
                    $pColl->setInitialized(false);
341
342
                    $reflField = $class->reflFields[$field];
343
                    $reflField->setValue($entity, $pColl);
344
345
                    if ($assoc['fetch'] == ClassMetadata::FETCH_EAGER) {
346
                        $this->loadCollection($pColl);
347
                        $pColl->takeSnapshot();
348
                    }
349
350
                    $this->setParentOriginalEntityDataField($oid, $field, $pColl);
351
                    break;
352
            }
353
        }
354
355
        if ($overrideLocalValues) {
356
            // defer invoking of postLoad event to hydration complete step
357
            $this->getParentPrivatePropertyValue('hydrationCompleteHandler')->deferPostLoadInvoking($class, $entity);
358
        }
359
360
        return $entity;
361
    }
362
363
    /**
364
     * Fix bug when entity is added / removed before flush, and extraUpdate are added
365
     * scheduleForDelete() do not remove $entity from extraUpdates
366
     * Ex : persist($entity), $uow->scheduleExtraUpdate($entity, [...]), $uow->scheduleForDelete($entity)
367
     * @param object $entity
368
     */
369
    public function scheduleForDelete($entity)
370
    {
371
        $oid = spl_object_hash($entity);
372
        $extraUpdates = $this->getParentPrivatePropertyValue('extraUpdates');
373
        if (
374
            isset($this->getParentPrivatePropertyValue('entityInsertions')[$oid])
375
            && isset($extraUpdates[$oid])
376
        ) {
377
            unset($extraUpdates[$oid]);
378
            $this->setParentPrivatePropertyValue('extraUpdates', $extraUpdates);
379
        }
380
381
        parent::scheduleForDelete($entity);
382
    }
383
384
    /**
385
     * @param string $name
386
     * @return mixed
387
     */
388
    protected function callParentPrivateMethod($name)
389
    {
390
        $reflectionMethod = new \ReflectionMethod(get_parent_class($this), $name);
391
        $reflectionMethod->setAccessible(true);
392
        $args = func_get_args();
393
        unset($args[0]);
394
        $args = array_values($args);
395
        $return = $reflectionMethod->invokeArgs($this, $args);
396
        $reflectionMethod->setAccessible(false);
397
398
        return $return;
399
    }
400
401
    /**
402
     * @param string $rootEntityName
403
     * @param string $idHash
404
     * @param object $entity
405
     * @return $this
406
     */
407
    protected function setParentIdentityMap($rootEntityName, $idHash, $entity)
408
    {
409
        $identityMap = $this->getParentPrivatePropertyValue('identityMap');
410
        $identityMap[$rootEntityName][$idHash] = $entity;
411
        $this->setParentPrivatePropertyValue('identityMap', $identityMap);
412
413
        return $this;
414
    }
415
416
    /**
417
     * @param string $oid
418
     * @param mixed $data
419
     * @return $this
420
     */
421 View Code Duplication
    protected function setParentOriginalEntityData($oid, $data)
422
    {
423
        $originalEntityData = $this->getParentPrivatePropertyValue('originalEntityData');
424
        $originalEntityData[$oid] = $data;
425
        $this->setParentPrivatePropertyValue('originalEntityData', $originalEntityData);
426
427
        return $this;
428
    }
429
430
    /**
431
     * @param string $oid
432
     * @param string $field
433
     * @param mixed $data
434
     * @return $this
435
     */
436 View Code Duplication
    protected function setParentOriginalEntityDataField($oid, $field, $data)
437
    {
438
        $originalEntityData = $this->getParentPrivatePropertyValue('originalEntityData');
439
        $originalEntityData[$oid][$field] = $data;
440
        $this->setParentPrivatePropertyValue('originalEntityData', $originalEntityData);
441
442
        return $this;
443
    }
444
445
    /**
446
     * @param string $oid
447
     * @param string $id
448
     * @return $this
449
     */
450
    protected function setParentEntityIdentifiers($oid, $id)
451
    {
452
        $entityIdentifiers = $this->getParentPrivatePropertyValue('entityIdentifiers');
453
        $entityIdentifiers[$oid] = $id;
454
        $this->setParentPrivatePropertyValue('entityIdentifiers', $entityIdentifiers);
455
456
        return $this;
457
    }
458
459
    /**
460
     * @param string $oid
461
     * @param int $state
462
     * @return $this
463
     */
464
    protected function setParentEntityStates($oid, $state)
465
    {
466
        $entityStates = $this->getParentPrivatePropertyValue('entityStates');
467
        $entityStates[$oid] = $state;
468
        $this->setParentPrivatePropertyValue('entityStates', $entityStates);
469
470
        return $this;
471
    }
472
473
    /**
474
     * @param string $className
475
     * @param string $idHash
476
     * @param string $id
477
     * @return $this
478
     */
479
    protected function setParentEagerLoadingEntities($className, $idHash, $id)
480
    {
481
        $eagerLoadingEntities = $this->getParentPrivatePropertyValue('eagerLoadingEntities');
482
        $eagerLoadingEntities[$className][$idHash] = current($id);
483
        $this->setParentPrivatePropertyValue('eagerLoadingEntities', $eagerLoadingEntities);
484
485
        return $this;
486
    }
487
488
    /**
489
     * @param string $className
490
     * @param string|null $idHash
491
     * @return $this
492
     */
493
    protected function unsetParentEagerLoadingEntities($className, $idHash = null)
494
    {
495
        $eagerLoadingEntities = $this->getParentPrivatePropertyValue('eagerLoadingEntities');
496
        if ($idHash === null) {
497
            unset($eagerLoadingEntities[$className]);
498
        } else {
499
            unset($eagerLoadingEntities[$className][$idHash]);
500
        }
501
        $this->setParentPrivatePropertyValue('eagerLoadingEntities', $eagerLoadingEntities);
502
503
        return $this;
504
    }
505
506
    /**
507
     * @param ClassMetadata $classMetadata
508
     * @return ObjectManagerAware|object
509
     */
510
    protected function newInstance($classMetadata)
511
    {
512
        $entity = $this->callParentPrivateMethod('newInstance', $classMetadata);
513
        $this->dispatchOnNewEntityInstance($classMetadata, $entity);
514
515
        return $entity;
516
    }
517
518
    /**
519
     * @param string $className
520
     * @param array $data
521
     * @param array $hints
522
     * @param bool $override
523
     * @return bool
524
     */
525
    protected function dispatchOnCreateEntityOverrideLocalValues($className, array $data, array $hints, $override)
526
    {
527
        if ($this->em->getEventManager()->hasListeners(OnCreateEntityOverrideLocalValuesEventArgs::EVENT_NAME)) {
528
            $eventArgs = new OnCreateEntityOverrideLocalValuesEventArgs(
529
                $this->em,
530
                $className,
531
                $data,
532
                $hints,
533
                $override
534
            );
535
            $this->em->getEventManager()->dispatchEvent(
536
                OnCreateEntityOverrideLocalValuesEventArgs::EVENT_NAME,
537
                $eventArgs
538
            );
539
            $return = $eventArgs->getOverrideLocalValues();
540
        } else {
541
            $return = $override;
542
        }
543
544
        return $return;
545
    }
546
547
    /**
548
     * @param string $className
549
     * @param array $data
550
     * @param array $hints
551
     * @param object $entity
552
     * @return OnCreateEntityDefineFieldValuesEventArgs
553
     */
554
    protected function dispatchOnCreateEntityDefineFieldValues($className, array $data, array $hints, $entity)
555
    {
556
        $eventArgs = new OnCreateEntityDefineFieldValuesEventArgs($this->em, $className, $data, $hints, $entity);
557
        $this->em->getEventManager()->dispatchEvent(OnCreateEntityDefineFieldValuesEventArgs::EVENT_NAME, $eventArgs);
558
559
        return $eventArgs;
560
    }
561
562
    /**
563
     * @param ClassMetadata $classMetadata
564
     * @param object $entity
565
     */
566
    protected function dispatchOnNewEntityInstance(ClassMetadata $classMetadata, $entity)
567
    {
568
        if ($this->em->getEventManager()->hasListeners(OnNewEntityInstanceEventArgs::EVENT_NAME)) {
569
            $this->em->getEventManager()->dispatchEvent(
570
                OnNewEntityInstanceEventArgs::EVENT_NAME,
571
                new OnNewEntityInstanceEventArgs($this->em, $classMetadata, $entity)
572
            );
573
        }
574
    }
575
}
576