Passed
Pull Request — master (#7)
by Vincent
06:41 queued 01:19
created

Form::submitToChildrenAndValidate()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3

Importance

Changes 0
Metric Value
eloc 6
c 0
b 0
f 0
dl 0
loc 13
ccs 7
cts 7
cp 1
rs 10
cc 3
nc 3
nop 2
crap 3
1
<?php
2
3
namespace Bdf\Form\Aggregate;
4
5
use BadMethodCallException;
6
use Bdf\Form\Aggregate\Collection\ChildrenCollectionInterface;
7
use Bdf\Form\Aggregate\Value\ValueGenerator;
8
use Bdf\Form\Aggregate\Value\ValueGeneratorInterface;
9
use Bdf\Form\Aggregate\View\FormView;
10
use Bdf\Form\Child\ChildInterface;
11
use Bdf\Form\Child\Http\HttpFieldPath;
12
use Bdf\Form\ElementInterface;
13
use Bdf\Form\Error\FormError;
14
use Bdf\Form\RootElementInterface;
15
use Bdf\Form\Transformer\NullTransformer;
16
use Bdf\Form\Transformer\TransformerInterface;
17
use Bdf\Form\Util\ContainerTrait;
18
use Bdf\Form\Validator\ConstraintValueValidator;
19
use Bdf\Form\Validator\ValueValidatorInterface;
20
use Bdf\Form\View\ElementViewInterface;
21
use Exception;
22
use Iterator;
23
24
/**
25
 * The base form element
26
 * A form is an static aggregate of elements, unlike ArrayElement which is a dynamic aggregate
27
 *
28
 * The form will submit HTTP value to all it's children, and then perform it's validation process (if defined)
29
 * A form cannot have a "global" error if there is at least one child on error
30
 *
31
 * To access to children elements, use array access : `$form['child']->element()`
32
 * Note: The return value of array access is a ChildInterface. Use `ChildInterface::element()` to get the element
33
 *
34
 * <code>
35
 * // Show form view
36
 * $view = $form->import($entity)->view();
37
 *
38
 * echo $view['foo']->id('foo')->class('form-control');
39
 * echo $view['bar']->id('bar')->class('form-control');
40
 *
41
 * // Submit form
42
 * if (!$form->submit($request->post())->valid()) {
43
 *     throw new ApiException($form->error()->print(new ApiErrorPrinter()));
44
 * }
45
 *
46
 * $entity = $form->attach($entity)->value();
47
 * </code>
48
 *
49
 * @template T
50
 * @implements FormInterface<T>
51
 */
52
final class Form implements FormInterface
53
{
54
    use ContainerTrait;
55
56
    /**
57
     * @var ValueValidatorInterface<T>
58
     */
59
    private $validator;
60
61
    /**
62
     * Transformer to view value
63
     *
64
     * @var TransformerInterface
65
     */
66
    private $transformer;
67
68
    /**
69
     * @var ChildrenCollectionInterface
70
     */
71
    private $children;
72
73
    /**
74
     * @var ValueGeneratorInterface<T>
75
     */
76
    private $generator;
77
78
    /**
79
     * Does the form is optional ?
80
     * An optional form will not be validated if the form is empty, and its value will be null
81
     *
82
     * @var bool
83
     */
84
    private $optional;
85
86
    /**
87
     * @var RootElementInterface|null
88
     */
89
    private $root;
90
91
    /**
92
     * @var FormError
93
     */
94
    private $error;
95
96
    /**
97
     * @var bool
98
     */
99
    private $valid = false;
100
101
    /**
102
     * The generated value
103
     * This value is reset on submit to force regeneration
104
     *
105
     * @var T|null
0 ignored issues
show
Bug introduced by
The type Bdf\Form\Aggregate\T was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
106
     */
107
    private $value;
108
109
    /**
110
     * Does the form has been submitted ?
111
     * In case of optional form, the form is not submitted if the value is empty
112
     *
113
     * @var bool
114
     */
115
    private $submitted = false;
116
117
    /**
118
     * Form constructor.
119
     *
120
     * @param ChildrenCollectionInterface $children
121
     * @param ValueValidatorInterface<T>|null $validator
122
     * @param TransformerInterface|null $transformer
123
     * @param ValueGeneratorInterface<T>|null $generator
124
     * @param bool $optional
125
     */
126 256
    public function __construct(ChildrenCollectionInterface $children, ?ValueValidatorInterface $validator = null, ?TransformerInterface $transformer = null, ?ValueGeneratorInterface $generator = null, bool $optional = false)
127
    {
128 256
        $this->children = $children->duplicate($this);
129 256
        $this->validator = $validator ?? ConstraintValueValidator::empty();
130 256
        $this->transformer = $transformer ?: NullTransformer::instance();
131 256
        $this->error = FormError::null();
132
        /** @var ValueGeneratorInterface<T> */
133 256
        $this->generator = $generator ?: new ValueGenerator();
0 ignored issues
show
Bug introduced by
Accessing generator on the interface Bdf\Form\Aggregate\Value\ValueGeneratorInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
134 256
        $this->optional = $optional;
135 256
    }
136
137
    /**
138
     * {@inheritdoc}
139
     */
140 73
    public function submit($data): ElementInterface
141
    {
142 73
        $this->valid = true;
143 73
        $this->value = null;
144
145 73
        if ($this->handleOptional($data)) {
146 4
            return $this;
147
        }
148
149 73
        $data = $this->transformHttpValue($data);
150
151 73
        $this->submitToChildrenAndValidate($data, 'submit');
152 73
        $this->submitted = true;
153
154 73
        return $this;
155
    }
156
157
    /**
158
     * {@inheritdoc}
159
     */
160 11
    public function patch($data): ElementInterface
161
    {
162 11
        $this->valid = true;
163 11
        $this->value = null;
164
165 11
        if ($this->handleOptional($data)) {
166 1
            return $this;
167
        }
168
169 11
        if ($data !== null) {
170 10
            $data = $this->transformHttpValue($data);
171
        }
172
173 11
        $this->submitToChildrenAndValidate($data, 'patch');
174 11
        $this->submitted = true;
175
176 11
        return $this;
177
    }
178
179
    /**
180
     * {@inheritdoc}
181
     */
182 50
    public function valid(): bool
183
    {
184 50
        return $this->valid;
185
    }
186
187
    /**
188
     * {@inheritdoc}
189
     */
190 32
    public function error(?HttpFieldPath $field = null): FormError
191
    {
192 32
        return $field ? $this->error->withField($field) : $this->error;
193
    }
194
195
    /**
196
     * {@inheritdoc}
197
     *
198
     * @param T|null $entity
199
     * @return $this
200
     */
201 21
    public function import($entity): ElementInterface
202
    {
203 21
        if ($entity) {
204 19
            $this->generator->attach($entity);
205
        }
206
207 21
        $this->value = $entity;
208
209 21
        foreach ($this->children as $child) {
210 21
            $child->import($entity);
211
        }
212
213 21
        return $this;
214
    }
215
216
    /**
217
     * {@inheritdoc}
218
     *
219
     * @return T
220
     */
221 51
    public function value()
222
    {
223 51
        if ($this->value !== null) {
224 15
            return $this->value;
225
        }
226
227 41
        if (!$this->submitted && $this->optional) {
228 3
            return null;
229
        }
230
231 41
        $this->value = $this->generator->generate($this);
232
233 41
        foreach ($this->children->reverseIterator() as $child) {
234 37
            $child->fill($this->value);
235
        }
236
237 41
        return $this->value;
238
    }
239
240
    /**
241
     * {@inheritdoc}
242
     */
243 6
    public function httpValue()
244
    {
245 6
        $http = [];
246
247 6
        foreach ($this->children as $child) {
248 6
            $http += $child->httpFields();
249
        }
250
251 6
        return $this->transformer->transformToHttp($http, $this);
252
    }
253
254
    /**
255
     * {@inheritdoc}
256
     */
257 148
    public function root(): RootElementInterface
258
    {
259 148
        if ($container = $this->container()) {
260 22
            return $container->parent()->root();
261
        }
262
263 146
        if ($this->root) {
264 97
            return $this->root;
265
        }
266
267 141
        return $this->root = new RootForm($this);
268
    }
269
270
    /**
271
     * {@inheritdoc}
272
     *
273
     * @return FormView
274
     */
275 8
    public function view(?HttpFieldPath $field = null): ElementViewInterface
276
    {
277 8
        $elements = [];
278
279 8
        foreach ($this->children as $child) {
280 8
            $elements[$child->name()] = $child->view($field);
281
        }
282
283 8
        return new FormView(self::class, $this->error->global(), $elements);
284
    }
285
286
    /**
287
     * {@inheritdoc}
288
     */
289 2
    public function getIterator(): Iterator
290
    {
291 2
        return $this->children->forwardIterator();
292
    }
293
294
    /**
295
     * {@inheritdoc}
296
     */
297 23
    public function offsetExists($offset): bool
298
    {
299 23
        return isset($this->children[$offset]);
300
    }
301
302
    /**
303
     * {@inheritdoc}
304
     */
305 70
    public function offsetGet($offset): ChildInterface
306
    {
307 70
        return $this->children[$offset];
308
    }
309
310
    /**
311
     * {@inheritdoc}
312
     */
313 1
    public function offsetSet($offset, $value): void
314
    {
315 1
        throw new BadMethodCallException(__CLASS__.' is immutable');
316
    }
317
318
    /**
319
     * {@inheritdoc}
320
     */
321 1
    public function offsetUnset($offset): void
322
    {
323 1
        throw new BadMethodCallException(__CLASS__.' is immutable');
324
    }
325
326
    /**
327
     * {@inheritdoc}
328
     */
329 29
    public function __clone()
330
    {
331 29
        $this->children = $this->children->duplicate($this);
332 29
    }
333
334
    /**
335
     * {@inheritdoc}
336
     */
337 4
    public function attach($entity): FormInterface
338
    {
339 4
        $this->generator->attach($entity);
340 4
        $this->value = null; // The value is only attached : it must be filled when calling value()
341
342 4
        return $this;
343
    }
344
345
    /**
346
     * Set the root element
347
     *
348
     * @param RootElementInterface $root
349
     *
350
     * @internal used by the builder
351
     */
352 5
    public function setRoot(RootElementInterface $root): void
353
    {
354 5
        $this->root = $root;
355 5
    }
356
357
    /**
358
     * Transform the submitted value
359
     * If the transformation fail the error will be set
360
     *
361
     * @param mixed $data
362
     *
363
     * @return mixed The transformed value
364
     */
365 80
    private function transformHttpValue($data)
366
    {
367
        try {
368 80
            $data = $this->transformer->transformFromHttp($data, $this);
369 3
        } catch (Exception $e) {
370 3
            $this->error = $this->validator->onTransformerException($e, $data, $this);
371 3
            $this->valid = $this->error->empty();
372
373
            // Reset children values
374 3
            foreach ($this->children->all() as $child) {
375 3
                $child->element()->import(null);
376
            }
377
378 3
            return null;
379
        }
380
381 77
        return $data;
382
    }
383
384
    /**
385
     * Submit the transformed http data to children and validate the value
386
     *
387
     * @param mixed $data Data to submit
388
     * @param string $method The submit method to call. Should be "submit" or "patch"
389
     */
390 81
    private function submitToChildrenAndValidate($data, string $method): void
391
    {
392 81
        if (!$this->submitToChildren($data, $method)) {
393 36
            return;
394
        }
395
396
        // Do not generate value if it's not necessary
397 59
        $this->error = $this->validator->hasConstraints()
398 6
            ? $this->validator->validate($this->value(), $this)
399 53
            : FormError::null()
400
        ;
401
402 59
        $this->valid = $this->error->empty();
403 59
    }
404
405
    /**
406
     * Submit the transformed http data to children
407
     *
408
     * @param mixed $data Data to submit
409
     * @param string $method The submit method to call. Should be "submit" or "patch"
410
     *
411
     * @return bool false on fail, or true on success
412
     */
413 81
    private function submitToChildren($data, string $method): bool
414
    {
415 81
        if (!$this->valid) {
416 1
            return false;
417
        }
418
419 80
        $errors = [];
420
421 80
        foreach ($this->children->reverseIterator() as $child) {
422 75
            if (!$child->$method($data)) {
423 35
                $this->valid = false;
424 35
                $errors[$child->name()] = $child->error();
425
            }
426
        }
427
428 80
        if (!$this->valid) {
429 35
            $this->error = FormError::aggregate($errors);
430
431 35
            return false;
432
        }
433
434 59
        return true;
435
    }
436
437
    /**
438
     * Handle the optional case
439
     * If the form is optional and the data is empty, the form will be considered as not submitted and validation will be ignored
440
     *
441
     * @param mixed $data Submitted HTTP data
442
     *
443
     * @return bool true if the form is optional and not submitted, false otherwise
444
     */
445 81
    private function handleOptional($data): bool
446
    {
447 81
        if (!$this->optional || $data !== null && $data !== [] && $data !== '') {
448 81
            return false;
449
        }
450
451 5
        $this->submitted = false;
452 5
        $this->error = FormError::null();
453
454
        // Reset children value
455 5
        foreach ($this->children as $child) {
456 5
            $child->element()->import(null);
457
        }
458
459 5
        return true;
460
    }
461
}
462