Completed
Pull Request — 3.x (#6136)
by Vincent
03:35
created

BaseFieldDescription::setOptions()   B

Complexity

Conditions 6
Paths 32

Size

Total Lines 31

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 31
rs 8.8017
c 0
b 0
f 0
cc 6
nc 32
nop 1
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\Admin;
15
16
use Doctrine\Inflector\InflectorFactory;
17
use Sonata\AdminBundle\Exception\NoValueException;
18
19
/**
20
 * A FieldDescription hold the information about a field. A typical
21
 * admin instance contains different collections of fields.
22
 *
23
 * - form: used by the form
24
 * - list: used by the list
25
 * - filter: used by the list filter
26
 *
27
 * Some options are global across the different contexts, other are
28
 * context specifics.
29
 *
30
 * Global options :
31
 *   - type (m): define the field type (use to tweak the form or the list)
32
 *   - template (o) : the template used to render the field
33
 *   - name (o) : the name used (label in the form, title in the list)
34
 *   - link_parameters (o) : add link parameter to the related Admin class when
35
 *                           the Admin.generateUrl is called
36
 *   - code : the method name to retrieve the related value
37
 *   - associated_tostring : (deprecated, use associated_property option)
38
 *                           the method to retrieve the "string" representation
39
 *                           of the collection element.
40
 *   - associated_property : property path to retrieve the "string" representation
41
 *                           of the collection element.
42
 *
43
 * Form Field options :
44
 *   - field_type (o): the widget class to use to render the field
45
 *   - field_options (o): the options to give to the widget
46
 *   - edit (o) : list|inline|standard (only used for associated admin)
47
 *      - list : open a popup where the user can search, filter and click on one field
48
 *               to select one item
49
 *      - inline : the associated form admin is embedded into the current form
50
 *      - standard : the associated admin is created through a popup
51
 *
52
 * List Field options :
53
 *   - identifier (o): if set to true a link appear on to edit the element
54
 *
55
 * Filter Field options :
56
 *   - options (o): options given to the Filter object
57
 *   - field_type (o): the widget class to use to render the field
58
 *   - field_options (o): the options to give to the widget
59
 *
60
 * @author Thomas Rabaix <[email protected]>
61
 */
62
abstract class BaseFieldDescription implements FieldDescriptionInterface
63
{
64
    /**
65
     * @var string the field name
66
     */
67
    protected $name;
68
69
    /**
70
     * @var string|int the type
71
     */
72
    protected $type;
73
74
    /**
75
     * @var string|int the original mapping type
76
     */
77
    protected $mappingType;
78
79
    /**
80
     * @var string the field name (of the form)
81
     */
82
    protected $fieldName;
83
84
    /**
85
     * @var array the ORM association mapping
86
     */
87
    protected $associationMapping;
88
89
    /**
90
     * @var array the ORM field information
91
     */
92
    protected $fieldMapping;
93
94
    /**
95
     * @var array the ORM parent mapping association
96
     */
97
    protected $parentAssociationMappings;
98
99
    /**
100
     * @var string the template name
101
     */
102
    protected $template;
103
104
    /**
105
     * @var array the option collection
106
     */
107
    protected $options = [];
108
109
    /**
110
     * @var AdminInterface|null the parent Admin instance
111
     */
112
    protected $parent;
113
114
    /**
115
     * @var AdminInterface|null the related admin instance
116
     */
117
    protected $admin;
118
119
    /**
120
     * @var AdminInterface|null the associated admin class if the object is associated to another entity
121
     */
122
    protected $associationAdmin;
123
124
    /**
125
     * @var string the help message to display
126
     */
127
    protected $help;
128
129
    /**
130
     * @var array[] cached object field getters
131
     */
132
    private static $fieldGetters = [];
133
134
    public function setFieldName($fieldName)
135
    {
136
        $this->fieldName = $fieldName;
137
    }
138
139
    public function getFieldName()
140
    {
141
        return $this->fieldName;
142
    }
143
144
    public function setName($name)
145
    {
146
        $this->name = $name;
147
148
        if (!$this->getFieldName()) {
149
            $this->setFieldName(substr(strrchr('.'.$name, '.'), 1));
150
        }
151
    }
152
153
    public function getName()
154
    {
155
        return $this->name;
156
    }
157
158
    public function getOption($name, $default = null)
159
    {
160
        return isset($this->options[$name]) ? $this->options[$name] : $default;
161
    }
162
163
    public function setOption($name, $value)
164
    {
165
        $this->options[$name] = $value;
166
    }
167
168
    public function setOptions(array $options)
169
    {
170
        // set the type if provided
171
        if (isset($options['type'])) {
172
            $this->setType($options['type']);
173
            unset($options['type']);
174
        }
175
176
        // remove property value
177
        if (isset($options['template'])) {
178
            $this->setTemplate($options['template']);
179
            unset($options['template']);
180
        }
181
182
        // set help if provided
183
        if (isset($options['help'])) {
184
            $this->setHelp($options['help']);
185
            unset($options['help']);
186
        }
187
188
        // set default placeholder
189
        if (!isset($options['placeholder'])) {
190
            $options['placeholder'] = 'short_object_description_placeholder';
191
        }
192
193
        if (!isset($options['link_parameters'])) {
194
            $options['link_parameters'] = [];
195
        }
196
197
        $this->options = $options;
198
    }
199
200
    public function getOptions()
201
    {
202
        return $this->options;
203
    }
204
205
    public function setTemplate($template)
206
    {
207
        $this->template = $template;
208
    }
209
210
    public function getTemplate()
211
    {
212
        if (null !== $this->template && !\is_string($this->template) && 'sonata_deprecation_mute' !== (\func_get_args()[0] ?? null)) {
213
            @trigger_error(sprintf(
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...
214
                'Returning other type than string or null in method %s() is deprecated since sonata-project/admin-bundle 3.65. It will return only those types in version 4.0.',
215
                __METHOD__
216
            ), E_USER_DEPRECATED);
217
        }
218
219
        return $this->template;
220
    }
221
222
    public function setType($type)
223
    {
224
        $this->type = $type;
225
    }
226
227
    public function getType()
228
    {
229
        return $this->type;
230
    }
231
232
    public function setParent(AdminInterface $parent)
233
    {
234
        $this->parent = $parent;
235
    }
236
237
    public function getParent()
238
    {
239
        if (!$this->hasParent()) {
240
            @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...
241
                sprintf(
242
                    'Calling %s() when there is no parent is deprecated since sonata-project/admin-bundle 3.x'
243
                    .' and will throw an exception in 4.0. Use %s::hasParent() to know if there is a parent.',
244
                    __METHOD__,
245
                    __CLASS__
246
                ), E_USER_DEPRECATED
247
            );
248
            // NEXT_MAJOR : remove the previous `trigger_error()` call, uncomment the following exception and declare AdminInterface as return type
249
            // throw new \LogicException(sprintf('%s has no parent.', static::class));
250
        }
251
252
        return $this->parent;
253
    }
254
255
    public function hasParent()
256
    {
257
        return null !== $this->parent;
258
    }
259
260
    public function getAssociationMapping()
261
    {
262
        return $this->associationMapping;
263
    }
264
265
    public function getFieldMapping()
266
    {
267
        return $this->fieldMapping;
268
    }
269
270
    public function getParentAssociationMappings()
271
    {
272
        return $this->parentAssociationMappings;
273
    }
274
275
    public function setAssociationAdmin(AdminInterface $associationAdmin)
276
    {
277
        $this->associationAdmin = $associationAdmin;
278
        $this->associationAdmin->setParentFieldDescription($this);
279
    }
280
281
    public function getAssociationAdmin()
282
    {
283
        if (!$this->hasAssociationAdmin()) {
284
            @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...
285
                sprintf(
286
                    'Calling %s() when there is no association admin is deprecated since sonata-project/admin-bundle 3.x'
287
                    .' and will throw an exception in 4.0. Use %s::hasAssociationAdmin() to know if there is an association admin.',
288
                    __METHOD__,
289
                    __CLASS__
290
                ), E_USER_DEPRECATED
291
            );
292
            // NEXT_MAJOR : remove the previous `trigger_error()` call, uncomment the following exception and declare AdminInterface as return type
293
            // throw new \LogicException(sprintf('%s has no association admin.', static::class));
294
        }
295
296
        return $this->associationAdmin;
297
    }
