ModelManager::lock()   A
last analyzed

Complexity

Conditions 3
Paths 4

Size

Total Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 15
rs 9.7666
c 0
b 0
f 0
cc 3
nc 4
nop 2
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\OptimisticLockException;
23
use Doctrine\ORM\Query;
24
use Doctrine\ORM\QueryBuilder;
25
use Doctrine\ORM\UnitOfWork;
26
use Doctrine\Persistence\ManagerRegistry;
27
use Doctrine\Persistence\Mapping\ClassMetadata;
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\PropertyAccess\PropertyAccess;
40
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
41
42
class ModelManager implements ModelManagerInterface, LockInterface
43
{
44
    public const ID_SEPARATOR = '~';
45
46
    /**
47
     * @var ManagerRegistry
48
     */
49
    protected $registry;
50
51
    /**
52
     * @var PropertyAccessorInterface
53
     */
54
    protected $propertyAccessor;
55
56
    /**
57
     * @var EntityManager[]
58
     */
59
    protected $cache = [];
60
61
    /**
62
     * NEXT_MAJOR: Make $propertyAccessor mandatory.
63
     */
64
    public function __construct(ManagerRegistry $registry, ?PropertyAccessorInterface $propertyAccessor = null)
65
    {
66
        $this->registry = $registry;
67
68
        // NEXT_MAJOR: Remove this block.
69
        if (null === $propertyAccessor) {
70
            @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...
71
                'Constructing "%s" without passing an instance of "%s" as second argument is deprecated since'
72
                .' sonata-project/doctrine-orm-admin-bundle 3.x and will be mandatory in 4.0',
73
                __CLASS__,
74
                PropertyAccessorInterface::class
75
            ), E_USER_DEPRECATED);
76
77
            $propertyAccessor = PropertyAccess::createPropertyAccessor();
78
        }
79
80
        $this->propertyAccessor = $propertyAccessor;
81
    }
82
83
    /**
84
     * @param string $class
85
     *
86
     * @return ClassMetadata
87
     */
88
    public function getMetadata($class)
89
    {
90
        return $this->getEntityManager($class)->getMetadataFactory()->getMetadataFor($class);
91
    }
92
93
    /**
94
     * Returns the model's metadata holding the fully qualified property, and the last
95
     * property name.
96
     *
97
     * @param string $baseClass        The base class of the model holding the fully qualified property
98
     * @param string $propertyFullName The name of the fully qualified property (dot ('.') separated
99
     *                                 property string)
100
     *
101
     * @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...
102
     *                \Doctrine\ORM\Mapping\ClassMetadata $parentMetadata,
103
     *                string $lastPropertyName,
104
     *                array $parentAssociationMappings
105
     *                )
106
     */
107
    public function getParentMetadataForProperty($baseClass, $propertyFullName)
108
    {
109
        $nameElements = explode('.', $propertyFullName);
110
        $lastPropertyName = array_pop($nameElements);
111
        $class = $baseClass;
112
        $parentAssociationMappings = [];
113
114
        foreach ($nameElements as $nameElement) {
115
            $metadata = $this->getMetadata($class);
116
117
            if (isset($metadata->associationMappings[$nameElement])) {
0 ignored issues
show
Bug introduced by
Accessing associationMappings on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
118
                $parentAssociationMappings[] = $metadata->associationMappings[$nameElement];
0 ignored issues
show
Bug introduced by
Accessing associationMappings on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
119
                $class = $metadata->getAssociationTargetClass($nameElement);
120
121
                continue;
122
            }
123
124
            break;
125
        }
126
127
        $properties = \array_slice($nameElements, \count($parentAssociationMappings));
128
        $properties[] = $lastPropertyName;
129
130
        return [$this->getMetadata($class), implode('.', $properties), $parentAssociationMappings];
131
    }
132
133
    /**
134
     * @param string $class
135
     *
136
     * @return bool
137
     */
138
    public function hasMetadata($class)
139
    {
140
        return $this->getEntityManager($class)->getMetadataFactory()->hasMetadataFor($class);
141
    }
