Completed
Pull Request — 3.x (#547)
by Jeroen
02:34
created

ModelManager::getNormalizedIdentifier()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 19
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
c 2
b 0
f 1
dl 0
loc 19
rs 8.8571
cc 5
eloc 9
nc 4
nop 1
1
<?php
2
3
/*
4
 * This file is part of the Sonata Project package.
5
 *
6
 * (c) Thomas Rabaix <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Sonata\DoctrineORMAdminBundle\Model;
13
14
use Doctrine\Common\Util\ClassUtils;
15
use Doctrine\DBAL\DBALException;
16
use Doctrine\DBAL\LockMode;
17
use Doctrine\DBAL\Types\Type;
18
use Doctrine\ORM\EntityManager;
19
use Doctrine\ORM\Mapping\ClassMetadata;
20
use Doctrine\ORM\OptimisticLockException;
21
use Doctrine\ORM\QueryBuilder;
22
use Exporter\Source\DoctrineORMQuerySourceIterator;
23
use Sonata\AdminBundle\Admin\FieldDescriptionInterface;
24
use Sonata\AdminBundle\Datagrid\DatagridInterface;
25
use Sonata\AdminBundle\Datagrid\ProxyQueryInterface;
26
use Sonata\AdminBundle\Exception\LockException;
27
use Sonata\AdminBundle\Exception\ModelManagerException;
28
use Sonata\AdminBundle\Model\LockInterface;
29
use Sonata\AdminBundle\Model\ModelManagerInterface;
30
use Sonata\DoctrineORMAdminBundle\Admin\FieldDescription;
31
use Sonata\DoctrineORMAdminBundle\Datagrid\ProxyQuery;
32
use Symfony\Bridge\Doctrine\RegistryInterface;
33
use Symfony\Component\Form\Exception\PropertyAccessDeniedException;
34
35
class ModelManager implements ModelManagerInterface, LockInterface
36
{
37
    const ID_SEPARATOR = '~';
38
    /**
39
     * @var RegistryInterface
40
     */
41
    protected $registry;
42
43
    /**
44
     * @var EntityManager[]
45
     */
46
    protected $cache = array();
47
48
    /**
49
     * @param RegistryInterface $registry
50
     */
51
    public function __construct(RegistryInterface $registry)
52
    {
53
        $this->registry = $registry;
54
    }
55
56
    /**
57
     * @param string $class
58
     *
59
     * @return ClassMetadata
60
     */
61
    public function getMetadata($class)
62
    {
63
        return $this->getEntityManager($class)->getMetadataFactory()->getMetadataFor($class);
64
    }
65
66
    /**
67
     * Returns the model's metadata holding the fully qualified property, and the last
68
     * property name.
69
     *
70
     * @param string $baseClass        The base class of the model holding the fully qualified property.
71
     * @param string $propertyFullName The name of the fully qualified property (dot ('.') separated
72
     *                                 property string)
73
     *
74
     * @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...
75
     *                \Doctrine\ORM\Mapping\ClassMetadata $parentMetadata,
76
     *                string $lastPropertyName,
77
     *                array $parentAssociationMappings
78
     *                )
79
     */
80
    public function getParentMetadataForProperty($baseClass, $propertyFullName)
81
    {
82
        $nameElements = explode('.', $propertyFullName);
83
        $lastPropertyName = array_pop($nameElements);
84
        $class = $baseClass;
85
        $parentAssociationMappings = array();
86
87
        foreach ($nameElements as $nameElement) {
88
            $metadata = $this->getMetadata($class);
89
90
            if (isset($metadata->associationMappings[$nameElement])) {
91
                $parentAssociationMappings[] = $metadata->associationMappings[$nameElement];
92
                $class = $metadata->getAssociationTargetClass($nameElement);
93
            } elseif (isset($metadata->embeddedClasses[$nameElement])) {
94
                $parentAssociationMappings = array();
95
                $lastPropertyName = $propertyFullName;
96
                $class = $baseClass;
97
            }
98
        }
99
100
        return array($this->getMetadata($class), $lastPropertyName, $parentAssociationMappings);
101
    }
102
103
    /**
104
     * @param string $class
105
     *
106
     * @return bool
107
     */
108
    public function hasMetadata($class)
109
    {
110
        return $this->getEntityManager($class)->getMetadataFactory()->hasMetadataFor($class);
111
    }
112
113
    /**
114
     * {@inheritdoc}
115
     */
116
    public function getNewFieldDescriptionInstance($class, $name, array $options = array())
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'] = array();
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
        if (isset($metadata->associationMappings[$propertyName])) {
138
            $fieldDescription->setAssociationMapping($metadata->associationMappings[$propertyName]);
139
        }
140
141
        if (isset($metadata->fieldMappings[$propertyName])) {
142
            $fieldDescription->setFieldMapping($metadata->fieldMappings[$propertyName]);
143
        }
144
145
        return $fieldDescription;
146
    }
