BaseFieldDescription   F
last analyzed

Complexity

Total Complexity 70

Size/Duplication

Total Lines 395
Duplicated Lines 0 %

Coupling/Cohesion

Components 3
Dependencies 5

Importance

Changes 0
Metric Value
wmc 70
lcom 3
cbo 5
dl 0
loc 395
rs 2.8
c 0
b 0
f 0

39 Methods

Rating   Name   Duplication   Size   Complexity  
A hasCachedFieldGetter() 0 6 1
A callCachedGetter() 0 14 3
A cacheFieldGetter() 0 12 3
A mergeOption() 0 12 3
A mergeOptions() 0 4 1
A setMappingType() 0 4 1
A getMappingType() 0 4 1
A setFieldName() 0 4 1
A getFieldName() 0 4 1
A setName() 0 8 2
A getName() 0 4 1
A getOption() 0 4 2
A setOption() 0 4 1
A setOptions() 0 25 5
A getOptions() 0 4 1
A setTemplate() 0 4 1
A getTemplate() 0 4 1
A setType() 0 4 1
A getType() 0 4 1
A setParent() 0 4 1
A getParent() 0 8 2
A hasParent() 0 4 1
A getAssociationMapping() 0 4 1
A getFieldMapping() 0 4 1
A getParentAssociationMappings() 0 4 1
A setAssociationAdmin() 0 5 1
A getAssociationAdmin() 0 8 2
A hasAssociationAdmin() 0 4 1
C getFieldValue() 0 57 13
A setAdmin() 0 4 1
A getAdmin() 0 8 2
A hasAdmin() 0 4 1
A getLabel() 0 4 1
A isSortable() 0 4 1
A getSortFieldMapping() 0 4 1
A getSortParentAssociationMapping() 0 4 1
A getTranslationDomain() 0 4 2
A isVirtual() 0 4 1
A getFieldGetterKey() 0 15 4

How to fix   Complexity   

Complex Class

Complex classes like BaseFieldDescription often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use BaseFieldDescription, and based on these observations, apply Extract Interface, too.

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 array[] cached object field getters
123
     */
124
    private static $fieldGetters = [];
125
126
    public function setFieldName(?string $fieldName): void
127
    {
128
        $this->fieldName = $fieldName;
129
    }
130
131
    public function getFieldName(): ?string
132
    {
133
        return $this->fieldName;
134
    }
135
136
    public function setName(?string $name): void
137
    {
138
        $this->name = $name;
139
140
        if (!$this->getFieldName()) {
141
            $this->setFieldName(substr(strrchr('.'.$name, '.'), 1));
142
        }
143
    }
144
145
    public function getName(): ?string
146
    {
147
        return $this->name;
148
    }
149
150
    public function getOption(string $name, $default = null)
151
    {
152
        return isset($this->options[$name]) ? $this->options[$name] : $default;
153
    }
154
155
    public function setOption(string $name, $value): void
156
    {
157
        $this->options[$name] = $value;
158
    }
159
160
    public function setOptions(array $options): void
161
    {
162
        // set the type if provided
163
        if (isset($options['type'])) {
164
            $this->setType($options['type']);
165
            unset($options['type']);
166
        }
167
168
        // remove property value
169
        if (isset($options['template'])) {
170
            $this->setTemplate($options['template']);
171
            unset($options['template']);
172
        }
173
174
        // set default placeholder
175
        if (!isset($options['placeholder'])) {
176
            $options['placeholder'] = 'short_object_description_placeholder';
177
        }
178
179
        if (!isset($options['link_parameters'])) {
180
            $options['link_parameters'] = [];
181
        }
182
183
        $this->options = $options;
184
    }
185
186
    public function getOptions(): array
187
    {
188
        return $this->options;
189
    }
190
191
    public function setTemplate(?string $template): void
192
    {
193
        $this->template = $template;
194
    }
195
196
    public function getTemplate(): ?string
197
    {
198
        return $this->template;
199
    }
200
201
    public function setType(?string $type): void
202
    {
203
        $this->type = $type;
204
    }
205
206
    public function getType(): ?string
207
    {
208
        return $this->type;
209
    }
210
211
    public function setParent(AdminInterface $parent): void
212
    {
213
        $this->parent = $parent;
214
    }
215
216
    public function getParent(): AdminInterface
217
    {
218
        if (!$this->hasParent()) {
219
            throw new \LogicException(sprintf('%s has no parent.', static::class));
220
        }
221
222
        return $this->parent;
223
    }
224
225
    public function hasParent(): bool
226
    {
227
        return null !== $this->parent;
228
    }
229
230
    public function getAssociationMapping(): array
231
    {
232
        return $this->associationMapping;
233
    }
234
235
    public function getFieldMapping(): array
236
    {
237
        return $this->fieldMapping;
238
    }
239
240
    public function getParentAssociationMappings(): array
241
    {
242
        return $this->parentAssociationMappings;
243
    }
244
245
    public function setAssociationAdmin(AdminInterface $associationAdmin): void
246
    {
247
        $this->associationAdmin = $associationAdmin;
248
        $this->associationAdmin->setParentFieldDescription($this);
249
    }
250
251
    public function getAssociationAdmin(): AdminInterface
252
    {
253
        if (!$this->hasAssociationAdmin()) {
254
            throw new \LogicException(sprintf('%s has no association admin.', static::class));
255
        }
256
257
        return $this->associationAdmin;
258
    }
259
260
    public function hasAssociationAdmin(): bool
261
    {
262
        return null !== $this->associationAdmin;
263
    }
264
265
    public function getFieldValue(?object $object, ?string $fieldName)