142
143
    public function getNewFieldDescriptionInstance($class, $name, array $options = [])
144
    {
145
        if (!\is_string($name)) {
146
            throw new \RuntimeException('The name argument must be a string');
147
        }
148
149
        if (!isset($options['route']['name'])) {
150
            $options['route']['name'] = 'edit';
151
        }
152
153
        if (!isset($options['route']['parameters'])) {
154
            $options['route']['parameters'] = [];
155
        }
156
157
        list($metadata, $propertyName, $parentAssociationMappings) = $this->getParentMetadataForProperty($class, $name);
158
159
        $fieldDescription = new FieldDescription();
160
        $fieldDescription->setName($name);
161
        $fieldDescription->setOptions($options);
162
        $fieldDescription->setParentAssociationMappings($parentAssociationMappings);
163
164
        if (isset($metadata->associationMappings[$propertyName])) {
0 ignored issues
show
Bug introduced by
Accessing associationMappings on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
165
            $fieldDescription->setAssociationMapping($metadata->associationMappings[$propertyName]);
0 ignored issues
show
Bug introduced by
Accessing associationMappings on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
166
        }
167
168
        if (isset($metadata->fieldMappings[$propertyName])) {
0 ignored issues
show
Bug introduced by
Accessing fieldMappings on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
169
            $fieldDescription->setFieldMapping($metadata->fieldMappings[$propertyName]);
0 ignored issues
show
Bug introduced by
Accessing fieldMappings on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
170
        }
171
172
        return $fieldDescription;
173
    }
174
175
    public function create($object): void
176
    {
177
        try {
178
            $entityManager = $this->getEntityManager($object);
179
            $entityManager->persist($object);
180
            $entityManager->flush();
181
        } catch (\PDOException $e) {
182
            throw new ModelManagerException(
183
                sprintf('Failed to create object: %s', ClassUtils::getClass($object)),
184
                $e->getCode(),
185
                $e
186
            );
187
        } catch (DBALException $e) {
188
            throw new ModelManagerException(
189
                sprintf('Failed to create object: %s', ClassUtils::getClass($object)),
190
                $e->getCode(),
191
                $e
192
            );
193
        }
194
    }
195
196
    public function update($object): void
197
    {
198
        try {
199
            $entityManager = $this->getEntityManager($object);
200
            $entityManager->persist($object);
201
            $entityManager->flush();
202
        } catch (\PDOException $e) {
203
            throw new ModelManagerException(
204
                sprintf('Failed to update object: %s', ClassUtils::getClass($object)),
205
                $e->getCode(),
206
                $e
207
            );
208
        } catch (DBALException $e) {
209
            throw new ModelManagerException(
210
                sprintf('Failed to update object: %s', ClassUtils::getClass($object)),
211
                $e->getCode(),
212
                $e
213
            );
214
        }
215
    }
216
217
    public function delete($object): void
218
    {
219
        try {
220
            $entityManager = $this->getEntityManager($object);
221
            $entityManager->remove($object);
222
            $entityManager->flush();
223
        } catch (\PDOException $e) {
224
            throw new ModelManagerException(
225
                sprintf('Failed to delete object: %s', ClassUtils::getClass($object)),
226
                $e->getCode(),
227
                $e
228
            );
229
        } catch (DBALException $e) {
230
            throw new ModelManagerException(
231
                sprintf('Failed to delete object: %s', ClassUtils::getClass($object)),
232
                $e->getCode(),
233
                $e
234
            );
235
        }
236
    }
237
238
    public function getLockVersion($object)
239
    {
240
        $metadata = $this->getMetadata(ClassUtils::getClass($object));
241
242
        if (!$metadata->isVersioned) {
0 ignored issues
show
Bug introduced by
Accessing isVersioned on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
243
            return null;
244
        }
245
246
        return $metadata->reflFields[$metadata->versionField]->getValue($object);
0 ignored issues
show
Bug introduced by
Accessing reflFields on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
Bug introduced by
Accessing versionField on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
247
    }
248
249
    public function lock($object, $expectedVersion): void
