ModelManager   F
last analyzed

Complexity

Total Complexity 64

Size/Duplication

Total Lines 525
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 16

Importance

Changes 0
Metric Value
wmc 64
lcom 1
cbo 16
dl 0
loc 525
rs 3.28
c 0
b 0
f 0

36 Methods

Rating   Name   Duplication   Size   Complexity  
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 find() 0 12 3
A getNewFieldDescriptionInstance() 0 22 4
A findBy() 0 4 1
A findOneBy() 0 4 1
A getDocumentManager() 0 4 1
A getParentFieldDescription() 0 14 1
A createQuery() 0 7 1
A executeQuery() 0 4 1
A getModelIdentifier() 0 4 1
A getIdentifierValues() 0 7 1
A getIdentifierFieldNames() 0 4 1
A getNormalizedIdentifier() 0 15 4
A getUrlsafeIdentifier() 0 7 2
A addIdentifiersToQuery() 0 12 2
A getBackendId() 0 4 2
A batchDelete() 0 20 4
A getModelInstance() 0 4 1
A getSortParameters() 0 19 3
A getPaginationParameters() 0 9 1
A getDefaultSortValues() 0 8 1
A modelTransform() 0 4 1
B modelReverseTransform() 0 43 10
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
A camelize() 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
declare(strict_types=1);
4
5
/*
6
 * This file is part of the Sonata Project package.
7
 *
8
 * (c) Thomas Rabaix <[email protected]>
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace Sonata\DoctrinePHPCRAdminBundle\Model;
15
16
use Doctrine\Common\Collections\ArrayCollection;
17
use Doctrine\Common\Util\ClassUtils;
18
use Doctrine\ODM\PHPCR\DocumentManager;
19
use Doctrine\ODM\PHPCR\Mapping\ClassMetadata;
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
use Sonata\AdminBundle\Model\ModelManagerInterface;
25
use Sonata\DoctrinePHPCRAdminBundle\Admin\FieldDescription;
26
use Sonata\DoctrinePHPCRAdminBundle\Datagrid\ProxyQuery;
27
use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
28
29
class ModelManager implements ModelManagerInterface
30
{
31
    /**
32
     * @var DocumentManager
33
     */
34
    protected $dm;
35
36
    public function __construct(DocumentManager $dm)
37
    {
38
        $this->dm = $dm;
39
    }
40
41
    /**
42
     * Returns the related model's metadata.
43
     *
44
     * @param string $class
45
     *
46
     * @return ClassMetadata
47
     */
48
    public function getMetadata($class)
49
    {
50
        return $this->dm->getMetadataFactory()->getMetadataFor($class);
51
    }
52
53
    /**
54
     * Returns true is the model has some metadata.
55
     *
56
     * @param string $class
57
     *
58
     * @return bool
59
     */
60
    public function hasMetadata($class)
61
    {
62
        return $this->dm->getMetadataFactory()->hasMetadataFor($class);
63
    }
64
65
    /**
66
     * {@inheritdoc}
67
     *
68
     * @throws ModelManagerException if the document manager throws any exception
69
     */
70
    public function create($object): void
71
    {
72
        try {
73
            $this->dm->persist($object);
74
            $this->dm->flush();
75
        } catch (\Exception $e) {
76
            throw new ModelManagerException('', 0, $e);
77
        }
78
    }
79
80
    /**
81
     * {@inheritdoc}
82
     *
83
     * @throws ModelManagerException if the document manager throws any exception
84
     */
85
    public function update($object): void
86
    {
87
        try {
88
            $this->dm->persist($object);
89
            $this->dm->flush();
90
        } catch (\Exception $e) {
91
            throw new ModelManagerException('', 0, $e);
92
        }
93
    }
94
95
    /**
96
     * {@inheritdoc}
97
     *
98
     * @throws ModelManagerException if the document manager throws any exception
99
     */
100
    public function delete($object): void
101
    {
102
        try {
103
            $this->dm->remove($object);
104
            $this->dm->flush();
105
        } catch (\Exception $e) {
106
            throw new ModelManagerException('', 0, $e);
107
        }
108
    }
109
110
    /**
111
     * Find one object from the given class repository.
112
     *
113
     * {@inheritdoc}
114
     */
115
    public function find($class, $id)
116
    {
117
        if (!isset($id)) {
118
            return;
119
        }
120
121
        if (null === $class) {
122
            return $this->dm->find(null, $id);
123
        }
124
125
        return $this->dm->getRepository($class)->find($id);
126
    }
127
128
    /**
129
     * {@inheritdoc}
130
     *
131
     * @throws \RunTimeException if $name is not a string
132
     *
133
     * @return FieldDescription
134
     */
