Completed
Push — 3.x ( a9c151...a0f815 )
by Grégoire
03:04
created

BaseFieldDescription::hasParent()   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\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
                ),
247
                E_USER_DEPRECATED
248
            );
249
            // NEXT_MAJOR : remove the previous `trigger_error()` call, uncomment the following exception and declare AdminInterface as return type
250
            // throw new \LogicException(sprintf('%s has no parent.', static::class));
251
        }
252
253
        return $this->parent;
254
    }
255
256
    public function hasParent()
257
    {
258
        return null !== $this->parent;
259
    }
260
261
    public function getAssociationMapping()
262
    {
263
        return $this->associationMapping;
264
    }
265
266
    public function getFieldMapping()
267
    {
268
        return $this->fieldMapping;
269
    }
270
271
    public function getParentAssociationMappings()
272
    {
273
        return $this->parentAssociationMappings;
274
    }
275
276
    public function setAssociationAdmin(AdminInterface $associationAdmin)
277
    {
278
        $this->associationAdmin = $associationAdmin;
279
        $this->associationAdmin->setParentFieldDescription($this);
280
    }
281
282
    public function getAssociationAdmin()
283
    {
284
        if (!$this->hasAssociationAdmin()) {
285
            @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...
286
                sprintf(
287
                    'Calling %s() when there is no association admin is deprecated since sonata-project/admin-bundle 3.x'
288
                    .' and will throw an exception in 4.0. Use %s::hasAssociationAdmin() to know if there is an association admin.',
289
                    __METHOD__,
290
                    __CLASS__
291
                ),
292
                E_USER_DEPRECATED
293
            );
294
            // NEXT_MAJOR : remove the previous `trigger_error()` call, uncomment the following exception and declare AdminInterface as return type
295
            // throw new \LogicException(sprintf('%s has no association admin.', static::class));
296
        }
297
298
        return $this->associationAdmin;
299
    }
300
301
    public function hasAssociationAdmin()
302
    {
303
        return null !== $this->associationAdmin;
304
    }
305
306
    public function getFieldValue($object, $fieldName)
307
    {
308
        if ($this->isVirtual() || null === $object) {
309
            return null;
310
        }
311
312
        $getters = [];
313
        $parameters = [];
314
315
        // prefer method name given in the code option
316
        if ($this->getOption('code')) {
317
            $getters[] = $this->getOption('code');
318
        }
319
        // parameters for the method given in the code option
320
        if ($this->getOption('parameters')) {
321
            $parameters = $this->getOption('parameters');
322
        }
323
324
        if (\is_string($fieldName) && '' !== $fieldName) {
325
            if ($this->hasCachedFieldGetter($object, $fieldName)) {
326
                return $this->callCachedGetter($object, $fieldName, $parameters);
327
            }
328
329
            $camelizedFieldName = InflectorFactory::create()->build()->classify($fieldName);
330
331
            $getters[] = 'get'.$camelizedFieldName;
332
            $getters[] = 'is'.$camelizedFieldName;
333
            $getters[] = 'has'.$camelizedFieldName;
334
        }
335
336
        foreach ($getters as $getter) {
337
            if (method_exists($object, $getter) && \is_callable([$object, $getter])) {
338
                $this->cacheFieldGetter($object, $fieldName, 'getter', $getter);
339
340
                return $object->{$getter}(...$parameters);
341
            }
342
        }
343
344
        if (method_exists($object, '__call')) {
345
            $this->cacheFieldGetter($object, $fieldName, 'call');
346
347
            return $object->{$fieldName}(...$parameters);
348
        }
349
350
        if (isset($object->{$fieldName})) {
351
            $this->cacheFieldGetter($object, $fieldName, 'var');
352
353
            return $object->{$fieldName};
354
        }
355
356
        throw new NoValueException(sprintf(
357
            'Neither the property "%s" nor one of the methods "%s()" exist and have public access in class "%s".',
358
            $this->getName(),
359
            implode('()", "', $getters),
360
            \get_class($object)
361
        ));
362
    }
363
364
    public function setAdmin(AdminInterface $admin)
365
    {
366
        $this->admin = $admin;
367
    }
368
369
    public function getAdmin()
370
    {
371
        if (!$this->hasAdmin()) {
372
            @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...
373
                sprintf(
374
                    'Calling %s() when there is no admin is deprecated since sonata-project/admin-bundle 3.x'
375
                    .' and will throw an exception in 4.0. Use %s::hasAdmin() to know if there is an admin.',
376
                    __METHOD__,
377
                    __CLASS__
378
                ),
379
                E_USER_DEPRECATED
380
            );
381
            // NEXT_MAJOR : remove the previous `trigger_error()` call, uncomment the following exception and declare AdminInterface as return type
382
            // throw new \LogicException(sprintf('%s has no admin.', static::class));
383
        }
