Completed
Push — master ( 75c36b...4386d7 )
by Grégoire
15s queued 11s
created

ModelManager::getNormalizedIdentifier()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 46

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 46
rs 8.867
c 0
b 0
f 0
cc 5
nc 5
nop 1
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): void
152
    {
153
        try {
154
            $entityManager = $this->getEntityManager($object);
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): void
173
    {
174
        try {
175
            $entityManager = $this->getEntityManager($object);
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): void
194
    {
195
        try {
196
            $entityManager = $this->getEntityManager($object);
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): void
226
    {
227
        $metadata = $this->getMetadata(ClassUtils::getClass($object));
228
229
        if (!$metadata->isVersioned) {
230
            return;
231
        }
232
233
        try {
234
            $entityManager = $this->getEntityManager($object);
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 (null === $id) {
244
            @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...
245
                '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.',
246
                __METHOD__
247
            ), E_USER_DEPRECATED);
248
249
            return null;
250
        }
251
252
        $values = array_combine($this->getIdentifierFieldNames($class), explode(self::ID_SEPARATOR, (string) $id));
253
254
        return $this->getEntityManager($class)->getRepository($class)->find($values);
255
    }
256
257
    public function findBy($class, array $criteria = [])
258
    {
259
        return $this->getEntityManager($class)->getRepository($class)->findBy($criteria);
260
    }
261
262
    public function findOneBy($class, array $criteria = [])
263
    {
264
        return $this->getEntityManager($class)->getRepository($class)->findOneBy($criteria);
265
    }
266
267
    /**
268
     * @param string|object $class
269
     *
270
     * @return EntityManager
271
     */
272
    public function getEntityManager($class)
273
    {
274
        if (\is_object($class)) {
275
            $class = \get_class($class);
276
        }
277
278
        if (!isset($this->cache[$class])) {
279
            $em = $this->registry->getManagerForClass($class);
280
281
            if (!$em) {
282
                throw new \RuntimeException(sprintf('No entity manager defined for class %s', $class));
283
            }
284
285
            $this->cache[$class] = $em;
286
        }
287
288
        return $this->cache[$class];
289
    }
290
291
    public function getParentFieldDescription($parentAssociationMapping, $class)
292
    {
293
        $fieldName = $parentAssociationMapping['fieldName'];
294
295
        $metadata = $this->getMetadata($class);
296
297
        $associatingMapping = $metadata->associationMappings[$parentAssociationMapping];
298
299
        $fieldDescription = $this->getNewFieldDescriptionInstance($class, $fieldName);
300
        $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...
301
        $fieldDescription->setAssociationMapping($associatingMapping);
302
303
        return $fieldDescription;
304
    }
305
306
    public function createQuery($class, $alias = 'o')
307
    {
308
        $repository = $this->getEntityManager($class)->getRepository($class);
309
310
        return new ProxyQuery($repository->createQueryBuilder($alias));
311
    }
312
313
    public function executeQuery($query)
314
    {
315
        if ($query instanceof QueryBuilder) {
316
            return $query->getQuery()->execute();
317
        }
318
319
        return $query->execute();
320
    }
321
322
    /**
323
     * NEXT_MAJOR: Remove this function.
324
     *
325
     * @deprecated since sonata-project/doctrine-orm-admin-bundle 3.18. To be removed in 4.0.
326
     */
327
    public function getModelIdentifier($class)
328
    {
329
        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...
330
    }
331
332
    public function getIdentifierValues($entity)
333
    {
334
        // Fix code has an impact on performance, so disable it ...
335
        //$entityManager = $this->getEntityManager($entity);
336
        //if (!$entityManager->getUnitOfWork()->isInIdentityMap($entity)) {
337
        //    throw new \RuntimeException('Entities passed to the choice field must be managed');
338
        //}
339
340
        $class = ClassUtils::getClass($entity);
341
        $metadata = $this->getMetadata($class);
342
        $platform = $this->getEntityManager($class)->getConnection()->getDatabasePlatform();
343
344
        $identifiers = [];
345
346
        foreach ($metadata->getIdentifierValues($entity) as $name => $value) {
347
            if (!\is_object($value)) {
348
                $identifiers[] = $value;
349
350
                continue;
351
            }
352
353
            $fieldType = $metadata->getTypeOfField($name);
354
            $type = $fieldType && Type::hasType($fieldType) ? Type::getType($fieldType) : null;
355
            if ($type) {
356
                $identifiers[] = $this->getValueFromType($value, $type, $fieldType, $platform);
357
358
                continue;
359
            }
360
361
            $identifierMetadata = $this->getMetadata(ClassUtils::getClass($value));
362
363
            foreach ($identifierMetadata->getIdentifierValues($value) as $value) {
364
                $identifiers[] = $value;
365
            }
366
        }
367
368
        return $identifiers;
369
    }
370
371
    public function getIdentifierFieldNames($class)
372
    {
373
        return $this->getMetadata($class)->getIdentifierFieldNames();
374
    }
375
376
    public function getNormalizedIdentifier($entity)
377
    {
378
        // NEXT_MAJOR: Remove the following 2 checks and declare "object" as type for argument 1.
379
        if (null === $entity) {
380
            @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...
381
                '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.',
382
                __METHOD__
383
            ), E_USER_DEPRECATED);
384
385
            return null;
386
        }
387
388
        if (!\is_object($entity)) {
389
            throw new \RuntimeException('Invalid argument, object or null required');
390
        }
391
392
        if (\in_array($this->getEntityManager($entity)->getUnitOfWork()->getEntityState($entity), [
393
            UnitOfWork::STATE_NEW,
394
            UnitOfWork::STATE_REMOVED,
395
        ], true)) {
396
            // NEXT_MAJOR: Uncomment the following exception, remove the deprecation and the return statement inside this conditional block.
397
            // throw new \InvalidArgumentException(sprintf(
398
            //    'Can not get the normalized identifier for %s since it is in state %u.',
399
            //    ClassUtils::getClass($entity),
400
            //    $this->getEntityManager($entity)->getUnitOfWork()->getEntityState($entity)
401
            // ));
402
403
            @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...
404
                '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'
405
                .'and will be not allowed in version 4.0.',
406
                UnitOfWork::STATE_NEW,
407
                UnitOfWork::STATE_REMOVED,
408
                __METHOD__
409
            ), E_USER_DEPRECATED);