250
    {
251
        $metadata = $this->getMetadata(ClassUtils::getClass($object));
252
253
        if (!$metadata->isVersioned) {
0 ignored issues
show
Bug introduced by
Accessing isVersioned on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
254
            return;
255
        }
256
257
        try {
258
            $entityManager = $this->getEntityManager($object);
259
            $entityManager->lock($object, LockMode::OPTIMISTIC, $expectedVersion);
260
        } catch (OptimisticLockException $e) {
261
            throw new LockException($e->getMessage(), $e->getCode(), $e);
262
        }
263
    }
264
265
    public function find($class, $id)
266
    {
267
        if (null === $id) {
268
            @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...
269
                '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.',
270
                __METHOD__
271
            ), E_USER_DEPRECATED);
272
273
            return null;
274
        }
275
276
        $values = array_combine($this->getIdentifierFieldNames($class), explode(self::ID_SEPARATOR, (string) $id));
277
278
        return $this->getEntityManager($class)->getRepository($class)->find($values);
279
    }
280
281
    public function findBy($class, array $criteria = [])
282
    {
283
        return $this->getEntityManager($class)->getRepository($class)->findBy($criteria);
284
    }
285
286
    public function findOneBy($class, array $criteria = [])
287
    {
288
        return $this->getEntityManager($class)->getRepository($class)->findOneBy($criteria);
289
    }
290
291
    /**
292
     * @param string|object $class
293
     *
294
     * @return EntityManager
295
     */
296
    public function getEntityManager($class)
297
    {
298
        if (\is_object($class)) {
299
            $class = \get_class($class);
300
        }
301
302
        if (!isset($this->cache[$class])) {
303
            $em = $this->registry->getManagerForClass($class);
304
305
            if (!$em) {
306
                throw new \RuntimeException(sprintf('No entity manager defined for class %s', $class));
307
            }
308
309
            $this->cache[$class] = $em;
310
        }
311
312
        return $this->cache[$class];
313
    }
314
315
    /**
316
     * NEXT_MAJOR: Remove this method.
317
     *
318
     * @deprecated since sonata-project/doctrine-orm-admin-bundle 3.x and will be removed in version 4.0
319
     */
320
    public function getParentFieldDescription($parentAssociationMapping, $class)
321
    {
322
        @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...
323
            'Method %s() is deprecated since sonata-project/doctrine-orm-admin-bundle 3.x and will be removed in 4.0',
324
            __METHOD__
325
        ), E_USER_DEPRECATED);
326
327
        $fieldName = $parentAssociationMapping['fieldName'];
328
329
        $metadata = $this->getMetadata($class);
330
331
        $associatingMapping = $metadata->associationMappings[$parentAssociationMapping];
0 ignored issues
show
Bug introduced by
Accessing associationMappings on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
332
333
        $fieldDescription = $this->getNewFieldDescriptionInstance($class, $fieldName);
334
        $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...
335
        $fieldDescription->setAssociationMapping($associatingMapping);
336
337
        return $fieldDescription;
338
    }
339
340
    public function createQuery($class, $alias = 'o')
341
    {
342
        $repository = $this->getEntityManager($class)->getRepository($class);
343
344
        return new ProxyQuery($repository->createQueryBuilder($alias));
345
    }
346
347
    public function executeQuery($query)
348
    {
349
        if ($query instanceof QueryBuilder) {
350
            return $query->getQuery()->execute();
351
        }
352
353
        return $query->execute();
354
    }
355
356
    /**
357
     * NEXT_MAJOR: Remove this function.
358
     *
359
     * @deprecated since sonata-project/doctrine-orm-admin-bundle 3.18. To be removed in 4.0.
360
     */
361
    public function getModelIdentifier($class)
362
    {
363
        return $this->getMetadata($class)->identifier;
0 ignored issues
show
Bug introduced by
Accessing identifier on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
364
    }
365
366
    public function getIdentifierValues($entity)
