Passed
Push — master ( 52cb59...719d42 )
by Vincent
04:52
created

ArrayElement::count()   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
 * @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 76
    public function __construct(ElementInterface $templateElement, ?TransformerInterface $transformer = null, ?ValueValidatorInterface $validator = null, ?ChoiceInterface $choices = null)
92
    {
93 76
        $this->templateElement = $templateElement;
94 76
        $this->transformer = $transformer ?: NullTransformer::instance();
95 76
        $this->validator = $validator ?: NullValueValidator::instance();
96 76
        $this->error = FormError::null();
97 76
        $this->choices = $choices;
98 76
    }
99
100
    /**
101
     * {@inheritdoc}
102
     */
103 6
    public function offsetGet($offset): ChildInterface
104
    {
105 6
        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 59
    public function submit($data): ElementInterface
160
    {
161 59
        $this->valid = true;
162
163 59
        $lastChildren = $this->children;
164 59
        $this->children = [];
165
166
        try {
167 59
            $data = (array) $this->transformer->transformFromHttp($data, $this);
168 2
        } catch (Exception $e) {
169 2
            $this->valid = false;
170 2
            $this->error = FormError::message($e->getMessage(), 'TRANSFORM_ERROR');
171
172 2
            return $this;
173
        }
174
175 57
        $errors = [];
176
177 57
        foreach ($data as $key => $value) {
178 48
            $child = $lastChildren[$key] ?? (new Child($key, $this->templateElement))->setParent($this);
179
180 48
            if (!$child->element()->submit($value)->valid()) {
181 9
                $this->valid = false;
182 9
                $errors[$key] = $child->error();
183 9
                $this->children[$key] = $child;
184
185 9
                continue;
186
            }
187
188
            // Remove null elements
189 45
            if ($child->element()->value() !== null) {
190 45
                $this->children[$key] = $child;
191
            }
192
        }
193
194 57
        $this->validate($errors);
195
196 57
        return $this;
197
    }
198
199
    /**
200
     * {@inheritdoc}
201
     */
202 8
    public function patch($data): ElementInterface
203
    {
204 8
        $this->valid = true;
205
206 8
        if ($data !== null) {
207 5
            return $this->submit($data);
208
        }
209
210 3
        $errors = [];
211
212
        // Keep all elements, and propagate the patch
213 3
        foreach ($this->children as $key => $child) {
214 3
            if (!$child->element()->patch(null)->valid()) {
215 1
                $this->valid = false;
216 1
                $errors[$key] = $child->error();
217 1
                $this->children[$key] = $child;
218
            }
219
        }
220
221 3
        $this->validate($errors);
222
223 3
        return $this;
224
    }
225
226
    /**
227
     * {@inheritdoc}
228
     */
229 11
    public function import($entity): ElementInterface
230
    {
231 11
        $this->children = [];
232
233
        // @todo optimise ? Do not recreate children
234 11
        foreach ((array) $entity as $key => $value) {
235 11
            $child = new Child($key, $this->templateElement);
236 11
            $child->setParent($this);
237 11
            $child->element()->import($value);
238
239 11
            $this->children[$key] = $child;
240
        }
241
242 11
        return $this;
243
    }
244
245
    /**
246
     * {@inheritdoc}
247
     *
248
     * @return T[]
249
     */
250 57
    public function value(): array
251
    {
252 57
        $value = [];
253
254 57
        foreach ($this->children as $child) {
255 43
            $value[$child->name()] = $child->element()->value();
256
        }
257
258 57
        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...
259
    }
260
261
    /**
262
     * {@inheritdoc}
263
     */
264 13
    public function httpValue()
265
    {
266 13
        $value = [];
267
268 13
        foreach ($this->children as $child) {
269 9
            $value[$child->name()] = $child->element()->httpValue();
270
        }
271
272 13
        return $this->transformer->transformToHttp($value, $this);
273
    }
274
275
    /**
276
     * {@inheritdoc}
277
     */
278 38
    public function valid(): bool
279
    {
280 38
        return $this->valid;
281
    }
282
283
    /**
284
     * {@inheritdoc}
285
     */
286 20
    public function error(?HttpFieldPath $field = null): FormError
287
    {
288 20
        return $field ? $this->error->withField($field) : $this->error;
289
    }
290
291
    /**
292
     * {@inheritdoc}
293
     */
294 25
    public function root(): RootElementInterface
295
    {
296 25
        if ($this->container) {
297 3
            return $this->container->parent()->root();
298
        }
299
300
        // @todo Use root form ?
301 22
        return new LeafRootElement($this);
302
    }
303
304
    /**
305
     * {@inheritdoc}
306
     */
307 9
    public function view(?HttpFieldPath $field = null): ElementViewInterface
308
    {
309 9
        $elements = [];
310
311 9
        foreach ($this->children as $key => $child) {
312 6
            $elements[$key] = $child->view($field);
313
        }
314
315 9
        $constraints = ConstraintsNormalizer::normalize($this->validator);
316
317 9
        return new ArrayElementView(
318 9
            self::class,
319 9
            (string) $field,
320 9
            $this->httpValue(),
321 9
            $this->error->global(),
322 9
            $elements,
323 9
            isset($constraints[NotBlank::class]),
324 9
            $constraints,
325 9
            $this->choiceView()
326
        );
327
    }
328
329
    /**
330
     * {@inheritdoc}
331
     */
332 19
    public function __clone()
333
    {
334 19
        $children = $this->children;
335 19
        $this->children = [];
336
337 19
        foreach ($children as $name => $child) {
338 1
            $this->children[$name] = $child->setParent($this);
339
        }
340 19
    }
341
342
    /**
343
     * Get the choice view and apply value transformation and selected value
344
     *
345
     * @return array|null
346
     * @see ChoiceInterface::view()
347
     */
348 9
    private function choiceView(): ?array
349
    {
350 9
        if ($this->choices === null) {
351 7
            return null;
352
        }
353
354
        // Use inner element for transform choice values
355 2
        $innerElement = clone $this->templateElement;
356
357
        return $this->choices->view(function (ChoiceView $view) use($innerElement) {
358 2
            $view->setSelected(in_array($view->value(), $this->value()));
359 2
            $view->setValue($innerElement->import($view->value())->httpValue());
360 2
        });
361
    }
362
363
    /**
364
     * Validate the array value
365
     *
366
     * @param array $childrenErrors The children errors
367
     */
368 58
    private function validate(array $childrenErrors): void
369
    {
370 58
        if (!$this->valid) {
371 9
            $this->error = FormError::aggregate($childrenErrors);
372 9
            return;
373
        }
374
375 49
        $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

375
        $this->error = $this->validator->validate(/** @scrutinizer ignore-type */ $this->value(), $this);
Loading history...
376 49
        $this->valid = $this->error->empty();
377 49
    }
378
}
379