Completed
Pull Request — master (#6210)
by Jordi Sala
29:29
created

BaseFieldDescription::setOptions()   A

Complexity

Conditions 5
Paths 16

Size

Total Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

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