Completed
Push — 2.x ( 359f6e )
by Sullivan
16:25 queued 14:10
created

ModelManager   F

Complexity

Total Complexity 80

Size/Duplication

Total Lines 577
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 16

Importance

Changes 6
Bugs 2 Features 0
Metric Value
wmc 80
lcom 1
cbo 16
dl 0
loc 577
rs 1.5789
c 6
b 2
f 0

36 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A getMetadata() 0 4 1
A getParentMetadataForProperty() 0 15 2
A hasMetadata() 0 4 1
B getNewFieldDescriptionInstance() 0 31 6
A create() 0 12 3
A update() 0 12 3
A delete() 0 12 3
A find() 0 10 2
A findBy() 0 4 1
A findOneBy() 0 4 1
A getEntityManager() 0 18 4
A getParentFieldDescription() 0 14 1
A createQuery() 0 6 1
A executeQuery() 0 8 2
A getModelIdentifier() 0 4 1
B getIdentifierValues() 0 28 4
A getIdentifierFieldNames() 0 4 1
A getNormalizedIdentifier() 0 19 4
A getUrlsafeIdentifier() 0 4 1
A addIdentifiersToQuery() 0 22 3
B batchDelete() 0 25 5
A getDataSourceIterator() 0 17 2
A getExportFields() 0 6 1
A getModelInstance() 0 9 2
B getSortParameters() 0 18 5
A getPaginationParameters() 0 9 1
A getDefaultSortValues() 0 9 1
A modelTransform() 0 4 1
D modelReverseTransform() 0 45 10
A camelize() 0 4 1
A getModelCollectionInstance() 0 4 1
A collectionClear() 0 4 1
A collectionHasElement() 0 4 1
A collectionAddElement() 0 4 1
A collectionRemoveElement() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like ModelManager 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 ModelManager, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/*
4
 * This file is part of the Sonata package.
5
 *
6
 * (c) Thomas Rabaix <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Sonata\DoctrineORMAdminBundle\Model;
13
14
use Doctrine\Common\Util\ClassUtils;
15
use Doctrine\ORM\EntityManager;
16
use Sonata\DoctrineORMAdminBundle\Admin\FieldDescription;
17
use Sonata\DoctrineORMAdminBundle\Datagrid\ProxyQuery;
18
19
use Sonata\AdminBundle\Model\ModelManagerInterface;
20
use Sonata\AdminBundle\Admin\FieldDescriptionInterface;
21
use Sonata\AdminBundle\Datagrid\DatagridInterface;
22
use Sonata\AdminBundle\Datagrid\ProxyQueryInterface;
23
use Sonata\AdminBundle\Exception\ModelManagerException;
24
25
use Doctrine\ORM\QueryBuilder;
26
use Doctrine\DBAL\DBALException;
27
28
use Symfony\Component\Form\Exception\PropertyAccessDeniedException;
29
use Symfony\Bridge\Doctrine\RegistryInterface;
30
31
use Exporter\Source\DoctrineORMQuerySourceIterator;
32
33
class ModelManager implements ModelManagerInterface
34
{
35
    protected $registry;
36
37
    protected $cache = array();
38
39
    const ID_SEPARATOR = '~';
40
41
    /**
42
     * @param \Symfony\Bridge\Doctrine\RegistryInterface $registry
43
     */
44
    public function __construct(RegistryInterface $registry)
45
    {
46
        $this->registry = $registry;
47
    }
48
49
    /**
50
     * {@inheritdoc}
51
     */
52
    public function getMetadata($class)
53
    {
54
        return $this->getEntityManager($class)->getMetadataFactory()->getMetadataFor($class);
55
    }
56
57
    /**
58
     * Returns the model's metadata holding the fully qualified property, and the last
59
     * property name
60
     *
61
     * @param string $baseClass        The base class of the model holding the fully qualified property.
62
     * @param string $propertyFullName The name of the fully qualified property (dot ('.') separated
63
     *                                 property string)
64
     *
65
     * @return array(
0 ignored issues
show
Documentation introduced by
The doc-type array( could not be parsed: Expected "|" or "end of type", but got "(" at position 5. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
66
     *     \Doctrine\ORM\Mapping\ClassMetadata $parentMetadata,
67
     *     string $lastPropertyName,
68
     *     array $parentAssociationMappings
69
     * )
70
     */
71
    public function getParentMetadataForProperty($baseClass, $propertyFullName)
