Completed
Pull Request — 3.x (#6206)
by
unknown
03:45 queued 36s
created

BaseFieldDescription::getSortFieldMapping()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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