135
    public function getNewFieldDescriptionInstance($class, $name, array $options = [])
136
    {
137
        if (!\is_string($name)) {
138
            throw new \RunTimeException('The name argument must be a string');
139
        }
140
141
        $metadata = $this->getMetadata($class);
142
143
        $fieldDescription = new FieldDescription();
144
        $fieldDescription->setName($name);
145
        $fieldDescription->setOptions($options);
146
147
        if (isset($metadata->associationMappings[$name])) {
0 ignored issues
show
Bug introduced by Sjoerd Peters
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...
148
            $fieldDescription->setAssociationMapping($metadata->associationMappings[$name]);
0 ignored issues
show
Bug introduced by Sjoerd Peters
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...
149
        }
150
151
        if (isset($metadata->fieldMappings[$name])) {
152
            $fieldDescription->setFieldMapping($metadata->fieldMappings[$name]);
153
        }
154
155
        return $fieldDescription;
156
    }
157
158
    /**
159
     * {@inheritdoc}
160
     */
161
    public function findBy($class, array $criteria = [])
162
    {
163
        return $this->dm->getRepository($class)->findBy($criteria);
164
    }
165
166
    /**
167
     * {@inheritdoc}
168
     */
169
    public function findOneBy($class, array $criteria = [])
170
    {
171
        return $this->dm->getRepository($class)->findOneBy($criteria);
172
    }
173
174
    /**
175
     * @return DocumentManager The PHPCR-ODM document manager responsible for
176
     *                         this model
177
     */
178
    public function getDocumentManager()
179
    {
180
        return $this->dm;
181
    }
182
183
    /**
184
     * {@inheritdoc}
185
     *
186
     * @return FieldDescriptionInterface
187
     */
188
    public function getParentFieldDescription($parentAssociationMapping, $class)
189
    {
190
        $fieldName = $parentAssociationMapping['fieldName'];
191
192
        $metadata = $this->getMetadata($class);
193
194
        $associatingMapping = $metadata->associationMappings[$parentAssociationMapping];
0 ignored issues
show
Bug introduced by Nacho Martín
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...
195
196
        $fieldDescription = $this->getNewFieldDescriptionInstance($class, $fieldName);
197
        $fieldDescription->setName($parentAssociationMapping);
0 ignored issues
show
Documentation introduced by Nacho Martín
$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...
198
        $fieldDescription->setAssociationMapping($associatingMapping);
199
200
        return $fieldDescription;
201
    }
202
203
    /**
204
     * @param string $class the fully qualified class name to search for
205
     * @param string $alias alias to use for this class when accessing fields,
206
     *                      defaults to 'a'
207
     *
208
     * @throws \InvalidArgumentException if alias is not a string or an empty string
209
     *
210
     * @return ProxyQueryInterface
211
     */
212
    public function createQuery($class, $alias = 'a')
213
    {
214
        $qb = $this->getDocumentManager()->createQueryBuilder();
215
        $qb->from()->document($class, $alias);
216
217
        return new ProxyQuery($qb, $alias);
218
    }
219
220
    /**
221
     * @param ProxyQuery $query
222
     *
223
     * @return mixed
224
     */
225
    public function executeQuery($query)
226
    {
227
        return $query->execute();
228
    }
229
230
    /**
231
     * {@inheritdoc}
232
     */
233
    public function getModelIdentifier($classname)
234
    {
235
        return $this->getMetadata($classname)->identifier;
236
    }
237
238
    /**
239
     * Transforms the document into the PHPCR path.
240
     *
241
     * Note: This is returning an array because Doctrine ORM for example can
242
     * have multiple identifiers, e.g. if the primary key is composed of
243
     * several columns. We only ever have one, but return that wrapped into an
244
     * array to adhere to the interface.
245
     *
246
     * {@inheritdoc}
247
     */
248
    public function getIdentifierValues($document)
249
    {
250
        $class = $this->getMetadata(ClassUtils::getClass($document));
251
        $path = $class->reflFields[$class->identifier]->getValue($document);
252
253
        return [$path];
254
    }
255
256
    /**
257
     * {@inheritdoc}
258
     */
259
    public function getIdentifierFieldNames($class)
260
    {
261
        return [$this->getModelIdentifier($class)];
262
    }
263
264
    /**
265
     * This is just taking the id out of the array again.
266
     *
267
     * {@inheritdoc}
268
     *
269
     * @throws \InvalidArgumentException if $document is not an object or null
270
     */
271
    public function getNormalizedIdentifier($document)