367
    {
368
        // Fix code has an impact on performance, so disable it ...
369
        //$entityManager = $this->getEntityManager($entity);
370
        //if (!$entityManager->getUnitOfWork()->isInIdentityMap($entity)) {
371
        //    throw new \RuntimeException('Entities passed to the choice field must be managed');
372
        //}
373
374
        $class = ClassUtils::getClass($entity);
375
        $metadata = $this->getMetadata($class);
376
        $platform = $this->getEntityManager($class)->getConnection()->getDatabasePlatform();
377
378
        $identifiers = [];
379
380
        foreach ($metadata->getIdentifierValues($entity) as $name => $value) {
381
            if (!\is_object($value)) {
382
                $identifiers[] = $value;
383
384
                continue;
385
            }
386
387
            $fieldType = $metadata->getTypeOfField($name);
388
            $type = $fieldType && Type::hasType($fieldType) ? Type::getType($fieldType) : null;
389
            if ($type) {
390
                $identifiers[] = $this->getValueFromType($value, $type, $fieldType, $platform);
391
392
                continue;
393
            }
394
395
            $identifierMetadata = $this->getMetadata(ClassUtils::getClass($value));
396
397
            foreach ($identifierMetadata->getIdentifierValues($value) as $value) {
398
                $identifiers[] = $value;
399
            }
400
        }
401
402
        return $identifiers;
403
    }
404
405
    public function getIdentifierFieldNames($class)
406
    {
407
        return $this->getMetadata($class)->getIdentifierFieldNames();
408
    }
409
410
    public function getNormalizedIdentifier($entity)
411
    {
412
        // NEXT_MAJOR: Remove the following 2 checks and declare "object" as type for argument 1.
413
        if (null === $entity) {
414
            @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...
415
                '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.',
416
                __METHOD__
417
            ), E_USER_DEPRECATED);
418
419
            return null;
420
        }
421
422
        if (!\is_object($entity)) {
423
            throw new \RuntimeException('Invalid argument, object or null required');
424
        }
425
426
        if (\in_array($this->getEntityManager($entity)->getUnitOfWork()->getEntityState($entity), [
427
            UnitOfWork::STATE_NEW,
428
            UnitOfWork::STATE_REMOVED,
429
        ], true)) {
430
            // NEXT_MAJOR: Uncomment the following exception, remove the deprecation and the return statement inside this conditional block.
431
            // throw new \InvalidArgumentException(sprintf(
432
            //    'Can not get the normalized identifier for %s since it is in state %u.',
433
            //    ClassUtils::getClass($entity),
434
            //    $this->getEntityManager($entity)->getUnitOfWork()->getEntityState($entity)
435
            // ));
436
437
            @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...
438
                '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'
439
                .'and will be not allowed in version 4.0.',
440
                UnitOfWork::STATE_NEW,
441
                UnitOfWork::STATE_REMOVED,
442
                __METHOD__
443
            ), E_USER_DEPRECATED);
444
445
            return null;
446
        }
447
448
        $values = $this->getIdentifierValues($entity);
449
450
        if (0 === \count($values)) {
451
            return null;
452
        }
453
454
        return implode(self::ID_SEPARATOR, $values);
455
    }
456
457
    /**
458
     * {@inheritdoc}
459
     *
460
     * The ORM implementation does nothing special but you still should use
461
     * this method when using the id in a URL to allow for future improvements.
462
     */
463
    public function getUrlSafeIdentifier($entity)
464
    {
465
        // NEXT_MAJOR: Remove the following check and declare "object" as type for argument 1.
466
        if (!\is_object($entity)) {
467
            @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...
468
                '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.',
469
                __METHOD__
470
            ), E_USER_DEPRECATED);
471
472
            return null;
473
        }
474
475
        return $this->getNormalizedIdentifier($entity);
476
    }
477
478
    public function addIdentifiersToQuery($class, ProxyQueryInterface $queryProxy, array $idx): void