410
411
            return null;
412
        }
413
414
        $values = $this->getIdentifierValues($entity);
415
416
        if (0 === \count($values)) {
417
            return null;
418
        }
419
420
        return implode(self::ID_SEPARATOR, $values);
421
    }
422
423
    /**
424
     * {@inheritdoc}
425
     *
426
     * The ORM implementation does nothing special but you still should use
427
     * this method when using the id in a URL to allow for future improvements.
428
     */
429
    public function getUrlSafeIdentifier($entity)
430
    {
431
        // NEXT_MAJOR: Remove the following check and declare "object" as type for argument 1.
432
        if (!\is_object($entity)) {
433
            @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...
434
                '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.',
435
                __METHOD__
436
            ), E_USER_DEPRECATED);
437
438
            return null;
439
        }
440
441
        return $this->getNormalizedIdentifier($entity);
442
    }
443
444
    public function addIdentifiersToQuery($class, ProxyQueryInterface $queryProxy, array $idx): void
445
    {
446
        $fieldNames = $this->getIdentifierFieldNames($class);
447
        $qb = $queryProxy->getQueryBuilder();
448
449
        $prefix = uniqid();
450
        $sqls = [];
451
        foreach ($idx as $pos => $id) {
452
            $ids = explode(self::ID_SEPARATOR, $id);
453
454
            $ands = [];
455
            foreach ($fieldNames as $posName => $name) {
456
                $parameterName = sprintf('field_%s_%s_%d', $prefix, $name, $pos);
457
                $ands[] = sprintf('%s.%s = :%s', current($qb->getRootAliases()), $name, $parameterName);
458
                $qb->setParameter($parameterName, $ids[$posName]);
459
            }
460
461
            $sqls[] = implode(' AND ', $ands);
462
        }
463
464
        $qb->andWhere(sprintf('( %s )', implode(' OR ', $sqls)));
465
    }
