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

ModelManager::collectionAddElement()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nc 1
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\Common\Persistence\Mapping\ClassMetadata as CommonClassMetadata;
18
use Doctrine\ODM\MongoDB\Query\Builder;
19
use Doctrine\Persistence\Mapping\ClassMetadata;
20
use Sonata\AdminBundle\Admin\FieldDescriptionInterface;
21
use Sonata\AdminBundle\Datagrid\DatagridInterface;
22
use Sonata\AdminBundle\Datagrid\ProxyQueryInterface;
23
use Sonata\AdminBundle\Model\ModelManagerInterface;
24
use Sonata\DoctrineMongoDBAdminBundle\Admin\FieldDescription;
25
use Sonata\DoctrineMongoDBAdminBundle\Datagrid\ProxyQuery;
26
use Sonata\Exporter\Source\DoctrineODMQuerySourceIterator;
27
use Symfony\Bridge\Doctrine\ManagerRegistry;
28
use Symfony\Component\PropertyAccess\PropertyAccess;
29
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
30
31
class ModelManager implements ModelManagerInterface
32
{
33
    public const ID_SEPARATOR = '-';
34
35
    /**
36
     * @var ManagerRegistry
37
     */
38
    protected $registry;
39
40
    /**
41
     * @var PropertyAccessorInterface
42
     */
43
    private $propertyAccessor;
44
45
    /**
46
     * NEXT_MAJOR: Make $propertyAccessor mandatory.
47
     */
48
    public function __construct(ManagerRegistry $registry, ?PropertyAccessorInterface $propertyAccessor = null)
49
    {
50
        $this->registry = $registry;
51
52
        // NEXT_MAJOR: Remove this block.
53
        if (null === $propertyAccessor) {
54
            @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...
55
                'Constructing "%s" without passing an instance of "%s" as second argument is deprecated since'
56
                .' sonata-project/doctrine-mongodb-admin-bundle 3.x and will be mandatory in 4.0',
57
                static::class,
58
                PropertyAccessorInterface::class
59
            ), E_USER_DEPRECATED);
60
61
            $propertyAccessor = PropertyAccess::createPropertyAccessor();
62
        }
63
64
        $this->propertyAccessor = $propertyAccessor;
65
    }
66
67
    /**
68
     * {@inheritdoc}
69
     */
70
    public function getMetadata($class)
71
    {
72
        return $this->getDocumentManager($class)->getMetadataFactory()->getMetadataFor($class);
73
    }
74
75
    /**
76
     * Returns the model's metadata holding the fully qualified property, and the last
77
     * property name.
78
     *
79
     * @param string $baseClass        The base class of the model holding the fully qualified property
80
     * @param string $propertyFullName The name of the fully qualified property (dot ('.') separated
81
     *                                 property string)
82
     *
83
     * @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...
84
     *                \Doctrine\ODM\MongoDB\Mapping\ClassMetadata $parentMetadata,
85
     *                string $lastPropertyName,
86
     *                array $parentAssociationMappings
87
     *                )
88
     */
89
    public function getParentMetadataForProperty($baseClass, $propertyFullName)
90
    {
91
        $nameElements = explode('.', $propertyFullName);
92
        $lastPropertyName = array_pop($nameElements);
93
        $class = $baseClass;
94
        $parentAssociationMappings = [];
95
96
        foreach ($nameElements as $nameElement) {
97
            $metadata = $this->getMetadata($class);
98
            $parentAssociationMappings[] = $metadata->associationMappings[$nameElement];
99
            $class = $metadata->getAssociationTargetClass($nameElement);
100
        }
101
102
        return [$this->getMetadata($class), $lastPropertyName, $parentAssociationMappings];
103
    }
104
105
    /**
106
     * {@inheritdoc}
107
     */
108
    public function hasMetadata($class)
109
    {
110
        return $this->getDocumentManager($class)->getMetadataFactory()->hasMetadataFor($class);
111
    }
112
113
    /**
114
     * {@inheritdoc}
115
     */
116
    public function getNewFieldDescriptionInstance($class, $name, array $options = [])
