Completed
Push — master ( 1c10e3...d08b1d )
by
unknown
08:13
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\DoctrineMongoDBAdminBundle\Model;
15
16
use Doctrine\ODM\MongoDB\Query\Builder;
17
use Exporter\Source\DoctrineODMQuerySourceIterator;
18
use Sonata\AdminBundle\Admin\FieldDescriptionInterface;
19
use Sonata\AdminBundle\Datagrid\DatagridInterface;
20
use Sonata\AdminBundle\Datagrid\ProxyQueryInterface;
21
use Sonata\AdminBundle\Model\ModelManagerInterface;
22
use Sonata\DoctrineMongoDBAdminBundle\Admin\FieldDescription;
23
use Sonata\DoctrineMongoDBAdminBundle\Datagrid\ProxyQuery;
24
use Symfony\Bridge\Doctrine\ManagerRegistry;
25
use Symfony\Component\Form\Exception\PropertyAccessDeniedException;
26
27
class ModelManager implements ModelManagerInterface
28
{
29
    public const ID_SEPARATOR = '-';
30
    protected $registry;
31
32
    /**
33
     * @param \Symfony\Bridge\Doctrine\ManagerRegistry $registry
34
     */
35
    public function __construct(ManagerRegistry $registry)
36
    {
37
        $this->registry = $registry;
38
    }
39
40
    /**
41
     * {@inheritdoc}
42
     */
43
    public function getMetadata($class)
44
    {
45
        return $this->getDocumentManager($class)->getMetadataFactory()->getMetadataFor($class);
46
    }
47
48
    /**
49
     * Returns the model's metadata holding the fully qualified property, and the last
50
     * property name.
51
     *
52
     * @param string $baseClass        The base class of the model holding the fully qualified property
53
     * @param string $propertyFullName The name of the fully qualified property (dot ('.') separated
54
     *                                 property string)
55
     *
56
     * @return array(
57
     *                \Doctrine\ODM\MongoDB\Mapping\ClassMetadata $parentMetadata,
58
     *                string $lastPropertyName,
59
     *                array $parentAssociationMappings
60
     *                )
61
     */
62
    public function getParentMetadataForProperty($baseClass, $propertyFullName)
63
    {
64
        $nameElements = explode('.', $propertyFullName);
65
        $lastPropertyName = array_pop($nameElements);
66
        $class = $baseClass;
67
        $parentAssociationMappings = [];
68
69
        foreach ($nameElements as $nameElement) {
70
            $metadata = $this->getMetadata($class);
71
            $parentAssociationMappings[] = $metadata->associationMappings[$nameElement];
72
            $class = $metadata->getAssociationTargetClass($nameElement);
73
        }
74
75
        return [$this->getMetadata($class), $lastPropertyName, $parentAssociationMappings];
76
    }
77
78
    /**
79
     * {@inheritdoc}
80
     */
81
    public function hasMetadata($class)
82
    {
83
        return $this->getDocumentManager($class)->getMetadataFactory()->hasMetadataFor($class);
84
    }
85
86
    /**
87
     * {@inheritdoc}
88
     */
89
    public function getNewFieldDescriptionInstance($class, $name, array $options = [])
90
    {
91
        if (!is_string($name)) {
92
            throw new \RunTimeException('The name argument must be a string');
93
        }
94
95
        if (!isset($options['route']['name'])) {
96
            $options['route']['name'] = 'edit';
97
        }
98
99
        if (!isset($options['route']['parameters'])) {
100
            $options['route']['parameters'] = [];
101
        }
102
103
        list($metadata, $propertyName, $parentAssociationMappings) = $this->getParentMetadataForProperty($class, $name);
104
105
        $fieldDescription = new FieldDescription();
106
        $fieldDescription->setName($name);
107
        $fieldDescription->setOptions($options);
108
        $fieldDescription->setParentAssociationMappings($parentAssociationMappings);
109
110
        /* @var ClassMetadata */
111
        if (isset($metadata->associationMappings[$propertyName])) {
112
            $fieldDescription->setAssociationMapping($metadata->associationMappings[$propertyName]);
113
        }
114
115
        if (isset($metadata->fieldMappings[$propertyName])) {
116
            $fieldDescription->setFieldMapping($metadata->fieldMappings[$propertyName]);
117
        }
118
119
        return $fieldDescription;
120
    }
121
122
    /**
123
     * {@inheritdoc}
124
     */
125
    public function create($object): void
126
    {
127
        $documentManager = $this->getDocumentManager($object);
128
        $documentManager->persist($object);
129
        $documentManager->flush();
130
    }
131
132
    /**
133
     * {@inheritdoc}
134
     */
135
    public function update($object): void
136
    {
137
        $documentManager = $this->getDocumentManager($object);
138
        $documentManager->persist($object);
139
        $documentManager->flush();
140
    }
141
142
    /**
143
     * {@inheritdoc}
144
     */
145
    public function delete($object): void
146
    {
147
        $documentManager = $this->getDocumentManager($object);
148
        $documentManager->remove($object);
149
        $documentManager->flush();
150
    }
151
152
    /**
153
     * {@inheritdoc}
154
     */
155
    public function find($class, $id)
156
    {
157
        if (!isset($id)) {
158
            return;
159
        }
160
161
        $documentManager = $this->getDocumentManager($class);
162
163
        if (is_numeric($id)) {
164
            $value = $documentManager->getRepository($class)->find((int) $id);
165
166
            if (!empty($value)) {
167
                return $value;
168
            }
169
        }
170
171
        return $documentManager->getRepository($class)->find($id);
172
    }
173
174
    /**
175
     * {@inheritdoc}
176
     */
177
    public function findBy($class, array $criteria = [])
178
    {
179
        return $this->getDocumentManager($class)->getRepository($class)->findBy($criteria);
180
    }
181
182
    /**
183
     * {@inheritdoc}
184
     */
185
    public function findOneBy($class, array $criteria = [])
186
    {
187
        return $this->getDocumentManager($class)->getRepository($class)->findOneBy($criteria);
188
    }
189
190
    /**
191
     * @param string $class
192
     *
193
     * @throw \RuntimeException
194
     *
195
     * @return \Doctrine\ODM\MongoDB\DocumentManager
196
     */
197
    public function getDocumentManager($class)
198
    {
199
        if (is_object($class)) {
200
            $class = get_class($class);
201
        }
202
203
        $dm = $this->registry->getManagerForClass($class);
204
205
        if (!$dm) {
206
            throw new \RuntimeException(sprintf('No document manager defined for class %s', $class));
207
        }
208
209
        return $dm;
210
    }
211
212
    /**
213
     * {@inheritdoc}
214
     */
215
    public function getParentFieldDescription($parentAssociationMapping, $class)
216
    {
217
        $fieldName = $parentAssociationMapping['fieldName'];
218
219
        $metadata = $this->getMetadata($class);
220
221
        $associatingMapping = $metadata->associationMappings[$parentAssociationMapping];
222
223
        $fieldDescription = $this->getNewFieldDescriptionInstance($class, $fieldName);
224
        $fieldDescription->setName($parentAssociationMapping);
225
        $fieldDescription->setAssociationMapping($associatingMapping);
226
227
        return $fieldDescription;
228
    }
229
230
    /**
231
     * {@inheritdoc}
232
     */
233
    public function createQuery($class, $alias = 'o')
234
    {
235
        $repository = $this->getDocumentManager($class)->getRepository($class);
236
237
        return new ProxyQuery($repository->createQueryBuilder());
238
    }
239
240
    /**
241
     * {@inheritdoc}
242
     */
243
    public function executeQuery($query)
244
    {
245
        if ($query instanceof Builder) {
246
            return $query->getQuery()->execute();
247
        }
248
249
        return $query->execute();
250
    }
251
252
    /**
253
     * {@inheritdoc}
254
     */
255
    public function getModelIdentifier($class)
256
    {
257
        return $this->getMetadata($class)->identifier;
258
    }
259
260
    /**
261
     * {@inheritdoc}
262
     */
263
    public function getIdentifierValues($document)
264
    {
265
        return [$this->getDocumentManager($document)->getUnitOfWork()->getDocumentIdentifier($document)];
266
    }
267
268
    /**
269
     * {@inheritdoc}
270
     */
271
    public function getIdentifierFieldNames($class)
272
    {
273
        return [$this->getMetadata($class)->getIdentifier()];
274
    }
275
276
    /**
277
     * {@inheritdoc}
278
     */
279
    public function getNormalizedIdentifier($document)
280
    {
281
        if (is_scalar($document)) {
282
            throw new \RunTimeException('Invalid argument, object or null required');
283
        }
284
285
        // the entities is not managed
286
        if (!$document || !$this->getDocumentManager($document)->getUnitOfWork()->isInIdentityMap($document)) {
287
            return;
288
        }
289
290
        $values = $this->getIdentifierValues($document);
291
292
        return implode(self::ID_SEPARATOR, $values);
293
    }
294
295
    /**
296
     * {@inheritdoc}
297
     */
298
    public function getUrlsafeIdentifier($entity)
299
    {
300
        return $this->getNormalizedIdentifier($entity);
301
    }
302
303
    /**
304
     * {@inheritdoc}
305
     */
306
    public function addIdentifiersToQuery($class, ProxyQueryInterface $queryProxy, array $idx): void
307
    {
308
        $queryBuilder = $queryProxy->getQueryBuilder();
309
        $queryBuilder->field('_id')->in($idx);
310
    }
311
312
    /**
313
     * {@inheritdoc}
314
     */
315
    public function batchDelete($class, ProxyQueryInterface $queryProxy): void
316
    {
317
        /** @var Query $queryBuilder */
318
        $queryBuilder = $queryProxy->getQuery();
319
320
        $documentManager = $this->getDocumentManager($class);
321
322
        $i = 0;
323
        foreach ($queryBuilder->execute() as $object) {
324
            $documentManager->remove($object);
325
326
            if (0 == (++$i % 20)) {
327
                $documentManager->flush();
328
                $documentManager->clear();
329
            }
330
        }
331
332
        $documentManager->flush();
333
        $documentManager->clear();
334
    }
335
336
    /**
337
     * {@inheritdoc}
338
     */
339
    public function getDataSourceIterator(DatagridInterface $datagrid, array $fields, $firstResult = null, $maxResult = null)
340
    {
341
        $datagrid->buildPager();
342
        $query = $datagrid->getQuery();
343
344
        $query->setFirstResult($firstResult);
345
        $query->setMaxResults($maxResult);
346
347
        return new DoctrineODMQuerySourceIterator($query instanceof ProxyQuery ? $query->getQuery() : $query, $fields);
348
    }
349
350
    /**
351
     * {@inheritdoc}
352
     */
353
    public function getExportFields($class)
354
    {
355
        $metadata = $this->getDocumentManager($class)->getClassMetadata($class);
356
357
        return $metadata->getFieldNames();
358
    }
359
360
    /**
361
     * {@inheritdoc}
362
     */
363
    public function getModelInstance($class)
364
    {
365
        return new $class();
366
    }
367
368
    /**
369
     * {@inheritdoc}
370
     */
371
    public function getSortParameters(FieldDescriptionInterface $fieldDescription, DatagridInterface $datagrid)
372
    {
373
        $values = $datagrid->getValues();
374
375
        if ($fieldDescription->getName() == $values['_sort_by']->getName() || $values['_sort_by']->getName() === $fieldDescription->getOption('sortable')) {
376
            if ('ASC' == $values['_sort_order']) {
377
                $values['_sort_order'] = 'DESC';
378
            } else {
379
                $values['_sort_order'] = 'ASC';
380
            }
381
        } else {
382
            $values['_sort_order'] = 'ASC';
383
        }
384
385
        $values['_sort_by'] = is_string($fieldDescription->getOption('sortable')) ? $fieldDescription->getOption('sortable') : $fieldDescription->getName();
386
387
        return ['filter' => $values];
388
    }
389
390
    /**
391
     * {@inheritdoc}
392
     */
393
    public function getPaginationParameters(DatagridInterface $datagrid, $page)
394
    {
395
        $values = $datagrid->getValues();
396
397
        $values['_sort_by'] = $values['_sort_by']->getName();
398
        $values['_page'] = $page;
399
400
        return ['filter' => $values];
401
    }
402
403
    /**
404
     * {@inheritdoc}
405
     */
406
    public function getDefaultSortValues($class)
407
    {
408
        return [
409
            '_sort_order' => 'ASC',
410
            '_sort_by' => $this->getModelIdentifier($class),
411
            '_page' => 1,
412
            '_per_page' => 25,
413
        ];
414
    }
415
416
    /**
417
     * {@inheritdoc}
418
     */
419
    public function modelTransform($class, $instance)
420
    {
421
        return $instance;
422
    }
423
424
    /**
425
     * {@inheritdoc}
426
     */
427
    public function modelReverseTransform($class, array $array = [])
428
    {
429
        $instance = $this->getModelInstance($class);
430
        $metadata = $this->getMetadata($class);
431
432
        $reflClass = $metadata->reflClass;
433
        foreach ($array as $name => $value) {
434
            $reflection_property = false;
435
            // property or association ?
436
            if (array_key_exists($name, $metadata->fieldMappings)) {
437
                $property = $metadata->fieldMappings[$name]['fieldName'];
438
                $reflection_property = $metadata->reflFields[$name];
439
            } elseif (array_key_exists($name, $metadata->associationMappings)) {
440
                $property = $metadata->associationMappings[$name]['fieldName'];
441
            } else {
442
                $property = $name;
443
            }
444
445
            $setter = 'set'.$this->camelize($name);
446
447
            if ($reflClass->hasMethod($setter)) {
448
                if (!$reflClass->getMethod($setter)->isPublic()) {
449
                    throw new PropertyAccessDeniedException(sprintf('Method "%s()" is not public in class "%s"', $setter, $reflClass->getName()));
450
                }
451
452
                $instance->$setter($value);
453
            } elseif ($reflClass->hasMethod('__set')) {
454
                // needed to support magic method __set
455
                $instance->$property = $value;
456
            } elseif ($reflClass->hasProperty($property)) {
457
                if (!$reflClass->getProperty($property)->isPublic()) {
458
                    throw new PropertyAccessDeniedException(sprintf('Property "%s" is not public in class "%s". Maybe you should create the method "set%s()"?', $property, $reflClass->getName(), ucfirst($property)));
459
                }
460
461
                $instance->$property = $value;
462
            } elseif ($reflection_property) {
463
                $reflection_property->setValue($instance, $value);
464
            }
465
        }
466
467
        return $instance;
468
    }
469
470
    /**
471
     * {@inheritdoc}
472
     */
473
    public function getModelCollectionInstance($class)
474
    {
475
        return new \Doctrine\Common\Collections\ArrayCollection();
0 ignored issues
show
Bug Best Practice introduced by
The return type of return new \Doctrine\Com...ions\ArrayCollection(); (Doctrine\Common\Collections\ArrayCollection) is incompatible with the return type declared by the interface Sonata\AdminBundle\Model...ModelCollectionInstance of type array.

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...
476
    }
477
478
    /**
479
     * {@inheritdoc}
480
     */
481
    public function collectionClear(&$collection)
482
    {
483
        return $collection->clear();
484
    }
485
486
    /**
487
     * {@inheritdoc}
488
     */
489
    public function collectionHasElement(&$collection, &$element)
490
    {
491
        return $collection->contains($element);
492
    }
493
494
    /**
495
     * {@inheritdoc}
496
     */
497
    public function collectionAddElement(&$collection, &$element)
498
    {
499
        return $collection->add($element);
500
    }
501
502
    /**
503
     * {@inheritdoc}
504
     */
505
    public function collectionRemoveElement(&$collection, &$element)
506
    {
507
        return $collection->removeElement($element);
508
    }
509
510
    /**
511
     * method taken from PropertyPath.
512
     *
513
     * @param string $property
514
     *
515
     * @return mixed
516
     */
517
    protected function camelize($property)
518
    {
519
        return preg_replace(['/(^|_)+(.)/e', '/\.(.)/e'], ["strtoupper('\\2')", "'_'.strtoupper('\\1')"], $property);
520
    }
521
}
522