ConfigModelManager   F
last analyzed

Complexity

Total Complexity 99

Size/Duplication

Total Lines 656
Duplicated Lines 12.96 %

Coupling/Cohesion

Components 1
Dependencies 12

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 99
lcom 1
cbo 12
dl 85
loc 656
rs 3.7244
c 1
b 0
f 0

23 Methods

Rating   Name   Duplication   Size   Complexity  
A getEntityManager() 0 4 1
A clearCheckDatabase() 0 4 1
D findEntityModel() 0 37 9
C findFieldModel() 0 24 8
A getEntityModel() 9 19 4
B getFieldModel() 10 23 5
C changeFieldName() 0 34 7
C changeFieldType() 33 33 7
C changeFieldMode() 33 33 7
B changeEntityMode() 0 29 6
B getModels() 0 22 6
B createEntityModel() 0 24 4
B createFieldModel() 0 31 5
A clearCache() 0 10 1
C ensureEntityCacheWarmed() 0 23 8
A ensureFieldCacheWarmed() 0 14 4
B loadEntityModels() 0 23 4
A loadEntityModel() 0 6 1
A isValidMode() 0 8 1
B areAllEntitiesDetached() 0 15 5
A isEntityDetached() 0 8 1
A checkDatabase() 0 16 3
A __construct() 0 5 1

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 ConfigModelManager 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 ConfigModelManager, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Oro\Bundle\EntityConfigBundle\Config;
4
5
use Doctrine\ORM\EntityManager;
6
use Doctrine\ORM\UnitOfWork;
7
8
use Oro\Component\DependencyInjection\ServiceLink;
9
use Oro\Bundle\EntityBundle\Tools\SafeDatabaseChecker;
10
use Oro\Bundle\EntityConfigBundle\Entity\ConfigModel;
11
use Oro\Bundle\EntityConfigBundle\Entity\EntityConfigModel;
12
use Oro\Bundle\EntityConfigBundle\Entity\FieldConfigModel;
13
use Oro\Bundle\EntityConfigBundle\Exception\RuntimeException;
14
use Oro\Bundle\EntityConfigBundle\Tools\ConfigHelper;
15
16
/**
17
 * IMPORTANT: A performance of this class is very crucial, be careful during a refactoring.
18
 *
19
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
20
 */
