Completed
Push — master ( aed070...2ff8b4 )
by Grégoire
03:21
created

ModelChoiceList::getEntity()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 13
rs 9.2
c 0
b 0
f 0
cc 4
eloc 7
nc 4
nop 1
1
<?php
0 ignored issues
show
Coding Style Compatibility introduced by
For compatibility and reusability of your code, PSR1 recommends that a file should introduce either new symbols (like classes, functions, etc.) or have side-effects (like outputting something, or including other files), but not both at the same time. The first symbol is defined on line 39 and the first side effect is on line 26.

The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.

The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.

To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.

Loading history...
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\AdminBundle\Form\ChoiceList;
15
16
use Doctrine\Common\Util\ClassUtils;
17
use Doctrine\ORM\QueryBuilder;
18
use Sonata\AdminBundle\Model\ModelManagerInterface;
19
use Symfony\Component\Form\Exception\InvalidArgumentException;
20
use Symfony\Component\Form\Exception\RuntimeException;
21
use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList;
22
use Symfony\Component\PropertyAccess\PropertyAccess;
23
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
24
use Symfony\Component\PropertyAccess\PropertyPath;
25
26
@trigger_error(
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...
27
    'The '.__CLASS__.' class is deprecated since 3.24 and will be removed in 4.0.'
28
    .' Use '.__NAMESPACE__.'\ModelChoiceLoader instead.',
29
    E_USER_DEPRECATED
30
);
31
32
/**
33
 * NEXT_MAJOR: Remove this class.
34
 *
35
 * @deprecated Since 3.24, to be removed in 4.0. Use Sonata\AdminBundle\ModelChoiceLoader instead
36
 *
37
 * @author Thomas Rabaix <[email protected]>
38
 */
39
class ModelChoiceList extends SimpleChoiceList
40
{
41
    /**
42
     * @var ModelManagerInterface
43
     */
44
    private $modelManager;
45
46
    /**
47
     * @var string
48
     */
49
    private $class;
50
51
    /**
52
     * The entities from which the user can choose.
53
     *
54
     * This array is either indexed by ID (if the ID is a single field)
55
     * or by key in the choices array (if the ID consists of multiple fields)
56
     *
57
     * This property is initialized by initializeChoices(). It should only
58
     * be accessed through getEntity() and getEntities().
59
     *
60
     * @var mixed
61
     */
62
    private $entities = [];
63
64
    /**
65
     * Contains the query builder that builds the query for fetching the
66
     * entities.
67
     *
68
     * This property should only be accessed through queryBuilder.
69
     *
70
     * @var QueryBuilder
71
     */
72
    private $query;
73
74
    /**
75
     * The fields of which the identifier of the underlying class consists.
76
     *
77
     * This property should only be accessed through identifier.
78
     *
79
     * @var array
80
     */
81
    private $identifier = [];
82
83
    /**
84
     * A cache for \ReflectionProperty instances for the underlying class.
85
     *
86
     * This property should only be accessed through getReflProperty().
87
     *
88
     * @var array
89
     */
90
    private $reflProperties = [];
91
92
    /**
93
     * @var PropertyPath
94
     */
95
    private $propertyPath;
96
97
    /**
98
     * @var PropertyAccessorInterface
99
     */
100
    private $propertyAccessor;
101
102
    /**
103
     * @param ModelManagerInterface $modelManager
104
     * @param string                $class
105
     * @param string|null           $property
106
     * @param QueryBuilder|null     $query
107
     * @param array                 $choices
108
     */
109
    public function __construct(ModelManagerInterface $modelManager, $class, $property = null, $query = null, $choices = [], PropertyAccessorInterface $propertyAccessor = null)
110
    {
111
        $this->modelManager = $modelManager;
112
        $this->class = $class;
113
        $this->query = $query;
114
        $this->identifier = $this->modelManager->getIdentifierFieldNames($this->class);
115
116
        // The property option defines, which property (path) is used for
117
        // displaying entities as strings
118
        if ($property) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $property of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
119
            $this->propertyPath = new PropertyPath($property);
120
            $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
121
        }
122
123
        parent::__construct($this->load($choices));
124
    }
125
126
    /**
127
     * @return array
128
     */
129
    public function getIdentifier()
130
    {
131
        return $this->identifier;
132
    }
133
134
    /**
135
     * Returns the according entities for the choices.
136
     *
137
     * If the choices were not initialized, they are initialized now. This
138
     * is an expensive operation, except if the entities were passed in the
139
     * "choices" option.
140
     *
141
     * @return array An array of entities
142
     */
143
    public function getEntities()
144
    {
145
        return $this->entities;
146
    }
147
148
    /**
149
     * Returns the entity for the given key.
150
     *
151
     * If the underlying entities have composite identifiers, the choices
152
     * are initialized. The key is expected to be the index in the choices
153
     * array in this case.
154
     *
155
     * If they have single identifiers, they are either fetched from the
156
     * internal entity cache (if filled) or loaded from the database.
157
     *
158
     * @param string $key The choice key (for entities with composite
159
     *                    identifiers) or entity ID (for entities with single
160
     *                    identifiers)
161
     *
162
     * @return object The matching entity
163
     */
