Completed
Push — master ( f2b8b4...a21374 )
by Tom
24s queued 11s
created

Proxy::getObjectManager()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
crap 1
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\Common\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 is_callable;
24
use function is_object;
25
use function is_string;
26
use function method_exists;
27
use function sprintf;
28
use function strtolower;
29
use function trim;
30
31
class Proxy implements ObjectManagerAwareInterface
32
{
33
    use ArrayOrTraversableGuardTrait;
34
35
    /** @var mixed[]|Traversable */
36
    protected $objects;
37
38
    /** @var string */
39
    protected $targetClass;
40
41
    /** @var mixed[] */
42
    protected $valueOptions = [];
43
44
    /** @var mixed[] */
45
    protected $findMethod = [];
46
47
    /** @var mixed */
48
    protected $property;
49
50
    /** @var mixed[] */
51
    protected $optionAttributes = [];
52
53
    /** @var callable $labelGenerator A callable used to create a label based on an item in the collection an Entity */
54
    protected $labelGenerator;
55
56
    /** @var bool|null */
57
    protected $isMethod;
58
59
    /** @var ObjectManager */
60
    protected $objectManager;
61
62
    /** @var bool */
63
    protected $displayEmptyItem = false;
64
65
    /** @var string */
66
    protected $emptyItemLabel = '';
67
68
    /** @var string|null */
69
    protected $optgroupIdentifier;
70
71
    /** @var string|null */
72
    protected $optgroupDefault;
73
74
    /**
75
     * @param mixed[] $options
76
     */
77 23
    public function setOptions(array $options) : void
78
    {
79 23
        if (isset($options['object_manager'])) {
80 22
            $this->setObjectManager($options['object_manager']);
81
        }
82
83 23
        if (isset($options['target_class'])) {
84 22
            $this->setTargetClass($options['target_class']);
85
        }
86
87 23
        if (isset($options['property'])) {
88 3
            $this->setProperty($options['property']);
89
        }
90
91 23
        if (isset($options['label_generator'])) {
92 2
            $this->setLabelGenerator($options['label_generator']);
0 ignored issues
show
Documentation introduced by
$options['label_generator'] is of type *, but the function expects a callable.

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...
93
        }
94
95 23
        if (isset($options['find_method'])) {
96 4
            $this->setFindMethod($options['find_method']);
97
        }
98
99 23
        if (isset($options['is_method'])) {
100 1
            $this->setIsMethod($options['is_method']);
101
        }
102
103 23
        if (isset($options['display_empty_item'])) {
104 1
            $this->setDisplayEmptyItem($options['display_empty_item']);
105
        }
106
107 23
        if (isset($options['empty_item_label'])) {
108 1
            $this->setEmptyItemLabel($options['empty_item_label']);
109
        }
110
111 23
        if (isset($options['option_attributes'])) {
112 3
            $this->setOptionAttributes($options['option_attributes']);
113
        }
114
115 23
        if (isset($options['optgroup_identifier'])) {
116 4
            $this->setOptgroupIdentifier($options['optgroup_identifier']);
117
        }
118
119 23
        if (! isset($options['optgroup_default'])) {
120 23
            return;
121
        }
122
123 1
        $this->setOptgroupDefault($options['optgroup_default']);
124 1
    }
125
126
    /**
127
     * @return mixed
128
     */
129 22
    public function getValueOptions()
130
    {
131 22
        if (empty($this->valueOptions)) {
132 22
            $this->loadValueOptions();
133
        }
134
135 14
        return $this->valueOptions;
136
    }
137
138
    /**
139
     * @return mixed
140
     */
141 20
    public function getObjects()
142
    {
143 20
        $this->loadObjects();
144
145 16
        return $this->objects;
146
    }
147
148
    /**
149
     * Set the label for the empty option
150
     */
151 1
    public function setEmptyItemLabel(string $emptyItemLabel) : Proxy
152
    {
153 1
        $this->emptyItemLabel = $emptyItemLabel;
154
155 1
        return $this;
156
    }
157
158 1
    public function getEmptyItemLabel() : string
159
    {
160 1
        return $this->emptyItemLabel;
161
    }
162
163
    /**
164
     * @return mixed[]
165
     */
166 14
    public function getOptionAttributes() : array
167
    {
168 14
        return $this->optionAttributes;
169
    }
170
171
    /**
172
     * @param mixed[] $optionAttributes
173
     */
174 3
    public function setOptionAttributes(array $optionAttributes) : void
175
    {
176 3
        $this->optionAttributes = $optionAttributes;
177 3
    }
178
179
    /**
180
     * Set a flag, whether to include the empty option at the beginning or not
181
     */
182 1
    public function setDisplayEmptyItem(bool $displayEmptyItem) : Proxy
183
    {
184 1
        $this->displayEmptyItem = $displayEmptyItem;
185
186 1
        return $this;
187
    }
188
189
    public function getDisplayEmptyItem() : bool
190
    {
191
        return $this->displayEmptyItem;
192
    }
193
194
    /**
195
     * Set the object manager
196
     */
197 22
    public function setObjectManager(ObjectManager $objectManager) : void
198
    {
199 22
        $this->objectManager = $objectManager;
200 22
    }
201
202
    /**
203
     * Get the object manager
204
     */
205 20
    public function getObjectManager() : ObjectManager
206
    {
207 20
        return $this->objectManager;
208
    }
209
210
    /**
211
     * Set the FQCN of the target object
212
     */
213 22
    public function setTargetClass(string $targetClass) : Proxy
214
    {
215 22
        $this->targetClass = $targetClass;
216
217 22
        return $this;
218
    }
219
220
    /**
221
     * Get the target class
222
     */
223 20
    public function getTargetClass() : string
224
    {
225 20
        return $this->targetClass;
226
    }
227
228
    /**
229
     * Set the property to use as the label in the options
230
     */
231 3
    public function setProperty(string $property) : Proxy
232
    {
233 3
        $this->property = $property;
234
235 3
        return $this;
236
    }
237
238
    /**
239
     * @return mixed
240
     */
241
    public function getProperty()
242
    {
243
        return $this->property;
244
    }
245
246
    /**
247
     * Set the label generator callable that is responsible for generating labels for the items in the collection
248
     *
249
     * @param callable $callable A callable used to create a label based off of an Entity
250
     */
251 1
    public function setLabelGenerator(callable $callable) : void
252
    {
253 1
        $this->labelGenerator = $callable;
254 1
    }
255
256 14
    public function getLabelGenerator() : ?callable
257
    {
258 14
        return $this->labelGenerator;
259
    }
260
261 13
    public function getOptgroupIdentifier() : ?string
262
    {
263 13
        return $this->optgroupIdentifier;
264
    }
265
266 4
    public function setOptgroupIdentifier(string $optgroupIdentifier) : void
267
    {
268 4
        $this->optgroupIdentifier = (string) $optgroupIdentifier;
269 4
    }
270
271 2
    public function getOptgroupDefault() : ?string
272
    {
273 2
        return $this->optgroupDefault;
274
    }
275
276 1
    public function setOptgroupDefault(string $optgroupDefault) : void
277
    {
278 1
        $this->optgroupDefault = (string) $optgroupDefault;
279 1
    }
280
281
    /**
282
     * Set if the property is a method to use as the label in the options
283
     */
284 1
    public function setIsMethod(bool $method) : Proxy
285
    {
286 1
        $this->isMethod = (bool) $method;
287
288 1
        return $this;
289
    }
290
291 3
    public function getIsMethod() : ?bool
292
    {
293 3
        return $this->isMethod;
294
    }
295
296
    /** Set the findMethod property to specify the method to use on repository
297
     *
298
     * @param mixed[] $findMethod
299
     */
300 4
    public function setFindMethod(array $findMethod) : Proxy
301
    {
302 4
        $this->findMethod = $findMethod;
303
304 4
        return $this;
305
    }
306
307
    /**
308
     * Get findMethod definition
309
     *
310
     * @return mixed[]
311
     */
312 20
    public function getFindMethod() : array
313
    {
314 20
        return $this->findMethod;
315
    }
316
317
    /**
318
     * @param mixed $targetEntity
319
     */
320 14
    protected function generateLabel($targetEntity) : ?string
321
    {
322 14
        if ($this->getLabelGenerator() === null) {
323 13
            return null;
324
        }
325
326 1
        return call_user_func($this->getLabelGenerator(), $targetEntity);
327
    }
328
329
    /**
330
     * @param mixed $value
331
     *
332
     * @return mixed[]|mixed|object
333
     *
334
     * @throws RuntimeException
335
     */
336
    public function getValue($value)
337
    {
338
        if (! $this->getObjectManager()) {
339
            throw new RuntimeException('No object manager was set');
340
        }
341
342
        if (! $this->getTargetClass()) {
343
            throw new RuntimeException('No target class was set');
344
        }
345
346
        $metadata = $this->getObjectManager()->getClassMetadata($this->getTargetClass());
347
348
        if (is_object($value)) {
349
            if ($value instanceof Collection) {
350
                $data = [];
351
352
                foreach ($value as $object) {
353
                    $values = $metadata->getIdentifierValues($object);
354
                    $data[] = array_shift($values);
355
                }
356
357
                $value = $data;
358
            } else {
359
                $metadata   = $this->getObjectManager()->getClassMetadata(get_class($value));
360
                $identifier = $metadata->getIdentifierFieldNames();
361
362
                // TODO: handle composite (multiple) identifiers
363
                if ($identifier !== null && count($identifier) > 1) {
364
                    //$value = $key;
365
                    $todo = true;
0 ignored issues
show
Unused Code introduced by
$todo is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
366
                } else {
367
                    $value = current($metadata->getIdentifierValues($value));
368
                }
369
            }
370
        }
371
372
        return $value;
373
    }
374
375
    /**
376
     * Load objects
377
     *
378
     * @throws RuntimeException
379
     * @throws Exception\InvalidRepositoryResultException
380
     */
381 20
    protected function loadObjects() : void
382
    {
383 20
        if (! empty($this->objects)) {
384
            return;
385
        }
386
387 20
        $findMethod = (array) $this->getFindMethod();
388
389 20
        if (! $findMethod) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $findMethod of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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.

Loading history...
390 16
            $findMethodName = 'findAll';
391 16
            $repository     = $this->objectManager->getRepository($this->targetClass);
392 16
            $objects        = $repository->findAll();
393
        } else {
394 4
            if (! isset($findMethod['name'])) {
395 1
                throw new RuntimeException('No method name was set');
396
            }
397
398 3
            $findMethodName   = $findMethod['name'];
399 3
            $findMethodParams = isset($findMethod['params']) ? array_change_key_case($findMethod['params']) : [];
400 3
            $repository       = $this->objectManager->getRepository($this->targetClass);
401
402 3
            if (! method_exists($repository, $findMethodName)) {
403 1
                throw new RuntimeException(
404 1
                    sprintf(
405 1
                        'Method "%s" could not be found in repository "%s"',
406 1
                        $findMethodName,
407 1
                        get_class($repository)
408
                    )
409
                );
410
            }
411
412 2
            $r    = new ReflectionMethod($repository, $findMethodName);
413 2
            $args = [];
414
415 2
            foreach ($r->getParameters() as $param) {
416 2
                if (array_key_exists(strtolower($param->getName()), $findMethodParams)) {
0 ignored issues
show
Bug introduced by
Consider using $param->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
417 1
                    $args[] = $findMethodParams[strtolower($param->getName())];
0 ignored issues
show
Bug introduced by
Consider using $param->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
418 2
                } elseif ($param->isDefaultValueAvailable()) {
419 1
                    $args[] = $param->getDefaultValue();
420 1
                } elseif (! $param->isOptional()) {
421 1
                    throw new RuntimeException(
422 1
                        sprintf(
423
                            'Required parameter "%s" with no default value for method "%s" in repository "%s"'
424 1
                            . ' was not provided',
425 1
                            $param->getName(),
0 ignored issues
show
Bug introduced by
Consider using $param->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
426 1
                            $findMethodName,
427 1
                            get_class($repository)
428
                        )
429
                    );
430
                }
431
            }
432
433 1
            $objects = $r->invokeArgs($repository, $args);
434
        }
435
436 17
        $this->guardForArrayOrTraversable(
437 17
            $objects,
438 17
            sprintf('%s::%s() return value', get_class($repository), $findMethodName),
439 17
            'DoctrineModule\Form\Element\Exception\InvalidRepositoryResultException'
440
        );
441
442 16
        $this->objects = $objects;
443 16
    }
