Passed
Push — master ( b8a36e...fa4e25 )
by Steevan
05:40
created

UnitOfWork   F

Complexity

Total Complexity 78

Size/Duplication

Total Lines 535
Duplicated Lines 2.99 %

Coupling/Cohesion

Components 1
Dependencies 15

Importance

Changes 0
Metric Value
wmc 78
lcom 1
cbo 15
dl 16
loc 535
rs 2.1126
c 0
b 0
f 0

14 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 7 1
F createEntity() 0 314 62
A callParentPrivateMethod() 0 12 1
A setParentIdentityMap() 0 8 1
A setParentOriginalEntityData() 8 8 1
A setParentOriginalEntityDataField() 8 8 1
A setParentEntityIdentifiers() 0 8 1
A setParentEntityStates() 0 8 1
A setParentEagerLoadingEntities() 0 8 1
A unsetParentEagerLoadingEntities() 0 12 2
A newInstance() 0 7 1
A dispatchOnCreateEntityOverrideLocalValues() 0 21 2
A dispatchOnCreateEntityDefineFieldValues() 0 7 1
A dispatchOnNewEntityInstance() 0 9 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

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 Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

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
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
     * @param string $name
365
     * @return mixed
366
     */
367
    protected function callParentPrivateMethod($name)
368
    {
369
        $reflectionMethod = new \ReflectionMethod(get_parent_class($this), $name);
370
        $reflectionMethod->setAccessible(true);
371
        $args = func_get_args();
372
        unset($args[0]);
373
        $args = array_values($args);
374
        $return = $reflectionMethod->invokeArgs($this, $args);
375
        $reflectionMethod->setAccessible(false);
376
377
        return $return;
378
    }
379
380
    /**
381
     * @param string $rootEntityName
382
     * @param string $idHash
383
     * @param object $entity
384
     * @return $this
385
     */
386
    protected function setParentIdentityMap($rootEntityName, $idHash, $entity)
387
    {
388
        $identityMap = $this->getParentPrivatePropertyValue('identityMap');
389
        $identityMap[$rootEntityName][$idHash] = $entity;
390
        $this->setParentPrivatePropertyValue('identityMap', $identityMap);
391
392
        return $this;
393
    }
394
395
    /**
396
     * @param string $oid
397
     * @param mixed $data
398
     * @return $this
399
     */
400 View Code Duplication
    protected function setParentOriginalEntityData($oid, $data)
401
    {
402
        $originalEntityData = $this->getParentPrivatePropertyValue('originalEntityData');
403
        $originalEntityData[$oid] = $data;
404
        $this->setParentPrivatePropertyValue('originalEntityData', $originalEntityData);
405
406
        return $this;
407
    }
408
409
    /**
410
     * @param string $oid
411
     * @param string $field
412
     * @param mixed $data
413
     * @return $this
414
     */
415 View Code Duplication
    protected function setParentOriginalEntityDataField($oid, $field, $data)
416
    {
417
        $originalEntityData = $this->getParentPrivatePropertyValue('originalEntityData');
418
        $originalEntityData[$oid][$field] = $data;
419
        $this->setParentPrivatePropertyValue('originalEntityData', $originalEntityData);
420
421
        return $this;
422
    }
423
424
    /**
425
     * @param string $oid
426
     * @param string $id
427
     * @return $this
428
     */
429
    protected function setParentEntityIdentifiers($oid, $id)
430
    {
431
        $entityIdentifiers = $this->getParentPrivatePropertyValue('entityIdentifiers');
432
        $entityIdentifiers[$oid] = $id;
433
        $this->setParentPrivatePropertyValue('entityIdentifiers', $entityIdentifiers);
434
435
        return $this;
436
    }
437
438
    /**
439
     * @param string $oid
440
     * @param int $state
441
     * @return $this
442
     */
443
    protected function setParentEntityStates($oid, $state)
444
    {
445
        $entityStates = $this->getParentPrivatePropertyValue('entityStates');
446
        $entityStates[$oid] = $state;
447
        $this->setParentPrivatePropertyValue('entityStates', $entityStates);
448
449
        return $this;
450
    }
