Completed
Push — master ( 1fe708...6360f1 )
by Arnaud
16s queued 12s
created

FieldFactory   A

Complexity

Total Complexity 26

Size/Duplication

Total Lines 170
Duplicated Lines 0 %

Test Coverage

Coverage 60.24%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 26
eloc 79
c 2
b 0
f 0
dl 0
loc 170
ccs 50
cts 83
cp 0.6024
rs 10

7 Methods

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