21
class ConfigModelManager
22
{
23
    /** @deprecated since 1.9. Use ConfigModel::MODE_DEFAULT instead */
24
    const MODE_DEFAULT = ConfigModel::MODE_DEFAULT;
25
    /** @deprecated since 1.9. Use ConfigModel::MODE_HIDDEN instead */
26
    const MODE_HIDDEN = ConfigModel::MODE_HIDDEN;
27
    /** @deprecated since 1.9. Use ConfigModel::MODE_READONLY instead */
28
    const MODE_READONLY = ConfigModel::MODE_READONLY;
29
30
    /** @var EntityConfigModel[] [{class name} => EntityConfigModel, ...] */
31
    private $entities;
32
33
    /** @var bool */
34
    private $entitiesAreLoaded = false;
35
36
    /** @var array [{class name} => [{field name} => FieldConfigModel, ...], ...] */
37
    private $fields = [];
38
39
    /** @var bool */
40
    private $dbCheck;
41
42
    /** @var ServiceLink */
43
    protected $emLink;
44
45
    /** @var LockObject */
46
    protected $lockObject;
47
48
    private $requiredTables = [
49
        'oro_entity_config',
50
        'oro_entity_config_field',
51
        'oro_entity_config_index_value',
52
    ];
53
54
    /**
55
     * @param ServiceLink $emLink A link to the EntityManager
56
     * @param LockObject  $lockObject
57
     */
58
    public function __construct(ServiceLink $emLink, LockObject $lockObject)
59
    {
60
        $this->emLink     = $emLink;
61
        $this->lockObject = $lockObject;
62
    }
63
64
    /**
65
     * @return EntityManager
66
     */
67
    public function getEntityManager()
68
    {
69
        return $this->emLink->getService();
70
    }
71
72
    /**
73
     * @return bool
74
     */
75
    public function checkDatabase()
76
    {
77
        if (null !== $this->dbCheck) {
78
            return $this->dbCheck;
79
        }
80
        if ($this->lockObject->isLocked()) {
81
            return true;
82
        }
83
84
        $this->dbCheck = SafeDatabaseChecker::tablesExist(
85
            $this->getEntityManager()->getConnection(),
86
            $this->requiredTables
87
        );
88
89
        return $this->dbCheck;
90
    }
91
92
    public function clearCheckDatabase()
93
    {
94
        $this->dbCheck = null;
95
    }
96
97
    /**
98
     * Finds a model for an entity
99
     *
100
     * @param string $className
101
     *
102
     * @return EntityConfigModel|null An instance of EntityConfigModel or null if a model was not found
103
     */
104
    public function findEntityModel($className)
105
    {
106
        if (empty($className) || ConfigHelper::isConfigModelEntity($className)) {
107
            return null;
108
        }
109
110
        $this->ensureEntityCacheWarmed($className);
111
112
        $result = null;
113
114
        // check if a model exists in the local cache
115
        if (null !== $this->entities && array_key_exists($className, $this->entities)) {
116
            $result = $this->entities[$className];
117
            if ($result && $this->isEntityDetached($result)) {
118
                if ($this->areAllEntitiesDetached()) {
119
                    // reload all models because all of them are detached
120
                    $this->clearCache();
121
                    $result = $this->findEntityModel($className);
122
                } else {
123
                    // the detached model must be reloaded
124
                    $result = false;
125
126
                    $this->entities[$className] = null;
127
                    unset($this->fields[$className]);
128
                }
129
            }
130
        }
131
132
        // load a model if it was not found in the local cache
133
        if ($result === false) {
134
            $result = $this->loadEntityModel($className);
135
136
            $this->entities[$className] = $result;
137
        }
138
139
        return $result;
140
    }
141
142
    /**
143
     * Finds a model for an entity field
144
     *
145
     * @param string $className
146
     * @param string $fieldName
147
     *
148
     * @return FieldConfigModel|null An instance of FieldConfigModel or null if a model was not found
149
     */
150
    public function findFieldModel($className, $fieldName)
151
    {
152
        if (empty($className) || empty($fieldName) || ConfigHelper::isConfigModelEntity($className)) {
153
            return null;
154
        }
155
156
        $this->ensureFieldCacheWarmed($className);
157
158
        $result = null;
159
160
        // check if a model exists in the local cache
161
        if (isset($this->fields[$className]) && array_key_exists($fieldName, $this->fields[$className])) {
162
            $result = $this->fields[$className][$fieldName];
163
            if ($result && $this->isEntityDetached($result)) {
164
                // the detached model must be reloaded
165
                $this->entities[$className] = false;
166
                unset($this->fields[$className]);
167
168
                $result = $this->findFieldModel($className, $fieldName);
169
            }
170
        }
171
172
        return $result;
173
    }
174
175
    /**
176
     * @param string $className
177
     *
178
     * @return EntityConfigModel
179
     *
180
     * @throws \InvalidArgumentException if $className is empty
181
     * @throws RuntimeException if a model was not found
182
     */
183
    public function getEntityModel($className)
184
    {
185
        if (empty($className)) {
186
            throw new \InvalidArgumentException('$className must not be empty');
187
        }
188
189
        $model = $this->findEntityModel($className);
190 View Code Duplication
        if (!$model) {
191
            throw new RuntimeException(
192
                sprintf(
193
                    'A model for "%s" was not found.%s',
194
                    $className,
195
                    $this->lockObject->isLocked() ? ' Config models are locked.' : ''
196
                )
197
            );
198
        }
199
200
        return $model;
201
    }
202
203
    /**
204
     * @param string $className
205
     * @param string $fieldName
206
     *
207
     * @return FieldConfigModel
208
     *
209
     * @throws \InvalidArgumentException if $className or $fieldName is empty
210
     * @throws RuntimeException if a model was not found
211
     */
212
    public function getFieldModel($className, $fieldName)
213
    {
214
        if (empty($className)) {
215
            throw new \InvalidArgumentException('$className must not be empty');
216
        }
217
        if (empty($fieldName)) {
218
            throw new \InvalidArgumentException('$fieldName must not be empty');
219
        }
220
221
        $model = $this->findFieldModel($className, $fieldName);
222 View Code Duplication
        if (!$model) {
223
            throw new RuntimeException(
224
                sprintf(
225
                    'A model for "%s::%s" was not found.%s',
226
                    $className,
227
                    $fieldName,
228
                    $this->lockObject->isLocked() ? ' Config models are locked.' : ''
229
                )
230
            );
231
        }
232
233
        return $model;
234
    }
235
236
    /**
237
     * Renames a field
238
     * Important: this method do not save changes in a database. To do this you need to call entityManager->flush
239
     *
240
     * @param string $className
241
     * @param string $fieldName
242
     * @param string $newFieldName
243
     *
244
     * @return bool TRUE if the name was changed; otherwise, FALSE
245
     *
246
     * @throws \InvalidArgumentException if $className, $fieldName or $newFieldName is empty
247
     * @throws RuntimeException if models are locked
248
     */
249
    public function changeFieldName($className, $fieldName, $newFieldName)
250
    {
251
        if (empty($className)) {
252
            throw new \InvalidArgumentException('$className must not be empty');
253
        }
254
        if (empty($fieldName)) {
255
            throw new \InvalidArgumentException('$fieldName must not be empty');
256
        }
257
        if (empty($newFieldName)) {
258
            throw new \InvalidArgumentException('$newFieldName must not be empty');
259
        }
260
        if ($this->lockObject->isLocked()) {
261
            throw new RuntimeException(
262
                sprintf(
263
                    'Cannot change field name for "%s::%s" because config models are locked.',
264
                    $className,
265
                    $fieldName
266
                )
267
            );
268
        }
269
270
        $result     = false;
271
        $fieldModel = $this->findFieldModel($className, $fieldName);
272
        if ($fieldModel && $fieldModel->getFieldName() !== $newFieldName) {
273
            $fieldModel->setFieldName($newFieldName);
274
            $this->getEntityManager()->persist($fieldModel);
275
            unset($this->fields[$className][$fieldName]);
276
277
            $this->fields[$className][$newFieldName] = $fieldModel;
278
            $result                                  = true;
279
        }
280
281
        return $result;
282
    }
283
284
    /**
285
     * Changes a type of a field
286
     * Important: this method do not save changes in a database. To do this you need to call entityManager->flush
287
     *
288
     * @param string $className
289
     * @param string $fieldName
290
     * @param string $fieldType
291
     *
292
     * @return bool TRUE if the type was changed; otherwise, FALSE
293
     *
294
     * @throws \InvalidArgumentException if $className, $fieldName or $fieldType is empty
295
     * @throws RuntimeException if models are locked
296
     */
297 View Code Duplication
    public function changeFieldType($className, $fieldName, $fieldType)
298
    {
299
        if (empty($className)) {
300
            throw new \InvalidArgumentException('$className must not be empty');
301
        }
302
        if (empty($fieldName)) {
303
            throw new \InvalidArgumentException('$fieldName must not be empty');
304
        }
305
        if (empty($fieldType)) {
306
            throw new \InvalidArgumentException('$fieldType must not be empty');
307
        }
308
        if ($this->lockObject->isLocked()) {
309
            throw new RuntimeException(
310
                sprintf(
311
                    'Cannot change field type for "%s::%s" because config models are locked.',
312
                    $className,
313
                    $fieldName
314
                )
315
            );
316
        }
317
318
        $result     = false;
319
        $fieldModel = $this->findFieldModel($className, $fieldName);
320
        if ($fieldModel && $fieldModel->getType() !== $fieldType) {
321
            $fieldModel->setType($fieldType);
322
            $this->getEntityManager()->persist($fieldModel);
323
324
            $this->fields[$className][$fieldName] = $fieldModel;
325
            $result                               = true;
326
        }
327
328
        return $result;
329
    }
330
331
    /**
332
     * Changes a mode of a field
333
     * Important: this method do not save changes in a database. To do this you need to call entityManager->flush
334
     *
335
     * @param string $className
336
     * @param string $fieldName
337
     * @param string $mode Can be the value of one of ConfigModel::MODE_* constants
338
     *
339
     * @return bool TRUE if the mode was changed; otherwise, FALSE
340
     *
341
     * @throws \InvalidArgumentException if $className, $fieldName or $mode is empty
342
     * @throws RuntimeException if models are locked
343
     */
344 View Code Duplication
    public function changeFieldMode($className, $fieldName, $mode)
345
    {
346
        if (empty($className)) {
347
            throw new \InvalidArgumentException('$className must not be empty');
348
        }
349
        if (empty($fieldName)) {
350
            throw new \InvalidArgumentException('$fieldName must not be empty');
351
        }
352
        if (empty($mode)) {
353
            throw new \InvalidArgumentException('$mode must not be empty');
354
        }
355
        if ($this->lockObject->isLocked()) {
356
            throw new RuntimeException(
357
                sprintf(
358
                    'Cannot change field mode for "%s::%s" because config models are locked.',
359
                    $className,
360
                    $fieldName
361
                )
362
            );
363
        }
364
365
        $result     = false;
366
        $fieldModel = $this->findFieldModel($className, $fieldName);
367
        if ($fieldModel && $fieldModel->getMode() !== $mode) {
368
            $fieldModel->setMode($mode);
369
            $this->getEntityManager()->persist($fieldModel);
370
371
            $this->fields[$className][$fieldName] = $fieldModel;
372
            $result                               = true;
373
        }
374
375
        return $result;
376
    }
377
378
    /**
379
     * Changes a mode of an entity
380
     * Important: this method do not save changes in a database. To do this you need to call entityManager->flush
381
     *
382
     * @param string $className
383
     * @param string $mode Can be the value of one of ConfigModel::MODE_* constants
384
     *
385
     * @return bool TRUE if the type was changed; otherwise, FALSE
386
     *
387
     * @throws \InvalidArgumentException if $className or $mode is empty
388
     * @throws RuntimeException if models are locked
389
     */
390
    public function changeEntityMode($className, $mode)
391
    {
392
        if (empty($className)) {
393
            throw new \InvalidArgumentException('$className must not be empty');
394
        }
395
        if (empty($mode)) {
396
            throw new \InvalidArgumentException('$mode must not be empty');
397
        }
398
        if ($this->lockObject->isLocked()) {
399
            throw new RuntimeException(
400
                sprintf(
401
                    'Cannot change entity name for "%s" because config models are locked.',
402
                    $className
403
                )
404
            );
405
        }
406
407
        $result      = false;
408
        $entityModel = $this->findEntityModel($className);
409
        if ($entityModel && $entityModel->getMode() !== $mode) {
410
            $entityModel->setMode($mode);
411
            $this->getEntityManager()->persist($entityModel);
412
413
            $this->entities[$className] = $entityModel;
414
            $result                     = true;
415
        }
416
417
        return $result;
418
    }
419
420
    /**
421
     * @param string|null $className
422
     *
423
     * @return ConfigModel[]
424
     */
425
    public function getModels($className = null)
426
    {
427
        $result = [];
428
429
        if ($className) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $className of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
430
            $this->ensureFieldCacheWarmed($className);
431
            foreach ($this->fields[$className] as $model) {
432
                if ($model) {
433
                    $result[] = $model;
434
                }
435
            }
436
        } else {
437
            $this->ensureEntityCacheWarmed();
438
            foreach ($this->entities as $model) {
439
                if ($model) {
440
                    $result[] = $model;
441
                }
442
            }
443
        }
444
445
        return $result;
446
    }