266
    {
267
        if ($this->isVirtual() || null === $object) {
268
            return null;
269
        }
270
271
        $getters = [];
272
        $parameters = [];
273
274
        // prefer method name given in the code option
275
        if ($this->getOption('code')) {
276
            $getters[] = $this->getOption('code');
277
        }
278
        // parameters for the method given in the code option
279
        if ($this->getOption('parameters')) {
280
            $parameters = $this->getOption('parameters');
281
        }
282
283
        if (\is_string($fieldName) && '' !== $fieldName) {
284
            if ($this->hasCachedFieldGetter($object, $fieldName)) {
285
                return $this->callCachedGetter($object, $fieldName, $parameters);
286
            }
287
288
            $camelizedFieldName = InflectorFactory::create()->build()->classify($fieldName);
289
290
            $getters[] = sprintf('get%s', $camelizedFieldName);
291
            $getters[] = sprintf('is%s', $camelizedFieldName);
292
            $getters[] = sprintf('has%s', $camelizedFieldName);
293
        }
294
295
        foreach ($getters as $getter) {
296
            if (method_exists($object, $getter) && \is_callable([$object, $getter])) {
297
                $this->cacheFieldGetter($object, $fieldName, 'getter', $getter);
298
299
                return $object->$getter(...$parameters);
300
            }
301
        }
302
303
        if (method_exists($object, '__call')) {
304
            $this->cacheFieldGetter($object, $fieldName, 'call');
305
306
            return $object->$fieldName(...$parameters);
307
        }
308
309
        if (isset($object->{$fieldName})) {
310
            $this->cacheFieldGetter($object, $fieldName, 'var');
311
312
            return $object->{$fieldName};
313
        }
314
315
        throw new NoValueException(sprintf(
316
            'Neither the property "%s" nor one of the methods "%s()" exist and have public access in class "%s".',
317
            $this->getName(),
318
            implode('()", "', $getters),
319
            \get_class($object)
320
        ));
321
    }
322
323
    public function setAdmin(AdminInterface $admin): void
324
    {
325
        $this->admin = $admin;
326
    }
327
328
    public function getAdmin(): AdminInterface
329
    {
330
        if (!$this->hasAdmin()) {
331
            throw new \LogicException(sprintf('%s has no admin.', static::class));
332
        }
333
334
        return $this->admin;
335
    }
336
337
    public function hasAdmin(): bool
338
    {
339
        return null !== $this->admin;
340
    }
341
342
    public function mergeOption(string $name, array $options = []): void
343
    {
344
        if (!isset($this->options[$name])) {
345
            $this->options[$name] = [];
346
        }
347
348
        if (!\is_array($this->options[$name])) {
349
            throw new \RuntimeException(sprintf('The key `%s` does not point to an array value', $name));
350
        }
351
352
        $this->options[$name] = array_merge($this->options[$name], $options);
353
    }
354
355
    public function mergeOptions(array $options = []): void
356
    {
357
        $this->setOptions(array_merge_recursive($this->options, $options));
358
    }
359
360
    public function setMappingType($mappingType): void
361
    {
362
        $this->mappingType = $mappingType;
363
    }
364
365
    public function getMappingType()
366
    {
367
        return $this->mappingType;
368
    }
369
370
    /**
371
     * @return string|false|null
372
     */
373
    public function getLabel()
374
    {
375
        return $this->getOption('label');
376
    }
377
378
    public function isSortable(): bool
379
    {
380
        return false !== $this->getOption('sortable', false);
381
    }
382
383
    public function getSortFieldMapping(): array
384
    {
385
        return $this->getOption('sort_field_mapping');
386
    }
387
388
    public function getSortParentAssociationMapping(): array
389
    {
390
        return $this->getOption('sort_parent_association_mappings');
391
    }
392
393
    public function getTranslationDomain(): string
394
    {
395
        return $this->getOption('translation_domain') ?: $this->getAdmin()->getTranslationDomain();
396
    }
397
398
    public function isVirtual(): bool
399
    {
400
        return false !== $this->getOption('virtual_field', false);
401
    }
402
403
    private function getFieldGetterKey(object $object, ?string $fieldName): ?string
404
    {
405
        if (!\is_string($fieldName)) {
406
            return null;
407
        }
408
409
        $components = [\get_class($object), $fieldName];
410
411
        $code = $this->getOption('code');
412
        if (\is_string($code) && '' !== $code) {
413
            $components[] = $code;
414
        }
415
416
        return implode('-', $components);
417
    }
418
419
    private function hasCachedFieldGetter(object $object, string $fieldName): bool
420
    {
421
        return isset(
422
            self::$fieldGetters[$this->getFieldGetterKey($object, $fieldName)]
423
        );
424
    }
425
426
    private function callCachedGetter(object $object, string $fieldName, array $parameters = [])
427
    {
428
        $getterKey = $this->getFieldGetterKey($object, $fieldName);
429
430
        if ('getter' === self::$fieldGetters[$getterKey]['method']) {
431
            return $object->{self::$fieldGetters[$getterKey]['getter']}(...$parameters);
432
        }
433
434
        if ('call' === self::$fieldGetters[$getterKey]['method']) {
435
            return $object->__call($fieldName, $parameters);
436
        }
437
438
        return $object->{$fieldName};
439
    }
440
441
    private function cacheFieldGetter(object $object, ?string $fieldName, string $method, ?string $getter = null): void
442
    {
443
        $getterKey = $this->getFieldGetterKey($object, $fieldName);
444
        if (null !== $getterKey) {
445
            self::$fieldGetters[$getterKey] = [
446
                'method' => $method,
447
            ];
448
            if (null !== $getter) {
449
                self::$fieldGetters[$getterKey]['getter'] = $getter;
450
            }
451
        }
452
    }
453
}
454