Completed
Push — 1.2 ( d3ea0d...d8c512 )
by David
16:21
created

ModelManager   F

Complexity

Total Complexity 64

Size/Duplication

Total Lines 535
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 16

Importance

Changes 6
Bugs 2 Features 2
Metric Value
wmc 64
c 6
b 2
f 2
lcom 1
cbo 16
dl 0
loc 535
rs 3.4883

36 Methods

Rating   Name   Duplication   Size   Complexity  
A getDocumentManager() 0 4 1
A getIdentifierFieldNames() 0 4 1
A getNormalizedIdentifier() 0 15 4
A find() 0 12 3
B getNewFieldDescriptionInstance() 0 22 4
A createQuery() 0 7 1
A getIdentifierValues() 0 7 1
A addIdentifiersToQuery() 0 12 2
A getSortParameters() 0 19 3
A getDefaultSortValues() 0 8 1
D modelReverseTransform() 0 46 10
A __construct() 0 4 1
A getMetadata() 0 4 1
A hasMetadata() 0 4 1
A create() 0 9 2
A update() 0 9 2
A delete() 0 9 2
A findBy() 0 4 1
A findOneBy() 0 4 1
A getParentFieldDescription() 0 14 1
A executeQuery() 0 4 1
A getModelIdentifier() 0 4 1
A getUrlsafeIdentifier() 0 9 2
A getBackendId() 0 4 2
A batchDelete() 0 20 4
A getModelInstance() 0 4 1
A getPaginationParameters() 0 9 1
A modelTransform() 0 4 1
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
A getDataSourceIterator() 0 4 1
A getExportFields() 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\DoctrinePHPCRAdminBundle\Model;
13
14
use Sonata\DoctrinePHPCRAdminBundle\Admin\FieldDescription;
15
use Sonata\DoctrinePHPCRAdminBundle\Datagrid\ProxyQuery;
16
use Sonata\AdminBundle\Model\ModelManagerInterface;
17
use Sonata\AdminBundle\Admin\FieldDescriptionInterface;
18
use Sonata\AdminBundle\Datagrid\DatagridInterface;
19
use Sonata\AdminBundle\Datagrid\ProxyQueryInterface;
20
use Sonata\AdminBundle\Exception\ModelManagerException;
21
22
use Doctrine\ODM\PHPCR\Mapping\ClassMetadata;
23
use Doctrine\ODM\PHPCR\DocumentManager;
24
use Doctrine\Common\Collections\ArrayCollection;
25
use Doctrine\Common\Util\ClassUtils;
26
27
use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
28
29
class ModelManager implements ModelManagerInterface
30
{
31
    /**
32
     * @var DocumentManager
33
     */
34
    protected $dm;
35
36
    /**
37
     * @param DocumentManager $dm
38
     */
39
    public function __construct(DocumentManager $dm)
40
    {
41
        $this->dm = $dm;
42
    }
43
44
    /**
45
     * Returns the related model's metadata
46
     *
47
     * @param string $class
48
     *
49
     * @return ClassMetadata
50
     */
51
    public function getMetadata($class)
52
    {
53
        return $this->dm->getMetadataFactory()->getMetadataFor($class);
54
    }
55
56
    /**
57
     * Returns true is the model has some metadata.
58
     *
59
     * @param string $class
60
     *
61
     * @return boolean
62
     */
63
    public function hasMetadata($class)
64
    {
65
        return $this->dm->getMetadataFactory()->hasMetadataFor($class);
66
    }
67
68
    /**
69
     * {@inheritDoc}
70
     *
71
     * @throws ModelManagerException if the document manager throws any exception
72
     */
73
    public function create($object)
74
    {
75
        try {
76
            $this->dm->persist($object);
77
            $this->dm->flush();
78
        } catch (\Exception $e) {
79
            throw new ModelManagerException('', 0, $e);
80
        }
81
    }
82
83
    /**
84
     * {@inheritDoc}
85
     *
86
     * @throws ModelManagerException if the document manager throws any exception
87
     */
88
    public function update($object)
89
    {
90
        try {
91
            $this->dm->persist($object);
92
            $this->dm->flush();
93
        } catch (\Exception $e) {
94
            throw new ModelManagerException('', 0, $e);
95
        }
96
    }
97
98
    /**
99
     * {@inheritDoc}
100
     *
101
     * @throws ModelManagerException if the document manager throws any exception
102
     */
103
    public function delete($object)
104
    {
105
        try {
106
            $this->dm->remove($object);
107
            $this->dm->flush();
108
        } catch (\Exception $e) {
109
            throw new ModelManagerException('', 0, $e);
110
        }
111
    }
112
113
    /**
114
     * Find one object from the given class repository.
115
     *
116
     * {@inheritDoc}
117
     */
118
    public function find($class, $id)
119
    {
120
        if (!isset($id)) {
121
            return null;
122
        }
123
124
        if (null === $class) {
125
            return $this->dm->find(null, $id);
126
        }
127
128
        return $this->dm->getRepository($class)->find($id);
129
    }
130
131
    /**
132
     * {@inheritDoc}
133
     *
134
     * @return FieldDescription
135
     *
136
     * @throws \RunTimeException if $name is not a string.
137
     */
138
    public function getNewFieldDescriptionInstance($class, $name, array $options = array())
139
    {
140
        if (!is_string($name)) {
141
            throw new \RunTimeException('The name argument must be a string');
142
        }
143
144
        $metadata = $this->getMetadata($class);
145
146
        $fieldDescription = new FieldDescription;
147
        $fieldDescription->setName($name);
148
        $fieldDescription->setOptions($options);
149
150
        if (isset($metadata->associationMappings[$name])) {
0 ignored issues
show
Bug introduced by
The property associationMappings does not seem to exist. Did you mean mappings?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
151
            $fieldDescription->setAssociationMapping($metadata->associationMappings[$name]);
0 ignored issues
show
Bug introduced by
The property associationMappings does not seem to exist. Did you mean mappings?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
152
        }
153
154
        if (isset($metadata->fieldMappings[$name])) {
155
            $fieldDescription->setFieldMapping($metadata->fieldMappings[$name]);
156
        }
157
158
        return $fieldDescription;
159
    }
160
161
    /**
162
     * {@inheritDoc}
163
     */
164
    public function findBy($class, array $criteria = array())
165
    {
166
        return $this->dm->getRepository($class)->findBy($criteria);
167
    }
168
169
    /**
170
     * {@inheritDoc}
171
     */
172
    public function findOneBy($class, array $criteria = array())
173
    {
174
        return $this->dm->getRepository($class)->findOneBy($criteria);
175
    }
176
177
    /**
178
     * @return DocumentManager The PHPCR-ODM document manager responsible for
179
     *                         this model.
180
     */
181
    public function getDocumentManager()
182
    {
183
        return $this->dm;
184
    }
185
186
    /**
187
     * {@inheritDoc}
188
     *
189
     * @return FieldDescriptionInterface
190
     */
191
    public function getParentFieldDescription($parentAssociationMapping, $class)
192
    {
193
        $fieldName = $parentAssociationMapping['fieldName'];
194
195
        $metadata = $this->getMetadata($class);
196
197
        $associatingMapping = $metadata->associationMappings[$parentAssociationMapping];
0 ignored issues
show
Bug introduced by
The property associationMappings does not seem to exist. Did you mean mappings?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
198
199
        $fieldDescription = $this->getNewFieldDescriptionInstance($class, $fieldName);
200
        $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...
201
        $fieldDescription->setAssociationMapping($associatingMapping);
202
203
        return $fieldDescription;
204
    }
205
206
    /**
207
     * @param string $class the fully qualified class name to search for
208
     * @param string $alias alias to use for this class when accessing fields,
209
     *                      defaults to 'a'.
210
     *
211
     * @return ProxyQueryInterface
212
     *
213
     * @throws \InvalidArgumentException if alias is not a string or an empty string
214
     */
215
    public function createQuery($class, $alias = 'a')
216
    {
217
        $qb = $this->getDocumentManager()->createQueryBuilder();
218
        $qb->from()->document($class, $alias);
219
220
        return new ProxyQuery($qb, $alias);
221
    }
222
223
    /**
224
     * @param ProxyQuery $query
225
     *
226
     * @return mixed
227
     */
228
    public function executeQuery($query)
229
    {
230
        return $query->execute();
231
    }
232
233
    /**
234
     * {@inheritDoc}
235
     */
236
    public function getModelIdentifier($classname)
237
    {
238
        return $this->getMetadata($classname)->identifier;
239
    }
240
241
    /**
242
     * Transforms the document into the PHPCR path.
243
     *
244
     * Note: This is returning an array because Doctrine ORM for example can
245
     * have multiple identifiers, e.g. if the primary key is composed of
246
     * several columns. We only ever have one, but return that wrapped into an
247
     * array to adhere to the interface.
248
     *
249
     * {@inheritDoc}
250
     */
251
    public function getIdentifierValues($document)
252
    {
253
        $class = $this->getMetadata(ClassUtils::getClass($document));
254
        $path = $class->reflFields[$class->identifier]->getValue($document);
255
256
        return array($path);
257
    }
258
259
    /**
260
     * {@inheritDoc}
261
     */
262
    public function getIdentifierFieldNames($class)
263
    {
264
        return array($this->getModelIdentifier($class));
265
    }
266
267
    /**
268
     * This is just taking the id out of the array again.
269
     *
270
     * {@inheritDoc}
271
     *
272
     * @throws \InvalidArgumentException if $document is not an object or null
273
     */
274
    public function getNormalizedIdentifier($document)
275
    {
276
        if (is_scalar($document)) {
277
            throw new \InvalidArgumentException('Invalid argument, object or null required');
278
        }
279
280
        // the document is not managed
281
        if (!$document || !$this->getDocumentManager()->contains($document)) {
282
            return null;
283
        }
284
285
        $values = $this->getIdentifierValues($document);
286
287
        return $values[0];
288
    }
289
290
    /**
291
     * Currently only the leading slash is removed.
292
     *
293
     * TODO: do we also have to encode certain characters like spaces or does that happen automatically?
294
     *
295
     * @param object $document
296
     *
297
     * @return null|string
298
     */
299
    public function getUrlsafeIdentifier($document)
300
    {
301
        $id = $this->getNormalizedIdentifier($document);
302
        if (null !== $id) {
303
            return substr($id, 1);
304
        }
305
306
        return null;
307
    }
308
309
    /**
310
     * {@inheritDoc}
311
     */
312
    public function addIdentifiersToQuery($class, ProxyQueryInterface $queryProxy, array $idx)
313
    {
314
        /** @var $queryProxy ProxyQuery */
315
        $qb = $queryProxy->getQueryBuilder();
316
317
        $orX = $qb->andWhere()->orX();
318
319
        foreach ($idx as $id) {
320
            $path = $this->getBackendId($id);
321
            $orX->same($path, $queryProxy->getAlias());
322
        }
323
    }
324
325
    /**
326
     * Add leading slash to construct valid phpcr document id.
327
     *
328
     * The phpcr-odm QueryBuilder uses absolute paths and expects id´s to start with a forward slash
329
     * because SonataAdmin uses object id´s for constructing URL´s it has to use id´s without the
330
     * leading slash.
331
     *
332
     * @param string $id
333
     *
334
     * @return string
335
     */
336
    public function getBackendId($id)
337
    {
338
        return substr($id, 0, 1) === '/' ? $id : '/'.$id;
339
    }
340
341
    /**
342
     * {@inheritDoc}
343
     *
344
     * @throws ModelManagerException if anything goes wrong during query execution.
345
     */
346
    public function batchDelete($class, ProxyQueryInterface $queryProxy)
347
    {
348
        try {
349
            $i = 0;
350
            $res = $queryProxy->execute();
351
            foreach ($res as $object) {
352
                $this->dm->remove($object);
353
354
                if ((++$i % 20) == 0) {
355
                    $this->dm->flush();
356
                    $this->dm->clear();
357
                }
358
            }
359
360
            $this->dm->flush();
361
            $this->dm->clear();
362
        } catch (\Exception $e) {
363
            throw new ModelManagerException('', 0, $e);
364
        }
365
    }
366
367
    /**
368
     * {@inheritDoc}
369
     *
370
     * @return object
371
     */
372
    public function getModelInstance($class)
373
    {
374
        return new $class;
375
    }
376
377
    /**
378
     * {@inheritDoc}
379
     */
380
    public function getSortParameters(FieldDescriptionInterface $fieldDescription, DatagridInterface $datagrid)
381
    {
382
        $values = $datagrid->getValues();
383
384
        if ($fieldDescription->getName() == $values['_sort_by']->getName()) {
385
            if ($values['_sort_order'] == 'ASC') {
386
                $values['_sort_order'] = 'DESC';
387
            } else {
388
                $values['_sort_order'] = 'ASC';
389
            }
390
391
            $values['_sort_by']    = $fieldDescription->getName();
392
        } else {
393
            $values['_sort_order'] = 'ASC';
394
            $values['_sort_by'] = $fieldDescription->getName();
395
        }
396
397
        return array('filter' => $values);
398
    }
399
400
    /**
401
     * {@inheritDoc}
402
     */
403
    public function getPaginationParameters(DatagridInterface $datagrid, $page)
404
    {
405
        $values = $datagrid->getValues();
406
407
        $values['_sort_by'] = $values['_sort_by']->getName();
408
        $values['_page'] = $page;
409
410
        return array('filter' => $values);
411
    }
412
413
    /**
414
     * {@inheritDoc}
415
     */
416
    public function getDefaultSortValues($class)
417
    {
418
        return array(
419
            '_sort_order' => 'ASC',
420
            '_sort_by'    => $this->getModelIdentifier($class),
421
            '_page'       => 1
422
        );
423
    }
424
425
    /**
426
     * {@inheritDoc}
427
     *
428
     * @return object
429
     */
430
    public function modelTransform($class, $instance)
431
    {
432
        return $instance;
433
    }
434
435
    /**
436
     * {@inheritDoc}
437
     *
438
     * @return object
439
     *
440
     * @throws NoSuchPropertyException if the class has no magic setter and
441
     *      public property for a field in array.
442
     */
443
    public function modelReverseTransform($class, array $array = array())
444
    {
445
        $instance = $this->getModelInstance($class);
446
        $metadata = $this->getMetadata($class);
447
448
        $reflClass = $metadata->reflClass;
449
        foreach ($array as $name => $value) {
450
451
            $reflection_property = false;
452
            // property or association ?
453
            if (array_key_exists($name, $metadata->fieldMappings)) {
454
455
                $property = $metadata->fieldMappings[$name]['fieldName'];
456
                $reflection_property = $metadata->reflFields[$name];
457
458
            } else if (array_key_exists($name, $metadata->associationMappings)) {
0 ignored issues
show
Bug introduced by
The property associationMappings does not seem to exist. Did you mean mappings?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
459
                $property = $metadata->associationMappings[$name]['fieldName'];
0 ignored issues
show
Bug introduced by
The property associationMappings does not seem to exist. Did you mean mappings?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
460
            } else {
461
                $property = $name;
462
            }
463
464
            // TODO: use PropertyAccess https://github.com/sonata-project/SonataDoctrinePhpcrAdminBundle/issues/187
465
            $setter = 'set' . $this->camelize($name);
0 ignored issues
show
Deprecated Code introduced by
The method Sonata\DoctrinePHPCRAdmi...odelManager::camelize() has been deprecated.

This method has been deprecated.

Loading history...
466
467
            if ($reflClass->hasMethod($setter)) {
468
                if (!$reflClass->getMethod($setter)->isPublic()) {
469
                    throw new NoSuchPropertyException(sprintf('Method "%s()" is not public in class "%s"', $setter, $reflClass->getName()));
470
                }
471
472
                $instance->$setter($value);
473
            } else if ($reflClass->hasMethod('__set')) {
474
                // needed to support magic method __set
475
                $instance->$property = $value;
476
            } else if ($reflClass->hasProperty($property)) {
477
                if (!$reflClass->getProperty($property)->isPublic()) {
478
                    throw new NoSuchPropertyException(sprintf('Property "%s" is not public in class "%s". Maybe you should create the method "set%s()"?', $property, $reflClass->getName(), ucfirst($property)));
479
                }
480
481
                $instance->$property = $value;
482
            } else if ($reflection_property) {
483
                $reflection_property->setValue($instance, $value);
484
            }
485
        }
486
487
        return $instance;
488
    }
489
490
    /**
491
     * Method taken from PropertyPath.
492
     *
493
     * TODO: remove when doing https://github.com/sonata-project/SonataDoctrinePhpcrAdminBundle/issues/187
494
     *
495
     * @param string $property
496
     *
497
     * @return string
498
     *
499
     * @deprecated
500
     */
501
    protected function camelize($property)
502
    {
503
        return preg_replace(array('/(^|_)+(.)/e', '/\.(.)/e'), array("strtoupper('\\2')", "'_'.strtoupper('\\1')"), $property);
504
    }
505
506
    /**
507
     * {@inheritDoc}
508
     */
509
    public function getModelCollectionInstance($class)
510
    {
511
        return new 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...
512
    }
513
514
    /**
515
     * {@inheritDoc}
516
     */
517
    public function collectionClear(&$collection)
518
    {
519
        return $collection->clear();
520
    }
521
522
    /**
523
     * {@inheritDoc}
524
     */
525
    public function collectionHasElement(&$collection, &$element)
526
    {
527
        return $collection->contains($element);
528
    }
529
530
    /**
531
     * {@inheritDoc}
532
     */
533
    public function collectionAddElement(&$collection, &$element)
534
    {
535
        return $collection->add($element);
536
    }
537
538
    /**
539
     * {@inheritDoc}
540
     */
541
    public function collectionRemoveElement(&$collection, &$element)
542
    {
543
        return $collection->removeElement($element);
544
    }
545
546
    /**
547
     * {@inheritDoc}
548
     */
549
    public function getDataSourceIterator(DatagridInterface $datagrid, array $fields, $firstResult = null, $maxResult = null)
550
    {
551
        throw new \RuntimeException("Datasourceiterator not implemented.");
552
    }
553
554
    /**
555
     * {@inheritDoc}
556
     *
557
     * Not really implemented.
558
     */
559
    public function getExportFields($class)
560
    {
561
        return array();
562
    }
563
}
564