447
448
    /**
449
     * @param string|null $className
450
     * @param string|null $mode
451
     *
452
     * @return EntityConfigModel
453
     *
454
     * @throws \InvalidArgumentException if $mode is invalid
455
     * @throws RuntimeException if models are locked
456
     */
457
    public function createEntityModel($className = null, $mode = ConfigModel::MODE_DEFAULT)
458
    {
459
        if (!$this->isValidMode($mode)) {
460
            throw new \InvalidArgumentException(sprintf('Invalid $mode: "%s"', $mode));
461
        }
462
        if ($this->lockObject->isLocked()) {
463
            throw new RuntimeException(
464
                sprintf(
465
                    'Cannot create entity model for "%s" because config models are locked.',
466
                    $className
467
                )
468
            );
469
        }
470
471
        $entityModel = new EntityConfigModel($className);
472
        $entityModel->setMode($mode);
473
474
        if (!empty($className)) {
475
            $this->ensureEntityCacheWarmed();
476
            $this->entities[$className] = $entityModel;
477
        }
478
479
        return $entityModel;
480
    }
481
482
    /**
483
     * @param string $className
484
     * @param string $fieldName
485
     * @param string $fieldType
486
     * @param string $mode
487
     *
488
     * @return FieldConfigModel
489
     *
490
     * @throws \InvalidArgumentException if $className is empty or $mode is invalid
491
     * @throws RuntimeException if models are locked
492
     */