479
    {
480
        $fieldNames = $this->getIdentifierFieldNames($class);
481
        $qb = $queryProxy->getQueryBuilder();
482
483
        $prefix = uniqid();
484
        $sqls = [];
485
        foreach ($idx as $pos => $id) {
486
            $ids = explode(self::ID_SEPARATOR, $id);
487
488
            $ands = [];
489
            foreach ($fieldNames as $posName => $name) {
490
                $parameterName = sprintf('field_%s_%s_%d', $prefix, $name, $pos);
491
                $ands[] = sprintf('%s.%s = :%s', current($qb->getRootAliases()), $name, $parameterName);
492
                $qb->setParameter($parameterName, $ids[$posName]);
493
            }
494
495
            $sqls[] = implode(' AND ', $ands);
496
        }
497
498
        $qb->andWhere(sprintf('( %s )', implode(' OR ', $sqls)));
499
    }
500
501
    public function batchDelete($class, ProxyQueryInterface $queryProxy): void
502
    {
503
        $queryProxy->select('DISTINCT '.current($queryProxy->getRootAliases()));
504
505
        try {
506
            $entityManager = $this->getEntityManager($class);
507
508
            $i = 0;
509
            foreach ($queryProxy->getQuery()->iterate() as $pos => $object) {
510
                $entityManager->remove($object[0]);
511
512
                if (0 === (++$i % 20)) {
513
                    $entityManager->flush();
514
                    $entityManager->clear();
515
                }
516
            }
517
518
            $entityManager->flush();
519
            $entityManager->clear();
520
        } catch (\PDOException | DBALException $e) {
521
            throw new ModelManagerException('', 0, $e);
522
        }
523
    }
524
525
    public function getDataSourceIterator(DatagridInterface $datagrid, array $fields, $firstResult = null, $maxResult = null)
526
    {
527
        $datagrid->buildPager();
528
        $query = $datagrid->getQuery();
529
530
        $query->select('DISTINCT '.current($query->getRootAliases()));
531
        $query->setFirstResult($firstResult);
532
        $query->setMaxResults($maxResult);
533
534
        if ($query instanceof ProxyQueryInterface) {
535
            $sortBy = $query->getSortBy();
536
537
            if (!empty($sortBy)) {
538
                $query->addOrderBy($sortBy, $query->getSortOrder());
539
                $query = $query->getQuery();
540
                $query->setHint(Query::HINT_CUSTOM_TREE_WALKERS, [OrderByToSelectWalker::class]);
541
            } else {
542
                $query = $query->getQuery();
543
            }
544
        }
545
546
        return new DoctrineORMQuerySourceIterator($query, $fields);
547
    }
548
549
    public function getExportFields($class)
550
    {
551
        $metadata = $this->getEntityManager($class)->getClassMetadata($class);
552
553
        return $metadata->getFieldNames();
554
    }
555
556
    public function getModelInstance($class)
557
    {
558
        $r = new \ReflectionClass($class);
559
        if ($r->isAbstract()) {
560
            throw new \RuntimeException(sprintf('Cannot initialize abstract class: %s', $class));
561
        }
562
563
        $constructor = $r->getConstructor();
564
565
        if (null !== $constructor && (!$constructor->isPublic() || $constructor->getNumberOfRequiredParameters() > 0)) {
566
            return $r->newInstanceWithoutConstructor();
567
        }
568
569
        return new $class();
570
    }
571
572
    public function getSortParameters(FieldDescriptionInterface $fieldDescription, DatagridInterface $datagrid)
573
    {
574
        $values = $datagrid->getValues();
575
576
        if ($this->isFieldAlreadySorted($fieldDescription, $datagrid)) {
577
            if ('ASC' === $values['_sort_order']) {
578
                $values['_sort_order'] = 'DESC';
579
            } else {
580
                $values['_sort_order'] = 'ASC';
581
            }
582
        } else {
583
            $values['_sort_order'] = 'ASC';
584
        }
585
586
        $values['_sort_by'] = \is_string($fieldDescription->getOption('sortable')) ? $fieldDescription->getOption('sortable') : $fieldDescription->getName();
587
588
        return ['filter' => $values];
589
    }
590
591
    public function getPaginationParameters(DatagridInterface $datagrid, $page)
592
    {
593
        $values = $datagrid->getValues();
594
595
        if (isset($values['_sort_by']) && $values['_sort_by'] instanceof FieldDescriptionInterface) {
596
            $values['_sort_by'] = $values['_sort_by']->getName();
597
        }
598
        $values['_page'] = $page;
599
600
        return ['filter' => $values];
601
    }