451
452
    /**
453
     * @param string $className
454
     * @param string $idHash
455
     * @param string $id
456
     * @return $this
457
     */
458
    protected function setParentEagerLoadingEntities($className, $idHash, $id)
459
    {
460
        $eagerLoadingEntities = $this->getParentPrivatePropertyValue('eagerLoadingEntities');
461
        $eagerLoadingEntities[$className][$idHash] = current($id);
462
        $this->setParentPrivatePropertyValue('eagerLoadingEntities', $eagerLoadingEntities);
463
464
        return $this;
465
    }
466
467
    /**
468
     * @param string $className
469
     * @param string|null $idHash
470
     * @return $this
471
     */
472
    protected function unsetParentEagerLoadingEntities($className, $idHash = null)
473
    {
474
        $eagerLoadingEntities = $this->getParentPrivatePropertyValue('eagerLoadingEntities');
475
        if ($idHash === null) {
476
            unset($eagerLoadingEntities[$className]);
477
        } else {
478
            unset($eagerLoadingEntities[$className][$idHash]);
479
        }
480
        $this->setParentPrivatePropertyValue('eagerLoadingEntities', $eagerLoadingEntities);
481
482
        return $this;
483
    }
484
485
    /**
486
     * @param ClassMetadata $classMetadata
487
     * @return ObjectManagerAware|object
488
     */
489
    protected function newInstance(ClassMetadata $classMetadata)
490
    {
491
        $entity = $this->callParentPrivateMethod('newInstance', $classMetadata);
492
        $this->dispatchOnNewEntityInstance($classMetadata, $entity);
493
494
        return $entity;
495
    }
496
497
    /**
498
     * @param string $className
499
     * @param array $data
500
     * @param array $hints
501
     * @param bool $override
502
     * @return bool
503
     */
504
    protected function dispatchOnCreateEntityOverrideLocalValues($className, array $data, array $hints, $override)
505
    {
506
        if ($this->em->getEventManager()->hasListeners(OnCreateEntityOverrideLocalValuesEventArgs::EVENT_NAME)) {
507
            $eventArgs = new OnCreateEntityOverrideLocalValuesEventArgs(
508
                $this->em,
509
                $className,
510
                $data,
511
                $hints,
512
                $override
513
            );
514
            $this->em->getEventManager()->dispatchEvent(
515
                OnCreateEntityOverrideLocalValuesEventArgs::EVENT_NAME,
516
                $eventArgs
517
            );
518
            $return = $eventArgs->getOverrideLocalValues();
519
        } else {
520
            $return = $override;
521
        }
522
523
        return $return;
524
    }
525
526
    /**
527
     * @param string $className
528
     * @param array $data
529
     * @param array $hints
530
     * @param object $entity
531
     * @return OnCreateEntityDefineFieldValuesEventArgs
532
     */
533
    protected function dispatchOnCreateEntityDefineFieldValues($className, array $data, array $hints, $entity)
534
    {
535
        $eventArgs = new OnCreateEntityDefineFieldValuesEventArgs($this->em, $className, $data, $hints, $entity);
536
        $this->em->getEventManager()->dispatchEvent(OnCreateEntityDefineFieldValuesEventArgs::EVENT_NAME, $eventArgs);
537
538
        return $eventArgs;
539
    }
540
541
    /**
542
     * @param ClassMetadata $classMetadata
543
     * @param object $entity
544
     */
545
    protected function dispatchOnNewEntityInstance(ClassMetadata $classMetadata, $entity)
546
    {
547
        if ($this->em->getEventManager()->hasListeners(OnNewEntityInstanceEventArgs::EVENT_NAME)) {
548
            $this->em->getEventManager()->dispatchEvent(
549
                OnNewEntityInstanceEventArgs::EVENT_NAME,
550
                new OnNewEntityInstanceEventArgs($this->em, $classMetadata, $entity)
551
            );
552
        }
553
    }
554
}
555