Passed
Branch master (3daac1)
by Vincent
07:53
created

ArrayElement::valid()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 1
c 1
b 0
f 0
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
cc 1
nc 1
nop 0
crap 1
1
<?php
2
3
namespace Bdf\Form\Aggregate;
4
5
use ArrayIterator;
6
use BadMethodCallException;
7
use Bdf\Form\Aggregate\View\ArrayElementView;
8
use Bdf\Form\Child\Child;
9
use Bdf\Form\Child\ChildInterface;
10
use Bdf\Form\Child\Http\HttpFieldPath;
11
use Bdf\Form\Choice\Choiceable;
12
use Bdf\Form\Choice\ChoiceInterface;
13
use Bdf\Form\Choice\ChoiceView;
14
use Bdf\Form\ElementInterface;
15
use Bdf\Form\Error\FormError;
16
use Bdf\Form\Leaf\LeafRootElement;
17
use Bdf\Form\RootElementInterface;
18
use Bdf\Form\Transformer\NullTransformer;
19
use Bdf\Form\Transformer\TransformerInterface;
20
use Bdf\Form\Util\ContainerTrait;
21
use Bdf\Form\Validator\NullValueValidator;
22
use Bdf\Form\Validator\ValueValidatorInterface;
23
use Bdf\Form\View\ConstraintsNormalizer;
24
use Bdf\Form\View\ElementViewInterface;
25
use Countable;
26
use Exception;
27
use Symfony\Component\Validator\Constraints\NotBlank;
28
29
/**
30
 * Dynamic collection of elements
31
 * This element may be a simple list array (i.e. with numeric offset), or associative array (with string offset)
32
 * Contrary to Form, all components elements are identically
33
 *
34
 * Array element can be used as leaf element (like with CSV string), or root of embedded forms
35
 *
36
 * @see ArrayElementBuilder For build the element
37
 */