72
    {
73
        $nameElements = explode('.', $propertyFullName);
74
        $lastPropertyName = array_pop($nameElements);
75
        $class = $baseClass;
76
        $parentAssociationMappings = array();
77
78
        foreach ($nameElements as $nameElement) {
79
            $metadata = $this->getMetadata($class);
80
            $parentAssociationMappings[] = $metadata->associationMappings[$nameElement];
81
            $class = $metadata->getAssociationTargetClass($nameElement);
82
        }
83
84
        return array($this->getMetadata($class), $lastPropertyName, $parentAssociationMappings);
85
    }
86
87
    /**
88
     * {@inheritdoc}
89
     */
90
    public function hasMetadata($class)
91
    {
92
        return $this->getEntityManager($class)->getMetadataFactory()->hasMetadataFor($class);
93
    }
94
95
    /**
96
     * {@inheritdoc}
97
     */
98
    public function getNewFieldDescriptionInstance($class, $name, array $options = array())
99
    {
100
        if (!is_string($name)) {
101
            throw new \RunTimeException('The name argument must be a string');
102
        }
103
104
        if (!isset($options['route']['name'])) {
105
            $options['route']['name'] = 'edit';
106
        }
107
108
        if (!isset($options['route']['parameters'])) {
109
            $options['route']['parameters'] = array();
110
        }
111
112
        list($metadata, $propertyName, $parentAssociationMappings) = $this->getParentMetadataForProperty($class, $name);
113
114
        $fieldDescription = new FieldDescription;
115
        $fieldDescription->setName($name);
116
        $fieldDescription->setOptions($options);
117
        $fieldDescription->setParentAssociationMappings($parentAssociationMappings);
118
119
        if (isset($metadata->associationMappings[$propertyName])) {
120
            $fieldDescription->setAssociationMapping($metadata->associationMappings[$propertyName]);
121
        }
122
123
        if (isset($metadata->fieldMappings[$propertyName])) {
124
            $fieldDescription->setFieldMapping($metadata->fieldMappings[$propertyName]);
125
        }
126
127
        return $fieldDescription;
128
    }
129
130
    /**
131
     * {@inheritdoc}
132
     */
133
    public function create($object)
134
    {
135
        try {
136
            $entityManager = $this->getEntityManager($object);
137
            $entityManager->persist($object);
138
            $entityManager->flush();
139
        } catch (\PDOException $e) {
140
            throw new ModelManagerException(sprintf('Failed to create object: %s', ClassUtils::getClass($object)), $e->getCode(), $e);
141
        } catch (DBALException $e) {
142
            throw new ModelManagerException(sprintf('Failed to create object: %s', ClassUtils::getClass($object)), $e->getCode(), $e);
143
        }
144
    }
145
146
    /**
147
     * {@inheritdoc}
148
     */
149
    public function update($object)
150
    {
151
        try {
152
            $entityManager = $this->getEntityManager($object);
153
            $entityManager->persist($object);
154
            $entityManager->flush();
155
        } catch (\PDOException $e) {
156
            throw new ModelManagerException(sprintf('Failed to update object: %s', ClassUtils::getClass($object)), $e->getCode(), $e);
157
        } catch (DBALException $e) {
158
            throw new ModelManagerException(sprintf('Failed to update object: %s', ClassUtils::getClass($object)), $e->getCode(), $e);
159
        }
160
    }
161
162
    /**
163
     * {@inheritdoc}
164
     */
165
    public function delete($object)
166
    {
167
        try {
168
            $entityManager = $this->getEntityManager($object);
0 ignored issues
show
Documentation introduced by
$object is of type object, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
169
            $entityManager->remove($object);
170
            $entityManager->flush();
171
        } catch (\PDOException $e) {
172
            throw new ModelManagerException(sprintf('Failed to delete object: %s', ClassUtils::getClass($object)), $e->getCode(), $e);
173
        } catch (DBALException $e) {
174
            throw new ModelManagerException(sprintf('Failed to delete object: %s', ClassUtils::getClass($object)), $e->getCode(), $e);
175
        }
176
    }
177
178
    /**
179
     * {@inheritdoc}
180
     */
181
    public function find($class, $id)
182
    {
183
        if (!isset($id)) {
184
            return null;
185
        }
186
187
        $values = array_combine($this->getIdentifierFieldNames($class), explode(self::ID_SEPARATOR, $id));
188
189
        return $this->getEntityManager($class)->getRepository($class)->find($values);
190
    }
191
192
    /**
193
     * {@inheritdoc}
194
     */