493
    public function createFieldModel($className, $fieldName, $fieldType, $mode = ConfigModel::MODE_DEFAULT)
494
    {
495
        if (empty($className)) {
496
            throw new \InvalidArgumentException('$className must not be empty');
497
        }
498
        if (!$this->isValidMode($mode)) {
499
            throw new \InvalidArgumentException(sprintf('Invalid $mode: "%s"', $mode));
500
        }
501
        if ($this->lockObject->isLocked()) {
502
            throw new RuntimeException(
503
                sprintf(
504
                    'Cannot create field model for "%s::%s" because config models are locked.',
505
                    $className,
506
                    $fieldName
507
                )
508
            );
509
        }
510
511
        $entityModel = $this->getEntityModel($className);
512
513
        $fieldModel = new FieldConfigModel($fieldName, $fieldType);
514
        $fieldModel->setMode($mode);
515
        $entityModel->addField($fieldModel);
516
517
        if (!empty($fieldName)) {
518
            $this->ensureFieldCacheWarmed($className);
519
            $this->fields[$className][$fieldName] = $fieldModel;
520
        }
521
522
        return $fieldModel;
523
    }
524
525
    /**
526
     * Removes all cached data
527
     */
528
    public function clearCache()
529
    {
530
        $this->entities          = null;
0 ignored issues
show
Documentation Bug introduced by
It seems like null of type null is incompatible with the declared type array<integer,object<Oro...ity\EntityConfigModel>> of property $entities.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
531
        $this->entitiesAreLoaded = false;
532
        $this->fields            = [];
533
534
        $em = $this->getEntityManager();
535
        $em->clear('Oro\Bundle\EntityConfigBundle\Entity\FieldConfigModel');
536
        $em->clear('Oro\Bundle\EntityConfigBundle\Entity\EntityConfigModel');
537
    }
