Completed
Push — master ( 50010f...44089f )
by
unknown
09:33
created

src/Model/ModelManager.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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\Types\Type;
20
use Doctrine\ORM\EntityManager;
21
use Doctrine\ORM\Mapping\ClassMetadata;
22
use Doctrine\ORM\OptimisticLockException;
23
use Doctrine\ORM\Query;
24
use Doctrine\ORM\QueryBuilder;
25
use Doctrine\ORM\UnitOfWork;
26
use Exporter\Source\DoctrineORMQuerySourceIterator;
27
use Sonata\AdminBundle\Admin\FieldDescriptionInterface;
28
use Sonata\AdminBundle\Datagrid\DatagridInterface;
29
use Sonata\AdminBundle\Datagrid\ProxyQueryInterface;
30
use Sonata\AdminBundle\Exception\LockException;
31
use Sonata\AdminBundle\Exception\ModelManagerException;
32
use Sonata\AdminBundle\Model\LockInterface;
33
use Sonata\AdminBundle\Model\ModelManagerInterface;
34
use Sonata\DoctrineORMAdminBundle\Admin\FieldDescription;
35
use Sonata\DoctrineORMAdminBundle\Datagrid\OrderByToSelectWalker;
36
use Sonata\DoctrineORMAdminBundle\Datagrid\ProxyQuery;
37
use Symfony\Bridge\Doctrine\RegistryInterface;
38
use Symfony\Component\Form\Exception\PropertyAccessDeniedException;
39
40
class ModelManager implements ModelManagerInterface, LockInterface
41
{
42
    public const ID_SEPARATOR = '~';
43
    /**
44
     * @var RegistryInterface
45
     */
46
    protected $registry;
47
48
    /**
49
     * @var EntityManager[]
50
     */
51
    protected $cache = [];
52
53
    public function __construct(RegistryInterface $registry)
54
    {
55
        $this->registry = $registry;
56
    }
57
58
    /**
59
     * @param string $class
60
     *
61
     * @return ClassMetadata
62
     */
63
    public function getMetadata($class)
64
    {
65
        return $this->getEntityManager($class)->getMetadataFactory()->getMetadataFor($class);
66
    }
67
68
    /**
69
     * Returns the model's metadata holding the fully qualified property, and the last
70
     * property name.
71
     *
72
     * @param string $baseClass        The base class of the model holding the fully qualified property
73
     * @param string $propertyFullName The name of the fully qualified property (dot ('.') separated
74
     *                                 property string)
75
     *
76
     * @return array(
77
     *                \Doctrine\ORM\Mapping\ClassMetadata $parentMetadata,
78
     *                string $lastPropertyName,
79
     *                array $parentAssociationMappings
80
     *                )
81
     */
82
    public function getParentMetadataForProperty($baseClass, $propertyFullName)
83
    {
84
        $nameElements = explode('.', $propertyFullName);
85
        $lastPropertyName = array_pop($nameElements);
86
        $class = $baseClass;
87
        $parentAssociationMappings = [];
88
89
        foreach ($nameElements as $nameElement) {
90
            $metadata = $this->getMetadata($class);
91
92
            if (isset($metadata->associationMappings[$nameElement])) {
93
                $parentAssociationMappings[] = $metadata->associationMappings[$nameElement];
94
                $class = $metadata->getAssociationTargetClass($nameElement);
95
96
                continue;
97
            }
98
99
            break;
100
        }
101
102
        $properties = \array_slice($nameElements, \count($parentAssociationMappings));
103
        $properties[] = $lastPropertyName;
104
105
        return [$this->getMetadata($class), implode('.', $properties), $parentAssociationMappings];
106
    }
107
108
    /**
109
     * @param string $class
110
     *
111
     * @return bool
112
     */
113
    public function hasMetadata($class)
114
    {
115
        return $this->getEntityManager($class)->getMetadataFactory()->hasMetadataFor($class);
116
    }
117
118
    public function getNewFieldDescriptionInstance($class, $name, array $options = [])
119
    {
120
        if (!\is_string($name)) {
121
            throw new \RuntimeException('The name argument must be a string');
122
        }
123
124
        if (!isset($options['route']['name'])) {
125
            $options['route']['name'] = 'edit';
126
        }
127
128
        if (!isset($options['route']['parameters'])) {
129
            $options['route']['parameters'] = [];
130
        }
131
132
        list($metadata, $propertyName, $parentAssociationMappings) = $this->getParentMetadataForProperty($class, $name);
133
134
        $fieldDescription = new FieldDescription();
135
        $fieldDescription->setName($name);
136
        $fieldDescription->setOptions($options);
137
        $fieldDescription->setParentAssociationMappings($parentAssociationMappings);
138
139
        if (isset($metadata->associationMappings[$propertyName])) {
140
            $fieldDescription->setAssociationMapping($metadata->associationMappings[$propertyName]);
141
        }
142
143
        if (isset($metadata->fieldMappings[$propertyName])) {
144
            $fieldDescription->setFieldMapping($metadata->fieldMappings[$propertyName]);
145
        }
146
147
        return $fieldDescription;
148
    }
149
150
    public function create($object): void
151
    {
152
        try {
153
            $entityManager = $this->getEntityManager($object);
154
            $entityManager->persist($object);
155
            $entityManager->flush();
156
        } catch (\PDOException $e) {
157
            throw new ModelManagerException(
158
                sprintf('Failed to create object: %s', ClassUtils::getClass($object)),
159
                $e->getCode(),
160
                $e
161
            );
162
        } catch (DBALException $e) {
163
            throw new ModelManagerException(
164
                sprintf('Failed to create object: %s', ClassUtils::getClass($object)),
165
                $e->getCode(),
166
                $e
167
            );
168
        }
169
    }
170
171
    public function update($object): void
172
    {
173
        try {
174
            $entityManager = $this->getEntityManager($object);
175
            $entityManager->persist($object);
176
            $entityManager->flush();
177
        } catch (\PDOException $e) {
178
            throw new ModelManagerException(
179
                sprintf('Failed to update object: %s', ClassUtils::getClass($object)),
180
                $e->getCode(),
181
                $e
182
            );
183
        } catch (DBALException $e) {
184
            throw new ModelManagerException(
185
                sprintf('Failed to update object: %s', ClassUtils::getClass($object)),
186
                $e->getCode(),
187
                $e
188
            );
189
        }
190
    }
191
192
    public function delete($object): void
193
    {
194
        try {
195
            $entityManager = $this->getEntityManager($object);
196
            $entityManager->remove($object);
197
            $entityManager->flush();
198
        } catch (\PDOException $e) {
199
            throw new ModelManagerException(
200
                sprintf('Failed to delete object: %s', ClassUtils::getClass($object)),
201
                $e->getCode(),
202
                $e
203
            );
204
        } catch (DBALException $e) {
205
            throw new ModelManagerException(
206
                sprintf('Failed to delete object: %s', ClassUtils::getClass($object)),
207
                $e->getCode(),
208
                $e
209
            );
210
        }
211
    }
212
213
    public function getLockVersion($object)
214
    {
215
        $metadata = $this->getMetadata(ClassUtils::getClass($object));
216
217
        if (!$metadata->isVersioned) {
218
            return;
219
        }
220
221
        return $metadata->reflFields[$metadata->versionField]->getValue($object);
222
    }
223
224
    public function lock($object, $expectedVersion): void
225
    {
226
        $metadata = $this->getMetadata(ClassUtils::getClass($object));
227
228
        if (!$metadata->isVersioned) {
229
            return;
230
        }
231
232
        try {
233
            $entityManager = $this->getEntityManager($object);
234
            $entityManager->lock($object, LockMode::OPTIMISTIC, $expectedVersion);
235
        } catch (OptimisticLockException $e) {
236
            throw new LockException($e->getMessage(), $e->getCode(), $e);
237
        }
238
    }
239
240
    public function find($class, $id)
241
    {
242
        if (!isset($id)) {
243
            return;
244
        }
245
246
        $values = array_combine($this->getIdentifierFieldNames($class), explode(self::ID_SEPARATOR, $id));
247
248
        return $this->getEntityManager($class)->getRepository($class)->find($values);
249
    }
250
251
    public function findBy($class, array $criteria = [])
252
    {
253
        return $this->getEntityManager($class)->getRepository($class)->findBy($criteria);
254
    }
255
256
    public function findOneBy($class, array $criteria = [])
257
    {
258
        return $this->getEntityManager($class)->getRepository($class)->findOneBy($criteria);
259
    }
260
261
    /**
262
     * @param string $class
263
     *
264
     * @return EntityManager
265
     */
266
    public function getEntityManager($class)
267
    {
268
        if (\is_object($class)) {
269
            $class = \get_class($class);
270
        }
271
272
        if (!isset($this->cache[$class])) {
273
            $em = $this->registry->getManagerForClass($class);
274
275
            if (!$em) {
276
                throw new \RuntimeException(sprintf('No entity manager defined for class %s', $class));
277
            }
278
279
            $this->cache[$class] = $em;
280
        }
281
282
        return $this->cache[$class];
283
    }
284
285
    public function getParentFieldDescription($parentAssociationMapping, $class)
286
    {
287
        $fieldName = $parentAssociationMapping['fieldName'];
288
289
        $metadata = $this->getMetadata($class);
290
291
        $associatingMapping = $metadata->associationMappings[$parentAssociationMapping];
292
293
        $fieldDescription = $this->getNewFieldDescriptionInstance($class, $fieldName);
294
        $fieldDescription->setName($parentAssociationMapping);
295
        $fieldDescription->setAssociationMapping($associatingMapping);
296
297
        return $fieldDescription;
298
    }
299
300
    public function createQuery($class, $alias = 'o')
301
    {
302
        $repository = $this->getEntityManager($class)->getRepository($class);
303
304
        return new ProxyQuery($repository->createQueryBuilder($alias));
305
    }
306
307
    public function executeQuery($query)
308
    {
309
        if ($query instanceof QueryBuilder) {
310
            return $query->getQuery()->execute();
311
        }
312
313
        return $query->execute();
314
    }
315
316
    public function getModelIdentifier($class)
317
    {
318
        return $this->getMetadata($class)->identifier;
319
    }
320
321
    public function getIdentifierValues($entity)
322
    {
323
        // Fix code has an impact on performance, so disable it ...
324
        //$entityManager = $this->getEntityManager($entity);
325
        //if (!$entityManager->getUnitOfWork()->isInIdentityMap($entity)) {
326
        //    throw new \RuntimeException('Entities passed to the choice field must be managed');
327
        //}
328
329
        $class = ClassUtils::getClass($entity);
330
        $metadata = $this->getMetadata($class);
331
        $platform = $this->getEntityManager($class)->getConnection()->getDatabasePlatform();
332
333
        $identifiers = [];
334
335
        foreach ($metadata->getIdentifierValues($entity) as $name => $value) {
336
            if (!\is_object($value)) {
337
                $identifiers[] = $value;
338
339
                continue;
340
            }
341
342
            if (method_exists($value, '__toString')) {
343
                $identifiers[] = (string) $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[] = $type->convertToDatabaseValue($value, $platform);
352
353
                continue;
354
            }
355
356
            $metadata = $this->getMetadata(ClassUtils::getClass($value));
357
358
            foreach ($metadata->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 (is_scalar($entity)) {
374
            throw new \RuntimeException('Invalid argument, object or null required');
375
        }
376
377
        if (!$entity) {
378
            return;
379
        }
380
381
        if (\in_array($this->getEntityManager($entity)->getUnitOfWork()->getEntityState($entity), [
382
            UnitOfWork::STATE_NEW,
383
            UnitOfWork::STATE_REMOVED,
384
        ], true)) {
385
            return;
386
        }
387
388
        $values = $this->getIdentifierValues($entity);
389
390
        if (0 === \count($values)) {
391
            return;
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): void
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): void
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);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return new \Exporter\Sou...rator($query, $fields); (Exporter\Source\DoctrineORMQuerySourceIterator) is incompatible with the return type declared by the interface Sonata\AdminBundle\Model...::getDataSourceIterator of type Exporter\Source\SourceIteratorInterface.

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...
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
        return new $class();
496
    }
497
498
    public function getSortParameters(FieldDescriptionInterface $fieldDescription, DatagridInterface $datagrid)
499
    {
500
        $values = $datagrid->getValues();
501
502
        if ($fieldDescription->getName() == $values['_sort_by']->getName() || $values['_sort_by']->getName() === $fieldDescription->getOption('sortable')) {
503
            if ('ASC' == $values['_sort_order']) {
504
                $values['_sort_order'] = 'DESC';
505
            } else {
506
                $values['_sort_order'] = 'ASC';
507
            }
508
        } else {
509
            $values['_sort_order'] = 'ASC';
510
        }
511
512
        $values['_sort_by'] = \is_string($fieldDescription->getOption('sortable')) ? $fieldDescription->getOption('sortable') : $fieldDescription->getName();
513
514
        return ['filter' => $values];
515
    }
516
517
    public function getPaginationParameters(DatagridInterface $datagrid, $page)
518
    {
519
        $values = $datagrid->getValues();
520
521
        $values['_sort_by'] = $values['_sort_by']->getName();
522
        $values['_page'] = $page;
523
524
        return ['filter' => $values];
525
    }
526
527
    public function getDefaultSortValues($class)
528
    {
529
        return [
530
            '_sort_order' => 'ASC',
531
            '_sort_by' => implode(',', $this->getModelIdentifier($class)),
532
            '_page' => 1,
533
            '_per_page' => 25,
534
        ];
535
    }
536
537
    public function modelTransform($class, $instance)
538
    {
539
        return $instance;
540
    }
541
542
    public function modelReverseTransform($class, array $array = [])
543
    {
544
        $instance = $this->getModelInstance($class);
545
        $metadata = $this->getMetadata($class);
546
547
        $reflClass = $metadata->reflClass;
548
        foreach ($array as $name => $value) {
549
            $reflection_property = false;
550
            // property or association ?
551
            if (array_key_exists($name, $metadata->fieldMappings)) {
552
                $property = $metadata->fieldMappings[$name]['fieldName'];
553
                $reflection_property = $metadata->reflFields[$name];
554
            } elseif (array_key_exists($name, $metadata->associationMappings)) {
555
                $property = $metadata->associationMappings[$name]['fieldName'];
556
            } else {
557
                $property = $name;
558
            }
559
560
            $setter = 'set'.$this->camelize($name);
561
562
            if ($reflClass->hasMethod($setter)) {
563
                if (!$reflClass->getMethod($setter)->isPublic()) {
564
                    throw new PropertyAccessDeniedException(sprintf(
565
                        'Method "%s()" is not public in class "%s"',
566
                        $setter,
567
                        $reflClass->getName()
568
                    ));
569
                }
570
571
                $instance->$setter($value);
572
            } elseif ($reflClass->hasMethod('__set')) {
573
                // needed to support magic method __set
574
                $instance->$property = $value;
575
            } elseif ($reflClass->hasProperty($property)) {
576
                if (!$reflClass->getProperty($property)->isPublic()) {
577
                    throw new PropertyAccessDeniedException(sprintf(
578
                        'Property "%s" is not public in class "%s". Maybe you should create the method "set%s()"?',
579
                            $property,
580
                            $reflClass->getName(),
581
                            ucfirst($property)
582
                    ));
583
                }
584
585
                $instance->$property = $value;
586
            } elseif ($reflection_property) {
587
                $reflection_property->setValue($instance, $value);
588
            }
589
        }
590
591
        return $instance;
592
    }
593
594
    public function getModelCollectionInstance($class)
595
    {
596
        return new \Doctrine\Common\Collections\ArrayCollection();
597
    }
598
599
    public function collectionClear(&$collection)
600
    {
601
        return $collection->clear();
602
    }
603
604
    public function collectionHasElement(&$collection, &$element)
605
    {
606
        return $collection->contains($element);
607
    }
608
609
    public function collectionAddElement(&$collection, &$element)
610
    {
611
        return $collection->add($element);
612
    }
613
614
    public function collectionRemoveElement(&$collection, &$element)
615
    {
616
        return $collection->removeElement($element);
617
    }
618
619
    /**
620
     * method taken from Symfony\Component\PropertyAccess\PropertyAccessor.
621
     *
622
     * @param string $property
623
     *
624
     * @return mixed
625
     */
626
    protected function camelize($property)
627
    {
628
        return str_replace(' ', '', ucwords(str_replace('_', ' ', $property)));
629
    }
630
}
631