298
299
    public function hasAssociationAdmin()
300
    {
301
        return null !== $this->associationAdmin;
302
    }
303
304
    public function getFieldValue($object, $fieldName)
305
    {
306
        if ($this->isVirtual() || null === $object) {
307
            return null;
308
        }
309
310
        $getters = [];
311
        $parameters = [];
312
313
        // prefer method name given in the code option
314
        if ($this->getOption('code')) {
315
            $getters[] = $this->getOption('code');
316
        }
317
        // parameters for the method given in the code option
318
        if ($this->getOption('parameters')) {
319
            $parameters = $this->getOption('parameters');
320
        }
321
322
        if (\is_string($fieldName) && '' !== $fieldName) {
323
            if ($this->hasCachedFieldGetter($object, $fieldName)) {
324
                return $this->callCachedGetter($object, $fieldName, $parameters);
325
            }
326
327
            $camelizedFieldName = InflectorFactory::create()->build()->classify($fieldName);
328
329
            $getters[] = 'get'.$camelizedFieldName;
330
            $getters[] = 'is'.$camelizedFieldName;
331
            $getters[] = 'has'.$camelizedFieldName;
332
        }
333
334
        foreach ($getters as $getter) {
335
            if (method_exists($object, $getter) && \is_callable([$object, $getter])) {
336
                $this->cacheFieldGetter($object, $fieldName, 'getter', $getter);
337
338
                return $object->{$getter}(...$parameters);
339
            }
340
        }
341
342
        if (method_exists($object, '__call')) {
343
            $this->cacheFieldGetter($object, $fieldName, 'call');
344
345
            return $object->{$fieldName}(...$parameters);
346
        }
347
348
        if (isset($object->{$fieldName})) {
349
            $this->cacheFieldGetter($object, $fieldName, 'var');
350
351
            return $object->{$fieldName};
352
        }
353
354
        throw new NoValueException(sprintf(
355
            'Neither the property "%s" nor one of the methods "%s()" exist and have public access in class "%s".',
356
            $this->getName(),
357
            implode('()", "', $getters),
358
            \get_class($object)
359
        ));
360
    }
