Form::valid()   A
last analyzed

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 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
     * @var RootElementInterface|null
80
     */
81
    private $root;
82
83
    /**
84
     * @var FormError
85
     */
86
    private $error;
87
88
    /**
89
     * @var bool
90
     */
91
    private $valid = false;
92
93
    /**
94
     * The generated value
95
     * This value is reset on submit to force regeneration
96
     *
97
     * @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...
98
     */
99
    private $value;
100
101
    /**
102
     * Form constructor.
103
     *
104
     * @param ChildrenCollectionInterface $children
105
     * @param ValueValidatorInterface<T>|null $validator
106
     * @param TransformerInterface|null $transformer
107
     * @param ValueGeneratorInterface<T>|null $generator
108
     */
109 251
    public function __construct(ChildrenCollectionInterface $children, ?ValueValidatorInterface $validator = null, ?TransformerInterface $transformer = null, ?ValueGeneratorInterface $generator = null)
110
    {
111 251
        $this->children = $children->duplicate($this);
112 251
        $this->validator = $validator ?? ConstraintValueValidator::empty();
113 251
        $this->transformer = $transformer ?: NullTransformer::instance();
114 251
        $this->error = FormError::null();
115
        /** @var ValueGeneratorInterface<T> */
116 251
        $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...
117 251
    }
118
119
    /**
120
     * {@inheritdoc}
121
     */
122 69
    public function submit($data): ElementInterface
123
    {
124 69
        $this->valid = true;
125 69
        $this->value = null;
126
127 69
        $data = $this->transformHttpValue($data);
128
129 69
        $this->submitToChildrenAndValidate($data, 'submit');
130
131 69
        return $this;
132
    }
133
134
    /**
135
     * {@inheritdoc}
136
     */
137 10
    public function patch($data): ElementInterface
138
    {
139 10
        $this->valid = true;
140 10
        $this->value = null;
141
142 10
        if ($data !== null) {
143 9
            $data = $this->transformHttpValue($data);
144
        }
145
146 10
        $this->submitToChildrenAndValidate($data, 'patch');
147
148 10
        return $this;
149
    }
150
151
    /**
152
     * {@inheritdoc}
153
     */
154 45
    public function valid(): bool
155
    {
156 45
        return $this->valid;
157
    }
158
159
    /**
160
     * {@inheritdoc}
161
     */
162 28
    public function error(?HttpFieldPath $field = null): FormError
163
    {
164 28
        return $field ? $this->error->withField($field) : $this->error;
165
    }
166
167
    /**
168
     * {@inheritdoc}
169
     *
170
     * @param T|null $entity
171
     * @return $this
172
     */
173 21
    public function import($entity): ElementInterface
174
    {
175 21
        if ($entity) {
176 19
            $this->generator->attach($entity);
177
        }
178
179 21
        $this->value = $entity;
180
181 21
        foreach ($this->children as $child) {
182 21
            $child->import($entity);
183
        }
184
185 21
        return $this;
186
    }
187
188
    /**
189
     * {@inheritdoc}
190
     *
191
     * @return T
192
     */
193 48
    public function value()
194
    {
195 48
        if ($this->value !== null) {
196 15
            return $this->value;
197
        }
198
199 38
        $this->value = $this->generator->generate($this);
200
201 38
        foreach ($this->children->reverseIterator() as $child) {
202 34
            $child->fill($this->value);
203
        }
204
205 38
        return $this->value;
206
    }
207
208
    /**
209
     * {@inheritdoc}
210
     */
211 6
    public function httpValue()
212
    {
213 6
        $http = [];
214
215 6
        foreach ($this->children as $child) {
216 6
            $http += $child->httpFields();
217
        }
218
219 6
        return $this->transformer->transformToHttp($http, $this);
220
    }
221
222
    /**
223
     * {@inheritdoc}
224
     */
225 143
    public function root(): RootElementInterface
226
    {
227 143
        if ($container = $this->container()) {
228 20
            return $container->parent()->root();
229
        }
230
231 141
        if ($this->root) {
232 92
            return $this->root;
233
        }
234
235 136
        return $this->root = new RootForm($this);
236
    }
237
238
    /**
239
     * {@inheritdoc}
240
     *
241
     * @return FormView
242
     */
243 8
    public function view(?HttpFieldPath $field = null): ElementViewInterface