147
148
    /**
149
     * {@inheritdoc}
150
     */
151
    public function create($object)
152
    {
153
        try {
154
            $entityManager = $this->getEntityManager($object);
155
            $entityManager->persist($object);
156
            $entityManager->flush();
157
        } catch (\PDOException $e) {
158
            throw new ModelManagerException(sprintf('Failed to create object: %s', ClassUtils::getClass($object)), $e->getCode(), $e);
159
        } catch (DBALException $e) {
160
            throw new ModelManagerException(sprintf('Failed to create object: %s', ClassUtils::getClass($object)), $e->getCode(), $e);
161
        }
162
    }
163
164
    /**
165
     * {@inheritdoc}
166
     */
167
    public function update($object)
168
    {
169
        try {
170
            $entityManager = $this->getEntityManager($object);
171
            $entityManager->persist($object);
172
            $entityManager->flush();
173
        } catch (\PDOException $e) {
174
            throw new ModelManagerException(sprintf('Failed to update object: %s', ClassUtils::getClass($object)), $e->getCode(), $e);
175
        } catch (DBALException $e) {
176
            throw new ModelManagerException(sprintf('Failed to update object: %s', ClassUtils::getClass($object)), $e->getCode(), $e);
177
        }
178
    }
179
180
    /**
181
     * {@inheritdoc}
182
     */
183
    public function delete($object)
184
    {
185
        try {
186
            $entityManager = $this->getEntityManager($object);
0 ignored issues
show
Documentation introduced by
$object is of type object, 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...
187
            $entityManager->remove($object);
188
            $entityManager->flush();
189
        } catch (\PDOException $e) {
190
            throw new ModelManagerException(sprintf('Failed to delete object: %s', ClassUtils::getClass($object)), $e->getCode(), $e);
191
        } catch (DBALException $e) {
192
            throw new ModelManagerException(sprintf('Failed to delete object: %s', ClassUtils::getClass($object)), $e->getCode(), $e);
193
        }
194
    }
195
196
    /**
197
     * {@inheritdoc}
198
     */
199
    public function getLockVersion($object)
200
    {
201
        $metadata = $this->getMetadata(ClassUtils::getClass($object));
202
203
        if (!$metadata->isVersioned) {
204
            return;
205
        }
206
207
        return $metadata->reflFields[$metadata->versionField]->getValue($object);
208
    }
209
210
    /**
211
     * {@inheritdoc}
212
     */
213
    public function lock($object, $expectedVersion)
214
    {
215
        $metadata = $this->getMetadata(ClassUtils::getClass($object));
216
217
        if (!$metadata->isVersioned) {
218
            return;
219
        }
220
221
        try {
222
            $entityManager = $this->getEntityManager($object);
0 ignored issues
show
Documentation introduced by
$object is of type object, 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...
223
            $entityManager->lock($object, LockMode::OPTIMISTIC, $expectedVersion);
224
        } catch (OptimisticLockException $e) {
225
            throw new LockException($e->getMessage(), $e->getCode(), $e);
226
        }
227
    }
228
229
    /**
230
     * {@inheritdoc}
231
     */
232
    public function find($class, $id)
233
    {
234
        if (!isset($id)) {
235
            return;
236
        }
237
238
        $values = array_combine($this->getIdentifierFieldNames($class), explode(self::ID_SEPARATOR, $id));
239
240
        return $this->getEntityManager($class)->getRepository($class)->find($values);
241
    }
242
243
    /**
244
     * {@inheritdoc}
245
     */
246
    public function findBy($class, array $criteria = array())
247
    {
248
        return $this->getEntityManager($class)->getRepository($class)->findBy($criteria);
249
    }
250
251
    /**
252
     * {@inheritdoc}
253
     */
254
    public function findOneBy($class, array $criteria = array())
255
    {
256
        return $this->getEntityManager($class)->getRepository($class)->findOneBy($criteria);
257
    }
258
259
    /**
260
     * @param string $class
261
     *
262
     * @return EntityManager
263
     */
264
    public function getEntityManager($class)