38
final class ArrayElement implements ChildAggregateInterface, Countable, Choiceable
39
{
40
    use ContainerTrait;
41
42
    /**
43
     * @var ElementInterface
44
     */
45
    private $templateElement;
46
47
    /**
48
     * @var TransformerInterface
49
     */
50
    private $transformer;
51
52
    /**
53
     * @var ValueValidatorInterface
54
     */
55
    private $validator;
56
57
    /**
58
     * @var ChoiceInterface|null
59
     */
60
    private $choices;
61
62
    /**
63
     * @var bool
64
     */
65
    private $valid = false;
66
67
    /**
68
     * @var FormError
69
     */
70
    private $error;
71
72
    /**
73
     * @var ChildInterface[]
74
     */
75
    private $children = [];
76
77
78
    /**
79
     * ArrayElement constructor.
80
     *
81
     * @param ElementInterface $templateElement Inner element
82
     * @param TransformerInterface|null $transformer
83
     * @param ValueValidatorInterface|null $validator
84
     * @param ChoiceInterface|null $choices
85
     */
86 74
    public function __construct(ElementInterface $templateElement, ?TransformerInterface $transformer = null, ?ValueValidatorInterface $validator = null, ?ChoiceInterface $choices = null)
87
    {
88 74
        $this->templateElement = $templateElement;
89 74
        $this->transformer = $transformer ?: NullTransformer::instance();
90 74
        $this->validator = $validator ?: NullValueValidator::instance();
91 74
        $this->error = FormError::null();
92 74
        $this->choices = $choices;
93 74
    }
94
95
    /**
96
     * {@inheritdoc}
97
     */
98 6
    public function offsetGet($offset): ChildInterface
99
    {
100 6
        return $this->children[$offset];
101
    }
102
103
    /**
104
     * {@inheritdoc}
105
     */
106 1
    public function offsetExists($offset): bool
107
    {
108 1
        return isset($this->children[$offset]);
109
    }
110
111
    /**
112
     * {@inheritdoc}
113
     */
114 1
    public function offsetSet($offset, $value)
115
    {
116 1
        throw new BadMethodCallException('Use import() or submit() for set an offset value');
117
    }
118
119
    /**
120
     * {@inheritdoc}
121
     */
122 1
    public function offsetUnset($offset)
123
    {
124 1
        throw new BadMethodCallException('Use import() or submit() for set an offset value');
125
    }
126
127
    /**
128
     * {@inheritdoc}
129
     */
130 2
    public function getIterator()
131
    {
132 2
        return new ArrayIterator($this->children);
133
    }
134
135
    /**
136
     * {@inheritdoc}
137
     */
138 2
    public function count(): int
139
    {
140 2
        return count($this->children);
141
    }
142
143
    /**
144
     * {@inheritdoc}
145
     */
146 1
    public function choices(): ?ChoiceInterface
147
    {
148 1
        return $this->choices;
149
    }
150
151
    /**
152
     * {@inheritdoc}
153
     */
154 57
    public function submit($data): ElementInterface
155
    {
156 57
        $this->valid = true;
157
158 57
        $lastChildren = $this->children;
159 57
        $this->children = [];
160
161
        try {
162 57
            $data = (array) $this->transformer->transformFromHttp($data, $this);
163 2
        } catch (Exception $e) {
164 2
            $this->valid = false;
165 2
            $this->error = FormError::message($e->getMessage(), 'TRANSFORM_ERROR');
166
167 2
            return $this;
168
        }
169
170 55
        $errors = [];
171
172 55
        foreach ($data as $key => $value) {
173 46
            $child = $lastChildren[$key] ?? (new Child($key, $this->templateElement))->setParent($this);
174
175 46
            if (!$child->element()->submit($value)->valid()) {
176 7
                $this->valid = false;
177 7
                $errors[$key] = $child->error();
178 7
                $this->children[$key] = $child;
179
180 7
                continue;
181
            }
182
183
            // Remove null elements
184 44
            if ($child->element()->value() !== null) {
185 44
                $this->children[$key] = $child;
186
            }
187
        }
188
189 55
        $this->validate($errors);
190
191 55
        return $this;
192
    }
193
194
    /**
195
     * {@inheritdoc}
196
     */
197 7
    public function patch($data): ElementInterface
198
    {
199 7
        $this->valid = true;
200
201 7
        if ($data !== null) {
202 4
            return $this->submit($data);
203
        }
204
205 3
        $errors = [];
206
207
        // Keep all elements, and propagate the patch
208 3
        foreach ($this->children as $key => $child) {
209 3
            if (!$child->element()->patch(null)->valid()) {
210 1
                $this->valid = false;
211 1
                $errors[$key] = $child->error();
212 1
                $this->children[$key] = $child;
213
            }
214
        }
215
216 3
        $this->validate($errors);
217
218 3
        return $this;
219
    }
220
221
    /**
222
     * {@inheritdoc}
223
     */
224 11
    public function import($entity): ElementInterface
225
    {
226 11
        $this->children = [];
227
228
        // @todo optimise ? Do not recreate children
229 11
        foreach ((array) $entity as $key => $value) {
230 11
            $child = new Child($key, $this->templateElement);
231 11
            $child->setParent($this);
232 11
            $child->element()->import($value);
233
234 11
            $this->children[$key] = $child;
235
        }
236
237 11
        return $this;
238
    }
239
240
    /**
241
     * {@inheritdoc}
242
     */
243 57
    public function value()
244
    {
245 57
        $value = [];
246
247 57
        foreach ($this->children as $child) {
248 43
            $value[$child->name()] = $child->element()->value();
249
        }
250
251 57
        return $value;
252
    }
253
254
    /**
255
     * {@inheritdoc}
256
     */
257 12
    public function httpValue()
258
    {
259 12
        $value = [];
260
261 12
        foreach ($this->children as $child) {
262 8
            $value[$child->name()] = $child->element()->httpValue();
263
        }
264
265 12
        return $this->transformer->transformToHttp($value, $this);
266
    }
267
268
    /**
269
     * {@inheritdoc}
270
     */
271 36
    public function valid(): bool
272
    {
273 36
        return $this->valid;
274
    }
275
276
    /**
277
     * {@inheritdoc}
278
     */
279 18
    public function error(): FormError
280
    {
281 18
        return $this->error;
282
    }
283
284
    /**
285
     * {@inheritdoc}
286
     */
287 23
    public function root(): RootElementInterface
288
    {
289 23
        if ($this->container) {
290 2
            return $this->container->parent()->root();
291
        }
292
293
        // @todo Use root form ?
294 21
        return new LeafRootElement($this);
295
    }
296
297
    /**
298
     * {@inheritdoc}
299
     */
300 8
    public function view(?HttpFieldPath $field = null): ElementViewInterface
301
    {
302 8
        $elements = [];
303
304 8
        foreach ($this->children as $key => $child) {
305 5
            $elements[$key] = $child->view($field);
306
        }
307
308 8
        $constraints = ConstraintsNormalizer::normalize($this->validator);
309
310 8
        return new ArrayElementView(
311 8
            self::class,
312 8
            (string) $field,
313 8
            $this->httpValue(),
314 8
            $this->error->global(),
315 8
            $elements,
316 8
            isset($constraints[NotBlank::class]),
317 8
            $constraints,
318 8
            $this->choiceView()
319
        );
320
    }
321
322
    /**
323
     * {@inheritdoc}
324
     */
325 18
    public function __clone()
326
    {
327 18
        $children = $this->children;
328 18
        $this->children = [];
329
330 18
        foreach ($children as $name => $child) {
331 1
            $this->children[$name] = $child->setParent($this);
332
        }
333 18
    }
334
335
    /**
336
     * Get the choice view and apply value transformation and selected value
337
     *
338
     * @return array|null
339
     * @see ChoiceInterface::view()
340
     */
341 8
    private function choiceView(): ?array
342
    {
343 8
        if ($this->choices === null) {
344 6
            return null;
345
        }
346
347
        // Use inner element for transform choice values
348 2
        $innerElement = clone $this->templateElement;
349
350
        return $this->choices->view(function (ChoiceView $view) use($innerElement) {
351 2
            $view->setSelected(in_array($view->value(), $this->value()));
352 2
            $view->setValue($innerElement->import($view->value())->httpValue());
353 2
        });
354
    }
355
356
    /**
357
     * Validate the array value
358
     *
359
     * @param array $childrenErrors The children errors
360
     */
361 56
    private function validate(array $childrenErrors): void
362
    {
363 56
        if (!$this->valid) {
364 7
            $this->error = FormError::aggregate($childrenErrors);
365 7
            return;
366
        }
367
368 49
        $this->error = $this->validator->validate($this->value(), $this);
369 49
        $this->valid = $this->error->empty();
370 49
    }
371
}
372