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

FieldFactory::create()   A

Complexity

Conditions 6
Paths 22

Size

Total Lines 31
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 6.042

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 19
c 1
b 0
f 0
nc 22
nop 3
dl 0
loc 31
ccs 17
cts 19
cp 0.8947
crap 6.042
rs 9.0111
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