117
    {
118
        if (!\is_string($name)) {
119
            throw new \RuntimeException('The name argument must be a string');
120
        }
121
122
        if (!isset($options['route']['name'])) {
123
            $options['route']['name'] = 'edit';
124
        }
125
126
        if (!isset($options['route']['parameters'])) {
127
            $options['route']['parameters'] = [];
128
        }
129
130
        list($metadata, $propertyName, $parentAssociationMappings) = $this->getParentMetadataForProperty($class, $name);
131
132
        $fieldDescription = new FieldDescription();
133
        $fieldDescription->setName($name);
134
        $fieldDescription->setOptions($options);
135
        $fieldDescription->setParentAssociationMappings($parentAssociationMappings);
136
137
        /* @var ClassMetadata */
138
        if (isset($metadata->associationMappings[$propertyName])) {
139
            $fieldDescription->setAssociationMapping($metadata->associationMappings[$propertyName]);
140
        }
141
142
        if (isset($metadata->fieldMappings[$propertyName])) {
143
            $fieldDescription->setFieldMapping($metadata->fieldMappings[$propertyName]);
144
        }
145
146
        return $fieldDescription;
147
    }
148
149
    /**
150
     * {@inheritdoc}
151
     */
152
    public function create($object)
153
    {
154
        $documentManager = $this->getDocumentManager($object);
155
        $documentManager->persist($object);
156
        $documentManager->flush();
157
    }
158
159
    /**
160
     * {@inheritdoc}
161
     */
162
    public function update($object)
163
    {
164
        $documentManager = $this->getDocumentManager($object);
165
        $documentManager->persist($object);
166
        $documentManager->flush();
167
    }
168
169
    /**
170
     * {@inheritdoc}
171
     */
172
    public function delete($object)
173
    {
174
        $documentManager = $this->getDocumentManager($object);
175
        $documentManager->remove($object);
176
        $documentManager->flush();
177
    }
178
179
    /**
180
     * {@inheritdoc}
181
     */
182
    public function find($class, $id)
183
    {
184
        if (!isset($id)) {
185
            return null;
186
        }
187
188
        $documentManager = $this->getDocumentManager($class);
189
190
        if (is_numeric($id)) {
191
            $value = $documentManager->getRepository($class)->find((int) $id);
192
193
            if (!empty($value)) {
194
                return $value;
195
            }
196
        }
197
198
        return $documentManager->getRepository($class)->find($id);
199
    }
200
201
    /**
202
     * {@inheritdoc}
203
     */
204
    public function findBy($class, array $criteria = [])
205
    {
206
        return $this->getDocumentManager($class)->getRepository($class)->findBy($criteria);
207
    }
208
209
    /**
210
     * {@inheritdoc}
211
     */
212
    public function findOneBy($class, array $criteria = [])
213
    {
214
        return $this->getDocumentManager($class)->getRepository($class)->findOneBy($criteria);
215
    }
216
217
    /**
218
     * @param object|string $class
219
     *
220
     * @throw \RuntimeException
221
     *
222
     * @return \Doctrine\ODM\MongoDB\DocumentManager
223
     */
224
    public function getDocumentManager($class)
225
    {
226
        if (\is_object($class)) {
227
            $class = \get_class($class);
228
        }
229
230
        $dm = $this->registry->getManagerForClass($class);
231
232
        if (!$dm) {
233
            throw new \RuntimeException(sprintf('No document manager defined for class %s', $class));
234
        }
235
236
        return $dm;
237
    }
238
239
    /**
240
     * {@inheritdoc}
241
     */
242
    public function getParentFieldDescription($parentAssociationMapping, $class)
243
    {
244
        $fieldName = $parentAssociationMapping['fieldName'];
245
246
        $metadata = $this->getMetadata($class);
247
248
        $associatingMapping = $metadata->associationMappings[$parentAssociationMapping];
249
250
        $fieldDescription = $this->getNewFieldDescriptionInstance($class, $fieldName);
251
        $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...
252
        $fieldDescription->setAssociationMapping($associatingMapping);
253
254
        return $fieldDescription;
255
    }
256
257
    /**
258
     * {@inheritdoc}
259
     */
260
    public function createQuery($class, $alias = 'o')
261
    {
262
        $repository = $this->getDocumentManager($class)->getRepository($class);
263
264
        return new ProxyQuery($repository->createQueryBuilder());
265
    }
266
267
    /**
268
     * {@inheritdoc}
269
     */
270
    public function executeQuery($query)
271
    {
272
        if ($query instanceof Builder) {
273
            return $query->getQuery()->execute();
274
        }
275
276
        return $query->execute();
277
    }
278
279
    /**
280
     * {@inheritdoc}
281
     */
