ArrayElement::submit()   B
last analyzed

Complexity

Conditions 6
Paths 9

Size

Total Lines 44
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 23
CRAP Score 6

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 23
c 1
b 0
f 0
dl 0
loc 44
ccs 23
cts 23
cp 1
rs 8.9297
cc 6
nc 9
nop 1
crap 6
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\ConstraintValueValidator;
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 Iterator;
28
use Symfony\Component\Validator\Constraints\NotBlank;
29
use TypeError;
30
31
/**
32
 * Dynamic collection of elements
33
 * This element may be a simple list array (i.e. with numeric offset), or associative array (with string offset)
34
 * Contrary to Form, all components elements are identically
35
 *
36
 * Array element can be used as leaf element (like with CSV string), or root of embedded forms
37
 *
38
 * @see ArrayElementBuilder For build the element
39
 *
40
 * @template T
41
 *
42
 * @implements ChildAggregateInterface<T[]>
43
 * @implements Choiceable<T>
44
 */
45
final class ArrayElement implements ChildAggregateInterface, Countable, Choiceable
46
{
47
    use ContainerTrait;
48
49
    /**
50
     * @var ElementInterface<T>
51
     */
52
    private $templateElement;
53
54
    /**
55
     * @var TransformerInterface
56
     */
57
    private $transformer;
58
59
    /**
60
     * @var ValueValidatorInterface
61
     */
62
    private $validator;
63
64
    /**
65
     * @var ChoiceInterface|null
66
     */
67
    private $choices;
68
69
    /**
70
     * @var bool
71
     */
72
    private $valid = false;
73
74
    /**
75
     * @var FormError
76
     */
77
    private $error;
78
79
    /**
80
     * @var ChildInterface[]
81
     */
82
    private $children = [];
83
84
85
    /**
86
     * ArrayElement constructor.
87
     *
88
     * @param ElementInterface<T> $templateElement Inner element
89
     * @param TransformerInterface|null $transformer
90
     * @param ValueValidatorInterface|null $validator
91
     * @param ChoiceInterface<T>|null $choices
92
     */
93 96
    public function __construct(ElementInterface $templateElement, ?TransformerInterface $transformer = null, ?ValueValidatorInterface $validator = null, ?ChoiceInterface $choices = null)
94
    {
95 96
        $this->templateElement = $templateElement;
96 96
        $this->transformer = $transformer ?: NullTransformer::instance();
97 96
        $this->validator = $validator ?: ConstraintValueValidator::empty();
98 96
        $this->error = FormError::null();
99 96
        $this->choices = $choices;
100 96
    }
101
102
    /**
103
     * {@inheritdoc}
104
     */
105 8
    public function offsetGet($offset): ChildInterface
106
    {
107 8
        return $this->children[$offset];
108
    }
109
110
    /**
111
     * {@inheritdoc}
112
     */
113 1
    public function offsetExists($offset): bool
114
    {
115 1
        return isset($this->children[$offset]);
116
    }
117
118
    /**
119
     * {@inheritdoc}
120
     */
121 1
    public function offsetSet($offset, $value): void
122
    {
123 1
        throw new BadMethodCallException('Use import() or submit() for set an offset value');
124
    }
125
126
    /**
127
     * {@inheritdoc}
128
     */
129 1
    public function offsetUnset($offset): void
130
    {
131 1
        throw new BadMethodCallException('Use import() or submit() for set an offset value');
132
    }
133
134
    /**
135
     * {@inheritdoc}
136
     */
137 2
    public function getIterator(): Iterator
138
    {
139 2
        return new ArrayIterator($this->children);
140
    }
141
142
    /**
143
     * {@inheritdoc}
144
     */
145 2
    public function count(): int
146
    {
147 2
        return count($this->children);
148
    }
149
150
    /**
151
     * {@inheritdoc}
152
     */
153 1
    public function choices(): ?ChoiceInterface
154
    {
155 1
        return $this->choices;
156
    }
157
158
    /**
159
     * {@inheritdoc}
160
     */
161 68
    public function submit($data): ElementInterface
162
    {
163 68
        $this->valid = true;
164
165 68
        $lastChildren = $this->children;
166 68
        $this->children = [];
167
168
        try {
169 68
            $data = (array) $this->transformer->transformFromHttp($data, $this);
170 4
        } catch (Exception $e) {
171 4
            $this->error = $this->validator->onTransformerException($e, $data, $this);
172
173 4
            if (!$this->valid = $this->error->empty()) {
174 2
                return $this;
175
            }
176
177 2
            $data = [];
178
        }
179
180 66
        $errors = [];
181
182 66
        foreach ($data as $key => $value) {
183 55
            $child = $lastChildren[$key] ?? (new Child($key, $this->templateElement))->setParent($this);
184
185 55
            $child->element()->submit($value);
186
187
            // Remove null elements
188 55
            if ($child->element()->value() === null) {
189 2
                continue;
190
            }
191
192 55
            $this->children[$key] = $child;
193
194 55
            if (!$child->element()->valid()) {
195 11
                $this->valid = false;
196 11
                $errors[$key] = $child->error();
197
198 11
                continue;
199
            }
200
        }
201
202 66
        $this->validate($errors);
203
204 66
        return $this;
205
    }
206
207
    /**
208
     * {@inheritdoc}
209
     */
210 9
    public function patch($data): ElementInterface
211
    {
212 9
        $this->valid = true;
213
214 9
        if ($data !== null) {
215 5
            return $this->submit($data);
216
        }
217
218 4
        $errors = [];
219
220
        // Keep all elements, and propagate the patch
221 4
        foreach ($this->children as $key => $child) {
222 4
            if (!$child->element()->patch(null)->valid()) {
223 1
                $this->valid = false;
224 1
                $errors[$key] = $child->error();
225 1
                $this->children[$key] = $child;
226
            }
227
        }
228
229 4
        $this->validate($errors);
230
231 4
        return $this;
232
    }
233
234
    /**
235
     * {@inheritdoc}
236
     */
237 21
    public function import($entity): ElementInterface
238
    {
239 21
        if ($entity === null) {
240 1
            $entity = [];
241 21
        } elseif (!is_iterable($entity)) {
242 8
            throw new TypeError('The import()\'ed value of a '.static::class.' must be iterable or null');
243
        }
244
245 13
        $this->children = [];
246
247
        // @todo optimise ? Do not recreate children
248 13
        foreach ($entity as $key => $value) {
249 13
            $child = new Child($key, $this->templateElement);
250 13
            $child->setParent($this);
251 13
            $child->element()->import($value);
252
253 13
            $this->children[$key] = $child;
254
        }
255
256 13
        return $this;
257
    }
258
259
    /**
260
     * {@inheritdoc}
261
     *
262
     * @return T[]
263
     */
264 69
    public function value(): array
265
    {
266 69
        $value = [];
267
268 69
        foreach ($this->children as $child) {
269 51
            $value[$child->name()] = $child->element()->value();
270
        }
271
272 69
        return $value;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $value returns the type array which is incompatible with the return type mandated by Bdf\Form\ElementInterface::value() of Bdf\Form\T|null.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
273
    }
274
275
    /**
276
     * {@inheritdoc}
277
     */
278 14
    public function httpValue()
279
    {
280 14
        $value = [];
281
282 14
        foreach ($this->children as $child) {
283 9
            $value[$child->name()] = $child->element()->httpValue();
284
        }
285
286 14
        return $this->transformer->transformToHttp($value, $this);
287
    }
288
289
    /**
290
     * {@inheritdoc}
291
     */
292 48
    public function valid(): bool
293
    {
294 48
        return $this->valid;
295
    }
296
297
    /**
298
     * {@inheritdoc}
299
     */
300 23
    public function error(?HttpFieldPath $field = null): FormError
301
    {
302 23
        return $field ? $this->error->withField($field) : $this->error;
303
    }
304
305
    /**
306
     * {@inheritdoc}
307
     */
308 30
    public function root(): RootElementInterface
309
    {
310 30
        if ($container = $this->container()) {
311 4
            return $container->parent()->root();
312
        }
313
314
        // @todo Use root form ?
315 26
        return new LeafRootElement($this);
316
    }
317
318
    /**
319
     * {@inheritdoc}
320
     */
321 10
    public function view(?HttpFieldPath $field = null): ElementViewInterface
322
    {
323 10
        $elements = [];
324
325 10
        foreach ($this->children as $key => $child) {
326 6
            $elements[$key] = $child->view($field);
327
        }
328
329 10
        $constraints = ConstraintsNormalizer::normalize($this->validator);
330
331 10
        return new ArrayElementView(
332 10
            self::class,
333 10
            (string) $field,
334 10
            $this->httpValue(),
335 10
            $this->error->global(),
336 10
            $elements,
337 10
            isset($constraints[NotBlank::class]),
338 10
            $constraints,
339 10
            $this->choiceView()
340
        );
341
    }
342
343
    /**
344
     * {@inheritdoc}
345
     */
346 26
    public function __clone()
347
    {
348 26
        $children = $this->children;
349 26
        $this->children = [];
350
351 26
        foreach ($children as $name => $child) {
352 1
            $this->children[$name] = $child->setParent($this);
353
        }
354 26
    }
355
356
    /**
357
     * Get the choice view and apply value transformation and selected value
358
     *
359
     * @return array|null
360
     * @see ChoiceInterface::view()
361
     */
362 10
    private function choiceView(): ?array
363
    {
364 10
        if ($this->choices === null) {
365 7
            return null;
366
        }
367
368
        // Use inner element for transform choice values
369 3
        $innerElement = clone $this->templateElement;
370
371
        return $this->choices->view(function (ChoiceView $view) use($innerElement) {
372 3
            $view->setSelected(in_array($view->value(), $this->value()));
373 3
            $view->setValue($innerElement->import($view->value())->httpValue());
374 3
        });
375
    }
376
377
    /**
378
     * Validate the array value
379
     *
380
     * @param array $childrenErrors The children errors
381
     */
382 68
    private function validate(array $childrenErrors): void
383
    {
384 68
        if (!$this->valid) {
385 11
            $this->error = FormError::aggregate($childrenErrors);
386 11
            return;
387
        }
388
389 57
        $this->error = $this->validator->validate($this->value(), $this);
0 ignored issues
show
Bug introduced by
$this->value() of type array is incompatible with the type Bdf\Form\Validator\T|null expected by parameter $value of Bdf\Form\Validator\Value...orInterface::validate(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

389
        $this->error = $this->validator->validate(/** @scrutinizer ignore-type */ $this->value(), $this);
Loading history...
390 57
        $this->valid = $this->error->empty();
391 57
    }
392
}
393