Completed
Pull Request — 3.x (#1032)
by Javier
01:35
created

ModelManager   F

Complexity

Total Complexity 104

Size/Duplication

Total Lines 634
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 21

Importance

Changes 0
Metric Value
wmc 104
lcom 1
cbo 21
dl 0
loc 634
rs 1.966
c 0
b 0
f 0

40 Methods

Rating   Name   Duplication   Size   Complexity  
A getIdentifierFieldNames() 0 4 1
A getNormalizedIdentifier() 0 25 5
A addIdentifiersToQuery() 0 22 3
A batchDelete() 0 25 5
A getDataSourceIterator() 0 23 3
A getExportFields() 0 6 1
A getModelInstance() 0 15 5
A __construct() 0 4 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 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 isFieldAlreadySorted() 0 11 4
A getValueFromType() 0 20 5
A getModelIdentifier() 0 4 1
B getIdentifierValues() 0 38 7
A getSortParameters() 0 18 4
A getPaginationParameters() 0 11 3
A getDefaultSortValues() 0 9 1
A modelTransform() 0 4 1
B modelReverseTransform() 0 51 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 camelize() 0 4 1
A getUrlSafeIdentifier() 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\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
41
class ModelManager implements ModelManagerInterface, LockInterface
42
{
43
    public const ID_SEPARATOR = '~';
44
    /**
45
     * @var ManagerRegistry
46
     */
47
    protected $registry;
48
49
    /**
50
     * @var EntityManager[]
51
     */
52
    protected $cache = [];
53
54
    public function __construct(ManagerRegistry $registry)
55
    {
56
        $this->registry = $registry;
57
    }
58
59
    /**
60
     * @param string $class
61
     *
62
     * @return ClassMetadata
63
     */
64
    public function getMetadata($class)
65
    {
66
        return $this->getEntityManager($class)->getMetadataFactory()->getMetadataFor($class);
67
    }
68
69
    /**
70
     * Returns the model's metadata holding the fully qualified property, and the last
71
     * property name.
72
     *
73
     * @param string $baseClass        The base class of the model holding the fully qualified property
74
     * @param string $propertyFullName The name of the fully qualified property (dot ('.') separated
75
     *                                 property string)
76
     *
77
     * @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...
78
     *                \Doctrine\ORM\Mapping\ClassMetadata $parentMetadata,
79
     *                string $lastPropertyName,
80
     *                array $parentAssociationMappings
81
     *                )
82
     */
83
    public function getParentMetadataForProperty($baseClass, $propertyFullName)
84
    {
85
        $nameElements = explode('.', $propertyFullName);
86
        $lastPropertyName = array_pop($nameElements);
87
        $class = $baseClass;
88
        $parentAssociationMappings = [];
89
90
        foreach ($nameElements as $nameElement) {
91
            $metadata = $this->getMetadata($class);
92
93
            if (isset($metadata->associationMappings[$nameElement])) {
94
                $parentAssociationMappings[] = $metadata->associationMappings[$nameElement];
95
                $class = $metadata->getAssociationTargetClass($nameElement);
96
97
                continue;
98
            }
99
100
            break;
101
        }
102
103
        $properties = \array_slice($nameElements, \count($parentAssociationMappings));
104
        $properties[] = $lastPropertyName;
105
106
        return [$this->getMetadata($class), implode('.', $properties), $parentAssociationMappings];
107
    }
108
109
    /**
110
     * @param string $class
111
     *
112
     * @return bool
113
     */
114
    public function hasMetadata($class)
115
    {
116
        return $this->getEntityManager($class)->getMetadataFactory()->hasMetadataFor($class);
117
    }
118
119
    public function getNewFieldDescriptionInstance($class, $name, array $options = [])
120
    {
121
        if (!\is_string($name)) {
122
            throw new \RuntimeException('The name argument must be a string');
123
        }
124
125
        if (!isset($options['route']['name'])) {
126
            $options['route']['name'] = 'edit';
127
        }
128
129
        if (!isset($options['route']['parameters'])) {
130
            $options['route']['parameters'] = [];
131
        }
132
133
        list($metadata, $propertyName, $parentAssociationMappings) = $this->getParentMetadataForProperty($class, $name);
134
135
        $fieldDescription = new FieldDescription();
136
        $fieldDescription->setName($name);
137
        $fieldDescription->setOptions($options);
138
        $fieldDescription->setParentAssociationMappings($parentAssociationMappings);
139
140
        if (isset($metadata->associationMappings[$propertyName])) {
141
            $fieldDescription->setAssociationMapping($metadata->associationMappings[$propertyName]);
142
        }
143
144
        if (isset($metadata->fieldMappings[$propertyName])) {
145
            $fieldDescription->setFieldMapping($metadata->fieldMappings[$propertyName]);
146
        }
147
148
        return $fieldDescription;
149
    }
150
151
    public function create($object)
152
    {
153
        try {
154
            $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...
155
            $entityManager->persist($object);
156
            $entityManager->flush();
157
        } catch (\PDOException $e) {
158
            throw new ModelManagerException(
159
                sprintf('Failed to create object: %s', ClassUtils::getClass($object)),
160
                $e->getCode(),
161
                $e
162
            );
163
        } catch (DBALException $e) {
164
            throw new ModelManagerException(
165
                sprintf('Failed to create object: %s', ClassUtils::getClass($object)),
166
                $e->getCode(),
167
                $e
168
            );
169
        }
170
    }
171
172
    public function update($object)
173
    {
174
        try {
175
            $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...
176
            $entityManager->persist($object);
177
            $entityManager->flush();
178
        } catch (\PDOException $e) {
179
            throw new ModelManagerException(
180
                sprintf('Failed to update object: %s', ClassUtils::getClass($object)),
181
                $e->getCode(),
182
                $e
183
            );
184
        } catch (DBALException $e) {
185
            throw new ModelManagerException(
186
                sprintf('Failed to update object: %s', ClassUtils::getClass($object)),
187
                $e->getCode(),
188
                $e
189
            );
190
        }
191
    }
192
193
    public function delete($object)
194
    {
195
        try {
196
            $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...
197
            $entityManager->remove($object);
198
            $entityManager->flush();
199
        } catch (\PDOException $e) {
200
            throw new ModelManagerException(
201
                sprintf('Failed to delete object: %s', ClassUtils::getClass($object)),
202
                $e->getCode(),
203
                $e
204
            );
205
        } catch (DBALException $e) {
206
            throw new ModelManagerException(
207
                sprintf('Failed to delete object: %s', ClassUtils::getClass($object)),
208
                $e->getCode(),
209
                $e
210
            );
211
        }
212
    }
213
214
    public function getLockVersion($object)
215
    {
216
        $metadata = $this->getMetadata(ClassUtils::getClass($object));
217
218
        if (!$metadata->isVersioned) {
219
            return null;
220
        }
221
222
        return $metadata->reflFields[$metadata->versionField]->getValue($object);
223
    }
224
225
    public function lock($object, $expectedVersion)
226
    {
227
        $metadata = $this->getMetadata(ClassUtils::getClass($object));
228
229
        if (!$metadata->isVersioned) {
230
            return;
231
        }
232
233
        try {
234
            $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...
235
            $entityManager->lock($object, LockMode::OPTIMISTIC, $expectedVersion);
236
        } catch (OptimisticLockException $e) {
237
            throw new LockException($e->getMessage(), $e->getCode(), $e);
238
        }
239
    }
240
241
    public function find($class, $id)
242
    {
243
        if (!isset($id)) {
244
            return null;
245
        }
246
247
        $values = array_combine($this->getIdentifierFieldNames($class), explode(self::ID_SEPARATOR, (string) $id));
248
249
        return $this->getEntityManager($class)->getRepository($class)->find($values);
250
    }
251
252
    public function findBy($class, array $criteria = [])
253
    {
254
        return $this->getEntityManager($class)->getRepository($class)->findBy($criteria);
255
    }
256
257
    public function findOneBy($class, array $criteria = [])
258
    {
259
        return $this->getEntityManager($class)->getRepository($class)->findOneBy($criteria);
260
    }
261
262
    /**
263
     * @param string $class
264
     *
265
     * @return EntityManager
266
     */
267
    public function getEntityManager($class)
268
    {
269
        if (\is_object($class)) {
270
            $class = \get_class($class);
271
        }
272
273
        if (!isset($this->cache[$class])) {
274
            $em = $this->registry->getManagerForClass($class);
275
276
            if (!$em) {
277
                throw new \RuntimeException(sprintf('No entity manager defined for class %s', $class));
278
            }
279
280
            $this->cache[$class] = $em;
281
        }
282
283
        return $this->cache[$class];
284
    }
285
286
    public function getParentFieldDescription($parentAssociationMapping, $class)
287
    {
288
        $fieldName = $parentAssociationMapping['fieldName'];
289
290
        $metadata = $this->getMetadata($class);
291
292
        $associatingMapping = $metadata->associationMappings[$parentAssociationMapping];
293
294
        $fieldDescription = $this->getNewFieldDescriptionInstance($class, $fieldName);
295
        $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...
296
        $fieldDescription->setAssociationMapping($associatingMapping);
297
298
        return $fieldDescription;
299
    }
300
301
    public function createQuery($class, $alias = 'o')
302
    {
303
        $repository = $this->getEntityManager($class)->getRepository($class);
304
305
        return new ProxyQuery($repository->createQueryBuilder($alias));
306
    }
307
308
    public function executeQuery($query)
309
    {
310
        if ($query instanceof QueryBuilder) {
311
            return $query->getQuery()->execute();
312
        }
313
314
        return $query->execute();
315
    }
316
317
    /**
318
     * NEXT_MAJOR: Remove this function.
319
     *
320
     * @deprecated since sonata-project/doctrine-orm-admin-bundle 3.x. To be removed in 4.0.
321
     */
322
    public function getModelIdentifier($class)
323
    {
324
        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...
325
    }
326
327
    public function getIdentifierValues($entity)
328
    {
329
        // Fix code has an impact on performance, so disable it ...
330
        //$entityManager = $this->getEntityManager($entity);
331
        //if (!$entityManager->getUnitOfWork()->isInIdentityMap($entity)) {
332
        //    throw new \RuntimeException('Entities passed to the choice field must be managed');
333
        //}
334
335
        $class = ClassUtils::getClass($entity);
336
        $metadata = $this->getMetadata($class);
337
        $platform = $this->getEntityManager($class)->getConnection()->getDatabasePlatform();
338
339
        $identifiers = [];
340
341
        foreach ($metadata->getIdentifierValues($entity) as $name => $value) {
342
            if (!\is_object($value)) {
343
                $identifiers[] = $value;
344
345
                continue;
346
            }
347
348
            $fieldType = $metadata->getTypeOfField($name);
349
            $type = $fieldType && Type::hasType($fieldType) ? Type::getType($fieldType) : null;
350
            if ($type) {
351
                $identifiers[] = $this->getValueFromType($value, $type, $fieldType, $platform);
352
353
                continue;
354
            }
355
356
            $identifierMetadata = $this->getMetadata(ClassUtils::getClass($value));
357
358
            foreach ($identifierMetadata->getIdentifierValues($value) as $value) {
359
                $identifiers[] = $value;
360
            }
361
        }
362
363
        return $identifiers;
364
    }
365
366
    public function getIdentifierFieldNames($class)
367
    {
368
        return $this->getMetadata($class)->getIdentifierFieldNames();
369
    }
370
371
    public function getNormalizedIdentifier($entity)
372
    {
373
        if (null === $entity) {
374
            return null;
375
        }
376
377
        if (!\is_object($entity)) {
378
            throw new \RuntimeException('Invalid argument, object or null required');
379
        }
380
381
        if (\in_array($this->getEntityManager($entity)->getUnitOfWork()->getEntityState($entity), [
0 ignored issues
show
Documentation introduced by
$entity 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...
382
            UnitOfWork::STATE_NEW,
383
            UnitOfWork::STATE_REMOVED,
384
        ], true)) {
385
            return null;
386
        }
387
388
        $values = $this->getIdentifierValues($entity);
389
390
        if (0 === \count($values)) {
391
            return null;
392
        }
393
394
        return implode(self::ID_SEPARATOR, $values);
395
    }
396
397
    /**
398
     * {@inheritdoc}
399
     *
400
     * The ORM implementation does nothing special but you still should use
401
     * this method when using the id in a URL to allow for future improvements.
402
     */
403
    public function getUrlSafeIdentifier($entity)
404
    {
405
        return $this->getNormalizedIdentifier($entity);
406
    }
407
408
    public function addIdentifiersToQuery($class, ProxyQueryInterface $queryProxy, array $idx)
409
    {
410
        $fieldNames = $this->getIdentifierFieldNames($class);
411
        $qb = $queryProxy->getQueryBuilder();
412
413
        $prefix = uniqid();
414
        $sqls = [];
415
        foreach ($idx as $pos => $id) {
416
            $ids = explode(self::ID_SEPARATOR, $id);
417
418
            $ands = [];
419
            foreach ($fieldNames as $posName => $name) {
420
                $parameterName = sprintf('field_%s_%s_%d', $prefix, $name, $pos);
421
                $ands[] = sprintf('%s.%s = :%s', current($qb->getRootAliases()), $name, $parameterName);
422
                $qb->setParameter($parameterName, $ids[$posName]);
423
            }
424
425
            $sqls[] = implode(' AND ', $ands);
426
        }
427
428
        $qb->andWhere(sprintf('( %s )', implode(' OR ', $sqls)));
429
    }
430
431
    public function batchDelete($class, ProxyQueryInterface $queryProxy)
432
    {
433
        $queryProxy->select('DISTINCT '.current($queryProxy->getRootAliases()));
434
435
        try {
436
            $entityManager = $this->getEntityManager($class);
437
438
            $i = 0;
439
            foreach ($queryProxy->getQuery()->iterate() as $pos => $object) {
440
                $entityManager->remove($object[0]);
441
442
                if (0 === (++$i % 20)) {
443
                    $entityManager->flush();
444
                    $entityManager->clear();
445
                }
446
            }
447
448
            $entityManager->flush();
449
            $entityManager->clear();
450
        } catch (\PDOException $e) {
451
            throw new ModelManagerException('', 0, $e);
452
        } catch (DBALException $e) {
453
            throw new ModelManagerException('', 0, $e);
454
        }
455
    }
456
457
    public function getDataSourceIterator(DatagridInterface $datagrid, array $fields, $firstResult = null, $maxResult = null)
458
    {
459
        $datagrid->buildPager();
460
        $query = $datagrid->getQuery();
461
462
        $query->select('DISTINCT '.current($query->getRootAliases()));
463
        $query->setFirstResult($firstResult);
464
        $query->setMaxResults($maxResult);
465
466
        if ($query instanceof ProxyQueryInterface) {
467
            $sortBy = $query->getSortBy();
468
469
            if (!empty($sortBy)) {
470
                $query->addOrderBy($sortBy, $query->getSortOrder());
471
                $query = $query->getQuery();
472
                $query->setHint(Query::HINT_CUSTOM_TREE_WALKERS, [OrderByToSelectWalker::class]);
473
            } else {
474
                $query = $query->getQuery();
475
            }
476
        }
477
478
        return new DoctrineORMQuerySourceIterator($query, $fields);
479
    }
480
481
    public function getExportFields($class)
482
    {
483
        $metadata = $this->getEntityManager($class)->getClassMetadata($class);
484
485
        return $metadata->getFieldNames();
486
    }
487
488
    public function getModelInstance($class)
489
    {
490
        $r = new \ReflectionClass($class);
491
        if ($r->isAbstract()) {
492
            throw new \RuntimeException(sprintf('Cannot initialize abstract class: %s', $class));
493
        }
494
495
        $constructor = $r->getConstructor();
496
497
        if (null !== $constructor && (!$constructor->isPublic() || $constructor->getNumberOfRequiredParameters() > 0)) {
498
            return $r->newInstanceWithoutConstructor();
499
        }
500
501
        return new $class();
502
    }
503
504
    public function getSortParameters(FieldDescriptionInterface $fieldDescription, DatagridInterface $datagrid)
505
    {
506
        $values = $datagrid->getValues();
507
508
        if ($this->isFieldAlreadySorted($fieldDescription, $datagrid)) {
509
            if ('ASC' === $values['_sort_order']) {
510
                $values['_sort_order'] = 'DESC';
511
            } else {
512
                $values['_sort_order'] = 'ASC';
513
            }
514
        } else {
515
            $values['_sort_order'] = 'ASC';
516
        }
517
518
        $values['_sort_by'] = \is_string($fieldDescription->getOption('sortable')) ? $fieldDescription->getOption('sortable') : $fieldDescription->getName();
519
520
        return ['filter' => $values];
521
    }
522
523
    public function getPaginationParameters(DatagridInterface $datagrid, $page)
524
    {
525
        $values = $datagrid->getValues();
526
527
        if (isset($values['_sort_by']) && $values['_sort_by'] instanceof FieldDescriptionInterface) {
528
            $values['_sort_by'] = $values['_sort_by']->getName();
529
        }
530
        $values['_page'] = $page;
531
532
        return ['filter' => $values];
533
    }
534
535
    public function getDefaultSortValues($class)
536
    {
537
        return [
538
            '_sort_order' => 'ASC',
539
            '_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.x. 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...
540
            '_page' => 1,
541
            '_per_page' => 25,
542
        ];
543
    }
544
545
    public function modelTransform($class, $instance)
546
    {
547
        return $instance;
548
    }
549
550
    public function modelReverseTransform($class, array $array = [])
551
    {
552
        $instance = $this->getModelInstance($class);
553
        $metadata = $this->getMetadata($class);
554
555
        $reflClass = $metadata->reflClass;
556
        foreach ($array as $name => $value) {
557
            $reflection_property = false;
558
            // property or association ?
559
            if (\array_key_exists($name, $metadata->fieldMappings)) {
560
                $property = $metadata->fieldMappings[$name]['fieldName'];
561
                $reflection_property = $metadata->reflFields[$name];
562
            } elseif (\array_key_exists($name, $metadata->associationMappings)) {
563
                $property = $metadata->associationMappings[$name]['fieldName'];
564
            } else {
565
                $property = $name;
566
            }
567
568
            $setter = 'set'.$this->camelize($name);
569
570
            if ($reflClass->hasMethod($setter)) {
571
                if (!$reflClass->getMethod($setter)->isPublic()) {
572
                    throw new PropertyAccessDeniedException(sprintf(
573
                        'Method "%s()" is not public in class "%s"',
574
                        $setter,
575
                        $reflClass->getName()
0 ignored issues
show
Bug introduced by
Consider using $reflClass->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
576
                    ));
577
                }
578
579
                $instance->$setter($value);
580
            } elseif ($reflClass->hasMethod('__set')) {
581
                // needed to support magic method __set
582
                $instance->$property = $value;
583
            } elseif ($reflClass->hasProperty($property)) {
584
                if (!$reflClass->getProperty($property)->isPublic()) {
585
                    throw new PropertyAccessDeniedException(sprintf(
586
                        'Property "%s" is not public in class "%s". Maybe you should create the method "set%s()"?',
587
                        $property,
588
                        $reflClass->getName(),
0 ignored issues
show
Bug introduced by
Consider using $reflClass->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
589
                        ucfirst($property)
590
                    ));
591
                }
592
593
                $instance->$property = $value;
594
            } elseif ($reflection_property) {
595
                $reflection_property->setValue($instance, $value);
596
            }
597
        }
598
599
        return $instance;
600
    }
601
602
    public function getModelCollectionInstance($class)
603
    {
604
        return new \Doctrine\Common\Collections\ArrayCollection();
605
    }
606
607
    public function collectionClear(&$collection)
608
    {
609
        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...
610
    }
611
612
    public function collectionHasElement(&$collection, &$element)
613
    {
614
        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...
615
    }
616
617
    public function collectionAddElement(&$collection, &$element)
618
    {
619
        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...
620
    }
621
622
    public function collectionRemoveElement(&$collection, &$element)
623
    {
624
        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...
625
    }
626
627
    /**
628
     * method taken from Symfony\Component\PropertyAccess\PropertyAccessor.
629
     *
630
     * @param string $property
631
     *
632
     * @return mixed
633
     */
634
    protected function camelize($property)
635
    {
636
        return str_replace(' ', '', ucwords(str_replace('_', ' ', $property)));
637
    }
638
639
    private function isFieldAlreadySorted(FieldDescriptionInterface $fieldDescription, DatagridInterface $datagrid): bool
640
    {
641
        $values = $datagrid->getValues();
642
643
        if (!isset($values['_sort_by']) || !$values['_sort_by'] instanceof FieldDescriptionInterface) {
644
            return false;
645
        }
646
647
        return $values['_sort_by']->getName() === $fieldDescription->getName()
648
            || $values['_sort_by']->getName() === $fieldDescription->getOption('sortable');
649
    }
650
651
    /**
652
     * @param mixed $value
653
     */
654
    private function getValueFromType($value, Type $type, string $fieldType, AbstractPlatform $platform): string
655
    {
656
        if ($platform->hasDoctrineTypeMappingFor($fieldType) &&
657
            'binary' === $platform->getDoctrineTypeMapping($fieldType)
658
        ) {
659
            return (string) $type->convertToPHPValue($value, $platform);
660
        }
661
662
        // some libraries may have `toString()` implementation
663
        if (\is_callable([$value, 'toString'])) {
664
            return $value->toString();
665
        }
666
667
        // final fallback to magic `__toString()` which may throw an exception in 7.4
668
        if (method_exists($value, '__toString')) {
669
            return $value->__toString();
670
        }
671
672
        return (string) $type->convertToDatabaseValue($value, $platform);
673
    }
674
}
675