602
603
    public function getDefaultSortValues($class)
604
    {
605
        return [
606
            '_page' => 1,
607
            '_per_page' => 25,
608
        ];
609
    }
610
611
    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...
612
    {
613
        return [10, 25, 50, 100, 250];
614
    }
615
616
    public function modelTransform($class, $instance)
617
    {
618
        return $instance;
619
    }
620
621
    public function modelReverseTransform($class, array $array = [])
622
    {
623
        $instance = $this->getModelInstance($class);
624
        $metadata = $this->getMetadata($class);
625
626
        foreach ($array as $name => $value) {
627
            $property = $this->getFieldName($metadata, $name);
628
            $this->propertyAccessor->setValue($instance, $property, $value);
629
        }
630
631
        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...
632
    }
633
634
    public function getModelCollectionInstance($class)
635
    {
636
        return new \Doctrine\Common\Collections\ArrayCollection();
637
    }
638
639
    public function collectionClear(&$collection)
640
    {
641
        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...
642
    }
643
644
    public function collectionHasElement(&$collection, &$element)
645
    {
646
        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...
647
    }
648
649
    public function collectionAddElement(&$collection, &$element)
650
    {
651
        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...
652
    }
653
654
    public function collectionRemoveElement(&$collection, &$element)
655
    {
656
        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...
657
    }
658
659
    /**
660
     * NEXT_MAJOR: Remove this method.
661
     *
662
     * @param string $property
663
     *
664
     * @return mixed
665
     */
666
    protected function camelize($property)
667
    {
668
        @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...
669
            'Method %s() is deprecated since sonata-project/doctrine-orm-admin-bundle 3.x and will be removed in version 4.0.',
670
            __METHOD__
671
        ), E_USER_DEPRECATED);
672
673
        return str_replace(' ', '', ucwords(str_replace('_', ' ', $property)));
674
    }
675
676
    private function getFieldName(ClassMetadata $metadata, string $name): string
677
    {
678
        if (\array_key_exists($name, $metadata->fieldMappings)) {
0 ignored issues
show
Bug introduced by
Accessing fieldMappings on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
679
            return $metadata->fieldMappings[$name]['fieldName'];
0 ignored issues
show
Bug introduced by
Accessing fieldMappings on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
680
        }
681
682
        if (\array_key_exists($name, $metadata->associationMappings)) {
0 ignored issues
show
Bug introduced by
Accessing associationMappings on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
683
            return $metadata->associationMappings[$name]['fieldName'];
0 ignored issues
show
Bug introduced by
Accessing associationMappings on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
684
        }
685
686
        return $name;
687
    }
688
689
    private function isFieldAlreadySorted(FieldDescriptionInterface $fieldDescription, DatagridInterface $datagrid): bool
690
    {
691
        $values = $datagrid->getValues();
692
693
        if (!isset($values['_sort_by']) || !$values['_sort_by'] instanceof FieldDescriptionInterface) {
694
            return false;
695
        }
696
697
        return $values['_sort_by']->getName() === $fieldDescription->getName()
698
            || $values['_sort_by']->getName() === $fieldDescription->getOption('sortable');
699
    }
700
701
    /**
702
     * @param mixed $value
703
     */
704
    private function getValueFromType($value, Type $type, string $fieldType, AbstractPlatform $platform): string
705
    {
706
        if ($platform->hasDoctrineTypeMappingFor($fieldType) &&
707
            'binary' === $platform->getDoctrineTypeMapping($fieldType)
708
        ) {
709
            return (string) $type->convertToPHPValue($value, $platform);
710
        }
711
712
        // some libraries may have `toString()` implementation
713
        if (\is_callable([$value, 'toString'])) {
714
            return $value->toString();
715
        }
716
717
        // final fallback to magic `__toString()` which may throw an exception in 7.4
718
        if (method_exists($value, '__toString')) {
719
            return $value->__toString();
720
        }
721
722
        return (string) $type->convertToDatabaseValue($value, $platform);
723
    }
724
}
725