538
539
    /**
540
     * Makes sure that an entity model for the given class is loaded
541
     * or, if the class name is not specified, make sure that all entity models are loaded
542
     *
543
     * @param string|null $className
544
     */
545
    protected function ensureEntityCacheWarmed($className = null)
546
    {
547
        if ($this->lockObject->isLocked()) {
548
            return;
549
        }
550
551
        if (null === $this->entities) {
552
            $this->entities = [];
553
        }
554
        if ($className) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $className of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
555
            if (!array_key_exists($className, $this->entities)) {
556
                $this->entities[$className] = !$this->entitiesAreLoaded
557
                    ? $this->loadEntityModel($className)
558
                    : null;
559
            }
560
        } elseif (!$this->entitiesAreLoaded) {
561
            $entityModels = $this->loadEntityModels();
562
            foreach ($entityModels as $model) {
563
                $this->entities[$model->getClassName()] = $model;
564
            }
565
            $this->entitiesAreLoaded = true;
566
        }
567
    }
568
569
    /**
570
     * Checks $this->fields[$className] and if it is empty loads all fields models at once
571
     *
572
     * @param string $className
573
     */
574
    protected function ensureFieldCacheWarmed($className)
575
    {
576
        if (!isset($this->fields[$className])) {
577
            $this->fields[$className] = [];
578
579
            $entityModel = $this->findEntityModel($className);
580
            if ($entityModel) {
581
                $fields = $entityModel->getFields();
582
                foreach ($fields as $model) {
583
                    $this->fields[$className][$model->getFieldName()] = $model;
584
                }
585
            }
586
        }
587
    }