466
467
    public function batchDelete($class, ProxyQueryInterface $queryProxy): void
468
    {
469
        $queryProxy->select('DISTINCT '.current($queryProxy->getRootAliases()));
470
471
        try {
472
            $entityManager = $this->getEntityManager($class);
473
474
            $i = 0;
475
            foreach ($queryProxy->getQuery()->iterate() as $pos => $object) {
476
                $entityManager->remove($object[0]);
477
478
                if (0 === (++$i % 20)) {
479
                    $entityManager->flush();
480
                    $entityManager->clear();
481
                }
482
            }
483
484
            $entityManager->flush();
485
            $entityManager->clear();
486
        } catch (\PDOException | DBALException $e) {
487
            throw new ModelManagerException('', 0, $e);
488
        }
489
    }
490
491
    public function getDataSourceIterator(DatagridInterface $datagrid, array $fields, $firstResult = null, $maxResult = null)
492
    {
493
        $datagrid->buildPager();
494
        $query = $datagrid->getQuery();
495
496
        $query->select('DISTINCT '.current($query->getRootAliases()));
497
        $query->setFirstResult($firstResult);
498
        $query->setMaxResults($maxResult);
499
500
        if ($query instanceof ProxyQueryInterface) {
501
            $sortBy = $query->getSortBy();
502
503
            if (!empty($sortBy)) {
504
                $query->addOrderBy($sortBy, $query->getSortOrder());
505
                $query = $query->getQuery();
506
                $query->setHint(Query::HINT_CUSTOM_TREE_WALKERS, [OrderByToSelectWalker::class]);
507
            } else {
508
                $query = $query->getQuery();
509
            }
510
        }
511
512
        return new DoctrineORMQuerySourceIterator($query, $fields);
513
    }
514
515
    public function getExportFields($class)
516
    {
517
        $metadata = $this->getEntityManager($class)->getClassMetadata($class);
518
519
        return $metadata->getFieldNames();
520
    }
521
522
    public function getModelInstance($class)
523
    {
524
        $r = new \ReflectionClass($class);
525
        if ($r->isAbstract()) {
526
            throw new \RuntimeException(sprintf('Cannot initialize abstract class: %s', $class));
527
        }
528
529
        $constructor = $r->getConstructor();
530
531
        if (null !== $constructor && (!$constructor->isPublic() || $constructor->getNumberOfRequiredParameters() > 0)) {
532
            return $r->newInstanceWithoutConstructor();
533
        }
534
535
        return new $class();
536
    }
537
538
    public function getSortParameters(FieldDescriptionInterface $fieldDescription, DatagridInterface $datagrid)
539
    {
540
        $values = $datagrid->getValues();
541
542
        if ($this->isFieldAlreadySorted($fieldDescription, $datagrid)) {
543
            if ('ASC' === $values['_sort_order']) {
544
                $values['_sort_order'] = 'DESC';
545
            } else {
546
                $values['_sort_order'] = 'ASC';
547
            }
548
        } else {
549
            $values['_sort_order'] = 'ASC';
550
        }
551
552
        $values['_sort_by'] = \is_string($fieldDescription->getOption('sortable')) ? $fieldDescription->getOption('sortable') : $fieldDescription->getName();
553
554
        return ['filter' => $values];
555
    }
556
557
    public function getPaginationParameters(DatagridInterface $datagrid, $page)
558
    {
559
        $values = $datagrid->getValues();
560
561
        if (isset($values['_sort_by']) && $values['_sort_by'] instanceof FieldDescriptionInterface) {
562
            $values['_sort_by'] = $values['_sort_by']->getName();
563
        }
564
        $values['_page'] = $page;
565
566
        return ['filter' => $values];
567
    }
568
569
    public function getDefaultSortValues($class)
570
    {
571
        return [
572
            '_page' => 1,
573
            '_per_page' => 25,
574
        ];
575
    }
576
577
    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...
578
    {
579
        return [10, 25, 50, 100, 250];
580
    }
581
582
    public function modelTransform($class, $instance)
583
    {
584
        return $instance;
585
    }
586
587
    public function modelReverseTransform($class, array $array = [])