282
    public function getModelIdentifier($class)
283
    {
284
        return $this->getMetadata($class)->identifier;
285
    }
286
287
    /**
288
     * {@inheritdoc}
289
     */
290
    public function getIdentifierValues($document)
291
    {
292
        return [$this->getDocumentManager($document)->getUnitOfWork()->getDocumentIdentifier($document)];
293
    }
294
295
    /**
296
     * {@inheritdoc}
297
     */
298
    public function getIdentifierFieldNames($class)
299
    {
300
        return [$this->getMetadata($class)->getIdentifier()];
301
    }
302
303
    /**
304
     * {@inheritdoc}
305
     */
306
    public function getNormalizedIdentifier($document)
307
    {
308
        if (null === $document) {
309
            return null;
310
        }
311
312
        if (!\is_object($document)) {
313
            throw new \RuntimeException('Invalid argument, object or null required');
314
        }
315
316
        // the document is not managed
317
        if (!$this->getDocumentManager($document)->getUnitOfWork()->isInIdentityMap($document)) {
318
            return null;
319
        }
320
321
        $values = $this->getIdentifierValues($document);
322
323
        return implode(self::ID_SEPARATOR, $values);
324
    }
325
326
    /**
327
     * {@inheritdoc}
328
     */
329
    public function getUrlSafeIdentifier($document)
330
    {
331
        return $this->getNormalizedIdentifier($document);
332
    }
333
334
    /**
335
     * {@inheritdoc}
336
     */
337
    public function addIdentifiersToQuery($class, ProxyQueryInterface $queryProxy, array $idx)
338
    {
339
        $queryBuilder = $queryProxy->getQueryBuilder();
340
        $queryBuilder->field('_id')->in($idx);
341
    }
342
343
    /**
344
     * {@inheritdoc}
345
     */
346
    public function batchDelete($class, ProxyQueryInterface $queryProxy)
347
    {
348
        /** @var Query $queryBuilder */
349
        $queryBuilder = $queryProxy->getQuery();
350
351
        $documentManager = $this->getDocumentManager($class);
352
353
        $i = 0;
354
        foreach ($queryBuilder->execute() as $object) {
355
            $documentManager->remove($object);
356
357
            if (0 === (++$i % 20)) {
358
                $documentManager->flush();
359
                $documentManager->clear();
360
            }
361
        }
362
363
        $documentManager->flush();
364
        $documentManager->clear();
365
    }
366
367
    /**
368
     * {@inheritdoc}
369
     */
370
    public function getDataSourceIterator(DatagridInterface $datagrid, array $fields, $firstResult = null, $maxResult = null)
371
    {
372
        $datagrid->buildPager();
373
        $query = $datagrid->getQuery();
374
375
        $query->setFirstResult($firstResult);
376
        $query->setMaxResults($maxResult);
377
378
        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...
379
    }
380
381
    /**
382
     * {@inheritdoc}
383
     */
384
    public function getExportFields($class)
385
    {
386
        $metadata = $this->getDocumentManager($class)->getClassMetadata($class);
387
388
        return $metadata->getFieldNames();
389
    }
390
391
    /**
392
     * {@inheritdoc}
393
     */
394
    public function getModelInstance($class)
395
    {
396
        if (!class_exists($class)) {
397
            throw new \InvalidArgumentException(sprintf('Class "%s" not found', $class));
398
        }
399
400
        $r = new \ReflectionClass($class);
401
        if ($r->isAbstract()) {
402
            throw new \InvalidArgumentException(sprintf('Cannot initialize abstract class: %s', $class));
403
        }
404
405
        $constructor = $r->getConstructor();
406
407
        if (null !== $constructor && (!$constructor->isPublic() || $constructor->getNumberOfRequiredParameters() > 0)) {
408
            return $r->newInstanceWithoutConstructor();
409
        }
410
411
        return new $class();
412
    }
413
414
    /**
415
     * {@inheritdoc}
416
     */
417
    public function getSortParameters(FieldDescriptionInterface $fieldDescription, DatagridInterface $datagrid)
418
    {
419
        $values = $datagrid->getValues();
420
421
        if ($this->isFieldAlreadySorted($fieldDescription, $datagrid)) {
422
            if ('ASC' === $values['_sort_order']) {
423
                $values['_sort_order'] = 'DESC';
424
            } else {
425
                $values['_sort_order'] = 'ASC';
426
            }
427
        } else {
428
            $values['_sort_order'] = 'ASC';
429
        }
430
431
        $values['_sort_by'] = \is_string($fieldDescription->getOption('sortable')) ? $fieldDescription->getOption('sortable') : $fieldDescription->getName();
432
433
        return ['filter' => $values];
434
    }
