Completed
Pull Request — 3.x (#368)
by
unknown
33:14 queued 22:24
created

ModelManager::batchDelete()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

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