ModelManager::getParentFieldDescription()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 14
rs 9.7998
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\DoctrinePHPCRAdminBundle\Model;
15
16
use Doctrine\Common\Collections\ArrayCollection;
17
use Doctrine\Common\Util\ClassUtils;
18
use Doctrine\ODM\PHPCR\DocumentManager;
19
use Doctrine\ODM\PHPCR\Mapping\ClassMetadata;
20
use Sonata\AdminBundle\Admin\FieldDescriptionInterface;
21
use Sonata\AdminBundle\Datagrid\DatagridInterface;
22
use Sonata\AdminBundle\Datagrid\ProxyQueryInterface;
23
use Sonata\AdminBundle\Exception\ModelManagerException;
24
use Sonata\AdminBundle\Model\ModelManagerInterface;
25
use Sonata\DoctrinePHPCRAdminBundle\Admin\FieldDescription;
26
use Sonata\DoctrinePHPCRAdminBundle\Datagrid\ProxyQuery;
27
use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
28
29
class ModelManager implements ModelManagerInterface
30
{
31
    /**
32
     * @var DocumentManager
33
     */
34
    protected $dm;
35
36
    public function __construct(DocumentManager $dm)
37
    {
38
        $this->dm = $dm;
39
    }
40
41
    /**
42
     * Returns the related model's metadata.
43
     *
44
     * @param string $class
45
     *
46
     * @return ClassMetadata
47
     */
48
    public function getMetadata($class)
49
    {
50
        return $this->dm->getMetadataFactory()->getMetadataFor($class);
51
    }
52
53
    /**
54
     * Returns true is the model has some metadata.
55
     *
56
     * @param string $class
57
     *
58
     * @return bool
59
     */
60
    public function hasMetadata($class)
61
    {
62
        return $this->dm->getMetadataFactory()->hasMetadataFor($class);
63
    }
64
65
    /**
66
     * {@inheritdoc}
67
     *
68
     * @throws ModelManagerException if the document manager throws any exception
69
     */
70
    public function create($object): void
71
    {
72
        try {
73
            $this->dm->persist($object);
74
            $this->dm->flush();
75
        } catch (\Exception $e) {
76
            throw new ModelManagerException('', 0, $e);
77
        }
78
    }
79
80
    /**
81
     * {@inheritdoc}
82
     *
83
     * @throws ModelManagerException if the document manager throws any exception
84
     */
85
    public function update($object): void
86
    {
87
        try {
88
            $this->dm->persist($object);
89
            $this->dm->flush();
90
        } catch (\Exception $e) {
91
            throw new ModelManagerException('', 0, $e);
92
        }
93
    }
94
95
    /**
96
     * {@inheritdoc}
97
     *
98
     * @throws ModelManagerException if the document manager throws any exception
99
     */
100
    public function delete($object): void
101
    {
102
        try {
103
            $this->dm->remove($object);
104
            $this->dm->flush();
105
        } catch (\Exception $e) {
106
            throw new ModelManagerException('', 0, $e);
107
        }
108
    }
109
110
    /**
111
     * Find one object from the given class repository.
112
     *
113
     * {@inheritdoc}
114
     */
115
    public function find($class, $id)
116
    {
117
        if (!isset($id)) {
118
            return;
119
        }
120
121
        if (null === $class) {
122
            return $this->dm->find(null, $id);
123
        }
124
125
        return $this->dm->getRepository($class)->find($id);
126
    }
127
128
    /**
129
     * {@inheritdoc}
130
     *
131
     * @throws \RunTimeException if $name is not a string
132
     *
133
     * @return FieldDescription
134
     */
135
    public function getNewFieldDescriptionInstance($class, $name, array $options = [])
136
    {
137
        if (!\is_string($name)) {
138
            throw new \RunTimeException('The name argument must be a string');
139
        }
140
141
        $metadata = $this->getMetadata($class);
142
143
        $fieldDescription = new FieldDescription();
144
        $fieldDescription->setName($name);
145
        $fieldDescription->setOptions($options);
146
147
        if (isset($metadata->associationMappings[$name])) {
0 ignored issues
show
Bug introduced by
The property associationMappings does not seem to exist. Did you mean mappings?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
148
            $fieldDescription->setAssociationMapping($metadata->associationMappings[$name]);
0 ignored issues
show
Bug introduced by
The property associationMappings does not seem to exist. Did you mean mappings?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
149
        }
150
151
        if (isset($metadata->fieldMappings[$name])) {
152
            $fieldDescription->setFieldMapping($metadata->fieldMappings[$name]);
153
        }
154
155
        return $fieldDescription;
156
    }
157
158
    /**
159
     * {@inheritdoc}
160
     */
161
    public function findBy($class, array $criteria = [])
162
    {
163
        return $this->dm->getRepository($class)->findBy($criteria);
164
    }
165
166
    /**
167
     * {@inheritdoc}
168
     */
169
    public function findOneBy($class, array $criteria = [])
170
    {
171
        return $this->dm->getRepository($class)->findOneBy($criteria);
172
    }
173
174
    /**
175
     * @return DocumentManager The PHPCR-ODM document manager responsible for
176
     *                         this model
177
     */
178
    public function getDocumentManager()
179
    {
180
        return $this->dm;
181
    }
182
183
    /**
184
     * {@inheritdoc}
185
     *
186
     * @return FieldDescriptionInterface
187
     */
188
    public function getParentFieldDescription($parentAssociationMapping, $class)
189
    {
190
        $fieldName = $parentAssociationMapping['fieldName'];
191
192
        $metadata = $this->getMetadata($class);
193
194
        $associatingMapping = $metadata->associationMappings[$parentAssociationMapping];
0 ignored issues
show
Bug introduced by
The property associationMappings does not seem to exist. Did you mean mappings?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
195
196
        $fieldDescription = $this->getNewFieldDescriptionInstance($class, $fieldName);
197
        $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...
198
        $fieldDescription->setAssociationMapping($associatingMapping);
199
200
        return $fieldDescription;
201
    }
202
203
    /**
204
     * @param string $class the fully qualified class name to search for
205
     * @param string $alias alias to use for this class when accessing fields,
206
     *                      defaults to 'a'
207
     *
208
     * @throws \InvalidArgumentException if alias is not a string or an empty string
209
     *
210
     * @return ProxyQueryInterface
211
     */
212
    public function createQuery($class, $alias = 'a')
213
    {
214
        $qb = $this->getDocumentManager()->createQueryBuilder();
215
        $qb->from()->document($class, $alias);
216
217
        return new ProxyQuery($qb, $alias);
218
    }
219
220
    /**
221
     * @param ProxyQuery $query
222
     *
223
     * @return mixed
224
     */
225
    public function executeQuery($query)
226
    {
227
        return $query->execute();
228
    }
229
230
    /**
231
     * {@inheritdoc}
232
     */
233
    public function getModelIdentifier($classname)
234
    {
235
        return $this->getMetadata($classname)->identifier;
236
    }
237
238
    /**
239
     * Transforms the document into the PHPCR path.
240
     *
241
     * Note: This is returning an array because Doctrine ORM for example can
242
     * have multiple identifiers, e.g. if the primary key is composed of
243
     * several columns. We only ever have one, but return that wrapped into an
244
     * array to adhere to the interface.
245
     *
246
     * {@inheritdoc}
247
     */
248
    public function getIdentifierValues($document)
249
    {
250
        $class = $this->getMetadata(ClassUtils::getClass($document));
251
        $path = $class->reflFields[$class->identifier]->getValue($document);
252
253
        return [$path];
254
    }
255
256
    /**
257
     * {@inheritdoc}
258
     */
259
    public function getIdentifierFieldNames($class)
260
    {
261
        return [$this->getModelIdentifier($class)];
262
    }
263
264
    /**
265
     * This is just taking the id out of the array again.
266
     *
267
     * {@inheritdoc}
268
     *
269
     * @throws \InvalidArgumentException if $document is not an object or null
270
     */
271
    public function getNormalizedIdentifier($document)
272
    {
273
        if (is_scalar($document)) {
274
            throw new \InvalidArgumentException('Invalid argument, object or null required');
275
        }
276
277
        // the document is not managed
278
        if (!$document || !$this->getDocumentManager()->contains($document)) {
279
            return;
280
        }
281
282
        $values = $this->getIdentifierValues($document);
283
284
        return $values[0];
285
    }
286
287
    /**
288
     * Currently only the leading slash is removed.
289
     *
290
     * @param object $document
291
     *
292
     * @return string|null
293
     */
294
    public function getUrlsafeIdentifier($document)
295
    {
296
        $id = $this->getNormalizedIdentifier($document);
297
        if (null !== $id) {
298
            return substr($id, 1);
299
        }
300
    }
301
302
    /**
303
     * {@inheritdoc}
304
     */
305
    public function addIdentifiersToQuery($class, ProxyQueryInterface $queryProxy, array $idx): void
306
    {
307
        /* @var $queryProxy ProxyQuery */
308
        $qb = $queryProxy->getQueryBuilder();
309
310
        $orX = $qb->andWhere()->orX();
311
312
        foreach ($idx as $id) {
313
            $path = $this->getBackendId($id);
314
            $orX->same($path, $queryProxy->getAlias());
315
        }
316
    }
317
318
    /**
319
     * Add leading slash to construct valid phpcr document id.
320
     *
321
     * The phpcr-odm QueryBuilder uses absolute paths and expects id´s to start with a forward slash
322
     * because SonataAdmin uses object id´s for constructing URL´s it has to use id´s without the
323
     * leading slash.
324
     *
325
     * @param string $id
326
     *
327
     * @return string
328
     */
329
    public function getBackendId($id)
330
    {
331
        return '/' === substr($id, 0, 1) ? $id : '/'.$id;
332
    }
333
334
    /**
335
     * {@inheritdoc}
336
     *
337
     * @throws ModelManagerException if anything goes wrong during query execution
338
     */
339
    public function batchDelete($class, ProxyQueryInterface $queryProxy): void
340
    {
341
        try {
342
            $i = 0;
343
            $res = $queryProxy->execute();
344
            foreach ($res as $object) {
345
                $this->dm->remove($object);
346
347
                if (0 === (++$i % 20)) {
348
                    $this->dm->flush();
349
                    $this->dm->clear();
350
                }
351
            }
352
353
            $this->dm->flush();
354
            $this->dm->clear();
355
        } catch (\Exception $e) {
356
            throw new ModelManagerException('', 0, $e);
357
        }
358
    }
359
360
    /**
361
     * {@inheritdoc}
362
     *
363
     * @return object
364
     */
365
    public function getModelInstance($class)
366
    {
367
        return new $class();
368
    }
369
370
    /**
371
     * {@inheritdoc}
372
     */
373
    public function getSortParameters(FieldDescriptionInterface $fieldDescription, DatagridInterface $datagrid)
374
    {
375
        $values = $datagrid->getValues();
376
377
        if ($this->isFieldAlreadySorted($fieldDescription, $datagrid)) {
378
            if ('ASC' === $values['_sort_order']) {
379
                $values['_sort_order'] = 'DESC';
380
            } else {
381
                $values['_sort_order'] = 'ASC';
382
            }
383
384
            $values['_sort_by'] = $fieldDescription->getName();
385
        } else {
386
            $values['_sort_order'] = 'ASC';
387
            $values['_sort_by'] = $fieldDescription->getName();
388
        }
389
390
        return ['filter' => $values];
391
    }
392
393
    /**
394
     * {@inheritdoc}
395
     */
396
    public function getPaginationParameters(DatagridInterface $datagrid, $page)
397
    {
398
        $values = $datagrid->getValues();
399
400
        if (isset($values['_sort_by']) && $values['_sort_by'] instanceof FieldDescriptionInterface) {
401
            $values['_sort_by'] = $values['_sort_by']->getName();
402
        }
403
        $values['_page'] = $page;
404
405
        return ['filter' => $values];
406
    }
407
408
    /**
409
     * {@inheritdoc}
410
     */
411
    public function getDefaultSortValues($class)
412
    {
413
        return [
414
            '_page' => 1,
415
            '_per_page' => 25,
416
        ];
417
    }
418
419
    /**
420
     * {@inheritdoc}
421
     *
422
     * @return object
423
     */
424
    public function modelTransform($class, $instance)
425
    {
426
        return $instance;
427
    }
428
429
    /**
430
     * {@inheritdoc}
431
     *
432
     * @throws NoSuchPropertyException if the class has no magic setter and
433
     *                                 public property for a field in array
434
     *
435
     * @return object
436
     */
437
    public function modelReverseTransform($class, array $array = [])
438
    {
439
        $instance = $this->getModelInstance($class);
440
        $metadata = $this->getMetadata($class);
441
442
        $reflClass = $metadata->reflClass;
443
        foreach ($array as $name => $value) {
444
            $reflection_property = false;
445
            // property or association ?
446
            if (\array_key_exists($name, $metadata->fieldMappings)) {
447
                $property = $metadata->fieldMappings[$name]['fieldName'];
448
                $reflection_property = $metadata->reflFields[$name];
449
            } elseif (\array_key_exists($name, $metadata->associationMappings)) {
0 ignored issues
show
Bug introduced by
The property associationMappings does not seem to exist. Did you mean mappings?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
450
                $property = $metadata->associationMappings[$name]['fieldName'];
0 ignored issues
show
Bug introduced by
The property associationMappings does not seem to exist. Did you mean mappings?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
451
            } else {
452
                $property = $name;
453
            }
454
455
            // TODO: use PropertyAccess https://github.com/sonata-project/SonataDoctrinePhpcrAdminBundle/issues/187
456
            $setter = 'set'.$this->camelize($name);
0 ignored issues
show
Deprecated Code introduced by
The method Sonata\DoctrinePHPCRAdmi...odelManager::camelize() has been deprecated.

This method has been deprecated.

Loading history...
457
458
            if ($reflClass->hasMethod($setter)) {
459
                if (!$reflClass->getMethod($setter)->isPublic()) {
460
                    throw new NoSuchPropertyException(sprintf('Method "%s()" is not public in class "%s"', $setter, $reflClass->getName()));
0 ignored issues
show
Bug introduced by
Consider using $reflClass->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
461
                }
462
463
                $instance->$setter($value);
464
            } elseif ($reflClass->hasMethod('__set')) {
465
                // needed to support magic method __set
466
                $instance->$property = $value;
467
            } elseif ($reflClass->hasProperty($property)) {
468
                if (!$reflClass->getProperty($property)->isPublic()) {
469
                    throw new NoSuchPropertyException(sprintf('Property "%s" is not public in class "%s". Maybe you should create the method "set%s()"?', $property, $reflClass->getName(), ucfirst($property)));
0 ignored issues
show
Bug introduced by
Consider using $reflClass->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
470
                }
471
472
                $instance->$property = $value;
473
            } elseif ($reflection_property) {
474
                $reflection_property->setValue($instance, $value);
475
            }
476
        }
477
478
        return $instance;
479
    }
480
481
    /**
482
     * {@inheritdoc}
483
     */
484
    public function getModelCollectionInstance($class)
485
    {
486
        return new ArrayCollection();
487
    }
488
489
    /**
490
     * {@inheritdoc}
491
     */
492
    public function collectionClear(&$collection)
493
    {
494
        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...
495
    }
496
497
    /**
498
     * {@inheritdoc}
499
     */
500
    public function collectionHasElement(&$collection, &$element)
501
    {
502
        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...
503
    }
504
505
    /**
506
     * {@inheritdoc}
507
     */
508
    public function collectionAddElement(&$collection, &$element)
509
    {
510
        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...
511
    }
512
513
    /**
514
     * {@inheritdoc}
515
     */
516
    public function collectionRemoveElement(&$collection, &$element)
517
    {
518
        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...
519
    }
520
521
    /**
522
     * {@inheritdoc}
523
     */
524
    public function getDataSourceIterator(DatagridInterface $datagrid, array $fields, $firstResult = null, $maxResult = null): void
525
    {
526
        throw new \RuntimeException('Datasourceiterator not implemented.');
527
    }
528
529
    /**
530
     * {@inheritdoc}
531
     *
532
     * Not really implemented.
533
     */
534
    public function getExportFields($class)
535
    {
536
        return [];
537
    }
538
539
    /**
540
     * Method taken from PropertyPath.
541
     *
542
     * NEXT_MAJOR: remove when doing https://github.com/sonata-project/SonataDoctrinePhpcrAdminBundle/issues/187
543
     *
544
     * @param string $property
545
     *
546
     * @return string
547
     *
548
     * @deprecated
549
     */
550
    protected function camelize($property)
551
    {
552
        return preg_replace(['/(^|_)+(.)/e', '/\.(.)/e'], ["strtoupper('\\2')", "'_'.strtoupper('\\1')"], $property);
553
    }
554
555
    private function isFieldAlreadySorted(FieldDescriptionInterface $fieldDescription, DatagridInterface $datagrid): bool
556
    {
557
        $values = $datagrid->getValues();
558
559
        if (!isset($values['_sort_by']) || !$values['_sort_by'] instanceof FieldDescriptionInterface) {
560
            return false;
561
        }
562
563
        return $values['_sort_by']->getName() === $fieldDescription->getName()
564
            || $values['_sort_by']->getName() === $fieldDescription->getOption('sortable');
565
    }
566
}
567