444
445
    /**
446
     * Load value options
447
     *
448
     * @throws RuntimeException
449
     */
450 22
    protected function loadValueOptions() : void
451
    {
452 22
        if (! $this->objectManager) {
453 1
            throw new RuntimeException('No object manager was set');
454
        }
455
456 21
        if (! $this->targetClass) {
457 1
            throw new RuntimeException('No target class was set');
458
        }
459
460 20
        $metadata         = $this->getObjectManager()->getClassMetadata($this->getTargetClass());
461 20
        $identifier       = $metadata->getIdentifierFieldNames();
462 20
        $objects          = $this->getObjects();
463 16
        $options          = [];
464 16
        $optionAttributes = [];
465
466 16
        if ($this->displayEmptyItem) {
467 1
            $options[''] = $this->getEmptyItemLabel();
468
        }
469
470 16
        foreach ($objects as $key => $object) {
471 14
            $generatedLabel = $this->generateLabel($object);
472 14
            if ($generatedLabel !== null) {
473 1
                $label = $generatedLabel;
474 13
            } elseif ($this->property) {
475 3
                $property = $this->property;
476 3
                if (($this->getIsMethod() === false || $this->getIsMethod() === null)
477 3
                    && ! $metadata->hasField($property)
478
                ) {
479
                    throw new RuntimeException(
480
                        sprintf(
481
                            'Property "%s" could not be found in object "%s"',
482
                            $property,
483
                            $this->getTargetClass()
484
                        )
485
                    );
486
                }
487
488 3
                $getter = 'get' . Inflector::classify($property);
489
490 3
                if (! is_callable([$object, $getter])) {
491
                    throw new RuntimeException(
492
                        sprintf('Method "%s::%s" is not callable', $this->targetClass, $getter)
493
                    );
494
                }
495
496 3
                $label = $object->{$getter}();
497
            } else {
498 10
                if (! is_callable([$object, '__toString'])) {
499
                    throw new RuntimeException(
500
                        sprintf(
501
                            '%s must have a "__toString()" method defined if you have not set a property'
502
                            . ' or method to use.',
503
                            $this->getTargetClass()
504
                        )
505
                    );
506
                }
507
508 10
                $label = (string) $object;
509
            }
510
511 14
            if ($identifier !== null && count($identifier) > 1) {
512
                $value = $key;
513
            } else {
514 14
                $value = current($metadata->getIdentifierValues($object));
515
            }
516
517 14
            foreach ($this->getOptionAttributes() as $optionKey => $optionValue) {
518 3
                if (is_string($optionValue)) {
519 1
                    $optionAttributes[$optionKey] = $optionValue;
520
521 1
                    continue;
522
                }
523
524 2
                if (is_callable($optionValue)) {
525 1
                    $callableValue                = call_user_func($optionValue, $object);
526 1
                    $optionAttributes[$optionKey] = (string) $callableValue;
527
528 1
                    continue;
529
                }
530
531 1
                throw new RuntimeException(
532 1
                    sprintf(
533
                        'Parameter "option_attributes" expects an array of key => value where value is of type'
534 1
                        . '"string" or "callable". Value of type "%s" found.',
535 1
                        gettype($optionValue)
536
                    )
537
                );
538
            }
539
540
            // If no optgroup_identifier has been configured, apply default handling and continue
541 13
            if ($this->getOptgroupIdentifier() === null) {
542 9
                $options[] = ['label' => $label, 'value' => $value, 'attributes' => $optionAttributes];
543
544 9
                continue;
545
            }
546
547
            // optgroup_identifier found, handle grouping
548 4
            $optgroupGetter = 'get' . Inflector::classify($this->getOptgroupIdentifier());
549
550 4
            if (! is_callable([$object, $optgroupGetter])) {
551 1
                throw new RuntimeException(
552 1
                    sprintf('Method "%s::%s" is not callable', $this->targetClass, $optgroupGetter)
553
                );
554
            }
555
556 3
            $optgroup = $object->{$optgroupGetter}();
557
558
            // optgroup_identifier contains a valid group-name. Handle default grouping.
559 3
            if ($optgroup !== null && trim($optgroup) !== '') {
560 2
                $options[$optgroup]['label']     = $optgroup;
561 2
                $options[$optgroup]['options'][] = [
562 2
                    'label'      => $label,
563 2
                    'value'      => $value,
564 2
                    'attributes' => $optionAttributes,
565
                ];
566
567 2
                continue;
568
            }
569
570 2
            $optgroupDefault = $this->getOptgroupDefault();
571
572
            // No optgroup_default has been provided. Line up without a group
573 2
            if ($optgroupDefault === null) {
574 1
                $options[] = ['label' => $label, 'value' => $value, 'attributes' => $optionAttributes];
575
576 1
                continue;
577
            }
578
579
            // Line up entry with optgroup_default
580 1
            $options[$optgroupDefault]['label']     = $optgroupDefault;
581 1
            $options[$optgroupDefault]['options'][] = [
582 1
                'label'      => $label,
583 1
                'value'      => $value,
584 1
                'attributes' => $optionAttributes,
585
            ];
586
        }
587
588 14
        $this->valueOptions = $options;
589 14
    }
590
}
591