361
362
    public function setAdmin(AdminInterface $admin)
363
    {
364
        $this->admin = $admin;
365
    }
366
367
    public function getAdmin()
368
    {
369
        if (!$this->hasAdmin()) {
370
            @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...
371
                sprintf(
372
                    'Calling %s() when there is no admin is deprecated since sonata-project/admin-bundle 3.x'
373
                    .' and will throw an exception in 4.0. Use %s::hasAdmin() to know if there is an admin.',
374
                    __METHOD__,
375
                    __CLASS__
376
                ), E_USER_DEPRECATED
377
            );
378
            // NEXT_MAJOR : remove the previous `trigger_error()` call, uncomment the following exception and declare AdminInterface as return type
379
            // throw new \LogicException(sprintf('%s has no admin.', static::class));
380
        }
381
382
        return $this->admin;
383
    }
384
385
    public function hasAdmin()
386
    {
387
        return null !== $this->admin;
388
    }
389
390
    public function mergeOption($name, array $options = [])
391
    {
392
        if (!isset($this->options[$name])) {
393
            $this->options[$name] = [];
394
        }
395
396
        if (!\is_array($this->options[$name])) {
397
            throw new \RuntimeException(sprintf('The key `%s` does not point to an array value', $name));
398
        }
399
400
        $this->options[$name] = array_merge($this->options[$name], $options);
401
    }
402
403
    public function mergeOptions(array $options = [])
404
    {
405
        $this->setOptions(array_merge_recursive($this->options, $options));
406
    }
407
408
    public function setMappingType($mappingType)
409
    {
410
        $this->mappingType = $mappingType;
411
    }
412
413
    public function getMappingType()
414
    {
415
        return $this->mappingType;
416
    }