588
589
    /**
590
     * @return EntityConfigModel[]
591
     */
592
    protected function loadEntityModels()
593
    {
594
        $alreadyLoadedIds = [];
595
        foreach ($this->entities as $model) {
596
            if ($model) {
597
                $alreadyLoadedIds[] = $model->getId();
598
            }
599
        }
600
601
        if (empty($alreadyLoadedIds)) {
602
            return $this->getEntityManager()
603
                ->getRepository('Oro\Bundle\EntityConfigBundle\Entity\EntityConfigModel')
604
                ->findAll();
605
        } else {
606
            return $this->getEntityManager()
607
                ->getRepository('Oro\Bundle\EntityConfigBundle\Entity\EntityConfigModel')
608
                ->createQueryBuilder('e')
609
                ->where('e.id NOT IN (:exclusions)')
610
                ->setParameter('exclusions', $alreadyLoadedIds)
611
                ->getQuery()
612
                ->getResult();
613
        }
614
    }
615
616
    /**
617
     * @param string $className
618
     *
619
     * @return EntityConfigModel|null
620
     */
621
    protected function loadEntityModel($className)
622
    {
623
        return $this->getEntityManager()
624
            ->getRepository('Oro\Bundle\EntityConfigBundle\Entity\EntityConfigModel')
625
            ->findOneBy(['className' => $className]);
626
    }
627
628
    /**
629
     * @param string $mode
630
     *
631
     * @return bool
632
     */
633
    protected function isValidMode($mode)
634
    {
635
        return in_array(
636
            $mode,
637
            [ConfigModel::MODE_DEFAULT, ConfigModel::MODE_HIDDEN, ConfigModel::MODE_READONLY],
638
            true
639
        );
640
    }
641
642
    /**
643
     * Determines whether all entities in local cache are detached from an entity manager or not
644
     */
645
    protected function areAllEntitiesDetached()
646
    {
647
        $result = false;
648
        if (!empty($this->entities)) {
649
            $result = true;
650
            foreach ($this->entities as $model) {
651
                if ($model && !$this->isEntityDetached($model)) {
652
                    $result = false;
653
                    break;
654
                }
655
            }
656
        }
657
658
        return $result;
659
    }
660
661
    /**
662
     * Determines whether the given entity is managed by an entity manager or not
663
     *
664
     * @param object $entity
665
     *
666
     * @return bool
667
     */
668
    protected function isEntityDetached($entity)
669
    {
670
        $entityState = $this->getEntityManager()
671
            ->getUnitOfWork()
672
            ->getEntityState($entity);
673
674
        return $entityState === UnitOfWork::STATE_DETACHED;
675
    }
676
}
677