Passed
Branch master (3daac1)
by Vincent
07:53
created

Form   A

Complexity

Total Complexity 38

Size/Duplication

Total Lines 321
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 38
eloc 79
c 1
b 0
f 0
dl 0
loc 321
ccs 94
cts 94
cp 1
rs 9.36

21 Methods

Rating   Name   Duplication   Size   Complexity  
A submitToChildren() 0 22 5
A getIterator() 0 3 1
A root() 0 11 3
A valid() 0 3 1
A submitToChildrenAndValidate() 0 8 2
A attach() 0 5 1
A __construct() 0 7 4
A submit() 0 8 1
A offsetUnset() 0 3 1
A __clone() 0 3 1
A import() 0 9 2
A offsetGet() 0 3 1
A offsetExists() 0 3 1
A setRoot() 0 3 1
A transformHttpValue() 0 17 3
A patch() 0 11 2
A view() 0 9 2
A value() 0 9 2
A offsetSet() 0 3 1
A error() 0 3 1
A httpValue() 0 9 2
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\NullValueValidator;
19
use Bdf\Form\Validator\ValueValidatorInterface;
20
use Bdf\Form\View\ElementViewInterface;
21
use Exception;
22
23
/**
24
 * The base form element
25
 * A form is an static aggregate of elements, unlike ArrayElement which is a dynamic aggregate
26
 *
27
 * The form will submit HTTP value to all it's children, and then perform it's validation process (if defined)
28
 * A form cannot have a "global" error if there is at least one child on error
29
 *
30
 * To access to children elements, use array access : `$form['child']->element()`
31
 * Note: The return value of array access is a ChildInterface. Use `ChildInterface::element()` to get the element
32
 *
33
 * <code>
34
 * // Show form view
35
 * $view = $form->import($entity)->view();
36
 *
37
 * echo $view['foo']->id('foo')->class('form-control');
38
 * echo $view['bar']->id('bar')->class('form-control');
39
 *
40
 * // Submit form
41
 * if (!$form->submit($request->post())->valid()) {
42
 *     throw new ApiException($form->error()->print(new ApiErrorPrinter()));
43
 * }
44
 *
45
 * $entity = $form->attach($entity)->value();
46
 * </code>
47
 */