588
    {
589
        $instance = $this->getModelInstance($class);
590
        $metadata = $this->getMetadata($class);
591
592
        $reflClass = $metadata->reflClass;
593
        foreach ($array as $name => $value) {
594
            $reflection_property = false;
595
            // property or association ?
596
            if (\array_key_exists($name, $metadata->fieldMappings)) {
597
                $property = $metadata->fieldMappings[$name]['fieldName'];
598
                $reflection_property = $metadata->reflFields[$name];
599
            } elseif (\array_key_exists($name, $metadata->associationMappings)) {
600
                $property = $metadata->associationMappings[$name]['fieldName'];
601
            } else {
602
                $property = $name;
603
            }
604
605
            $setter = 'set'.$this->camelize($name);
606
607
            if ($reflClass->hasMethod($setter)) {
608
                if (!$reflClass->getMethod($setter)->isPublic()) {
609
                    throw new PropertyAccessDeniedException(sprintf(
610
                        'Method "%s()" is not public in class "%s"',
611
                        $setter,
612
                        $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...
613
                    ));
614
                }
615
616
                $instance->$setter($value);
617
            } elseif ($reflClass->hasMethod('__set')) {
618
                // needed to support magic method __set
619
                $instance->$property = $value;
620
            } elseif ($reflClass->hasProperty($property)) {
621
                if (!$reflClass->getProperty($property)->isPublic()) {
622
                    throw new PropertyAccessDeniedException(sprintf(
623
                        'Property "%s" is not public in class "%s". Maybe you should create the method "set%s()"?',
624
                        $property,
625
                        $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...
626
                        ucfirst($property)
627
                    ));
628
                }
629
630
                $instance->$property = $value;
631
            } elseif ($reflection_property) {
632
                $reflection_property->setValue($instance, $value);
633
            }
634
        }
635
636
        return $instance;
637
    }
638
639
    public function getModelCollectionInstance($class)
640
    {
641
        return new \Doctrine\Common\Collections\ArrayCollection();
642
    }
643
644
    public function collectionClear(&$collection)
645
    {
646
        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...
647
    }
648
649
    public function collectionHasElement(&$collection, &$element)
650
    {
651
        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...
652
    }
653
654
    public function collectionAddElement(&$collection, &$element)
655
    {
656
        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...
657
    }
658
659
    public function collectionRemoveElement(&$collection, &$element)
660
    {
661
        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...
662
    }
663
664
    /**
665
     * method taken from Symfony\Component\PropertyAccess\PropertyAccessor.
666
     *
667
     * @param string $property
668
     *
669
     * @return mixed
670
     */
671
    protected function camelize($property)
672
    {
673
        return str_replace(' ', '', ucwords(str_replace('_', ' ', $property)));
674
    }
675
676
    private function isFieldAlreadySorted(FieldDescriptionInterface $fieldDescription, DatagridInterface $datagrid): bool
677
    {
678
        $values = $datagrid->getValues();
679
680
        if (!isset($values['_sort_by']) || !$values['_sort_by'] instanceof FieldDescriptionInterface) {
681
            return false;
682
        }
683
684
        return $values['_sort_by']->getName() === $fieldDescription->getName()
685
            || $values['_sort_by']->getName() === $fieldDescription->getOption('sortable');
686
    }
687
688
    /**
689
     * @param mixed $value
690
     */
691
    private function getValueFromType($value, Type $type, string $fieldType, AbstractPlatform $platform): string
692
    {
693
        if ($platform->hasDoctrineTypeMappingFor($fieldType) &&
694
            'binary' === $platform->getDoctrineTypeMapping($fieldType)
695
        ) {
696
            return (string) $type->convertToPHPValue($value, $platform);
697
        }
698
699
        // some libraries may have `toString()` implementation
700
        if (\is_callable([$value, 'toString'])) {
701
            return $value->toString();
702
        }
703
704
        // final fallback to magic `__toString()` which may throw an exception in 7.4
705
        if (method_exists($value, '__toString')) {
706
            return $value->__toString();
707
        }
708
709
        return (string) $type->convertToDatabaseValue($value, $platform);
710
    }
711
}
712