Completed
Push — master ( 3fd2dd...aba624 )
by Grégoire
04:19
created

src/Form/ChoiceList/ModelChoiceList.php (2 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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\AdminBundle\Form\ChoiceList;
15
16
use Doctrine\Common\Util\ClassUtils;
17
use Doctrine\ORM\QueryBuilder;
18
use ReflectionProperty;
19
use Sonata\AdminBundle\Model\ModelManagerInterface;
20
use Symfony\Component\Form\Exception\InvalidArgumentException;
21
use Symfony\Component\Form\Exception\RuntimeException;
22
use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList;
23
use Symfony\Component\PropertyAccess\PropertyAccess;
24
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
25
use Symfony\Component\PropertyAccess\PropertyPath;
26
27
@trigger_error(
28
    'The '.__NAMESPACE__.'\ModelChoiceList class is deprecated since 3.24 and will be removed in 4.0.'
29
    .' Use '.__NAMESPACE__.'\ModelChoiceLoader instead.',
30
    E_USER_DEPRECATED
31
);
32
33
/**
34
 * NEXT_MAJOR: Remove this class.
35
 *
36
 * @deprecated Since 3.24, to be removed in 4.0. Use Sonata\AdminBundle\ModelChoiceLoader instead
37
 *
38
 * @author Thomas Rabaix <[email protected]>
39
 */
40
class ModelChoiceList extends SimpleChoiceList
41
{
42
    /**
43
     * @var ModelManagerInterface
44
     */
45
    private $modelManager;
46
47
    /**
48
     * @var string
49
     */
50
    private $class;
51
52
    /**
53
     * The entities from which the user can choose.
54
     *
55
     * This array is either indexed by ID (if the ID is a single field)
56
     * or by key in the choices array (if the ID consists of multiple fields)
57
     *
58
     * This property is initialized by initializeChoices(). It should only
59
     * be accessed through getEntity() and getEntities().
60
     *
61
     * @var mixed
62
     */
63
    private $entities = [];
64
65
    /**
66
     * Contains the query builder that builds the query for fetching the
67
     * entities.
68
     *
69
     * This property should only be accessed through queryBuilder.
70
     *
71
     * @var QueryBuilder
72
     */
73
    private $query;
74
75
    /**
76
     * The fields of which the identifier of the underlying class consists.
77
     *
78
     * This property should only be accessed through identifier.
79
     *
80
     * @var array
81
     */
82
    private $identifier = [];
83
84
    /**
85
     * A cache for \ReflectionProperty instances for the underlying class.
86
     *
87
     * This property should only be accessed through getReflProperty().
88
     *
89
     * @var array
90
     */
91
    private $reflProperties = [];
92
93
    /**
94
     * @var PropertyPath
95
     */
96
    private $propertyPath;
97
98
    /**
99
     * @var PropertyAccessorInterface
100
     */
101
    private $propertyAccessor;
102
103
    /**
104
     * @param string            $class
105
     * @param string|null       $property
106
     * @param QueryBuilder|null $query
107
     * @param array             $choices
108
     */
109
    public function __construct(
110
        ModelManagerInterface $modelManager,
111
        $class,
112
        $property = null,
113
        $query = null,
114
        $choices = [],
115
        PropertyAccessorInterface $propertyAccessor = null
116
    ) {
117
        $this->modelManager = $modelManager;
118
        $this->class = $class;
119
        $this->query = $query;
120
        $this->identifier = $this->modelManager->getIdentifierFieldNames($this->class);
121
122
        // The property option defines, which property (path) is used for
123
        // displaying entities as strings
124
        if ($property) {
125
            $this->propertyPath = new PropertyPath($property);
126
            $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
127
        }
128
129
        parent::__construct($this->load($choices));
130
    }
131
132
    /**
133
     * @return array
134
     */
135
    public function getIdentifier()
136
    {
137
        return $this->identifier;
138
    }
139
140
    /**
141
     * Returns the according entities for the choices.
142
     *
143
     * If the choices were not initialized, they are initialized now. This
144
     * is an expensive operation, except if the entities were passed in the
145
     * "choices" option.
146
     *
147
     * @return array An array of entities
148
     */
149
    public function getEntities()
150
    {
151
        return $this->entities;
152
    }
153
154
    /**
155
     * Returns the entity for the given key.
156
     *
157
     * If the underlying entities have composite identifiers, the choices
158
     * are initialized. The key is expected to be the index in the choices
159
     * array in this case.
160
     *
161
     * If they have single identifiers, they are either fetched from the
162
     * internal entity cache (if filled) or loaded from the database.
163
     *
164
     * @param string $key The choice key (for entities with composite
165
     *                    identifiers) or entity ID (for entities with single
166
     *                    identifiers)
167
     *
168
     * @return object The matching entity
169
     */
170
    public function getEntity($key)
171
    {
172
        if (\count($this->identifier) > 1) {
173
            // $key is a collection index
174
            $entities = $this->getEntities();
175
176
            return $entities[$key] ?? null;
177
        } elseif ($this->entities) {
178
            return isset($this->entities[$key]) ? $this->entities[$key] : null;
179
        }
180
181
        return $this->modelManager->find($this->class, $key);
182
    }
183
184
    /**
185
     * Returns the values of the identifier fields of an entity.
186
     *
187
     * Doctrine must know about this entity, that is, the entity must already
188
     * be persisted or added to the identity map before. Otherwise an
189
     * exception is thrown.
190
     *
191
     * @param object $entity The entity for which to get the identifier
192
     *
193
     * @throws InvalidArgumentException If the entity does not exist in Doctrine's
194
     *                                  identity map
195
     *
196
     * @return array
197
     */
198
    public function getIdentifierValues($entity)
199
    {
200
        try {
201
            return $this->modelManager->getIdentifierValues($entity);
202
        } catch (\Exception $e) {
203
            throw new InvalidArgumentException(sprintf('Unable to retrieve the identifier values for entity %s', ClassUtils::getClass($entity)), 0, $e);
204
        }
205
    }
206
207
    /**
208
     * @return ModelManagerInterface
209
     */
210
    public function getModelManager()
211
    {
212
        return $this->modelManager;
213
    }
214
215
    /**
216
     * @return string
217
     */
218
    public function getClass()
219
    {
220
        return $this->class;
221
    }
222
223
    /**
224
     * Initializes the choices and returns them.
225
     *
226
     * The choices are generated from the entities. If the entities have a
227
     * composite identifier, the choices are indexed using ascending integers.
228
     * Otherwise the identifiers are used as indices.
229
     *
230
     * If the entities were passed in the "choices" option, this method
231
     * does not have any significant overhead. Otherwise, if a query builder
232
     * was passed in the "query" option, this builder is now used to construct
233
     * a query which is executed. In the last case, all entities for the
234
     * underlying class are fetched from the repository.
235
     *
236
     * If the option "property" was passed, the property path in that option
237
     * is used as option values. Otherwise this method tries to convert
238
     * objects to strings using __toString().
239
     *
240
     * @return array An array of choices
241
     */
242
    protected function load($choices)
243
    {
244
        if (\is_array($choices) && \count($choices) > 0) {
245
            $entities = $choices;
246
        } elseif ($this->query) {
247
            $entities = $this->modelManager->executeQuery($this->query);
248
        } else {
249
            $entities = $this->modelManager->findBy($this->class);
250
        }
251
252
        if (null === $entities) {
253
            return [];
254
        }
255
256
        $choices = [];
257
        $this->entities = [];
258
259
        foreach ($entities as $key => $entity) {
260
            if ($this->propertyPath) {
261
                // If the property option was given, use it
262
                $value = $this->propertyAccessor->getValue($entity, $this->propertyPath);
263
            } else {
264
                // Otherwise expect a __toString() method in the entity
265
                try {
266
                    $value = (string) $entity;
267
                } catch (\Exception $e) {
0 ignored issues
show
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...
268
                    throw new RuntimeException(sprintf('Unable to convert the entity "%s" to string, provide '
269
                        .'"property" option or implement "__toString()" method in your entity.', ClassUtils::getClass($entity)), 0, $e);
270
                }
271
            }
272
273
            if (\count($this->identifier) > 1) {
274
                // When the identifier consists of multiple field, use
275
                // naturally ordered keys to refer to the choices
276
                $choices[$key] = $value;
277
                $this->entities[$key] = $entity;
278
            } else {
279
                // When the identifier is a single field, index choices by
280
                // entity ID for performance reasons
281
                $id = current($this->getIdentifierValues($entity));
282
                $choices[$id] = $value;
283
                $this->entities[$id] = $entity;
284
            }
285
        }
286
287
        return $choices;
288
    }
289
290
    /**
291
     * Returns the \ReflectionProperty instance for a property of the
292
     * underlying class.
293
     *
294
     * @param string $property The name of the property
295
     *
296
     * @return \ReflectionProperty The reflection instance
297
     */
298
    private function getReflProperty(string $property): ReflectionProperty
0 ignored issues
show
This method is not used, and could be removed.
Loading history...
299
    {
300
        if (!isset($this->reflProperties[$property])) {
301
            $this->reflProperties[$property] = new \ReflectionProperty($this->class, $property);
302
            $this->reflProperties[$property]->setAccessible(true);
303
        }
304
305
        return $this->reflProperties[$property];
306
    }
307
}
308