384
385
        return $this->admin;
386
    }
387
388
    public function hasAdmin()
389
    {
390
        return null !== $this->admin;
391
    }
392
393
    public function mergeOption($name, array $options = [])
394
    {
395
        if (!isset($this->options[$name])) {
396
            $this->options[$name] = [];
397
        }
398
399
        if (!\is_array($this->options[$name])) {
400
            throw new \RuntimeException(sprintf('The key `%s` does not point to an array value', $name));
401
        }
402
403
        $this->options[$name] = array_merge($this->options[$name], $options);
404
    }
405
406
    public function mergeOptions(array $options = [])
407
    {
408
        $this->setOptions(array_merge_recursive($this->options, $options));
409
    }
410
411
    public function setMappingType($mappingType)
412
    {
413
        $this->mappingType = $mappingType;
414
    }
415
416
    public function getMappingType()
417
    {
418
        return $this->mappingType;
419
    }
420
421
    /**
422
     * Camelize a string.
423
     *
424
     * NEXT_MAJOR: remove this method.
425
     *
426
     * @static
427
     *
428
     * @param string $property
429
     *
430
     * @return string
431
     *
432
     * @deprecated since sonata-project/admin-bundle 3.1. Use \Doctrine\Inflector\Inflector::classify() instead
433
     */
434
    public static function camelize($property)
435
    {
436
        @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...
437
            sprintf(
438
                'The %s method is deprecated since 3.1 and will be removed in 4.0. '.
439
                'Use \Doctrine\Inflector\Inflector::classify() instead.',
440
                __METHOD__
441
            ),
442
            E_USER_DEPRECATED
443
        );
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 getLabel()
464
    {
465
        $label = $this->getOption('label');
466
        if (null !== $label && false !== $label && !\is_string($label) && 'sonata_deprecation_mute' !== (\func_get_args()[0] ?? null)) {
467
            @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...
468
                '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.',
469
                __METHOD__
470
            ), E_USER_DEPRECATED);
471
        }
472
473
        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...
474
    }
475
476
    public function isSortable()
477
    {
478
        return false !== $this->getOption('sortable', false);
479
    }
480
481
    public function getSortFieldMapping()
482
    {
483
        return $this->getOption('sort_field_mapping');
484
    }
485
486
    public function getSortParentAssociationMapping()
487
    {
488
        return $this->getOption('sort_parent_association_mappings');
489
    }
490
491
    public function getTranslationDomain()
492
    {
493
        return $this->getOption('translation_domain') ?: $this->getAdmin()->getTranslationDomain();
494
    }
495
496
    /**
497
     * Return true if field is virtual.
498
     *
499
     * @return bool
500
     */
501
    public function isVirtual()
502
    {
503
        return false !== $this->getOption('virtual_field', false);
504
    }
505
506
    private function getFieldGetterKey($object, ?string $fieldName): ?string
507
    {
508
        if (!\is_string($fieldName)) {
509
            return null;
510
        }
511
        if (!\is_object($object)) {
512
            return null;
513
        }
514
        $components = [\get_class($object), $fieldName];
515
        $code = $this->getOption('code');
516
        if (\is_string($code) && '' !== $code) {
517
            $components[] = $code;
518
        }
519
520
        return implode('-', $components);
521
    }
522
523
    private function hasCachedFieldGetter($object, string $fieldName): bool
524
    {
525
        return isset(
526
            self::$fieldGetters[$this->getFieldGetterKey($object, $fieldName)]
527
        );
528
    }
529
530
    private function callCachedGetter($object, string $fieldName, array $parameters = [])
531
    {
532
        $getterKey = $this->getFieldGetterKey($object, $fieldName);
533
        if ('getter' === self::$fieldGetters[$getterKey]['method']) {
534
            return $object->{self::$fieldGetters[$getterKey]['getter']}(...$parameters);
535
        } elseif ('call' === self::$fieldGetters[$getterKey]['method']) {
536
            return $object->{$fieldName}(...$parameters);
537
        }
538
539
        return $object->{$fieldName};
540
    }
541
542
    private function cacheFieldGetter($object, ?string $fieldName, string $method, ?string $getter = null): void
543
    {
544
        $getterKey = $this->getFieldGetterKey($object, $fieldName);
545
        if (null !== $getterKey) {
546
            self::$fieldGetters[$getterKey] = [
547
                'method' => $method,
548
            ];
549
            if (null !== $getter) {
550
                self::$fieldGetters[$getterKey]['getter'] = $getter;
551
            }
552
        }
553
    }
554
}
555