417
418
    /**
419
     * Camelize a string.
420
     *
421
     * NEXT_MAJOR: remove this method.
422
     *
423
     * @static
424
     *
425
     * @param string $property
426
     *
427
     * @return string
428
     *
429
     * @deprecated since sonata-project/admin-bundle 3.1. Use \Doctrine\Inflector\Inflector::classify() instead
430
     */
431
    public static function camelize($property)
432
    {
433
        @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...
434
            sprintf(
435
                'The %s method is deprecated since 3.1 and will be removed in 4.0. '.
436
                'Use \Doctrine\Inflector\Inflector::classify() instead.',
437
                __METHOD__
438
            ),
439
            E_USER_DEPRECATED
440
        );
441
442
        return InflectorFactory::create()->build()->classify($property);
443
    }
444
445
    /**
446
     * Defines the help message.
447
     *
448
     * @param string $help
449
     */
450
    public function setHelp($help)
451
    {
452
        $this->help = $help;
453
    }
454
455
    public function getHelp()
456
    {
457
        return $this->help;
458
    }
459
460
    public function getLabel()
461
    {
462
        $label = $this->getOption('label');
463
        if (null !== $label && false !== $label && !\is_string($label) && 'sonata_deprecation_mute' !== (\func_get_args()[0] ?? null)) {
464
            @trigger_error(sprintf(
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...
465
                'Returning other type than string, false or null in method %s() is deprecated since sonata-project/admin-bundle 3.65. It will return only those types in version 4.0.',
466
                __METHOD__
467
            ), E_USER_DEPRECATED);
468
        }
469
470
        return $label;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $label; (object|integer|double|array|boolean|null|string) is incompatible with the return type declared by the interface Sonata\AdminBundle\Admin...tionInterface::getLabel of type string|false|null.

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...
471
    }
472
473
    public function isSortable()
474
    {
475
        return false !== $this->getOption('sortable', false);
476
    }
477
478
    public function getSortFieldMapping()
479
    {
480
        return $this->getOption('sort_field_mapping');
481
    }
482
483
    public function getSortParentAssociationMapping()
484
    {
485
        return $this->getOption('sort_parent_association_mappings');
486
    }
487
488
    public function getTranslationDomain()
489
    {
490
        return $this->getOption('translation_domain') ?: $this->getAdmin()->getTranslationDomain();
491
    }
492
493
    /**
494
     * Return true if field is virtual.
495
     *
496
     * @return bool
497
     */
498
    public function isVirtual()
499
    {
500
        return false !== $this->getOption('virtual_field', false);
501
    }
502
503
    private function getFieldGetterKey($object, ?string $fieldName): ?string
504
    {
505
        if (!\is_string($fieldName)) {
506
            return null;
507
        }
508
        if (!\is_object($object)) {
509
            return null;
510
        }
511
        $components = [\get_class($object), $fieldName];
512
        $code = $this->getOption('code');
513
        if (\is_string($code) && '' !== $code) {
514
            $components[] = $code;
515
        }
516
517
        return implode('-', $components);
518
    }
519
520
    private function hasCachedFieldGetter($object, string $fieldName): bool
521
    {
522
        return isset(
523
            self::$fieldGetters[$this->getFieldGetterKey($object, $fieldName)]
524
        );
525
    }
526
527
    private function callCachedGetter($object, string $fieldName, array $parameters = [])
528
    {
529
        $getterKey = $this->getFieldGetterKey($object, $fieldName);
530
        if ('getter' === self::$fieldGetters[$getterKey]['method']) {
531
            return $object->{self::$fieldGetters[$getterKey]['getter']}(...$parameters);
532
        } elseif ('call' === self::$fieldGetters[$getterKey]['method']) {
533
            return $object->{$fieldName}(...$parameters);
534
        }
535
536
        return $object->{$fieldName};
537
    }
538
539
    private function cacheFieldGetter($object, ?string $fieldName, string $method, ?string $getter = null): void
540
    {
541
        $getterKey = $this->getFieldGetterKey($object, $fieldName);
542
        if (null !== $getterKey) {
543
            self::$fieldGetters[$getterKey] = [
544
                'method' => $method,
545
            ];
546
            if (null !== $getter) {
547
                self::$fieldGetters[$getterKey]['getter'] = $getter;
548
            }
549
        }
550
    }
551
}
552