265
    {
266
        if (is_object($class)) {
267
            $class = get_class($class);
268
        }
269
270
        if (!isset($this->cache[$class])) {
271
            $em = $this->registry->getManagerForClass($class);
272
273
            if (!$em) {
274
                throw new \RuntimeException(sprintf('No entity manager defined for class %s', $class));
275
            }
276
277
            $this->cache[$class] = $em;
278
        }
279
280
        return $this->cache[$class];
281
    }
282
283
    /**
284
     * {@inheritdoc}
285
     */
286
    public function getParentFieldDescription($parentAssociationMapping, $class)
287
    {
288
        $fieldName = $parentAssociationMapping['fieldName'];
289
290
        $metadata = $this->getMetadata($class);
291
292
        $associatingMapping = $metadata->associationMappings[$parentAssociationMapping];
293
294
        $fieldDescription = $this->getNewFieldDescriptionInstance($class, $fieldName);
295
        $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...
296
        $fieldDescription->setAssociationMapping($associatingMapping);
297
298
        return $fieldDescription;
299
    }
300
301
    /**
302
     * {@inheritdoc}
303
     */
304
    public function createQuery($class, $alias = 'o')
305
    {
306
        $repository = $this->getEntityManager($class)->getRepository($class);
307
308
        return new ProxyQuery($repository->createQueryBuilder($alias));
309
    }
310
311
    /**
312
     * {@inheritdoc}
313
     */
314
    public function executeQuery($query)
315
    {
316
        if ($query instanceof QueryBuilder) {
317
            return $query->getQuery()->execute();
318
        }
319
320
        return $query->execute();
321
    }
322
323
    /**
324
     * {@inheritdoc}
325
     */
326
    public function getModelIdentifier($class)
327
    {
328
        return $this->getMetadata($class)->identifier;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->getMetadata($class)->identifier; (array) is incompatible with the return type declared by the interface Sonata\AdminBundle\Model...ace::getModelIdentifier of type string.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
329
    }
330
331
    /**
332
     * {@inheritdoc}
333
     */
334
    public function getIdentifierValues($entity)
335
    {
336
        // Fix code has an impact on performance, so disable it ...
337
        //$entityManager = $this->getEntityManager($entity);
338
        //if (!$entityManager->getUnitOfWork()->isInIdentityMap($entity)) {
339
        //    throw new \RuntimeException('Entities passed to the choice field must be managed');
340
        //}
341
342
        $class = ClassUtils::getClass($entity);
343
        $metadata = $this->getMetadata($class);
344
        $platform = $this->getEntityManager($class)->getConnection()->getDatabasePlatform();
345
346
        $identifiers = array();
347
348
        foreach ($metadata->getIdentifierValues($entity) as $name => $value) {
349
            if (!is_object($value)) {
350
                $identifiers[] = $value;
351
                continue;
352
            }
353
354
            $fieldType = $metadata->getTypeOfField($name);
355
            $type = Type::getType($fieldType);
1 ignored issue
show
Bug introduced by
It seems like $fieldType defined by $metadata->getTypeOfField($name) on line 354 can also be of type null or object<Doctrine\DBAL\Types\Type>; however, Doctrine\DBAL\Types\Type::getType() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
356
            if ($type) {
357
                $identifiers[] = $type->convertToDatabaseValue($value, $platform);
358
                continue;
359
            }
360
361
            $metadata = $this->getMetadata(ClassUtils::getClass($value));
362
363
            foreach ($metadata->getIdentifierValues($value) as $value) {
364
                $identifiers[] = $value;
365
            }
366
        }
367
368
        return $identifiers;
369
    }
370
371
    /**
372
     * {@inheritdoc}
373
     */
374
    public function getIdentifierFieldNames($class)
375
    {
376
        return $this->getMetadata($class)->getIdentifierFieldNames();
377
    }
378
379
    /**
380
     * {@inheritdoc}
381
     */
382
    public function getNormalizedIdentifier($entity)
383
    {
384
        if (is_scalar($entity)) {
385
            throw new \RunTimeException('Invalid argument, object or null required');
386
        }
387
388
        // the entities is not managed
389
        if (!$entity || !$this->getEntityManager($entity)->getUnitOfWork()->isInIdentityMap($entity)) {
0 ignored issues
show
Documentation introduced by
$entity is of type object, 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...
390
            return;
391
        }
392
393
        $values = $this->getIdentifierValues($entity);
394
395
        if (count($values) === 0) {
396
            return;
397
        }
398
399
        return implode(self::ID_SEPARATOR, $values);
400
    }
