Passed
Push — master ( 355586...94527c )
by Vincent
04:50
created

ArrayElement   A

Complexity

Total Complexity 39

Size/Duplication

Total Lines 340
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 39
eloc 103
c 1
b 0
f 0
dl 0
loc 340
ccs 115
cts 115
cp 1
rs 9.28

20 Methods

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

381
        $this->error = $this->validator->validate(/** @scrutinizer ignore-type */ $this->value(), $this);
Loading history...
382 57
        $this->valid = $this->error->empty();
383 57
    }
384
}
385