244
    {
245 8
        $elements = [];
246
247 8
        foreach ($this->children as $child) {
248 8
            $elements[$child->name()] = $child->view($field);
249
        }
250
251 8
        return new FormView(self::class, $this->error->global(), $elements);
252
    }
253
254
    /**
255
     * {@inheritdoc}
256
     */
257 2
    public function getIterator(): Iterator
258
    {
259 2
        return $this->children->forwardIterator();
260
    }
261
262
    /**
263
     * {@inheritdoc}
264
     */
265 22
    public function offsetExists($offset): bool
266
    {
267 22
        return isset($this->children[$offset]);
268
    }
269
270
    /**
271
     * {@inheritdoc}
272
     */
273 67
    public function offsetGet($offset): ChildInterface
274
    {
275 67
        return $this->children[$offset];
276
    }
277
278
    /**
279
     * {@inheritdoc}
280
     */
281 1
    public function offsetSet($offset, $value): void
282
    {
283 1
        throw new BadMethodCallException(__CLASS__.' is immutable');
284
    }
285
286
    /**
287
     * {@inheritdoc}
288
     */
289 1
    public function offsetUnset($offset): void
290
    {
291 1
        throw new BadMethodCallException(__CLASS__.' is immutable');
292
    }
293
294
    /**
295
     * {@inheritdoc}
296
     */
297 27
    public function __clone()
298
    {
299 27
        $this->children = $this->children->duplicate($this);
300 27
    }
301
302
    /**
303
     * {@inheritdoc}
304
     */
305 4
    public function attach($entity): FormInterface
306
    {
307 4
        $this->generator->attach($entity);
308 4
        $this->value = null; // The value is only attached : it must be filled when calling value()
309
310 4
        return $this;
311
    }
312
313
    /**
314
     * Set the root element
315
     *
316
     * @param RootElementInterface $root
317
     *
318
     * @internal used by the builder
319
     */
320 5
    public function setRoot(RootElementInterface $root): void
321
    {
322 5
        $this->root = $root;
323 5
    }
324
325
    /**
326
     * Transform the submitted value
327
     * If the transformation fail the error will be set
328
     *
329
     * @param mixed $data
330
     *
331
     * @return mixed The transformed value
332
     */
333 75
    private function transformHttpValue($data)
334
    {
335
        try {
336 75
            $data = $this->transformer->transformFromHttp($data, $this);
337 3
        } catch (Exception $e) {
338 3
            $this->error = $this->validator->onTransformerException($e, $data, $this);
339 3
            $this->valid = $this->error->empty();
340
341
            // Reset children values
342 3
            foreach ($this->children->all() as $child) {
343 3
                $child->element()->import(null);
344
            }
345
346 3
            return null;
347
        }
348
349 72
        return $data;
350
    }
351
352
    /**
353
     * Submit the transformed http data to children and validate the value
354
     *
355
     * @param mixed $data Data to submit
356
     * @param string $method The submit method to call. Should be "submit" or "patch"
357
     */
358 76
    private function submitToChildrenAndValidate($data, string $method): void
359
    {
360 76
        if (!$this->submitToChildren($data, $method)) {
361 31
            return;
362
        }
363
364
        // Do not generate value if it's not necessary
365 56
        $this->error = $this->validator->hasConstraints()
366 6
            ? $this->validator->validate($this->value(), $this)
367 50
            : FormError::null()
368
        ;
369
370 56
        $this->valid = $this->error->empty();
371 56
    }
372
373
    /**
374
     * Submit the transformed http data to children
375
     *
376
     * @param mixed $data Data to submit
377
     * @param string $method The submit method to call. Should be "submit" or "patch"
378
     *
379
     * @return bool false on fail, or true on success
380
     */
381 76
    private function submitToChildren($data, string $method): bool
382
    {
383 76
        if (!$this->valid) {
384 1
            return false;
385
        }
386
387 75
        $errors = [];
388
389 75
        foreach ($this->children->reverseIterator() as $child) {
390 70
            if (!$child->$method($data)) {
391 30
                $this->valid = false;
392 30
                $errors[$child->name()] = $child->error();
393
            }
394
        }
395
396 75
        if (!$this->valid) {
397 30
            $this->error = FormError::aggregate($errors);
398
399 30
            return false;
400
        }
401
402 56
        return true;
403
    }
404
}
405