401
402
    /**
403
     * {@inheritdoc}
404
     *
405
     * The ORM implementation does nothing special but you still should use
406
     * this method when using the id in a URL to allow for future improvements.
407
     */
408
    public function getUrlsafeIdentifier($entity)
409
    {
410
        return $this->getNormalizedIdentifier($entity);
411
    }
412
413
    /**
414
     * {@inheritdoc}
415
     */
416
    public function addIdentifiersToQuery($class, ProxyQueryInterface $queryProxy, array $idx)
417
    {
418
        $fieldNames = $this->getIdentifierFieldNames($class);
419
        $qb = $queryProxy->getQueryBuilder();
420
421
        $prefix = uniqid();
422
        $sqls = array();
423
        foreach ($idx as $pos => $id) {
424
            $ids = explode(self::ID_SEPARATOR, $id);
425
426
            $ands = array();
427
            foreach ($fieldNames as $posName => $name) {
428
                $parameterName = sprintf('field_%s_%s_%d', $prefix, $name, $pos);
429
                $ands[] = sprintf('%s.%s = :%s', $qb->getRootAlias(), $name, $parameterName);
430
                $qb->setParameter($parameterName, $ids[$posName]);
431
            }
432
433
            $sqls[] = implode(' AND ', $ands);
434
        }
435
436
        $qb->andWhere(sprintf('( %s )', implode(' OR ', $sqls)));
437
    }
438
439
    /**
440
     * {@inheritdoc}
441
     */
442
    public function batchDelete($class, ProxyQueryInterface $queryProxy)
443
    {
444
        $queryProxy->select('DISTINCT '.$queryProxy->getRootAlias());
445
446
        try {
447
            $entityManager = $this->getEntityManager($class);
448
449
            $i = 0;
450
            foreach ($queryProxy->getQuery()->iterate() as $pos => $object) {
451
                $entityManager->remove($object[0]);
452
453
                if ((++$i % 20) == 0) {
454
                    $entityManager->flush();
455
                    $entityManager->clear();
456
                }
457
            }
458
459
            $entityManager->flush();
460
            $entityManager->clear();
461
        } catch (\PDOException $e) {
462
            throw new ModelManagerException('', 0, $e);
463
        } catch (DBALException $e) {
464
            throw new ModelManagerException('', 0, $e);
465
        }
466
    }
467
468
    /**
469
     * {@inheritdoc}
470
     */
471
    public function getDataSourceIterator(DatagridInterface $datagrid, array $fields, $firstResult = null, $maxResult = null)
472
    {
473
        $datagrid->buildPager();
474
        $query = $datagrid->getQuery();
475
476
        $query->select('DISTINCT '.$query->getRootAlias());
477
        $query->setFirstResult($firstResult);
478
        $query->setMaxResults($maxResult);
479
480
        if ($query instanceof ProxyQueryInterface) {
481
            $query->addOrderBy($query->getSortBy(), $query->getSortOrder());
482
483
            $query = $query->getQuery();
484
        }
485
486
        return new DoctrineORMQuerySourceIterator($query, $fields);
487
    }
488
489
    /**
490
     * {@inheritdoc}
491
     */
492
    public function getExportFields($class)
493
    {
494
        $metadata = $this->getEntityManager($class)->getClassMetadata($class);
495
496
        return $metadata->getFieldNames();
497
    }
498
499
    /**
500
     * {@inheritdoc}
501
     */
502
    public function getModelInstance($class)
503
    {
504
        $r = new \ReflectionClass($class);
505
        if ($r->isAbstract()) {
506
            throw new \RuntimeException(sprintf('Cannot initialize abstract class: %s', $class));
507
        }
508
509
        return new $class();
510
    }
511
512
    /**
513
     * {@inheritdoc}
514
     */
515
    public function getSortParameters(FieldDescriptionInterface $fieldDescription, DatagridInterface $datagrid)
516
    {
517
        $values = $datagrid->getValues();
518
519
        if ($fieldDescription->getName() == $values['_sort_by']->getName() || $values['_sort_by']->getName() === $fieldDescription->getOption('sortable')) {
520
            if ($values['_sort_order'] == 'ASC') {
521
                $values['_sort_order'] = 'DESC';
522
            } else {
523
                $values['_sort_order'] = 'ASC';
524
            }
525
        } else {
526
            $values['_sort_order'] = 'ASC';
527
        }
528
529
        $values['_sort_by'] = is_string($fieldDescription->getOption('sortable')) ? $fieldDescription->getOption('sortable') : $fieldDescription->getName();
530
531
        return array('filter' => $values);
532
    }