272
    {
273
        if (is_scalar($document)) {
274
            throw new \InvalidArgumentException('Invalid argument, object or null required');
275
        }
276
277
        // the document is not managed
278
        if (!$document || !$this->getDocumentManager()->contains($document)) {
279
            return;
280
        }
281
282
        $values = $this->getIdentifierValues($document);
283
284
        return $values[0];
285
    }
286
287
    /**
288
     * Currently only the leading slash is removed.
289
     *
290
     * @param object $document
291
     *
292
     * @return string|null
293
     */
294
    public function getUrlsafeIdentifier($document)
295
    {
296
        $id = $this->getNormalizedIdentifier($document);
297
        if (null !== $id) {
298
            return substr($id, 1);
299
        }
300
    }
301
302
    /**
303
     * {@inheritdoc}
304
     */
305
    public function addIdentifiersToQuery($class, ProxyQueryInterface $queryProxy, array $idx): void
306
    {
307
        /* @var $queryProxy ProxyQuery */
308
        $qb = $queryProxy->getQueryBuilder();
309
310
        $orX = $qb->andWhere()->orX();
311
312
        foreach ($idx as $id) {
313
            $path = $this->getBackendId($id);
314
            $orX->same($path, $queryProxy->getAlias());
315
        }
316
    }
317
318
    /**
319
     * Add leading slash to construct valid phpcr document id.
320
     *
321
     * The phpcr-odm QueryBuilder uses absolute paths and expects id´s to start with a forward slash
322
     * because SonataAdmin uses object id´s for constructing URL´s it has to use id´s without the
323
     * leading slash.
324
     *
325
     * @param string $id
326
     *
327
     * @return string
328
     */
329
    public function getBackendId($id)
330
    {
331
        return '/' === substr($id, 0, 1) ? $id : '/'.$id;
332
    }
333
334
    /**
335
     * {@inheritdoc}
336
     *
337
     * @throws ModelManagerException if anything goes wrong during query execution
338
     */
339
    public function batchDelete($class, ProxyQueryInterface $queryProxy): void
340
    {
341
        try {
342
            $i = 0;
343
            $res = $queryProxy->execute();
344
            foreach ($res as $object) {
345
                $this->dm->remove($object);
346
347
                if (0 === (++$i % 20)) {
348
                    $this->dm->flush();
349
                    $this->dm->clear();
350
                }
351
            }
352
353
            $this->dm->flush();
354
            $this->dm->clear();
355
        } catch (\Exception $e) {
356
            throw new ModelManagerException('', 0, $e);
357
        }
358
    }
359
360
    /**
361
     * {@inheritdoc}
362
     *
363
     * @return object
364
     */
365
    public function getModelInstance($class)
366
    {
367
        return new $class();
368
    }
369
370
    /**
371
     * {@inheritdoc}
372
     */
373
    public function getSortParameters(FieldDescriptionInterface $fieldDescription, DatagridInterface $datagrid)
374
    {
375
        $values = $datagrid->getValues();
376
377
        if ($fieldDescription->getName() === $values['_sort_by']->getName()) {
378
            if ('ASC' === $values['_sort_order']) {
379
                $values['_sort_order'] = 'DESC';
380
            } else {
381
                $values['_sort_order'] = 'ASC';
382
            }
383
384
            $values['_sort_by'] = $fieldDescription->getName();
385
        } else {
386
            $values['_sort_order'] = 'ASC';
387
            $values['_sort_by'] = $fieldDescription->getName();
388
        }
389
390
        return ['filter' => $values];
391
    }
392
393
    /**
394
     * {@inheritdoc}
395
     */
396
    public function getPaginationParameters(DatagridInterface $datagrid, $page)
397
    {
398
        $values = $datagrid->getValues();
399
400
        $values['_sort_by'] = $values['_sort_by']->getName();
401
        $values['_page'] = $page;
402
403
        return ['filter' => $values];
404
    }
405
406
    /**
407
     * {@inheritdoc}
408
     */
409
    public function getDefaultSortValues($class)
410
    {
411
        return [
412
            '_sort_order' => 'ASC',
413
            '_sort_by' => $this->getModelIdentifier($class),
414
            '_page' => 1,
415
        ];
416
    }
417
418
    /**
419
     * {@inheritdoc}
420
     *
421
     * @return object
422
     */
423
    public function modelTransform($class, $instance)
424
    {
425
        return $instance;
426
    }
427
428
    /**
429
     * {@inheritdoc}
430
     *
431
     * @throws NoSuchPropertyException if the class has no magic setter and
432
     *                                 public property for a field in array
433
     *
434
     * @return object
435
     */
436
    public function modelReverseTransform($class, array $array = [])