164
    public function getEntity($key)
165
    {
166
        if (count($this->identifier) > 1) {
167
            // $key is a collection index
168
            $entities = $this->getEntities();
169
170
            return $entities[$key] ?? null;
171
        } elseif ($this->entities) {
172
            return isset($this->entities[$key]) ? $this->entities[$key] : null;
173
        }
174
175
        return $this->modelManager->find($this->class, $key);
176
    }
177
178
    /**
179
     * Returns the values of the identifier fields of an entity.
180
     *
181
     * Doctrine must know about this entity, that is, the entity must already
182
     * be persisted or added to the identity map before. Otherwise an
183
     * exception is thrown.
184
     *
185
     * @param object $entity The entity for which to get the identifier
186
     *
187
     * @throws InvalidArgumentException If the entity does not exist in Doctrine's
188
     *                                  identity map
189
     *
190
     * @return array
191
     */
192
    public function getIdentifierValues($entity)
193
    {
194
        try {
195
            return $this->modelManager->getIdentifierValues($entity);
196
        } catch (\Exception $e) {
197
            throw new InvalidArgumentException(sprintf('Unable to retrieve the identifier values for entity %s', ClassUtils::getClass($entity)), 0, $e);
198
        }
199
    }
200
201
    /**
202
     * @return ModelManagerInterface
203
     */
204
    public function getModelManager()
205
    {
206
        return $this->modelManager;
207
    }
208
209
    /**
210
     * @return string
211
     */
212
    public function getClass()
213
    {
214
        return $this->class;
215
    }
216
217
    /**
218
     * Initializes the choices and returns them.
219
     *
220
     * The choices are generated from the entities. If the entities have a
221
     * composite identifier, the choices are indexed using ascending integers.
222
     * Otherwise the identifiers are used as indices.
223
     *
224
     * If the entities were passed in the "choices" option, this method
225
     * does not have any significant overhead. Otherwise, if a query builder
226
     * was passed in the "query" option, this builder is now used to construct
227
     * a query which is executed. In the last case, all entities for the
228
     * underlying class are fetched from the repository.
229
     *
230
     * If the option "property" was passed, the property path in that option
231
     * is used as option values. Otherwise this method tries to convert
232
     * objects to strings using __toString().
233
     *
234
     * @param $choices
235
     *
236
     * @return array An array of choices
237
     */
238
    protected function load($choices)
239
    {
240
        if (is_array($choices) && count($choices) > 0) {
241
            $entities = $choices;
242
        } elseif ($this->query) {
243
            $entities = $this->modelManager->executeQuery($this->query);
244
        } else {
245
            $entities = $this->modelManager->findBy($this->class);
246
        }
247
248
        if (null === $entities) {
249
            return [];
250
        }
251
252
        $choices = [];
253
        $this->entities = [];
254
255
        foreach ($entities as $key => $entity) {
256
            if ($this->propertyPath) {
257
                // If the property option was given, use it
258
                $value = $this->propertyAccessor->getValue($entity, $this->propertyPath);
259
            } else {
260
                // Otherwise expect a __toString() method in the entity
261
                try {
262
                    $value = (string) $entity;
263
                } catch (\Exception $e) {
0 ignored issues
show
Unused Code introduced by
catch (\Exception $e) { ...ss($entity)), 0, $e); } does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
264
                    throw new RuntimeException(sprintf('Unable to convert the entity "%s" to string, provide '
265
                        .'"property" option or implement "__toString()" method in your entity.', ClassUtils::getClass($entity)), 0, $e);
266
                }
267
            }
268
269
            if (count($this->identifier) > 1) {
270
                // When the identifier consists of multiple field, use
271
                // naturally ordered keys to refer to the choices
272
                $choices[$key] = $value;
273
                $this->entities[$key] = $entity;
274
            } else {
275
                // When the identifier is a single field, index choices by
276
                // entity ID for performance reasons
277
                $id = current($this->getIdentifierValues($entity));
278
                $choices[$id] = $value;
279
                $this->entities[$id] = $entity;
280
            }
281
        }
282
283
        return $choices;
284
    }
285
286
    /**
287
     * Returns the \ReflectionProperty instance for a property of the
288
     * underlying class.
289
     *
290
     * @param string $property The name of the property
291
     *
292
     * @return \ReflectionProperty The reflection instance
293
     */
294
    private function getReflProperty($property)
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
295
    {
296
        if (!isset($this->reflProperties[$property])) {
297
            $this->reflProperties[$property] = new \ReflectionProperty($this->class, $property);
298
            $this->reflProperties[$property]->setAccessible(true);
299
        }
300
301
        return $this->reflProperties[$property];
302
    }
303
}
304