533
534
    /**
535
     * {@inheritdoc}
536
     */
537
    public function getPaginationParameters(DatagridInterface $datagrid, $page)
538
    {
539
        $values = $datagrid->getValues();
540
541
        $values['_sort_by'] = $values['_sort_by']->getName();
542
        $values['_page'] = $page;
543
544
        return array('filter' => $values);
545
    }
546
547
    /**
548
     * {@inheritdoc}
549
     */
550
    public function getDefaultSortValues($class)
551
    {
552
        return array(
553
            '_sort_order' => 'ASC',
554
            '_sort_by' => implode(',', $this->getModelIdentifier($class)),
555
            '_page' => 1,
556
            '_per_page' => 25,
557
        );
558
    }
559
560
    /**
561
     * {@inheritdoc}
562
     */
563
    public function modelTransform($class, $instance)
564
    {
565
        return $instance;
566
    }
567
568
    /**
569
     * {@inheritdoc}
570
     */
571
    public function modelReverseTransform($class, array $array = array())
572
    {
573
        $instance = $this->getModelInstance($class);
574
        $metadata = $this->getMetadata($class);
575
576
        $reflClass = $metadata->reflClass;
577
        foreach ($array as $name => $value) {
578
            $reflection_property = false;
579
            // property or association ?
580
            if (array_key_exists($name, $metadata->fieldMappings)) {
581
                $property = $metadata->fieldMappings[$name]['fieldName'];
582
                $reflection_property = $metadata->reflFields[$name];
583
            } elseif (array_key_exists($name, $metadata->associationMappings)) {
584
                $property = $metadata->associationMappings[$name]['fieldName'];
585
            } else {
586
                $property = $name;
587
            }
588
589
            $setter = 'set'.$this->camelize($name);
590
591
            if ($reflClass->hasMethod($setter)) {
592
                if (!$reflClass->getMethod($setter)->isPublic()) {
593
                    throw new PropertyAccessDeniedException(sprintf('Method "%s()" is not public in class "%s"', $setter, $reflClass->getName()));
594
                }
595
596
                $instance->$setter($value);
597
            } elseif ($reflClass->hasMethod('__set')) {
598
                // needed to support magic method __set
599
                $instance->$property = $value;
600
            } elseif ($reflClass->hasProperty($property)) {
601
                if (!$reflClass->getProperty($property)->isPublic()) {
602
                    throw new PropertyAccessDeniedException(
603
                        sprintf('Property "%s" is not public in class "%s". Maybe you should create the method "set%s()"?', $property, $reflClass->getName(), ucfirst($property))
604
                    );
605
                }
606
607
                $instance->$property = $value;
608
            } elseif ($reflection_property) {
609
                $reflection_property->setValue($instance, $value);
610
            }
611
        }
612
613
        return $instance;
614
    }
615
616
    /**
617
     * {@inheritdoc}
618
     */
619
    public function getModelCollectionInstance($class)
620
    {
621
        return new \Doctrine\Common\Collections\ArrayCollection();
0 ignored issues
show
Bug Best Practice introduced by
The return type of return new \Doctrine\Com...ions\ArrayCollection(); (Doctrine\Common\Collections\ArrayCollection) is incompatible with the return type declared by the interface Sonata\AdminBundle\Model...ModelCollectionInstance of type array.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
622
    }
623
624
    /**
625
     * {@inheritdoc}
626
     */
627
    public function collectionClear(&$collection)
628
    {
629
        return $collection->clear();
630
    }
631
632
    /**
633
     * {@inheritdoc}
634
     */
635
    public function collectionHasElement(&$collection, &$element)
636
    {
637
        return $collection->contains($element);
638
    }
639
640
    /**
641
     * {@inheritdoc}
642
     */
643
    public function collectionAddElement(&$collection, &$element)
644
    {
645
        return $collection->add($element);
646
    }
647
648
    /**
649
     * {@inheritdoc}
650
     */
651
    public function collectionRemoveElement(&$collection, &$element)
652
    {
653
        return $collection->removeElement($element);
654
    }
655
656
    /**
657
     * method taken from PropertyPath.
658
     *
659
     * @param string $property
660
     *
661
     * @return mixed
662
     */
663
    protected function camelize($property)
664
    {
665
        return preg_replace(array('/(^|_)+(.)/e', '/\.(.)/e'), array("strtoupper('\\2')", "'_'.strtoupper('\\1')"), $property);
666
    }
667
}
668