Completed
Pull Request — 3.x (#368)
by
unknown
01:16
created

ModelManager::getPaginationParameters()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 11
rs 9.9
c 0
b 0
f 0
cc 3
nc 2
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\DoctrineMongoDBAdminBundle\Model;
15
16
use Doctrine\Common\Collections\ArrayCollection;
17
use Doctrine\ODM\MongoDB\Query\Builder;
18
use Doctrine\Persistence\Mapping\ClassMetadata;
19
use Sonata\AdminBundle\Admin\FieldDescriptionInterface;
20
use Sonata\AdminBundle\Datagrid\DatagridInterface;
21
use Sonata\AdminBundle\Datagrid\ProxyQueryInterface;
22
use Sonata\AdminBundle\Model\ModelManagerInterface;
23
use Sonata\DoctrineMongoDBAdminBundle\Admin\FieldDescription;
24
use Sonata\DoctrineMongoDBAdminBundle\Datagrid\ProxyQuery;
25
use Sonata\Exporter\Source\DoctrineODMQuerySourceIterator;
26
use Symfony\Bridge\Doctrine\ManagerRegistry;
27
use Symfony\Component\PropertyAccess\PropertyAccess;
28
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
29
30
class ModelManager implements ModelManagerInterface
31
{
32
    public const ID_SEPARATOR = '-';
33
34
    /**
35
     * @var ManagerRegistry
36
     */
37
    protected $registry;
38
39
    /**
40
     * @var PropertyAccessorInterface
41
     */
42
    private $propertyAccessor;
43
44
    public function __construct(ManagerRegistry $registry, ?PropertyAccessorInterface $propertyAccessor = null)
45
    {
46
        $this->registry = $registry;
47
        $this->propertyAccessor = $propertyAccessor ?? PropertyAccess::createPropertyAccessor();
48
    }
49
50
    /**
51
     * {@inheritdoc}
52
     */
53
    public function getMetadata($class)
54
    {
55
        return $this->getDocumentManager($class)->getMetadataFactory()->getMetadataFor($class);
56
    }
57
58
    /**
59
     * Returns the model's metadata holding the fully qualified property, and the last
60
     * property name.
61
     *
62
     * @param string $baseClass        The base class of the model holding the fully qualified property
63
     * @param string $propertyFullName The name of the fully qualified property (dot ('.') separated
64
     *                                 property string)
65
     *
66
     * @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...
67
     *                \Doctrine\ODM\MongoDB\Mapping\ClassMetadata $parentMetadata,
68
     *                string $lastPropertyName,
69
     *                array $parentAssociationMappings
70
     *                )
71
     */
72
    public function getParentMetadataForProperty($baseClass, $propertyFullName)
73
    {
74
        $nameElements = explode('.', $propertyFullName);
75
        $lastPropertyName = array_pop($nameElements);
76
        $class = $baseClass;
77
        $parentAssociationMappings = [];
78
79
        foreach ($nameElements as $nameElement) {
80
            $metadata = $this->getMetadata($class);
81
            $parentAssociationMappings[] = $metadata->associationMappings[$nameElement];
82
            $class = $metadata->getAssociationTargetClass($nameElement);
83
        }
84
85
        return [$this->getMetadata($class), $lastPropertyName, $parentAssociationMappings];
86
    }
87
88
    /**
89
     * {@inheritdoc}
90
     */
91
    public function hasMetadata($class)
92
    {
93
        return $this->getDocumentManager($class)->getMetadataFactory()->hasMetadataFor($class);
94
    }
95
96
    /**
97
     * {@inheritdoc}
98
     */
99
    public function getNewFieldDescriptionInstance($class, $name, array $options = [])
100
    {
101
        if (!\is_string($name)) {
102
            throw new \RuntimeException('The name argument must be a string');
103
        }
104
105
        if (!isset($options['route']['name'])) {
106
            $options['route']['name'] = 'edit';
107
        }
108
109
        if (!isset($options['route']['parameters'])) {
110
            $options['route']['parameters'] = [];
111
        }
112
113
        list($metadata, $propertyName, $parentAssociationMappings) = $this->getParentMetadataForProperty($class, $name);
114
115
        $fieldDescription = new FieldDescription();
116
        $fieldDescription->setName($name);
117
        $fieldDescription->setOptions($options);
118
        $fieldDescription->setParentAssociationMappings($parentAssociationMappings);
119
120
        /* @var ClassMetadata */
121
        if (isset($metadata->associationMappings[$propertyName])) {
122
            $fieldDescription->setAssociationMapping($metadata->associationMappings[$propertyName]);
123
        }
124
125
        if (isset($metadata->fieldMappings[$propertyName])) {
126
            $fieldDescription->setFieldMapping($metadata->fieldMappings[$propertyName]);
127
        }
128
129
        return $fieldDescription;
130
    }
131
132
    /**
133
     * {@inheritdoc}
134
     */
135
    public function create($object)
136
    {
137
        $documentManager = $this->getDocumentManager($object);
138
        $documentManager->persist($object);
139
        $documentManager->flush();
140
    }
141
142
    /**
143
     * {@inheritdoc}
144
     */
145
    public function update($object)
146
    {
147
        $documentManager = $this->getDocumentManager($object);
148
        $documentManager->persist($object);
149
        $documentManager->flush();
150
    }
151
152
    /**
153
     * {@inheritdoc}
154
     */
155
    public function delete($object)
156
    {
157
        $documentManager = $this->getDocumentManager($object);
158
        $documentManager->remove($object);
159
        $documentManager->flush();
160
    }
161
162
    /**
163
     * {@inheritdoc}
164
     */
165
    public function find($class, $id)
166
    {
167
        if (!isset($id)) {
168
            return null;
169
        }
170
171
        $documentManager = $this->getDocumentManager($class);
172
173
        if (is_numeric($id)) {
174
            $value = $documentManager->getRepository($class)->find((int) $id);
175
176
            if (!empty($value)) {
177
                return $value;
178
            }
179
        }
180
181
        return $documentManager->getRepository($class)->find($id);
182
    }
183
184
    /**
185
     * {@inheritdoc}
186
     */
187
    public function findBy($class, array $criteria = [])
188
    {
189
        return $this->getDocumentManager($class)->getRepository($class)->findBy($criteria);
190
    }
191
192
    /**
193
     * {@inheritdoc}
194
     */
195
    public function findOneBy($class, array $criteria = [])
196
    {
197
        return $this->getDocumentManager($class)->getRepository($class)->findOneBy($criteria);
198
    }
199
200
    /**
201
     * @param object|string $class
202
     *
203
     * @throw \RuntimeException
204
     *
205
     * @return \Doctrine\ODM\MongoDB\DocumentManager
206
     */
207
    public function getDocumentManager($class)
208
    {
209
        if (\is_object($class)) {
210
            $class = \get_class($class);
211
        }
212
213
        $dm = $this->registry->getManagerForClass($class);
214
215
        if (!$dm) {
216
            throw new \RuntimeException(sprintf('No document manager defined for class %s', $class));
217
        }
218
219
        return $dm;
220
    }
221
222
    /**
223
     * {@inheritdoc}
224
     */
225
    public function getParentFieldDescription($parentAssociationMapping, $class)
226
    {
227
        $fieldName = $parentAssociationMapping['fieldName'];
228
229
        $metadata = $this->getMetadata($class);
230
231
        $associatingMapping = $metadata->associationMappings[$parentAssociationMapping];
232
233
        $fieldDescription = $this->getNewFieldDescriptionInstance($class, $fieldName);
234
        $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...
235
        $fieldDescription->setAssociationMapping($associatingMapping);
236
237
        return $fieldDescription;
238
    }
239
240
    /**
241
     * {@inheritdoc}
242
     */
243
    public function createQuery($class, $alias = 'o')
244
    {
245
        $repository = $this->getDocumentManager($class)->getRepository($class);
246
247
        return new ProxyQuery($repository->createQueryBuilder());
248
    }
249
250
    /**
251
     * {@inheritdoc}
252
     */
253
    public function executeQuery($query)
254
    {
255
        if ($query instanceof Builder) {
256
            return $query->getQuery()->execute();
257
        }
258
259
        return $query->execute();
260
    }
261
262
    /**
263
     * {@inheritdoc}
264
     */
265
    public function getModelIdentifier($class)
266
    {
267
        return $this->getMetadata($class)->identifier;
268
    }
269
270
    /**
271
     * {@inheritdoc}
272
     */
273
    public function getIdentifierValues($document)
274
    {
275
        return [$this->getDocumentManager($document)->getUnitOfWork()->getDocumentIdentifier($document)];
276
    }
277
278
    /**
279
     * {@inheritdoc}
280
     */
281
    public function getIdentifierFieldNames($class)
282
    {
283
        return [$this->getMetadata($class)->getIdentifier()];
284
    }
285
286
    /**
287
     * {@inheritdoc}
288
     */
289
    public function getNormalizedIdentifier($document)
290
    {
291
        if (null === $document) {
292
            return null;
293
        }
294
295
        if (!\is_object($document)) {
296
            throw new \RuntimeException('Invalid argument, object or null required');
297
        }
298
299
        // the document is not managed
300
        if (!$this->getDocumentManager($document)->getUnitOfWork()->isInIdentityMap($document)) {
301
            return null;
302
        }
303
304
        $values = $this->getIdentifierValues($document);
305
306
        return implode(self::ID_SEPARATOR, $values);
307
    }
308
309
    /**
310
     * {@inheritdoc}
311
     */
312
    public function getUrlSafeIdentifier($document)
313
    {
314
        return $this->getNormalizedIdentifier($document);
315
    }
316
317
    /**
318
     * {@inheritdoc}
319
     */
320
    public function addIdentifiersToQuery($class, ProxyQueryInterface $queryProxy, array $idx)
321
    {
322
        $queryBuilder = $queryProxy->getQueryBuilder();
323
        $queryBuilder->field('_id')->in($idx);
324
    }
325
326
    /**
327
     * {@inheritdoc}
328
     */
329
    public function batchDelete($class, ProxyQueryInterface $queryProxy)
330
    {
331
        /** @var Query $queryBuilder */
332
        $queryBuilder = $queryProxy->getQuery();
333
334
        $documentManager = $this->getDocumentManager($class);
335
336
        $i = 0;
337
        foreach ($queryBuilder->execute() as $object) {
338
            $documentManager->remove($object);
339
340
            if (0 === (++$i % 20)) {
341
                $documentManager->flush();
342
                $documentManager->clear();
343
            }
344
        }
345
346
        $documentManager->flush();
347
        $documentManager->clear();
348
    }
349
350
    /**
351
     * {@inheritdoc}
352
     */
353
    public function getDataSourceIterator(DatagridInterface $datagrid, array $fields, $firstResult = null, $maxResult = null)
354
    {
355
        $datagrid->buildPager();
356
        $query = $datagrid->getQuery();
357
358
        $query->setFirstResult($firstResult);
359
        $query->setMaxResults($maxResult);
360
361
        return new DoctrineODMQuerySourceIterator($query instanceof ProxyQuery ? $query->getQuery() : $query, $fields);
0 ignored issues
show
Bug introduced by
The method getQuery() does not exist on Sonata\DoctrineMongoDBAd...dle\Datagrid\ProxyQuery. Did you maybe mean getQueryBuilder()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
362
    }
363
364
    /**
365
     * {@inheritdoc}
366
     */
367
    public function getExportFields($class)
368
    {
369
        $metadata = $this->getDocumentManager($class)->getClassMetadata($class);
370
371
        return $metadata->getFieldNames();
372
    }
373
374
    /**
375
     * {@inheritdoc}
376
     */
377
    public function getModelInstance($class)
378
    {
379
        if (!class_exists($class)) {
380
            throw new \InvalidArgumentException(sprintf('Class "%s" not found', $class));
381
        }
382
383
        $r = new \ReflectionClass($class);
384
        if ($r->isAbstract()) {
385
            throw new \InvalidArgumentException(sprintf('Cannot initialize abstract class: %s', $class));
386
        }
387
388
        $constructor = $r->getConstructor();
389
390
        if (null !== $constructor && (!$constructor->isPublic() || $constructor->getNumberOfRequiredParameters() > 0)) {
391
            return $r->newInstanceWithoutConstructor();
392
        }
393
394
        return new $class();
395
    }
396
397
    /**
398
     * {@inheritdoc}
399
     */
400
    public function getSortParameters(FieldDescriptionInterface $fieldDescription, DatagridInterface $datagrid)
401
    {
402
        $values = $datagrid->getValues();
403
404
        if ($this->isFieldAlreadySorted($fieldDescription, $datagrid)) {
405
            if ('ASC' === $values['_sort_order']) {
406
                $values['_sort_order'] = 'DESC';
407
            } else {
408
                $values['_sort_order'] = 'ASC';
409
            }
410
        } else {
411
            $values['_sort_order'] = 'ASC';
412
        }
413
414
        $values['_sort_by'] = \is_string($fieldDescription->getOption('sortable')) ? $fieldDescription->getOption('sortable') : $fieldDescription->getName();
415
416
        return ['filter' => $values];
417
    }
418
419
    /**
420
     * {@inheritdoc}
421
     */
422
    public function getPaginationParameters(DatagridInterface $datagrid, $page)
423
    {
424
        $values = $datagrid->getValues();
425
426
        if (isset($values['_sort_by']) && $values['_sort_by'] instanceof FieldDescriptionInterface) {
427
            $values['_sort_by'] = $values['_sort_by']->getName();
428
        }
429
        $values['_page'] = $page;
430
431
        return ['filter' => $values];
432
    }
433
434
    /**
435
     * {@inheritdoc}
436
     */
437
    public function getDefaultSortValues($class)
438
    {
439
        return [
440
            '_sort_order' => 'ASC',
441
            '_sort_by' => $this->getModelIdentifier($class),
442
            '_page' => 1,
443
            '_per_page' => 25,
444
        ];
445
    }
446
447
    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...
448
    {
449
        return [10, 25, 50, 100, 250];
450
    }
451
452
    /**
453
     * {@inheritdoc}
454
     */
455
    public function modelTransform($class, $instance)
456
    {
457
        return $instance;
458
    }
459
460
    /**
461
     * {@inheritdoc}
462
     */
463
    public function modelReverseTransform($class, array $array = [])
464
    {
465
        $instance = $this->getModelInstance($class);
466
        $metadata = $this->getMetadata($class);
467
468
        foreach ($array as $name => $value) {
469
            $property = $this->getFieldName($metadata, $name);
470
471
            $this->propertyAccessor->setValue($instance, $property, $value);
472
        }
473
474
        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...
475
    }
476
477
    /**
478
     * {@inheritdoc}
479
     */
480
    public function getModelCollectionInstance($class)
481
    {
482
        return new ArrayCollection();
483
    }
484
485
    /**
486
     * {@inheritdoc}
487
     */
488
    public function collectionClear(&$collection)
489
    {
490
        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...
491
    }
492
493
    /**
494
     * {@inheritdoc}
495
     */
496
    public function collectionHasElement(&$collection, &$element)
497
    {
498
        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...
499
    }
500
501
    /**
502
     * {@inheritdoc}
503
     */
504
    public function collectionAddElement(&$collection, &$element)
505
    {
506
        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...
507
    }
508
509
    /**
510
     * {@inheritdoc}
511
     */
512
    public function collectionRemoveElement(&$collection, &$element)
513
    {
514
        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...
515
    }
516
517
    /**
518
     * NEXT_MAJOR: Remove this method.
519
     *
520
     * @deprecated since sonata-project/doctrine-mongodb-admin-bundle 3.x, to be removed in 4.0.'.
521
     *
522
     * @param string $property
523
     *
524
     * @return mixed
525
     */
526
    protected function camelize($property)
527
    {
528
        @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...
529
            'Method "%s()" is deprecated since sonata-project/doctrine-mongodb-admin-bundle 3.x and will be removed in version 4.0.',
530
            __METHOD__
531
        ), E_USER_DEPRECATED);
532
533
        return str_replace(' ', '', ucwords(str_replace('_', ' ', $property)));
534
    }
535
536
    private function getFieldName(ClassMetadata $metadata, string $name): string
537
    {
538
        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...
539
            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...
540
        }
541
542
        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...
543
            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...
544
        }
545
546
        return $name;
547
    }
548
549
    private function isFieldAlreadySorted(FieldDescriptionInterface $fieldDescription, DatagridInterface $datagrid): bool
550
    {
551
        $values = $datagrid->getValues();
552
553
        if (!isset($values['_sort_by']) || !$values['_sort_by'] instanceof FieldDescriptionInterface) {
554
            return false;
555
        }
556
557
        return $values['_sort_by']->getName() === $fieldDescription->getName()
558
            || $values['_sort_by']->getName() === $fieldDescription->getOption('sortable');
559
    }
560
}
561