437
    {
438
        $instance = $this->getModelInstance($class);
439
        $metadata = $this->getMetadata($class);
440
441
        $reflClass = $metadata->reflClass;
442
        foreach ($array as $name => $value) {
443
            $reflection_property = false;
444
            // property or association ?
445
            if (\array_key_exists($name, $metadata->fieldMappings)) {
446
                $property = $metadata->fieldMappings[$name]['fieldName'];
447
                $reflection_property = $metadata->reflFields[$name];
448
            } elseif (\array_key_exists($name, $metadata->associationMappings)) {
0 ignored issues
show
Bug introduced by Sonata CI
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...
449
                $property = $metadata->associationMappings[$name]['fieldName'];
0 ignored issues
show
Bug introduced by Nacho Martín
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...
450
            } else {
451
                $property = $name;
452
            }
453
454
            // TODO: use PropertyAccess https://github.com/sonata-project/SonataDoctrinePhpcrAdminBundle/issues/187
455
            $setter = 'set'.$this->camelize($name);
0 ignored issues
show
Deprecated Code introduced by Sullivan SENECHAL
The method Sonata\DoctrinePHPCRAdmi...odelManager::camelize() has been deprecated.

This method has been deprecated.

Loading history...
456
457
            if ($reflClass->hasMethod($setter)) {
458
                if (!$reflClass->getMethod($setter)->isPublic()) {
459
                    throw new NoSuchPropertyException(sprintf('Method "%s()" is not public in class "%s"', $setter, $reflClass->getName()));
0 ignored issues
show
Bug introduced by Sjoerd Peters
Consider using $reflClass->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
460
                }
461
462
                $instance->$setter($value);
463
            } elseif ($reflClass->hasMethod('__set')) {
464
                // needed to support magic method __set
465
                $instance->$property = $value;
466
            } elseif ($reflClass->hasProperty($property)) {
467
                if (!$reflClass->getProperty($property)->isPublic()) {
468
                    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)));
0 ignored issues
show
Bug introduced by Sjoerd Peters
Consider using $reflClass->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
469
                }
470
471
                $instance->$property = $value;
472
            } elseif ($reflection_property) {
473
                $reflection_property->setValue($instance, $value);
474
            }
475
        }
476
477
        return $instance;
478
    }
479
480
    /**
481
     * {@inheritdoc}
482
     */
483
    public function getModelCollectionInstance($class)
484
    {
485
        return new ArrayCollection();
486
    }
487
488
    /**
489
     * {@inheritdoc}
490
     */
491
    public function collectionClear(&$collection)
492
    {
493
        return $collection->clear();
0 ignored issues
show
Bug introduced by Nacho Martín
The method clear cannot be called on $collection (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
494
    }
495
496
    /**
497
     * {@inheritdoc}
498
     */
499
    public function collectionHasElement(&$collection, &$element)
500
    {
501
        return $collection->contains($element);
0 ignored issues
show
Bug introduced by Nacho Martín
The method contains cannot be called on $collection (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
502
    }
503
504
    /**
505
     * {@inheritdoc}
506
     */
507
    public function collectionAddElement(&$collection, &$element)
508
    {
509
        return $collection->add($element);
0 ignored issues
show
Bug introduced by Nacho Martín
The method add cannot be called on $collection (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
510
    }
511
512
    /**
513
     * {@inheritdoc}
514
     */
515
    public function collectionRemoveElement(&$collection, &$element)
516
    {
517
        return $collection->removeElement($element);
0 ignored issues
show
Bug introduced by Nacho Martín
The method removeElement cannot be called on $collection (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
518
    }
519
520
    /**
521
     * {@inheritdoc}
522
     */
523
    public function getDataSourceIterator(DatagridInterface $datagrid, array $fields, $firstResult = null, $maxResult = null): void
524
    {
525
        throw new \RuntimeException('Datasourceiterator not implemented.');
526
    }
527
528
    /**
529
     * {@inheritdoc}
530
     *
531
     * Not really implemented.
532
     */
533
    public function getExportFields($class)
534
    {
535
        return [];
536
    }
537
538
    /**
539
     * Method taken from PropertyPath.
540
     *
541
     * NEXT_MAJOR: remove when doing https://github.com/sonata-project/SonataDoctrinePhpcrAdminBundle/issues/187
542
     *
543
     * @param string $property
544
     *
545
     * @return string
546
     *
547
     * @deprecated
548
     */
549
    protected function camelize($property)
550
    {
551
        return preg_replace(['/(^|_)+(.)/e', '/\.(.)/e'], ["strtoupper('\\2')", "'_'.strtoupper('\\1')"], $property);
552
    }
553
}
554