195
    public function findBy($class, array $criteria = array())
196
    {
197
        return $this->getEntityManager($class)->getRepository($class)->findBy($criteria);
198
    }
199
200
    /**
201
     * {@inheritdoc}
202
     */
203
    public function findOneBy($class, array $criteria = array())
204
    {
205
        return $this->getEntityManager($class)->getRepository($class)->findOneBy($criteria);
206
    }
207
208
    /**
209
     * @param string $class
210
     *
211
     * @return EntityManager
212
     */
213
    public function getEntityManager($class)
214
    {
215
        if (is_object($class)) {
216
            $class = get_class($class);
217
        }
218
219
        if (!isset($this->cache[$class])) {
220
            $em = $this->registry->getManagerForClass($class);
221
222
            if (!$em) {
223
                throw new \RuntimeException(sprintf('No entity manager defined for class %s', $class));
224
            }
225
226
            $this->cache[$class] = $em;
227
        }
228
229
        return $this->cache[$class];
230
    }
231
232
    /**
233
     * {@inheritdoc}
234
     */
235
    public function getParentFieldDescription($parentAssociationMapping, $class)
236
    {
237
        $fieldName = $parentAssociationMapping['fieldName'];
238
239
        $metadata = $this->getMetadata($class);
240
241
        $associatingMapping = $metadata->associationMappings[$parentAssociationMapping];
242
243
        $fieldDescription = $this->getNewFieldDescriptionInstance($class, $fieldName);
244
        $fieldDescription->setName($parentAssociationMapping);
0 ignored issues
show
Documentation introduced by
$parentAssociationMapping is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
245
        $fieldDescription->setAssociationMapping($associatingMapping);
246
247
        return $fieldDescription;
248
    }
249
250
    /**
251
     * {@inheritdoc}
252
     */
253
    public function createQuery($class, $alias = 'o')
254
    {
255
        $repository = $this->getEntityManager($class)->getRepository($class);
256
257
        return new ProxyQuery($repository->createQueryBuilder($alias));
258
    }
259
260
    /**
261
     * {@inheritdoc}
262
     */
263
    public function executeQuery($query)
264
    {
265
        if ($query instanceof QueryBuilder) {
266
            return $query->getQuery()->execute();
267
        }
268
269
        return $query->execute();
270
    }
271
272
    /**
273
     * {@inheritdoc}
274
     */
275
    public function getModelIdentifier($class)
276
    {
277
        return $this->getMetadata($class)->identifier;
278
    }
279
280
    /**
281
     * {@inheritdoc}
282
     */
283
    public function getIdentifierValues($entity)
284
    {
285
        // Fix code has an impact on performance, so disable it ...
286
        //$entityManager = $this->getEntityManager($entity);
287
        //if (!$entityManager->getUnitOfWork()->isInIdentityMap($entity)) {
288
        //    throw new \RuntimeException('Entities passed to the choice field must be managed');
289
        //}
290
291
292
        $class = $this->getMetadata(ClassUtils::getClass($entity));
293
294
        $identifiers = array();
295
296
        foreach ($class->getIdentifierValues($entity) as $value) {
297
            if (!is_object($value)) {
298
                $identifiers[] = $value;
299
                continue;
300
            }
301
302
            $class = $this->getMetadata(ClassUtils::getClass($value));
303
304
            foreach ($class->getIdentifierValues($value) as $value) {
305
                $identifiers[] = $value;
306
            }
307
        }
308
309
        return $identifiers;
310
    }
311
312
    /**
313
     * {@inheritdoc}
314
     */
315
    public function getIdentifierFieldNames($class)
316
    {
317
        return $this->getMetadata($class)->getIdentifierFieldNames();
318
    }
319
320
    /**
321
     * {@inheritdoc}
322
     */
323
    public function getNormalizedIdentifier($entity)
324
    {
325
        if (is_scalar($entity)) {
326
            throw new \RunTimeException('Invalid argument, object or null required');
327
        }
328
329
        // the entities is not managed
330
        if (!$entity /*|| !$this->getEntityManager($entity)->getUnitOfWork()->isInIdentityMap($entity) // commented for perfomance concern */) {
331
            return null;
332
        }
333
334
        $values = $this->getIdentifierValues($entity);
335
336
        if (count($values) === 0) {
337
            return null;
338
        }
339
340
        return implode(self::ID_SEPARATOR, $values);
341
    }
