Passed
Push — master ( bb67c8...cd2c1d )
by Arnaud
14:35 queued 11:07
created

FieldFactory::configureField()   B

Complexity

Conditions 10
Paths 3

Size

Total Lines 85
Code Lines 55

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 35
CRAP Score 14.3577

Importance

Changes 0
Metric Value
cc 10
eloc 55
c 0
b 0
f 0
nc 3
nop 4
dl 0
loc 85
ccs 35
cts 54
cp 0.6481
crap 14.3577
rs 7.1151

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace LAG\AdminBundle\Factory;
4
5
use LAG\AdminBundle\Configuration\ApplicationConfiguration;
6
use LAG\AdminBundle\Configuration\FieldConfiguration;
7
use LAG\AdminBundle\Event\AdminEvents;
8
use LAG\AdminBundle\Event\Events\FieldDefinitionEvent;
9
use LAG\AdminBundle\Event\Events\FieldEvent;
10
use LAG\AdminBundle\Exception\Exception;
11
use LAG\AdminBundle\Exception\Field\FieldConfigurationException;
12
use LAG\AdminBundle\Exception\Field\FieldTypeNotFoundException;
13
use LAG\AdminBundle\Field\ApplicationAwareInterface;
14
use LAG\AdminBundle\Field\FieldInterface;
15
use Symfony\Component\OptionsResolver\Options;
16
use Symfony\Component\OptionsResolver\OptionsResolver;
17
use function Symfony\Component\String\u;
18
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
19
20
/**
21
 * Field factory. Instances fields.
22
 */
