Completed
Pull Request — 3.x (#1078)
by Vincent
17:41
created

ModelManager   F

Complexity

Total Complexity 99

Size/Duplication

Total Lines 654
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 23

Importance

Changes 0
Metric Value
wmc 99
lcom 1
cbo 23
dl 0
loc 654
rs 1.946
c 0
b 0
f 0

41 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A getMetadata() 0 4 1
A getParentMetadataForProperty() 0 25 3
A hasMetadata() 0 4 1
B getNewFieldDescriptionInstance() 0 31 6
A create() 0 20 3
A update() 0 20 3
A delete() 0 20 3
A getLockVersion() 0 10 2
A lock() 0 15 3
A find() 0 15 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 38 7
A getIdentifierFieldNames() 0 4 1
B getNormalizedIdentifier() 0 46 5
A getUrlSafeIdentifier() 0 14 2
A addIdentifiersToQuery() 0 22 3
A batchDelete() 0 23 4
A getDataSourceIterator() 0 23 3
A getExportFields() 0 6 1
A getModelInstance() 0 15 5
A getSortParameters() 0 18 4
A getPaginationParameters() 0 11 3
A getDefaultSortValues() 0 9 1
A getDefaultPerPageOptions() 0 4 1
A modelTransform() 0 4 1
A modelReverseTransform() 0 20 4
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 camelize() 0 9 1
A isFieldAlreadySorted() 0 11 4
A getValueFromType() 0 20 5

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\DoctrineORMAdminBundle\Model;
15
16
use Doctrine\Common\Util\ClassUtils;
17
use Doctrine\DBAL\DBALException;
18
use Doctrine\DBAL\LockMode;
19
use Doctrine\DBAL\Platforms\AbstractPlatform;
20
use Doctrine\DBAL\Types\Type;
21
use Doctrine\ORM\EntityManager;
22
use Doctrine\ORM\Mapping\ClassMetadata;
23
use Doctrine\ORM\OptimisticLockException;
24
use Doctrine\ORM\Query;
25
use Doctrine\ORM\QueryBuilder;
26
use Doctrine\ORM\UnitOfWork;
27
use Doctrine\Persistence\ManagerRegistry;
28
use Sonata\AdminBundle\Admin\FieldDescriptionInterface;
29
use Sonata\AdminBundle\Datagrid\DatagridInterface;
30
use Sonata\AdminBundle\Datagrid\ProxyQueryInterface;
31
use Sonata\AdminBundle\Exception\LockException;
32
use Sonata\AdminBundle\Exception\ModelManagerException;
33
use Sonata\AdminBundle\Model\LockInterface;
34
use Sonata\AdminBundle\Model\ModelManagerInterface;
35
use Sonata\DoctrineORMAdminBundle\Admin\FieldDescription;
36
use Sonata\DoctrineORMAdminBundle\Datagrid\OrderByToSelectWalker;
37
use Sonata\DoctrineORMAdminBundle\Datagrid\ProxyQuery;
38
use Sonata\Exporter\Source\DoctrineORMQuerySourceIterator;
39
use Symfony\Component\Form\Exception\PropertyAccessDeniedException;
40
use Symfony\Component\PropertyAccess\PropertyAccess;
41
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
42
43
class ModelManager implements ModelManagerInterface, LockInterface
44
{
45
    public const ID_SEPARATOR = '~';
46
47
    /**
48
     * @var ManagerRegistry
49
     */
50
    protected $registry;
51
52
    /**
53
     * @var PropertyAccessorInterface
54
     */
55
    protected $propertyAccessor;
56
57
    /**
58
     * @var EntityManager[]
59
     */
60
    protected $cache = [];
61
62
    public function __construct(ManagerRegistry $registry, ?PropertyAccessorInterface $propertyAccessor = null)
63
    {
64
        $this->registry = $registry;
65
        $this->propertyAccessor = $propertyAccessor ?? PropertyAccess::createPropertyAccessor();
66
    }
67
68
    /**
69
     * @param string $class
70
     *
71
     * @return ClassMetadata
72
     */
73
    public function getMetadata($class)
74
    {
75
        return $this->getEntityManager($class)->getMetadataFactory()->getMetadataFor($class);
76
    }
77
78
    /**
79
     * Returns the model's metadata holding the fully qualified property, and the last
80
     * property name.
81
     *
82
     * @param string $baseClass        The base class of the model holding the fully qualified property
83
     * @param string $propertyFullName The name of the fully qualified property (dot ('.') separated
84
     *                                 property string)
85
     *
86
     * @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...
87
     *                \Doctrine\ORM\Mapping\ClassMetadata $parentMetadata,
88
     *                string $lastPropertyName,
89
     *                array $parentAssociationMappings
90
     *                )
91
     */
92
    public function getParentMetadataForProperty($baseClass, $propertyFullName)
93
    {
94
        $nameElements = explode('.', $propertyFullName);
95
        $lastPropertyName = array_pop($nameElements);
96
        $class = $baseClass;
97
        $parentAssociationMappings = [];
98
99
        foreach ($nameElements as $nameElement) {
100
            $metadata = $this->getMetadata($class);
101
102
            if (isset($metadata->associationMappings[$nameElement])) {
103
                $parentAssociationMappings[] = $metadata->associationMappings[$nameElement];
104
                $class = $metadata->getAssociationTargetClass($nameElement);
105
106
                continue;
107
            }
108
109
            break;
110
        }
111
112
        $properties = \array_slice($nameElements, \count($parentAssociationMappings));
113
        $properties[] = $lastPropertyName;
114
115
        return [$this->getMetadata($class), implode('.', $properties), $parentAssociationMappings];
116
    }
117
118
    /**
119
     * @param string $class
120
     *
121
     * @return bool
122
     */
123
    public function hasMetadata($class)
124
    {
125
        return $this->getEntityManager($class)->getMetadataFactory()->hasMetadataFor($class);
126
    }
127
128
    public function getNewFieldDescriptionInstance($class, $name, array $options = [])
129
    {
130
        if (!\is_string($name)) {
131
            throw new \RuntimeException('The name argument must be a string');
132
        }
133
134
        if (!isset($options['route']['name'])) {
135
            $options['route']['name'] = 'edit';
136
        }
137
138
        if (!isset($options['route']['parameters'])) {
139
            $options['route']['parameters'] = [];
140
        }
141
142
        list($metadata, $propertyName, $parentAssociationMappings) = $this->getParentMetadataForProperty($class, $name);
143
144
        $fieldDescription = new FieldDescription();
145
        $fieldDescription->setName($name);
146
        $fieldDescription->setOptions($options);
147
        $fieldDescription->setParentAssociationMappings($parentAssociationMappings);
148
149
        if (isset($metadata->associationMappings[$propertyName])) {
150
            $fieldDescription->setAssociationMapping($metadata->associationMappings[$propertyName]);
151
        }
152
153
        if (isset($metadata->fieldMappings[$propertyName])) {
154
            $fieldDescription->setFieldMapping($metadata->fieldMappings[$propertyName]);
155
        }
156
157
        return $fieldDescription;
158
    }
159
160
    public function create($object)
161
    {
162
        try {
163
            $entityManager = $this->getEntityManager($object);
164
            $entityManager->persist($object);
165
            $entityManager->flush();
166
        } catch (\PDOException $e) {
167
            throw new ModelManagerException(
168
                sprintf('Failed to create object: %s', ClassUtils::getClass($object)),
169
                $e->getCode(),
170
                $e
171
            );
172
        } catch (DBALException $e) {
173
            throw new ModelManagerException(
174
                sprintf('Failed to create object: %s', ClassUtils::getClass($object)),
175
                $e->getCode(),
176
                $e
177
            );
178
        }
179
    }
180
181
    public function update($object)
182
    {
183
        try {
184
            $entityManager = $this->getEntityManager($object);
185
            $entityManager->persist($object);
186
            $entityManager->flush();
187
        } catch (\PDOException $e) {
188
            throw new ModelManagerException(
189
                sprintf('Failed to update object: %s', ClassUtils::getClass($object)),
190
                $e->getCode(),
191
                $e
192
            );
193
        } catch (DBALException $e) {
194
            throw new ModelManagerException(
195
                sprintf('Failed to update object: %s', ClassUtils::getClass($object)),
196
                $e->getCode(),
197
                $e
198
            );
199
        }
200
    }
201
202
    public function delete($object)
203
    {
204
        try {
205
            $entityManager = $this->getEntityManager($object);
206
            $entityManager->remove($object);
207
            $entityManager->flush();
208
        } catch (\PDOException $e) {
209
            throw new ModelManagerException(
210
                sprintf('Failed to delete object: %s', ClassUtils::getClass($object)),
211
                $e->getCode(),
212
                $e
213
            );
214
        } catch (DBALException $e) {
215
            throw new ModelManagerException(
216
                sprintf('Failed to delete object: %s', ClassUtils::getClass($object)),
217
                $e->getCode(),
218
                $e
219
            );
220
        }
221
    }
222
223
    public function getLockVersion($object)
224
    {
225
        $metadata = $this->getMetadata(ClassUtils::getClass($object));
226
227
        if (!$metadata->isVersioned) {
228
            return null;
229
        }
230
231
        return $metadata->reflFields[$metadata->versionField]->getValue($object);
232
    }
233
234
    public function lock($object, $expectedVersion)
235
    {
236
        $metadata = $this->getMetadata(ClassUtils::getClass($object));
237
238
        if (!$metadata->isVersioned) {
239
            return;
240
        }
241
242
        try {
243
            $entityManager = $this->getEntityManager($object);
244
            $entityManager->lock($object, LockMode::OPTIMISTIC, $expectedVersion);
245
        } catch (OptimisticLockException $e) {
246
            throw new LockException($e->getMessage(), $e->getCode(), $e);
247
        }
248
    }
249
250
    public function find($class, $id)
251
    {
252
        if (null === $id) {
253
            @trigger_error(sprintf(
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
254
                'Passing null as argument 1 for %s() is deprecated since sonata-project/doctrine-orm-admin-bundle 3.20 and will be not allowed in version 4.0.',
255
                __METHOD__
256
            ), E_USER_DEPRECATED);
257
258
            return null;
259
        }
260
261
        $values = array_combine($this->getIdentifierFieldNames($class), explode(self::ID_SEPARATOR, (string) $id));
262
263
        return $this->getEntityManager($class)->getRepository($class)->find($values);
264
    }
265
266
    public function findBy($class, array $criteria = [])
267
    {
268
        return $this->getEntityManager($class)->getRepository($class)->findBy($criteria);
269
    }
270
271
    public function findOneBy($class, array $criteria = [])
272
    {
273
        return $this->getEntityManager($class)->getRepository($class)->findOneBy($criteria);
274
    }
275
276
    /**
277
     * @param string|object $class
278
     *
279
     * @return EntityManager
280
     */
281
    public function getEntityManager($class)
282
    {
283
        if (\is_object($class)) {
284
            $class = \get_class($class);
285
        }
286
287
        if (!isset($this->cache[$class])) {
288
            $em = $this->registry->getManagerForClass($class);
289
290
            if (!$em) {
291
                throw new \RuntimeException(sprintf('No entity manager defined for class %s', $class));
292
            }
293
294
            $this->cache[$class] = $em;
295
        }
296
297
        return $this->cache[$class];
298
    }
299
300
    public function getParentFieldDescription($parentAssociationMapping, $class)
301
    {
302
        $fieldName = $parentAssociationMapping['fieldName'];
303
304
        $metadata = $this->getMetadata($class);
305
306
        $associatingMapping = $metadata->associationMappings[$parentAssociationMapping];
307
308
        $fieldDescription = $this->getNewFieldDescriptionInstance($class, $fieldName);
309
        $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...
310
        $fieldDescription->setAssociationMapping($associatingMapping);
311
312
        return $fieldDescription;
313
    }
314
315
    public function createQuery($class, $alias = 'o')
316
    {
317
        $repository = $this->getEntityManager($class)->getRepository($class);
318
319
        return new ProxyQuery($repository->createQueryBuilder($alias));
320
    }
321
322
    public function executeQuery($query)
323
    {
324
        if ($query instanceof QueryBuilder) {
325
            return $query->getQuery()->execute();
326
        }
327
328
        return $query->execute();
329
    }
330
331
    /**
332
     * NEXT_MAJOR: Remove this function.
333
     *
334
     * @deprecated since sonata-project/doctrine-orm-admin-bundle 3.18. To be removed in 4.0.
335
     */
336
    public function getModelIdentifier($class)
337
    {
338
        return $this->getMetadata($class)->identifier;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->getMetadata($class)->identifier; (array) is incompatible with the return type declared by the interface Sonata\AdminBundle\Model...ace::getModelIdentifier of type string.

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...
339
    }
340
341
    public function getIdentifierValues($entity)
342
    {
343
        // Fix code has an impact on performance, so disable it ...
344
        //$entityManager = $this->getEntityManager($entity);
345
        //if (!$entityManager->getUnitOfWork()->isInIdentityMap($entity)) {
346
        //    throw new \RuntimeException('Entities passed to the choice field must be managed');
347
        //}
348
349
        $class = ClassUtils::getClass($entity);
350
        $metadata = $this->getMetadata($class);
351
        $platform = $this->getEntityManager($class)->getConnection()->getDatabasePlatform();
352
353
        $identifiers = [];
354
355
        foreach ($metadata->getIdentifierValues($entity) as $name => $value) {
356
            if (!\is_object($value)) {
357
                $identifiers[] = $value;
358
359
                continue;
360
            }
361
362
            $fieldType = $metadata->getTypeOfField($name);
363
            $type = $fieldType && Type::hasType($fieldType) ? Type::getType($fieldType) : null;
364
            if ($type) {
365
                $identifiers[] = $this->getValueFromType($value, $type, $fieldType, $platform);
366
367
                continue;
368
            }
369
370
            $identifierMetadata = $this->getMetadata(ClassUtils::getClass($value));
371
372
            foreach ($identifierMetadata->getIdentifierValues($value) as $value) {
373
                $identifiers[] = $value;
374
            }
375
        }
376
377
        return $identifiers;
378
    }
379
380
    public function getIdentifierFieldNames($class)
381
    {
382
        return $this->getMetadata($class)->getIdentifierFieldNames();
383
    }
384
385
    public function getNormalizedIdentifier($entity)
386
    {
387
        // NEXT_MAJOR: Remove the following 2 checks and declare "object" as type for argument 1.
388
        if (null === $entity) {
389
            @trigger_error(sprintf(
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
390
                'Passing null as argument 1 for %s() is deprecated since sonata-project/doctrine-orm-admin-bundle 3.20 and will be not allowed in version 4.0.',
391
                __METHOD__
392
            ), E_USER_DEPRECATED);
393
394
            return null;
395
        }
396
397
        if (!\is_object($entity)) {
398
            throw new \RuntimeException('Invalid argument, object or null required');
399
        }
400
401
        if (\in_array($this->getEntityManager($entity)->getUnitOfWork()->getEntityState($entity), [
402
            UnitOfWork::STATE_NEW,
403
            UnitOfWork::STATE_REMOVED,
404
        ], true)) {
405
            // NEXT_MAJOR: Uncomment the following exception, remove the deprecation and the return statement inside this conditional block.
406
            // throw new \InvalidArgumentException(sprintf(
407
            //    'Can not get the normalized identifier for %s since it is in state %u.',
408
            //    ClassUtils::getClass($entity),
409
            //    $this->getEntityManager($entity)->getUnitOfWork()->getEntityState($entity)
410
            // ));
411
412
            @trigger_error(sprintf(
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
413
                'Passing an object which is in state %u (new) or %u (removed) as argument 1 for %s() is deprecated since sonata-project/doctrine-orm-admin-bundle 3.20'
414
                .'and will be not allowed in version 4.0.',
415
                UnitOfWork::STATE_NEW,
416
                UnitOfWork::STATE_REMOVED,
417
                __METHOD__
418
            ), E_USER_DEPRECATED);
419
420
            return null;
421
        }
422
423
        $values = $this->getIdentifierValues($entity);
424
425
        if (0 === \count($values)) {
426
            return null;
427
        }
428
429
        return implode(self::ID_SEPARATOR, $values);
430
    }
431
432
    /**
433
     * {@inheritdoc}
434
     *
435
     * The ORM implementation does nothing special but you still should use
436
     * this method when using the id in a URL to allow for future improvements.
437
     */
438
    public function getUrlSafeIdentifier($entity)
439
    {
440
        // NEXT_MAJOR: Remove the following check and declare "object" as type for argument 1.
441
        if (!\is_object($entity)) {
442
            @trigger_error(sprintf(
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
443
                'Passing other type than object for argument 1 for %s() is deprecated since sonata-project/doctrine-orm-admin-bundle 3.20 and will be not allowed in version 4.0.',
444
                __METHOD__
445
            ), E_USER_DEPRECATED);
446
447
            return null;
448
        }
449
450
        return $this->getNormalizedIdentifier($entity);
451
    }
452
453
    public function addIdentifiersToQuery($class, ProxyQueryInterface $queryProxy, array $idx)
454
    {
455
        $fieldNames = $this->getIdentifierFieldNames($class);
456
        $qb = $queryProxy->getQueryBuilder();
457
458
        $prefix = uniqid();
459
        $sqls = [];
460
        foreach ($idx as $pos => $id) {
461
            $ids = explode(self::ID_SEPARATOR, $id);
462
463
            $ands = [];
464
            foreach ($fieldNames as $posName => $name) {
465
                $parameterName = sprintf('field_%s_%s_%d', $prefix, $name, $pos);
466
                $ands[] = sprintf('%s.%s = :%s', current($qb->getRootAliases()), $name, $parameterName);
467
                $qb->setParameter($parameterName, $ids[$posName]);
468
            }
469
470
            $sqls[] = implode(' AND ', $ands);
471
        }
472
473
        $qb->andWhere(sprintf('( %s )', implode(' OR ', $sqls)));
474
    }
475
476
    public function batchDelete($class, ProxyQueryInterface $queryProxy)
477
    {
478
        $queryProxy->select('DISTINCT '.current($queryProxy->getRootAliases()));
479
480
        try {
481
            $entityManager = $this->getEntityManager($class);
482
483
            $i = 0;
484
            foreach ($queryProxy->getQuery()->iterate() as $pos => $object) {
485
                $entityManager->remove($object[0]);
486
487
                if (0 === (++$i % 20)) {
488
                    $entityManager->flush();
489
                    $entityManager->clear();
490
                }
491
            }
492
493
            $entityManager->flush();
494
            $entityManager->clear();
495
        } catch (\PDOException | DBALException $e) {
496
            throw new ModelManagerException('', 0, $e);
497
        }
498
    }
499
500
    public function getDataSourceIterator(DatagridInterface $datagrid, array $fields, $firstResult = null, $maxResult = null)
501
    {
502
        $datagrid->buildPager();
503
        $query = $datagrid->getQuery();
504
505
        $query->select('DISTINCT '.current($query->getRootAliases()));
506
        $query->setFirstResult($firstResult);
507
        $query->setMaxResults($maxResult);
508
509
        if ($query instanceof ProxyQueryInterface) {
510
            $sortBy = $query->getSortBy();
511
512
            if (!empty($sortBy)) {
513
                $query->addOrderBy($sortBy, $query->getSortOrder());
514
                $query = $query->getQuery();
515
                $query->setHint(Query::HINT_CUSTOM_TREE_WALKERS, [OrderByToSelectWalker::class]);
516
            } else {
517
                $query = $query->getQuery();
518
            }
519
        }
520
521
        return new DoctrineORMQuerySourceIterator($query, $fields);
522
    }
523
524
    public function getExportFields($class)
525
    {
526
        $metadata = $this->getEntityManager($class)->getClassMetadata($class);
527
528
        return $metadata->getFieldNames();
529
    }
530
531
    public function getModelInstance($class)
532
    {
533
        $r = new \ReflectionClass($class);
534
        if ($r->isAbstract()) {
535
            throw new \RuntimeException(sprintf('Cannot initialize abstract class: %s', $class));
536
        }
537
538
        $constructor = $r->getConstructor();
539
540
        if (null !== $constructor && (!$constructor->isPublic() || $constructor->getNumberOfRequiredParameters() > 0)) {
541
            return $r->newInstanceWithoutConstructor();
542
        }
543
544
        return new $class();
545
    }
546
547
    public function getSortParameters(FieldDescriptionInterface $fieldDescription, DatagridInterface $datagrid)
548
    {
549
        $values = $datagrid->getValues();
550
551
        if ($this->isFieldAlreadySorted($fieldDescription, $datagrid)) {
552
            if ('ASC' === $values['_sort_order']) {
553
                $values['_sort_order'] = 'DESC';
554
            } else {
555
                $values['_sort_order'] = 'ASC';
556
            }
557
        } else {
558
            $values['_sort_order'] = 'ASC';
559
        }
560
561
        $values['_sort_by'] = \is_string($fieldDescription->getOption('sortable')) ? $fieldDescription->getOption('sortable') : $fieldDescription->getName();
562
563
        return ['filter' => $values];
564
    }
565
566
    public function getPaginationParameters(DatagridInterface $datagrid, $page)
567
    {
568
        $values = $datagrid->getValues();
569
570
        if (isset($values['_sort_by']) && $values['_sort_by'] instanceof FieldDescriptionInterface) {
571
            $values['_sort_by'] = $values['_sort_by']->getName();
572
        }
573
        $values['_page'] = $page;
574
575
        return ['filter' => $values];
576
    }
577
578
    public function getDefaultSortValues($class)
579
    {
580
        return [
581
            '_sort_order' => 'ASC',
582
            '_sort_by' => implode(',', $this->getModelIdentifier($class)),
0 ignored issues
show
Deprecated Code introduced by
The method Sonata\DoctrineORMAdminB...r::getModelIdentifier() has been deprecated with message: since sonata-project/doctrine-orm-admin-bundle 3.18. To be removed in 4.0.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
583
            '_page' => 1,
584
            '_per_page' => 25,
585
        ];
586
    }
587
588
    public function getDefaultPerPageOptions(string $class): array
0 ignored issues
show
Unused Code introduced by
The parameter $class is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
589
    {
590
        return [10, 25, 50, 100, 250];
591
    }
592
593
    public function modelTransform($class, $instance)
594
    {
595
        return $instance;
596
    }
597
598
    public function modelReverseTransform($class, array $array = [])
599
    {
600
        $instance = $this->getModelInstance($class);
601
        $metadata = $this->getMetadata($class);
602
603
        foreach ($array as $name => $value) {
604
            // property or association ?
605
            if (\array_key_exists($name, $metadata->fieldMappings)) {
606
                $property = $metadata->fieldMappings[$name]['fieldName'];
607
            } elseif (\array_key_exists($name, $metadata->associationMappings)) {
608
                $property = $metadata->associationMappings[$name]['fieldName'];
609
            } else {
610
                $property = $name;
611
            }
612
613
            $this->propertyAccessor->setValue($instance, $property, $value);
614
        }
615
616
        return $instance;
0 ignored issues
show
Bug Compatibility introduced by
The expression return $instance; of type object|array is incompatible with the return type declared by the interface Sonata\AdminBundle\Model...::modelReverseTransform of type object as it can also be of type array which is not included in this return type.
Loading history...
617
    }
618
619
    public function getModelCollectionInstance($class)
620
    {
621
        return new \Doctrine\Common\Collections\ArrayCollection();
622
    }
623
624
    public function collectionClear(&$collection)
625
    {
626
        return $collection->clear();
0 ignored issues
show
Bug introduced by
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...
627
    }
628
629
    public function collectionHasElement(&$collection, &$element)
630
    {
631
        return $collection->contains($element);
0 ignored issues
show
Bug introduced by
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...
632
    }
633
634
    public function collectionAddElement(&$collection, &$element)
635
    {
636
        return $collection->add($element);
0 ignored issues
show
Bug introduced by
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...
637
    }
638
639
    public function collectionRemoveElement(&$collection, &$element)
640
    {
641
        return $collection->removeElement($element);
0 ignored issues
show
Bug introduced by
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...
642
    }
643
644
    /**
645
     * NEXT_MAJOR: Remove this method
646
     *
647
     * @param string $property
648
     *
649
     * @return mixed
650
     */
651
    protected function camelize($property)
652
    {
653
        @trigger_error(sprintf(
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
654
            'Method %s() is deprecated since sonata-project/doctrine-orm-admin-bundle 3.x and will be removed in version 4.0.',
655
            __METHOD__
656
        ), E_USER_DEPRECATED);
657
658
        return str_replace(' ', '', ucwords(str_replace('_', ' ', $property)));
659
    }
660
661
    private function isFieldAlreadySorted(FieldDescriptionInterface $fieldDescription, DatagridInterface $datagrid): bool
662
    {
663
        $values = $datagrid->getValues();
664
665
        if (!isset($values['_sort_by']) || !$values['_sort_by'] instanceof FieldDescriptionInterface) {
666
            return false;
667
        }
668
669
        return $values['_sort_by']->getName() === $fieldDescription->getName()
670
            || $values['_sort_by']->getName() === $fieldDescription->getOption('sortable');
671
    }
672
673
    /**
674
     * @param mixed $value
675
     */
676
    private function getValueFromType($value, Type $type, string $fieldType, AbstractPlatform $platform): string
677
    {
678
        if ($platform->hasDoctrineTypeMappingFor($fieldType) &&
679
            'binary' === $platform->getDoctrineTypeMapping($fieldType)
680
        ) {
681
            return (string) $type->convertToPHPValue($value, $platform);
682
        }
683
684
        // some libraries may have `toString()` implementation
685
        if (\is_callable([$value, 'toString'])) {
686
            return $value->toString();
687
        }
688
689
        // final fallback to magic `__toString()` which may throw an exception in 7.4
690
        if (method_exists($value, '__toString')) {
691
            return $value->__toString();
692
        }
693
694
        return (string) $type->convertToDatabaseValue($value, $platform);
695
    }
696
}
697