342
343
    /**
344
     * {@inheritDoc}
345
     *
346
     * The ORM implementation does nothing special but you still should use
347
     * this method when using the id in a URL to allow for future improvements.
348
     */
349
    public function getUrlsafeIdentifier($entity)
350
    {
351
        return $this->getNormalizedIdentifier($entity);
352
    }
353
354
    /**
355
     * {@inheritdoc}
356
     */
357
    public function addIdentifiersToQuery($class, ProxyQueryInterface $queryProxy, array $idx)
358
    {
359
        $fieldNames = $this->getIdentifierFieldNames($class);
360
        $qb = $queryProxy->getQueryBuilder();
361
362
        $prefix = uniqid();
363
        $sqls = array();
364
        foreach ($idx as $pos => $id) {
365
            $ids     = explode(self::ID_SEPARATOR, $id);
366
367
            $ands = array();
368
            foreach ($fieldNames as $posName => $name) {
369
                $parameterName = sprintf('field_%s_%s_%d', $prefix, $name, $pos);
370
                $ands[] = sprintf('%s.%s = :%s', $qb->getRootAlias(), $name, $parameterName);
371
                $qb->setParameter($parameterName, $ids[$posName]);
372
            }
373
374
            $sqls[] = implode(' AND ', $ands);
375
        }
376
377
        $qb->andWhere(sprintf('( %s )', implode(' OR ', $sqls)));
378
    }
379
380
    /**
381
     * {@inheritdoc}
382
     */
383
    public function batchDelete($class, ProxyQueryInterface $queryProxy)
384
    {
385
        $queryProxy->select('DISTINCT '.$queryProxy->getRootAlias());
386
387
        try {
388
            $entityManager = $this->getEntityManager($class);
389
390
            $i = 0;
391
            foreach ($queryProxy->getQuery()->iterate() as $pos => $object) {
392
                $entityManager->remove($object[0]);
393
394
                if ((++$i % 20) == 0) {
395
                    $entityManager->flush();
396
                    $entityManager->clear();
397
                }
398
            }
399
400
            $entityManager->flush();
401
            $entityManager->clear();
402
        } catch (\PDOException $e) {
403
            throw new ModelManagerException('', 0, $e);
404
        } catch (DBALException $e) {
405
            throw new ModelManagerException('', 0, $e);
406
        }
407
    }
408
409
    /**
410
     * {@inheritdoc}
411
     */
412
    public function getDataSourceIterator(DatagridInterface $datagrid, array $fields, $firstResult = null, $maxResult = null)
413
    {
414
        $datagrid->buildPager();
415
        $query = $datagrid->getQuery();
416
417
        $query->select('DISTINCT ' . $query->getRootAlias());
418
        $query->setFirstResult($firstResult);
419
        $query->setMaxResults($maxResult);
420
421
        if ($query instanceof ProxyQueryInterface) {
422
            $query->addOrderBy($query->getSortBy(), $query->getSortOrder());
423
424
            $query = $query->getQuery();
425
        }
426
427
        return new DoctrineORMQuerySourceIterator($query, $fields);
428
    }
429
430
    /**
431
     * {@inheritdoc}
432
     */
433
    public function getExportFields($class)
434
    {
435
        $metadata = $this->getEntityManager($class)->getClassMetadata($class);
436
437
        return $metadata->getFieldNames();
438
    }
439
440
    /**
441
     * {@inheritdoc}
442
     */
443
    public function getModelInstance($class)
444
    {
445
        $r = new \ReflectionClass($class);
446
        if ($r->isAbstract()) {
447
            throw new \RuntimeException(sprintf('Cannot initialize abstract class: %s', $class));
448
        }
449
450
        return new $class;
451
    }
452
453
    /**
454
     * {@inheritdoc}
455
     */
456
    public function getSortParameters(FieldDescriptionInterface $fieldDescription, DatagridInterface $datagrid)
457
    {
458
        $values = $datagrid->getValues();
459
460
        if ($fieldDescription->getName() == $values['_sort_by']->getName() || $values['_sort_by']->getName() === $fieldDescription->getOption('sortable')) {
461
            if ($values['_sort_order'] == 'ASC') {
462
                $values['_sort_order'] = 'DESC';
463
            } else {
464
                $values['_sort_order'] = 'ASC';
465
            }
466
        } else {
467
            $values['_sort_order'] = 'ASC';
468
        }
469
470
        $values['_sort_by'] = is_string($fieldDescription->getOption('sortable')) ? $fieldDescription->getOption('sortable') :  $fieldDescription->getName();
471
472
        return array('filter' => $values);
473
    }