48
final class Form implements FormInterface
49
{
50
    use ContainerTrait;
51
52
    /**
53
     * @var ValueValidatorInterface
54
     */
55
    private $validator;
56
57
    /**
58
     * Transformer to view value
59
     *
60
     * @var TransformerInterface
61
     */
62
    private $transformer;
63
64
    /**
65
     * @var ChildrenCollectionInterface
66
     */
67
    private $children;
68
69
    /**
70
     * @var ValueGeneratorInterface
71
     */
72
    private $generator;
73
74
    /**
75
     * @var RootElementInterface|null
76
     */
77
    private $root;
78
79
    /**
80
     * @var FormError
81
     */
82
    private $error;
83
84
    /**
85
     * @var bool
86
     */
87
    private $valid = false;
88
89
90
    /**
91
     * Form constructor.
92
     *
93
     * @param ChildrenCollectionInterface $children
94
     * @param ValueValidatorInterface|null $validator
95
     * @param TransformerInterface|null $transformer
96
     * @param ValueGeneratorInterface|null $generator
97
     */
98 200
    public function __construct(ChildrenCollectionInterface $children, ?ValueValidatorInterface $validator = null, ?TransformerInterface $transformer = null, ?ValueGeneratorInterface $generator = null)
99
    {
100 200
        $this->children = $children->duplicate($this);
101 200
        $this->validator = $validator ?: NullValueValidator::instance();
102 200
        $this->transformer = $transformer ?: NullTransformer::instance();
103 200
        $this->error = FormError::null();
104 200
        $this->generator = $generator ?: new ValueGenerator();
105 200
    }
106
107
    /**
108
     * {@inheritdoc}
109
     */
110 52
    public function submit($data): ElementInterface
111
    {
112 52
        $this->valid = true;
113 52
        $data = $this->transformHttpValue($data);
114
115 52
        $this->submitToChildrenAndValidate($data, 'submit');
116
117 52
        return $this;
118
    }
119
120
    /**
121
     * {@inheritdoc}
122
     */
123 9
    public function patch($data): ElementInterface
124
    {
125 9
        $this->valid = true;
126
127 9
        if ($data !== null) {
128 8
            $data = $this->transformHttpValue($data);
129
        }
130
131 9
        $this->submitToChildrenAndValidate($data, 'patch');
132
133 9
        return $this;
134
    }
135
136
    /**
137
     * {@inheritdoc}
138
     */
139 38
    public function valid(): bool
140
    {
141 38
        return $this->valid;
142
    }
143
144
    /**
145
     * {@inheritdoc}
146
     */
147 22
    public function error(): FormError
148
    {
149 22
        return $this->error;
150
    }
151
152
    /**
153
     * {@inheritdoc}
154
     */
155 12
    public function import($entity): ElementInterface
156
    {
157 12
        $this->generator->attach($entity);
158
159 12
        foreach ($this->children as $child) {
160 12
            $child->import($entity);
161
        }
162
163 12
        return $this;
164
    }
165
166
    /**
167
     * {@inheritdoc}
168
     */
169 54
    public function value()
170
    {
171 54
        $value = $this->generator->generate($this);
172
173 54
        foreach ($this->children as $child) {
174 50
            $child->fill($value);
175
        }
176
177 54
        return $value;
178
    }
179
180
    /**
181
     * {@inheritdoc}
182
     */
183 4
    public function httpValue()
184
    {
185 4
        $http = [];
186
187 4
        foreach ($this->children as $child) {
188 4
            $http += $child->httpFields();
189
        }
190
191 4
        return $this->transformer->transformToHttp($http, $this);
192
    }
193
194
    /**
195
     * {@inheritdoc}
196
     */
197 107
    public function root(): RootElementInterface
198
    {
199 107
        if ($this->container) {
200 16
            return $this->container->parent()->root();
201
        }
202
203 105
        if ($this->root) {
204 71
            return $this->root;
205
        }
206
207 100
        return $this->root = new RootForm($this);
208
    }
209
210
    /**
211
     * {@inheritdoc}
212
     *
213
     * @return FormView
214
     */
215 7
    public function view(?HttpFieldPath $field = null): ElementViewInterface
216
    {
217 7
        $elements = [];
218
219 7
        foreach ($this->children as $child) {
220 7
            $elements[$child->name()] = $child->view($field);
221
        }
222
223 7
        return new FormView(self::class, $this->error->global(), $elements);
224
    }
225
226
    /**
227
     * {@inheritdoc}
228
     */
229 2
    public function getIterator()
230
    {
231 2
        return $this->children->forwardIterator();
232
    }
233
234
    /**
235
     * {@inheritdoc}
236
     */
237 20
    public function offsetExists($offset): bool
238
    {
239 20
        return isset($this->children[$offset]);
240
    }
241
242
    /**
243
     * {@inheritdoc}
244
     */
245 54
    public function offsetGet($offset): ChildInterface
246
    {
247 54
        return $this->children[$offset];
248
    }
249
250
    /**
251
     * {@inheritdoc}
252
     */
253 1
    public function offsetSet($offset, $value)
254
    {
255 1
        throw new BadMethodCallException(__CLASS__.' is immutable');
256
    }
257
258
    /**
259
     * {@inheritdoc}
260
     */
261 1
    public function offsetUnset($offset)
262
    {
263 1
        throw new BadMethodCallException(__CLASS__.' is immutable');
264
    }
265
266
    /**
267
     * {@inheritdoc}
268
     */
269 23
    public function __clone()
270
    {
271 23
        $this->children = $this->children->duplicate($this);
272 23
    }
273
274
    /**
275
     * {@inheritdoc}
276
     */
277 2
    public function attach($entity): FormInterface
278
    {
279 2
        $this->generator->attach($entity);
280
281 2
        return $this;
282
    }
283
284
    /**
285
     * Set the root element
286
     *
287
     * @param RootElementInterface $root
288
     *
289
     * @internal used by the builder
290
     */
291 5
    public function setRoot(RootElementInterface $root): void
292
    {
293 5
        $this->root = $root;
294 5
    }
295
296
    /**
297
     * Transform the submitted value
298
     * If the transformation fail the error will be set
299
     *
300
     * @param mixed $data
301
     *
302
     * @return mixed The transformed value
303
     */
304 58
    private function transformHttpValue($data)
305
    {
306
        try {
307 58
            $data = $this->transformer->transformFromHttp($data, $this);
308 1
        } catch (Exception $e) {
309 1
            $this->error = FormError::message($e->getMessage(), 'TRANSFORM_ERROR');
310 1
            $this->valid = false;
311
312
            // Reset children values
313 1
            foreach ($this->children->all() as $child) {
314 1
                $child->element()->import(null);
315
            }
316
317 1
            return null;
318
        }
319
320 57
        return $data;
321
    }
322
323
    /**
324
     * Submit the transformed http data to children and validate the value
325
     *
326
     * @param mixed $data Data to submit
327
     * @param string $method The submit method to call. Should be "submit" or "patch"
328
     */
329 59
    private function submitToChildrenAndValidate($data, string $method): void
330
    {
331 59
        if (!$this->submitToChildren($data, $method)) {
332 23
            return;
333
        }
334
335 46
        $this->error = $this->validator->validate($this->value(), $this);
336 46
        $this->valid = $this->error->empty();
337 46
    }
338
339
    /**
340
     * Submit the transformed http data to children
341
     *
342
     * @param mixed $data Data to submit
343
     * @param string $method The submit method to call. Should be "submit" or "patch"
344
     *
345
     * @return bool false on fail, or true on success
346
     */
347 59
    private function submitToChildren($data, string $method): bool
348
    {
349 59
        if (!$this->valid) {
350 1
            return false;
351
        }
352
353 58
        $errors = [];
354
355 58
        foreach ($this->children->reverseIterator() as $child) {
356 54
            if (!$child->$method($data)) {
357 22
                $this->valid = false;
358 22
                $errors[$child->name()] = $child->error();
359
            }
360
        }
361
362 58
        if (!$this->valid) {
363 22
            $this->error = FormError::aggregate($errors);
364
365 22
            return false;
366
        }
367
368 46
        return true;
369
    }
370
}
371