23
class FieldFactory implements FieldFactoryInterface
24
{
25
    /**
26
     * Field class mapping array, indexed by field type.
27
     */
28
    protected array $fieldsMapping = [];
29
    protected EventDispatcherInterface $eventDispatcher;
30
    protected ApplicationConfiguration $appConfig;
31
32 17
    public function __construct(
33
        EventDispatcherInterface $eventDispatcher,
34
        ApplicationConfiguration $appConfig,
35
        array $fieldsMapping
36
    ) {
37 17
        $this->fieldsMapping = $fieldsMapping;
38 17
        $this->eventDispatcher = $eventDispatcher;
39 17
        $this->appConfig = $appConfig;
40 17
    }
41
42 16
    public function create(string $name, array $configuration, array $context = []): FieldInterface
43
    {
44
        try {
45 16
            $configuration = $this->resolveConfiguration($name, $configuration, $context);
46
            // Dispatch an event to allow dynamic changes on the form type
47 16
            $event = new FieldEvent($name, $configuration['type'], $configuration['options'], $context);
48 16
            $this->eventDispatcher->dispatch($event, AdminEvents::FIELD_CREATE);
49 16
            $type = $event->getType();
50
51 16
            if ($type === null) {
52
                $type = 'auto';
53
            }
54 16
            $options = array_merge($configuration['options'], $event->getOptions());
55
56
            // Allow the type to be a class name
57 16
            if (!\array_key_exists($type, $this->fieldsMapping) && !class_exists($type)) {
58
                throw new FieldTypeNotFoundException($type, $name, $context);
59
            }
60 16
            $field = $this->instanciateField($name, $type);
61
62 15
            if ($field instanceof ApplicationAwareInterface) {
63 10
                $field->setApplicationConfiguration($this->appConfig);
64
            }
65 15
            $this->configureField($field, $type, $options, $context);
66 1
        } catch (\Exception $exception) {
67 1
            throw new FieldConfigurationException($name, $context, $exception->getMessage(), $exception);
68
        }
69 15
        $event = new FieldEvent($name, $type, $options, $context);
70 15
        $this->eventDispatcher->dispatch($event, AdminEvents::FIELD_CREATED);
71
72 15
        return $field;
73
    }
74
75
    public function createDefinitions(string $class): array
76
    {
77
        $event = new FieldDefinitionEvent($class);
78
        $this->eventDispatcher->dispatch($event, AdminEvents::FIELD_DEFINITION_CREATE);
79
80
        return $event->getDefinitions();
81
    }
82
83 15
    private function configureField(
84
        FieldInterface $field,
85
        string $type,
86
        array $options,
87
        array $context
88
    ): void {
89 15
        $resolver = new OptionsResolver();
90
        $resolver
91 15
            ->setDefaults([
92 15
                'attr' => ['class' => 'admin-field admin-field-'.$type],
93 15
                'header_attr' => ['class' => 'admin-header admin-header-'.$type],
94
                'label' => null,
95
                'mapped' => false,
96 15
                'property_path' => $field->getName(),
97 15
                'template' => '@LAGAdmin/fields/auto.html.twig',
98
                'translation' => false, // Most of fields are values from database and should not be translated
99
                'translation_domain' => null,
100
                'sortable' => true,
101
            ])
102 15
            ->setAllowedTypes('attr', ['array', 'null'])
103 15
            ->setAllowedTypes('header_attr', ['array', 'null'])
104 15
            ->setAllowedTypes('label', ['string', 'null', 'boolean'])
105 15
            ->setAllowedTypes('mapped', ['boolean'])
106 15
            ->setAllowedTypes('property_path', ['string', 'null'])
107 15
            ->setAllowedTypes('template', ['string'])
108 15
            ->setAllowedTypes('translation', ['boolean'])
109 15
            ->setAllowedTypes('translation_domain', ['string', 'null'])
110 15
            ->setAllowedTypes('sortable', ['boolean'])
111 15
            ->setNormalizer('attr', function (Options $options, $attr) {
112 15
                if ($attr === null) {
113
                    $attr = [];
114
                }
115
116 15
                return $attr;
117 15
            })
118 15
            ->setNormalizer('header_attr', function (Options $options, $attr) {
119 15
                if ($attr === null) {
120
                    $attr = [];
121
                }
122
123 15
                return $attr;
124 15
            })
125 15
            ->setNormalizer('mapped', function (Options $options, $mapped) use ($field) {
126 15
                if (u($field->getName())->startsWith('_')) {
127
                    return true;
128
                }
129
130 15
                return $mapped;
131 15
            })
132 15
            ->setNormalizer('property_path', function (Options $options, $propertyPath) use ($field) {
133 15
                if (u($field->getName())->startsWith('_')) {
134
                    return null;
135
                }
136
137 15
                return $propertyPath;
138 15
            })
139
        ;
140
141 15
        if ($field->getParent()) {
142
            $currentField = $field;
143
            $parents = [];
144
            // Keep track of processed parent types to avoid a infinite loop
145
            $processedParents = [];
146
147
            while ($currentField->getParent() !== null) {
148
                if (\in_array($currentField->getParent(), $processedParents)) {
149
                    throw new FieldConfigurationException($field->getName(), $context, 'An inheritance loop is found in '.implode(', ', $processedParents));
150
                }
151
                $parent = $this->instanciateField($currentField->getName(), $currentField->getParent());
152
153
                if ($parent instanceof ApplicationAwareInterface) {
154
                    $parent->setApplicationConfiguration($this->appConfig);
155
                }
156
                $parents[] = $parent;
157
                $processedParents[] = $field->getParent();
158
                $currentField = $parent;
159
            }
160
            $parents = array_reverse($parents);
161
162
            foreach ($parents as $parent) {
163
                $parent->configureOptions($resolver);
164
            }
165
        }
166 15
        $field->configureOptions($resolver);
167 15
        $field->setOptions($resolver->resolve($options));
168 15
    }
169
170
    /**
171
     * Return field class according to the field type. If the type is not present in the field mapping array, an
172
     * exception will be thrown.
173
     *
174
     * @throws Exception
175
     */
176 16
    private function getFieldClass(string $type): string
177
    {
178 16
        if (\array_key_exists($type, $this->fieldsMapping)) {
179 15
            return $this->fieldsMapping[$type];
180
        }
181
182 1
        if (class_exists($type)) {
183 1
            return $type;
184
        }
185
186
        throw new Exception(sprintf('Field type "%s" not found in field mapping. Allowed fields are "%s"', $type, implode('", "', $this->fieldsMapping)));
187
    }
188
189 16
    private function instanciateField(string $name, string $type): FieldInterface
190
    {
191 16
        $fieldClass = $this->getFieldClass($type);
192 16
        $field = new $fieldClass($name, $type);
193
194 16
        if (!$field instanceof FieldInterface) {
195
            // TODO use an exception in the field namespace
196 1
            throw new Exception("Field class \"{$fieldClass}\" must implements ".FieldInterface::class);
197
        }
198
199 15
        return $field;
200
    }
201
202 16
    private function resolveConfiguration(string $name, array $configuration, array $context): array
203
    {
204 16
        $fieldConfiguration = new FieldConfiguration();
205 16
        $fieldConfiguration->configure($configuration);
206 16
        $configuration = $fieldConfiguration->toArray();
207
208 16
        $event = new FieldEvent($name, $configuration['type'], $configuration['options'], $context);
209 16
        $this->eventDispatcher->dispatch($event);
210 16
        $configuration['options'] = $event->getOptions();
211
212
        // For collection of fields, we resolve the configuration of each item
213 16
        if ('collection' == $fieldConfiguration->getType()) {
214
            $items = [];
215
216
            foreach ($fieldConfiguration->getOptions() as $itemFieldName => $itemFieldConfiguration) {
217
                // The configuration should be an array
218
                if (!$itemFieldConfiguration) {
219
                    $itemFieldConfiguration = [];
220
                }
221
222
                // The type should be defined
223
                if (!\array_key_exists('type', $itemFieldConfiguration)) {
224
                    throw new Exception("Missing type configuration for field {$itemFieldName}");
225
                }
226
227
                // The field options are optional
228
                if (!\array_key_exists('options', $itemFieldConfiguration)) {
229
                    $itemFieldConfiguration['options'] = [];
230
                }
231
232
                // create collection item
233
                $items[] = $this->create($itemFieldName, $itemFieldConfiguration, $context);
234
            }
235
            // add created item to the field options
236
            $configuration['options'] = [
237
                'fields' => $items,
238
            ];
239
        }
240
241 16
        return $configuration;
242
    }
243
}
244