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 | namespace DoctrineModule\Form\Element; |
||
6 | |||
7 | use Doctrine\Common\Collections\Collection; |
||
8 | use Doctrine\Common\Inflector\Inflector; |
||
9 | use Doctrine\Persistence\ObjectManager; |
||
10 | use DoctrineModule\Persistence\ObjectManagerAwareInterface; |
||
11 | use Laminas\Stdlib\Guard\ArrayOrTraversableGuardTrait; |
||
12 | use ReflectionMethod; |
||
13 | use RuntimeException; |
||
14 | use Traversable; |
||
15 | use function array_change_key_case; |
||
16 | use function array_key_exists; |
||
17 | use function array_shift; |
||
18 | use function call_user_func; |
||
19 | use function count; |
||
20 | use function current; |
||
21 | use function get_class; |
||
22 | use function gettype; |
||
23 | use function interface_exists; |
||
24 | use function is_callable; |
||
25 | use function is_object; |
||
26 | use function is_string; |
||
27 | use function method_exists; |
||
28 | use function sprintf; |
||
29 | use function strtolower; |
||
30 | use function trim; |
||
31 | |||
32 | class Proxy implements ObjectManagerAwareInterface |
||
33 | { |
||
34 | use ArrayOrTraversableGuardTrait; |
||
35 | |||
36 | /** @var mixed[]|Traversable */ |
||
37 | protected $objects; |
||
38 | |||
39 | /** @var string */ |
||
40 | protected $targetClass; |
||
41 | |||
42 | /** @var mixed[] */ |
||
43 | protected $valueOptions = []; |
||
44 | |||
45 | /** @var mixed[] */ |
||
46 | protected $findMethod = []; |
||
47 | |||
48 | /** @var mixed */ |
||
49 | protected $property; |
||
50 | |||
51 | /** @var mixed[] */ |
||
52 | protected $optionAttributes = []; |
||
53 | |||
54 | /** @var callable $labelGenerator A callable used to create a label based on an item in the collection an Entity */ |
||
55 | protected $labelGenerator; |
||
56 | |||
57 | /** @var bool|null */ |
||
58 | protected $isMethod; |
||
59 | |||
60 | /** @var ObjectManager */ |
||
61 | protected $objectManager; |
||
62 | |||
63 | /** @var bool */ |
||
64 | protected $displayEmptyItem = false; |
||
65 | |||
66 | /** @var string */ |
||
67 | protected $emptyItemLabel = ''; |
||
68 | |||
69 | /** @var string|null */ |
||
70 | protected $optgroupIdentifier; |
||
71 | |||
72 | /** @var string|null */ |
||
73 | protected $optgroupDefault; |
||
74 | |||
75 | /** |
||
76 | * @param mixed[] $options |
||
77 | */ |
||
78 | 23 | public function setOptions(array $options) : void |
|
79 | { |
||
80 | 23 | if (isset($options['object_manager'])) { |
|
81 | 22 | $this->setObjectManager($options['object_manager']); |
|
82 | } |
||
83 | |||
84 | 23 | if (isset($options['target_class'])) { |
|
85 | 22 | $this->setTargetClass($options['target_class']); |
|
86 | } |
||
87 | |||
88 | 23 | if (isset($options['property'])) { |
|
89 | 3 | $this->setProperty($options['property']); |
|
90 | } |
||
91 | |||
92 | 23 | if (isset($options['label_generator'])) { |
|
93 | 2 | $this->setLabelGenerator($options['label_generator']); |
|
94 | } |
||
95 | |||
96 | 23 | if (isset($options['find_method'])) { |
|
97 | 4 | $this->setFindMethod($options['find_method']); |
|
98 | } |
||
99 | |||
100 | 23 | if (isset($options['is_method'])) { |
|
101 | 1 | $this->setIsMethod($options['is_method']); |
|
102 | } |
||
103 | |||
104 | 23 | if (isset($options['display_empty_item'])) { |
|
105 | 1 | $this->setDisplayEmptyItem($options['display_empty_item']); |
|
106 | } |
||
107 | |||
108 | 23 | if (isset($options['empty_item_label'])) { |
|
109 | 1 | $this->setEmptyItemLabel($options['empty_item_label']); |
|
110 | } |
||
111 | |||
112 | 23 | if (isset($options['option_attributes'])) { |
|
113 | 3 | $this->setOptionAttributes($options['option_attributes']); |
|
114 | } |
||
115 | |||
116 | 23 | if (isset($options['optgroup_identifier'])) { |
|
117 | 4 | $this->setOptgroupIdentifier($options['optgroup_identifier']); |
|
118 | } |
||
119 | |||
120 | 23 | if (! isset($options['optgroup_default'])) { |
|
121 | 23 | return; |
|
122 | } |
||
123 | |||
124 | 1 | $this->setOptgroupDefault($options['optgroup_default']); |
|
125 | 1 | } |
|
126 | |||
127 | /** |
||
128 | * @return mixed |
||
129 | */ |
||
130 | 22 | public function getValueOptions() |
|
131 | { |
||
132 | 22 | if (empty($this->valueOptions)) { |
|
133 | 22 | $this->loadValueOptions(); |
|
134 | } |
||
135 | |||
136 | 14 | return $this->valueOptions; |
|
137 | } |
||
138 | |||
139 | /** |
||
140 | * @return mixed |
||
141 | */ |
||
142 | 20 | public function getObjects() |
|
143 | { |
||
144 | 20 | $this->loadObjects(); |
|
145 | |||
146 | 16 | return $this->objects; |
|
147 | } |
||
148 | |||
149 | /** |
||
150 | * Set the label for the empty option |
||
151 | */ |
||
152 | 1 | public function setEmptyItemLabel(string $emptyItemLabel) : Proxy |
|
153 | { |
||
154 | 1 | $this->emptyItemLabel = $emptyItemLabel; |
|
155 | |||
156 | 1 | return $this; |
|
157 | } |
||
158 | |||
159 | 1 | public function getEmptyItemLabel() : string |
|
160 | { |
||
161 | 1 | return $this->emptyItemLabel; |
|
162 | } |
||
163 | |||
164 | /** |
||
165 | * @return mixed[] |
||
166 | */ |
||
167 | 14 | public function getOptionAttributes() : array |
|
168 | { |
||
169 | 14 | return $this->optionAttributes; |
|
170 | } |
||
171 | |||
172 | /** |
||
173 | * @param mixed[] $optionAttributes |
||
174 | */ |
||
175 | 3 | public function setOptionAttributes(array $optionAttributes) : void |
|
176 | { |
||
177 | 3 | $this->optionAttributes = $optionAttributes; |
|
178 | 3 | } |
|
179 | |||
180 | /** |
||
181 | * Set a flag, whether to include the empty option at the beginning or not |
||
182 | */ |
||
183 | 1 | public function setDisplayEmptyItem(bool $displayEmptyItem) : Proxy |
|
184 | { |
||
185 | 1 | $this->displayEmptyItem = $displayEmptyItem; |
|
186 | |||
187 | 1 | return $this; |
|
188 | } |
||
189 | |||
190 | public function getDisplayEmptyItem() : bool |
||
191 | { |
||
192 | return $this->displayEmptyItem; |
||
193 | } |
||
194 | |||
195 | /** |
||
196 | * Set the object manager |
||
197 | */ |
||
198 | 22 | public function setObjectManager(ObjectManager $objectManager) : void |
|
199 | { |
||
200 | 22 | $this->objectManager = $objectManager; |
|
201 | 22 | } |
|
202 | |||
203 | /** |
||
204 | * Get the object manager |
||
205 | */ |
||
206 | 20 | public function getObjectManager() : ObjectManager |
|
207 | { |
||
208 | 20 | return $this->objectManager; |
|
209 | } |
||
210 | |||
211 | /** |
||
212 | * Set the FQCN of the target object |
||
213 | */ |
||
214 | 22 | public function setTargetClass(string $targetClass) : Proxy |
|
215 | { |
||
216 | 22 | $this->targetClass = $targetClass; |
|
217 | |||
218 | 22 | return $this; |
|
219 | } |
||
220 | |||
221 | /** |
||
222 | * Get the target class |
||
223 | */ |
||
224 | 20 | public function getTargetClass() : string |
|
225 | { |
||
226 | 20 | return $this->targetClass; |
|
227 | } |
||
228 | |||
229 | /** |
||
230 | * Set the property to use as the label in the options |
||
231 | */ |
||
232 | 3 | public function setProperty(string $property) : Proxy |
|
233 | { |
||
234 | 3 | $this->property = $property; |
|
235 | |||
236 | 3 | return $this; |
|
237 | } |
||
238 | |||
239 | /** |
||
240 | * @return mixed |
||
241 | */ |
||
242 | public function getProperty() |
||
243 | { |
||
244 | return $this->property; |
||
245 | } |
||
246 | |||
247 | /** |
||
248 | * Set the label generator callable that is responsible for generating labels for the items in the collection |
||
249 | * |
||
250 | * @param callable $callable A callable used to create a label based off of an Entity |
||
251 | */ |
||
252 | 1 | public function setLabelGenerator(callable $callable) : void |
|
253 | { |
||
254 | 1 | $this->labelGenerator = $callable; |
|
255 | 1 | } |
|
256 | |||
257 | 14 | public function getLabelGenerator() : ?callable |
|
258 | { |
||
259 | 14 | return $this->labelGenerator; |
|
260 | } |
||
261 | |||
262 | 13 | public function getOptgroupIdentifier() : ?string |
|
263 | { |
||
264 | 13 | return $this->optgroupIdentifier; |
|
265 | } |
||
266 | |||
267 | 4 | public function setOptgroupIdentifier(string $optgroupIdentifier) : void |
|
268 | { |
||
269 | 4 | $this->optgroupIdentifier = (string) $optgroupIdentifier; |
|
270 | 4 | } |
|
271 | |||
272 | 2 | public function getOptgroupDefault() : ?string |
|
273 | { |
||
274 | 2 | return $this->optgroupDefault; |
|
275 | } |
||
276 | |||
277 | 1 | public function setOptgroupDefault(string $optgroupDefault) : void |
|
278 | { |
||
279 | 1 | $this->optgroupDefault = (string) $optgroupDefault; |
|
280 | 1 | } |
|
281 | |||
282 | /** |
||
283 | * Set if the property is a method to use as the label in the options |
||
284 | */ |
||
285 | 1 | public function setIsMethod(bool $method) : Proxy |
|
286 | { |
||
287 | 1 | $this->isMethod = (bool) $method; |
|
288 | |||
289 | 1 | return $this; |
|
290 | } |
||
291 | |||
292 | 3 | public function getIsMethod() : ?bool |
|
293 | { |
||
294 | 3 | return $this->isMethod; |
|
295 | } |
||
296 | |||
297 | /** Set the findMethod property to specify the method to use on repository |
||
298 | * |
||
299 | * @param mixed[] $findMethod |
||
300 | */ |
||
301 | 4 | public function setFindMethod(array $findMethod) : Proxy |
|
302 | { |
||
303 | 4 | $this->findMethod = $findMethod; |
|
304 | |||
305 | 4 | return $this; |
|
306 | } |
||
307 | |||
308 | /** |
||
309 | * Get findMethod definition |
||
310 | * |
||
311 | * @return mixed[] |
||
312 | */ |
||
313 | 20 | public function getFindMethod() : array |
|
314 | { |
||
315 | 20 | return $this->findMethod; |
|
316 | } |
||
317 | |||
318 | /** |
||
319 | * @param mixed $targetEntity |
||
320 | */ |
||
321 | 14 | protected function generateLabel($targetEntity) : ?string |
|
322 | { |
||
323 | 14 | if ($this->getLabelGenerator() === null) { |
|
324 | 13 | return null; |
|
325 | } |
||
326 | |||
327 | 1 | return call_user_func($this->getLabelGenerator(), $targetEntity); |
|
328 | } |
||
329 | |||
330 | /** |
||
331 | * @param mixed $value |
||
332 | * |
||
333 | * @return mixed[]|mixed|object |
||
334 | * |
||
335 | * @throws RuntimeException |
||
336 | */ |
||
337 | public function getValue($value) |
||
338 | { |
||
339 | if (! $this->getObjectManager()) { |
||
340 | throw new RuntimeException('No object manager was set'); |
||
341 | } |
||
342 | |||
343 | if (! $this->getTargetClass()) { |
||
344 | throw new RuntimeException('No target class was set'); |
||
345 | } |
||
346 | |||
347 | $metadata = $this->getObjectManager()->getClassMetadata($this->getTargetClass()); |
||
348 | |||
349 | if (is_object($value)) { |
||
350 | if ($value instanceof Collection) { |
||
351 | $data = []; |
||
352 | |||
353 | foreach ($value as $object) { |
||
354 | $values = $metadata->getIdentifierValues($object); |
||
355 | $data[] = array_shift($values); |
||
356 | } |
||
357 | |||
358 | $value = $data; |
||
359 | } else { |
||
360 | $metadata = $this->getObjectManager()->getClassMetadata(get_class($value)); |
||
361 | $identifier = $metadata->getIdentifierFieldNames(); |
||
362 | |||
363 | // TODO: handle composite (multiple) identifiers |
||
364 | if ($identifier !== null && count($identifier) > 1) { |
||
365 | //$value = $key; |
||
366 | $todo = true; |
||
367 | } else { |
||
368 | $value = current($metadata->getIdentifierValues($value)); |
||
369 | } |
||
370 | } |
||
371 | } |
||
372 | |||
373 | return $value; |
||
374 | } |
||
375 | |||
376 | /** |
||
377 | * Load objects |
||
378 | * |
||
379 | * @throws RuntimeException |
||
380 | * @throws Exception\InvalidRepositoryResultException |
||
381 | */ |
||
382 | 20 | protected function loadObjects() : void |
|
383 | { |
||
384 | 20 | if (! empty($this->objects)) { |
|
385 | return; |
||
386 | } |
||
387 | |||
388 | 20 | $findMethod = (array) $this->getFindMethod(); |
|
389 | |||
390 | 20 | if (! $findMethod) { |
|
0 ignored issues
–
show
|
|||
391 | 16 | $findMethodName = 'findAll'; |
|
392 | 16 | $repository = $this->objectManager->getRepository($this->targetClass); |
|
393 | 16 | $objects = $repository->findAll(); |
|
394 | } else { |
||
395 | 4 | if (! isset($findMethod['name'])) { |
|
396 | 1 | throw new RuntimeException('No method name was set'); |
|
397 | } |
||
398 | |||
399 | 3 | $findMethodName = $findMethod['name']; |
|
400 | 3 | $findMethodParams = isset($findMethod['params']) ? array_change_key_case($findMethod['params']) : []; |
|
401 | 3 | $repository = $this->objectManager->getRepository($this->targetClass); |
|
402 | |||
403 | 3 | if (! method_exists($repository, $findMethodName)) { |
|
404 | 1 | throw new RuntimeException( |
|
405 | 1 | sprintf( |
|
406 | 1 | 'Method "%s" could not be found in repository "%s"', |
|
407 | 1 | $findMethodName, |
|
408 | 1 | get_class($repository) |
|
409 | ) |
||
410 | ); |
||
411 | } |
||
412 | |||
413 | 2 | $r = new ReflectionMethod($repository, $findMethodName); |
|
414 | 2 | $args = []; |
|
415 | |||
416 | 2 | foreach ($r->getParameters() as $param) { |
|
417 | 2 | if (array_key_exists(strtolower($param->getName()), $findMethodParams)) { |
|
0 ignored issues
–
show
Loading history...
|
|||
418 | 1 | $args[] = $findMethodParams[strtolower($param->getName())]; |
|
0 ignored issues
–
show
Loading history...
|
|||
419 | 2 | } elseif ($param->isDefaultValueAvailable()) { |
|
420 | 1 | $args[] = $param->getDefaultValue(); |
|
421 | 1 | } elseif (! $param->isOptional()) { |
|
422 | 1 | throw new RuntimeException( |
|
423 | 1 | sprintf( |
|
424 | 'Required parameter "%s" with no default value for method "%s" in repository "%s"' |
||
425 | 1 | . ' was not provided', |
|
426 | 1 | $param->getName(), |
|
427 | 1 | $findMethodName, |
|
428 | 1 | get_class($repository) |
|
429 | ) |
||
430 | ); |
||
431 | } |
||
432 | } |
||
433 | |||
434 | 1 | $objects = $r->invokeArgs($repository, $args); |
|
435 | } |
||
436 | |||
437 | 17 | $this->guardForArrayOrTraversable( |
|
438 | 17 | $objects, |
|
439 | 17 | sprintf('%s::%s() return value', get_class($repository), $findMethodName), |
|
440 | 17 | 'DoctrineModule\Form\Element\Exception\InvalidRepositoryResultException' |
|
441 | ); |
||
442 | |||
443 | 16 | $this->objects = $objects; |
|
444 | 16 | } |
|
445 | |||
446 | /** |
||
447 | * Load value options |
||
448 | * |
||
449 | * @throws RuntimeException |
||
450 | */ |
||
451 | 22 | protected function loadValueOptions() : void |
|
452 | { |
||
453 | 22 | if (! $this->objectManager) { |
|
454 | 1 | throw new RuntimeException('No object manager was set'); |
|
455 | } |
||
456 | |||
457 | 21 | if (! $this->targetClass) { |
|
458 | 1 | throw new RuntimeException('No target class was set'); |
|
459 | } |
||
460 | |||
461 | 20 | $metadata = $this->getObjectManager()->getClassMetadata($this->getTargetClass()); |
|
462 | 20 | $identifier = $metadata->getIdentifierFieldNames(); |
|
463 | 20 | $objects = $this->getObjects(); |
|
464 | 16 | $options = []; |
|
465 | 16 | $optionAttributes = []; |
|
466 | |||
467 | 16 | if ($this->displayEmptyItem) { |
|
468 | 1 | $options[''] = $this->getEmptyItemLabel(); |
|
469 | } |
||
470 | |||
471 | 16 | foreach ($objects as $key => $object) { |
|
472 | 14 | $generatedLabel = $this->generateLabel($object); |
|
473 | 14 | if ($generatedLabel !== null) { |
|
474 | 1 | $label = $generatedLabel; |
|
475 | 13 | } elseif ($this->property) { |
|
476 | 3 | $property = $this->property; |
|
477 | 3 | if (($this->getIsMethod() === false || $this->getIsMethod() === null) |
|
478 | 3 | && ! $metadata->hasField($property) |
|
479 | ) { |
||
480 | throw new RuntimeException( |
||
481 | sprintf( |
||
482 | 'Property "%s" could not be found in object "%s"', |
||
483 | $property, |
||
484 | $this->getTargetClass() |
||
485 | ) |
||
486 | ); |
||
487 | } |
||
488 | |||
489 | 3 | $getter = 'get' . Inflector::classify($property); |
|
490 | |||
491 | 3 | if (! is_callable([$object, $getter])) { |
|
492 | throw new RuntimeException( |
||
493 | sprintf('Method "%s::%s" is not callable', $this->targetClass, $getter) |
||
494 | ); |
||
495 | } |
||
496 | |||
497 | 3 | $label = $object->{$getter}(); |
|
498 | } else { |
||
499 | 10 | if (! is_callable([$object, '__toString'])) { |
|
500 | throw new RuntimeException( |
||
501 | sprintf( |
||
502 | '%s must have a "__toString()" method defined if you have not set a property' |
||
503 | . ' or method to use.', |
||
504 | $this->getTargetClass() |
||
505 | ) |
||
506 | ); |
||
507 | } |
||
508 | |||
509 | 10 | $label = (string) $object; |
|
510 | } |
||
511 | |||
512 | 14 | if ($identifier !== null && count($identifier) > 1) { |
|
513 | $value = $key; |
||
514 | } else { |
||
515 | 14 | $value = current($metadata->getIdentifierValues($object)); |
|
516 | } |
||
517 | |||
518 | 14 | foreach ($this->getOptionAttributes() as $optionKey => $optionValue) { |
|
519 | 3 | if (is_string($optionValue)) { |
|
520 | 1 | $optionAttributes[$optionKey] = $optionValue; |
|
521 | |||
522 | 1 | continue; |
|
523 | } |
||
524 | |||
525 | 2 | if (is_callable($optionValue)) { |
|
526 | 1 | $callableValue = call_user_func($optionValue, $object); |
|
527 | 1 | $optionAttributes[$optionKey] = (string) $callableValue; |
|
528 | |||
529 | 1 | continue; |
|
530 | } |
||
531 | |||
532 | 1 | throw new RuntimeException( |
|
533 | 1 | sprintf( |
|
534 | 'Parameter "option_attributes" expects an array of key => value where value is of type' |
||
535 | 1 | . '"string" or "callable". Value of type "%s" found.', |
|
536 | 1 | gettype($optionValue) |
|
537 | ) |
||
538 | ); |
||
539 | } |
||
540 | |||
541 | // If no optgroup_identifier has been configured, apply default handling and continue |
||
542 | 13 | if ($this->getOptgroupIdentifier() === null) { |
|
543 | 9 | $options[] = ['label' => $label, 'value' => $value, 'attributes' => $optionAttributes]; |
|
544 | |||
545 | 9 | continue; |
|
546 | } |
||
547 | |||
548 | // optgroup_identifier found, handle grouping |
||
549 | 4 | $optgroupGetter = 'get' . Inflector::classify($this->getOptgroupIdentifier()); |
|
550 | |||
551 | 4 | if (! is_callable([$object, $optgroupGetter])) { |
|
552 | 1 | throw new RuntimeException( |
|
553 | 1 | sprintf('Method "%s::%s" is not callable', $this->targetClass, $optgroupGetter) |
|
554 | ); |
||
555 | } |
||
556 | |||
557 | 3 | $optgroup = $object->{$optgroupGetter}(); |
|
558 | |||
559 | // optgroup_identifier contains a valid group-name. Handle default grouping. |
||
560 | 3 | if ($optgroup !== null && trim($optgroup) !== '') { |
|
561 | 2 | $options[$optgroup]['label'] = $optgroup; |
|
562 | 2 | $options[$optgroup]['options'][] = [ |
|
563 | 2 | 'label' => $label, |
|
564 | 2 | 'value' => $value, |
|
565 | 2 | 'attributes' => $optionAttributes, |
|
566 | ]; |
||
567 | |||
568 | 2 | continue; |
|
569 | } |
||
570 | |||
571 | 2 | $optgroupDefault = $this->getOptgroupDefault(); |
|
572 | |||
573 | // No optgroup_default has been provided. Line up without a group |
||
574 | 2 | if ($optgroupDefault === null) { |
|
575 | 1 | $options[] = ['label' => $label, 'value' => $value, 'attributes' => $optionAttributes]; |
|
576 | |||
577 | 1 | continue; |
|
578 | } |
||
579 | |||
580 | // Line up entry with optgroup_default |
||
581 | 1 | $options[$optgroupDefault]['label'] = $optgroupDefault; |
|
582 | 1 | $options[$optgroupDefault]['options'][] = [ |
|
583 | 1 | 'label' => $label, |
|
584 | 1 | 'value' => $value, |
|
585 | 1 | 'attributes' => $optionAttributes, |
|
586 | ]; |
||
587 | } |
||
588 | |||
589 | 14 | $this->valueOptions = $options; |
|
590 | 14 | } |
|
591 | } |
||
592 | |||
593 | interface_exists(ObjectManager::class); |
||
594 |
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.