435
436
    /**
437
     * {@inheritdoc}
438
     */
439
    public function getPaginationParameters(DatagridInterface $datagrid, $page)
440
    {
441
        $values = $datagrid->getValues();
442
443
        if (isset($values['_sort_by']) && $values['_sort_by'] instanceof FieldDescriptionInterface) {
444
            $values['_sort_by'] = $values['_sort_by']->getName();
445
        }
446
        $values['_page'] = $page;
447
448
        return ['filter' => $values];
449
    }
450
451
    /**
452
     * {@inheritdoc}
453
     */
454
    public function getDefaultSortValues($class)
455
    {
456
        return [
457
            '_sort_order' => 'ASC',
458
            '_sort_by' => $this->getModelIdentifier($class),
459
            '_page' => 1,
460
            '_per_page' => 25,
461
        ];
462
    }
463
464
    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...
465
    {
466
        return [10, 25, 50, 100, 250];
467
    }
468
469
    /**
470
     * {@inheritdoc}
471
     */
472
    public function modelTransform($class, $instance)
473
    {
474
        return $instance;
475
    }
476
477
    /**
478
     * {@inheritdoc}
479
     */
480
    public function modelReverseTransform($class, array $array = [])
481
    {
482
        $instance = $this->getModelInstance($class);
483
        $metadata = $this->getMetadata($class);
484
485
        foreach ($array as $name => $value) {
486
            $property = $this->getFieldName($metadata, $name);
487
488
            $this->propertyAccessor->setValue($instance, $property, $value);
489
        }
490
491
        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...
492
    }
493
494
    /**
495
     * {@inheritdoc}
496
     */
497
    public function getModelCollectionInstance($class)
498
    {
499
        return new ArrayCollection();
500
    }
501
502
    /**
503
     * {@inheritdoc}
504
     */
505
    public function collectionClear(&$collection)
506
    {
507
        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...
508
    }
509
510
    /**
511
     * {@inheritdoc}
512
     */
513
    public function collectionHasElement(&$collection, &$element)
514
    {
515
        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...
516
    }
517
518
    /**
519
     * {@inheritdoc}
520
     */
521
    public function collectionAddElement(&$collection, &$element)
522
    {
523
        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...
524
    }
525
526
    /**
527
     * {@inheritdoc}
528
     */
529
    public function collectionRemoveElement(&$collection, &$element)
530
    {
531
        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...
532
    }
533
534
    /**
535
     * NEXT_MAJOR: Remove this method.
536
     *
537
     * @deprecated since sonata-project/doctrine-mongodb-admin-bundle 3.x, to be removed in 4.0.'.
538
     *
539
     * @param string $property
540
     *
541
     * @return mixed
542
     */
543
    protected function camelize($property)
544
    {
545
        @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...
546
            'Method "%s()" is deprecated since sonata-project/doctrine-mongodb-admin-bundle 3.x and will be removed in version 4.0.',
547
            __METHOD__
548
        ), E_USER_DEPRECATED);
549
550
        return str_replace(' ', '', ucwords(str_replace('_', ' ', $property)));
551
    }
552
553
    /**
554
     * NEXT_MAJOR: Remove CommonClassMetadata when dropping doctrine/mongodb-odm 1.3.x.
555
     *
556
     * @param ClassMetadata|CommonClassMetadata $metadata
557
     */
558
    private function getFieldName($metadata, string $name): string
559
    {
560
        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...
561
            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...
562
        }
563
564
        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...
565
            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...
566
        }
567
568
        return $name;
569
    }
570
571
    private function isFieldAlreadySorted(FieldDescriptionInterface $fieldDescription, DatagridInterface $datagrid): bool
572
    {
573
        $values = $datagrid->getValues();
574
575
        if (!isset($values['_sort_by']) || !$values['_sort_by'] instanceof FieldDescriptionInterface) {
576
            return false;
577
        }
578
579
        return $values['_sort_by']->getName() === $fieldDescription->getName()
580
            || $values['_sort_by']->getName() === $fieldDescription->getOption('sortable');
581
    }
582
}
583