474
475
    /**
476
     * {@inheritdoc}
477
     */
478
    public function getPaginationParameters(DatagridInterface $datagrid, $page)
479
    {
480
        $values = $datagrid->getValues();
481
482
        $values['_sort_by'] = $values['_sort_by']->getName();
483
        $values['_page'] = $page;
484
485
        return array('filter' => $values);
486
    }
487
488
    /**
489
     * {@inheritdoc}
490
     */
491
    public function getDefaultSortValues($class)
492
    {
493
        return array(
494
            '_sort_order' => 'ASC',
495
            '_sort_by'    => implode(',', $this->getModelIdentifier($class)),
496
            '_page'       => 1,
497
            '_per_page'   => 25,
498
        );
499
    }
500
501
    /**
502
     * {@inheritdoc}
503
     */
504
    public function modelTransform($class, $instance)
505
    {
506
        return $instance;
507
    }
508
509
    /**
510
     * {@inheritdoc}
511
     */
512
    public function modelReverseTransform($class, array $array = array())
513
    {
514
        $instance = $this->getModelInstance($class);
515
        $metadata = $this->getMetadata($class);
516
517
        $reflClass = $metadata->reflClass;
518
        foreach ($array as $name => $value) {
519
520
            $reflection_property = false;
521
            // property or association ?
522
            if (array_key_exists($name, $metadata->fieldMappings)) {
523
524
                $property = $metadata->fieldMappings[$name]['fieldName'];
525
                $reflection_property = $metadata->reflFields[$name];
526
527
            } elseif (array_key_exists($name, $metadata->associationMappings)) {
528
                $property = $metadata->associationMappings[$name]['fieldName'];
529
            } else {
530
                $property = $name;
531
            }
532
533
            $setter = 'set'.$this->camelize($name);
534
535
            if ($reflClass->hasMethod($setter)) {
536
                if (!$reflClass->getMethod($setter)->isPublic()) {
537
                    throw new PropertyAccessDeniedException(sprintf('Method "%s()" is not public in class "%s"', $setter, $reflClass->getName()));
538
                }
539
540
                $instance->$setter($value);
541
            } elseif ($reflClass->hasMethod('__set')) {
542
                // needed to support magic method __set
543
                $instance->$property = $value;
544
            } elseif ($reflClass->hasProperty($property)) {
545
                if (!$reflClass->getProperty($property)->isPublic()) {
546
                    throw new PropertyAccessDeniedException(sprintf('Property "%s" is not public in class "%s". Maybe you should create the method "set%s()"?', $property, $reflClass->getName(), ucfirst($property)));
547
                }
548
549
                $instance->$property = $value;
550
            } elseif ($reflection_property) {
551
                $reflection_property->setValue($instance, $value);
552
            }
553
        }
554
555
        return $instance;
556
    }
557
558
    /**
559
     * method taken from PropertyPath
560
     *
561
     * @param string $property
562
     *
563
     * @return mixed
564
     */
565
    protected function camelize($property)
566
    {
567
        return preg_replace(array('/(^|_)+(.)/e', '/\.(.)/e'), array("strtoupper('\\2')", "'_'.strtoupper('\\1')"), $property);
568
    }
569
570
    /**
571
     * {@inheritdoc}
572
     */
573
    public function getModelCollectionInstance($class)
574
    {
575
        return new \Doctrine\Common\Collections\ArrayCollection();
0 ignored issues
show
Bug Best Practice introduced by
The return type of return new \Doctrine\Com...ions\ArrayCollection(); (Doctrine\Common\Collections\ArrayCollection) is incompatible with the return type declared by the interface Sonata\AdminBundle\Model...ModelCollectionInstance of type array.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
576
    }
577
578
    /**
579
     * {@inheritdoc}
580
     */
581
    public function collectionClear(&$collection)
582
    {
583
        return $collection->clear();
584
    }
585
586
    /**
587
     * {@inheritdoc}
588
     */
589
    public function collectionHasElement(&$collection, &$element)
590
    {
591
        return $collection->contains($element);
592
    }
593
594
    /**
595
     * {@inheritdoc}
596
     */
597
    public function collectionAddElement(&$collection, &$element)
598
    {
599
        return $collection->add($element);
600
    }
601
602
    /**
603
     * {@inheritdoc}
604
     */
605
    public function collectionRemoveElement(&$collection, &$element)
606
    {
607
        return $collection->removeElement($element);
608
    }
609
}
610