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

FormBuilder::optional()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 2
c 0
b 0
f 0
dl 0
loc 5
ccs 3
cts 3
cp 1
rs 10
cc 1
nc 1
nop 1
crap 1
1
<?php
2
3
namespace Bdf\Form\Aggregate;
4
5
use Bdf\Form\AbstractElementBuilder;
6
use Bdf\Form\Aggregate\Collection\ChildrenCollection;
7
use Bdf\Form\Aggregate\Value\ValueGenerator;
8
use Bdf\Form\Aggregate\Value\ValueGeneratorInterface;
9
use Bdf\Form\Button\ButtonBuilderInterface;
10
use Bdf\Form\Child\ChildBuilder;
11
use Bdf\Form\Child\ChildBuilderInterface;
12
use Bdf\Form\Csrf\CsrfElement;
13
use Bdf\Form\Csrf\CsrfElementBuilder;
14
use Bdf\Form\Custom\CustomForm;
15
use Bdf\Form\ElementBuilderInterface;
16
use Bdf\Form\ElementInterface;
17
use Bdf\Form\Leaf\BooleanElement;
18
use Bdf\Form\Leaf\BooleanElementBuilder;
19
use Bdf\Form\Leaf\Date\DateTimeChildBuilder;
20
use Bdf\Form\Leaf\Date\DateTimeElement;
21
use Bdf\Form\Leaf\Date\DateTimeElementBuilder;
22
use Bdf\Form\Leaf\FloatElement;
23
use Bdf\Form\Leaf\FloatElementBuilder;
24
use Bdf\Form\Leaf\Helper\EmailElement;
25
use Bdf\Form\Leaf\Helper\UrlElement;
26
use Bdf\Form\Leaf\IntegerElement;
27
use Bdf\Form\Leaf\IntegerElementBuilder;
28
use Bdf\Form\Leaf\StringElement;
29
use Bdf\Form\Leaf\StringElementBuilder;
30
use Bdf\Form\Phone\PhoneChildBuilder;
31
use Bdf\Form\Phone\PhoneElement;
32
use Bdf\Form\Phone\PhoneElementBuilder;
33
use Bdf\Form\Registry\RegistryInterface;
34
use Bdf\Form\RootElementInterface;
35
use Bdf\Form\Transformer\TransformerInterface;
36
use Bdf\Form\Validator\ValueValidatorInterface;
37
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
38
use Symfony\Component\Validator\Validator\ValidatorInterface;
39
40
/**
41
 * Builder for a form
42
 *
43
 * <code>
44
 * // Get the builder
45
 * $builder = $registry->elementBuilder(Form::class);
46
 * $builder->generates(MyEntity::class); // Define the generated entity
47
 *
48
 * // Declare fields
49
 * $builder->string('foo')->required()->setter();
50
 * $builder->integer('bar')->min(11)->required()->setter();
51
 *
52
 * // Build the form
53
 * $form = $builder->buildElement();
54
 *
55
 * // Submit and validate http data
56
 * if (!$form->submit($request->post())->valid()) {
57
 *     throw new FormError();
58
 * }
59
 *
60
 * // Get the generated entity, and save it
61
 * $repository->save($form->value());
62
 * </code>
63
 *
64
 * @see Form
65
 * @see CustomForm::configure()
66
 */
67
class FormBuilder extends AbstractElementBuilder implements FormBuilderInterface
68
{
69
    /**
70
     * @var array<non-empty-string, ChildBuilder>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<non-empty-string, ChildBuilder> at position 2 could not be parsed: Unknown type name 'non-empty-string' at position 2 in array<non-empty-string, ChildBuilder>.
Loading history...
71
     */
72
    private $children = [];
73
74
    /**
75
     * @var array<non-empty-string, ButtonBuilderInterface>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<non-empty-string, ButtonBuilderInterface> at position 2 could not be parsed: Unknown type name 'non-empty-string' at position 2 in array<non-empty-string, ButtonBuilderInterface>.
Loading history...
76
     */
77
    private $buttons = [];
78
79
    /**
80
     * @var PropertyAccessorInterface|null
81
     */
82
    private $propertyAccessor;
83
84
    /**
85
     * @var ValidatorInterface|null
86
     */
87
    private $validator;
88
89
    /**
90
     * @var ValueGeneratorInterface|null
91
     */
92
    private $generator;
93
94
    /**
95
     * @var bool
96
     */
97
    private $optional = false;
98
99
100
    /**
101
     * FormBuilder constructor.
102
     *
103
     * @param RegistryInterface|null $registry
104
     */
105 94
    public function __construct(?RegistryInterface $registry = null)
106
    {
107 94
        parent::__construct($registry);
108 94
    }
109
110
    /**
111
     * {@inheritdoc}
112
     *
113
     * @psalm-param non-empty-string $name
114
     * @psalm-param class-string<E> $element
115
     *
116
     * @template E as ElementInterface
117
     *
118
     * @psalm-return ChildBuilder<ElementBuilderInterface<E>>
119
     *
120
     * @psalm-suppress MoreSpecificReturnType
121
     * @psalm-suppress LessSpecificReturnStatement
122
     * @psalm-suppress PropertyTypeCoercion
123
     */
124 87
    public function add(string $name, string $element): ChildBuilderInterface
125
    {
126 87
        return $this->children[$name] = $this->registry()->childBuilder($element, $name);
127
    }
128
129
    /**
130
     * {@inheritdoc}
131
     *
132
     * @psalm-param non-empty-string $name
133
     * @psalm-return ChildBuilder<StringElementBuilder>
134
     *
135
     * @psalm-suppress MoreSpecificReturnType
136
     * @psalm-suppress LessSpecificReturnStatement
137
     */
138 56
    public function string(string $name, ?string $default = null): ChildBuilderInterface
139
    {
140 56
        return $this->add($name, StringElement::class)->default($default);
141
    }
142
143
    /**
144
     * {@inheritdoc}
145
     *
146
     * @psalm-param non-empty-string $name
147
     * @psalm-return ChildBuilder<IntegerElementBuilder>
148
     *
149
     * @psalm-suppress MoreSpecificReturnType
150
     * @psalm-suppress LessSpecificReturnStatement
151
     */
152 37
    public function integer(string $name, ?int $default = null): ChildBuilderInterface
153
    {
154 37
        return $this->add($name, IntegerElement::class)->default($default);
155
    }
156
157
    /**
158
     * {@inheritdoc}
159
     *
160
     * @psalm-param non-empty-string $name
161
     * @psalm-return ChildBuilder<FloatElementBuilder>
162
     *
163
     * @psalm-suppress MoreSpecificReturnType
164
     * @psalm-suppress LessSpecificReturnStatement
165
     */
166 1
    public function float(string $name, ?float $default = null): ChildBuilderInterface
167
    {
168 1
        return $this->add($name, FloatElement::class)->default($default);
169
    }
170
171
    /**
172
     * {@inheritdoc}
173
     *
174
     * @psalm-param non-empty-string $name
175
     * @psalm-return ChildBuilder<BooleanElementBuilder>
176
     *
177
     * @psalm-suppress MoreSpecificReturnType
178
     * @psalm-suppress LessSpecificReturnStatement
179
     */
180 1
    public function boolean(string $name): ChildBuilderInterface
181
    {
182 1
        return $this->add($name, BooleanElement::class);
183
    }
184
185
    /**
186
     * {@inheritdoc}
187
     *
188
     * @psalm-param non-empty-string $name
189
     * @psalm-return DateTimeChildBuilder&ChildBuilder<DateTimeElementBuilder>
190
     *
191
     * @psalm-suppress MoreSpecificReturnType
192
     * @psalm-suppress LessSpecificReturnStatement
193
     */
194 4
    public function dateTime(string $name): ChildBuilderInterface
195
    {
196 4
        return $this->add($name, DateTimeElement::class);
197
    }
198
199
    /**
200
     * {@inheritdoc}
201
     *
202
     * @psalm-param non-empty-string $name
203
     * @psalm-return PhoneChildBuilder&ChildBuilder<PhoneElementBuilder>
204
     *
205
     * @psalm-suppress MoreSpecificReturnType
206
     * @psalm-suppress LessSpecificReturnStatement
207
     */
208 8
    public function phone(string $name): ChildBuilderInterface
209
    {
210 8
        return $this->add($name, PhoneElement::class);
211
    }
212
213
    /**
214
     * {@inheritdoc}
215
     *
216
     * @psalm-param non-empty-string $name
217
     * @psalm-return ChildBuilder<CsrfElementBuilder>
218
     *
219
     * @psalm-suppress MoreSpecificReturnType
220
     * @psalm-suppress LessSpecificReturnStatement
221
     */
222 1
    public function csrf(string $name = '_token'): ChildBuilderInterface
223
    {
224 1
        return $this->add($name, CsrfElement::class);
225
    }
226
227
    /**
228
     * Add a new email element
229
     * Note: The email element is a simple string but with the email constraint
230
     *
231
     * <code>
232
     * $builder->email('contact')
233
     *     ->message('Invalid contact email')
234
     * ;
235
     * </code>
236
     *
237
     * @param non-empty-string $name The name of the input
0 ignored issues
show
Documentation Bug introduced by
The doc comment non-empty-string at position 0 could not be parsed: Unknown type name 'non-empty-string' at position 0 in non-empty-string.
Loading history...
238
     *
239
     * @return ChildBuilder|\Bdf\Form\Leaf\Helper\EmailElementBuilder
240
     * @psalm-return ChildBuilder<\Bdf\Form\Leaf\Helper\EmailElementBuilder>
241
     *
242
     * @psalm-suppress MoreSpecificReturnType
243
     * @psalm-suppress LessSpecificReturnStatement
244
     */
245 1
    public function email(string $name): ChildBuilderInterface
246
    {
247 1
        return $this->add($name, EmailElement::class);
248
    }
249
250
    /**
251
     * Add a new url element
252
     * Note: The url element is a simple string but with the url constraint
253
     *
254
     * <code>
255
     * $builder->url('home')->protocols('https');
256
     * </code>
257
     *
258
     * @param non-empty-string $name The name of the input
0 ignored issues
show
Documentation Bug introduced by
The doc comment non-empty-string at position 0 could not be parsed: Unknown type name 'non-empty-string' at position 0 in non-empty-string.
Loading history...
259
     *
260
     * @return ChildBuilder|\Bdf\Form\Leaf\Helper\UrlElementBuilder
261
     * @psalm-return ChildBuilder<\Bdf\Form\Leaf\Helper\UrlElementBuilder>
262
     *
263
     * @psalm-suppress MoreSpecificReturnType
264
     * @psalm-suppress LessSpecificReturnStatement
265
     */
266 1
    public function url(string $name): ChildBuilderInterface
267
    {
268 1
        return $this->add($name, UrlElement::class);
269
    }
270
271
    /**
272
     * {@inheritdoc}
273
     *
274
     * @psalm-param non-empty-string $name
275
     * @psalm-param callable|null $configurator
276
     * @psalm-return ChildBuilder<FormBuilder>
277
     *
278
     * @psalm-suppress MoreSpecificReturnType
279
     * @psalm-suppress LessSpecificReturnStatement
280
     */
281 13
    public function embedded(string $name, ?callable $configurator = null): ChildBuilderInterface
282
    {
283 13
        $builder = $this->add($name, Form::class);
284
285 13
        if ($configurator) {
286 12
            $configurator($builder);
287
        }
288
289 13
        return $builder;
290
    }
291
292
    /**
293
     * {@inheritdoc}
294
     *
295
     * @psalm-param non-empty-string $name
296
     * @psalm-param class-string<ElementInterface>|null $elementType
297
     * @psalm-param callable|null $elementConfigurator
298
     * @psalm-return ArrayChildBuilder&ChildBuilderInterface<ArrayElementBuilder>
299
     *
300
     * @psalm-suppress MoreSpecificReturnType
301
     * @psalm-suppress LessSpecificReturnStatement
302
     */
303 4
    public function array(string $name, ?string $elementType = null, ?callable $elementConfigurator = null): ChildBuilderInterface
304
    {
305
        /** @var ChildBuilderInterface<ArrayElementBuilder>&ArrayChildBuilder $builder */
306 4
        $builder = $this->add($name, ArrayElement::class);
307
308 4
        if ($elementType) {
309 1
            $builder->element($elementType, $elementConfigurator);
0 ignored issues
show
Bug introduced by
The method element() does not exist on Bdf\Form\Child\ChildBuilderInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Bdf\Form\Child\ChildBuilderInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

309
            $builder->/** @scrutinizer ignore-call */ 
310
                      element($elementType, $elementConfigurator);
Loading history...
310
        }
311
312 4
        return $builder;
313
    }
314
315
    /**
316
     * {@inheritdoc}
317
     */
318 3
    public function submit(string $name): ButtonBuilderInterface
319
    {
320 3
        return $this->buttons[$name] = $this->registry()->buttonBuilder($name);
321
    }
322
323
    /**
324
     * {@inheritdoc}
325
     */
326 1
    public function propertyAccessor(PropertyAccessorInterface $propertyAccessor): FormBuilderInterface
327
    {
328 1
        $this->propertyAccessor = $propertyAccessor;
329
330 1
        return $this;
331
    }
332
333
    /**
334
     * {@inheritdoc}
335
     */
336 1
    public function validator(ValidatorInterface $validator): FormBuilderInterface
337
    {
338 1
        $this->validator = $validator;
339
340 1
        return $this;
341
    }
342
343
    /**
344
     * {@inheritdoc}
345
     */
346 16
    public function generator(ValueGeneratorInterface $generator): FormBuilderInterface
347
    {
348 16
        $this->generator = $generator;
349
350 16
        return $this;
351
    }
352
353
    /**
354
     * {@inheritdoc}
355
     */
356 16
    public function generates($entity): FormBuilderInterface
357
    {
358 16
        return $this->generator(new ValueGenerator($entity));
359
    }
360
361
    /**
362
     * {@inheritdoc}
363
     */
364 3
    public function optional(bool $flag = true): FormBuilderInterface
365
    {
366 3
        $this->optional = $flag;
367
368 3
        return $this;
369
    }
370
371
    /**
372
     * {@inheritdoc}
373
     */
374 93
    final protected function createElement(ValueValidatorInterface $validator, TransformerInterface $transformer): ElementInterface
375
    {
376 93
        $children = new ChildrenCollection();
377
378 93
        foreach ($this->children as $child) {
379 87
            $children->add($child->buildChild());
380
        }
381
382 93
        $form = new Form($children, $validator, $transformer, $this->generator, $this->optional);
383
384
        // The root form is configured by the builder : set into the form
385 93
        if ($this->hasRootFormConfiguration()) {
386 5
            $form->setRoot($this->buildRootForm($form));
387
        }
388
389 93
        return $form;
390
    }
391
392
    /**
393
     * Check if there is at least one attribute of the root form that is configured by the builder
394
     *
395
     * @return bool
396
     */
397 93
    private function hasRootFormConfiguration(): bool
398
    {
399 93
        return $this->buttons || $this->validator || $this->propertyAccessor;
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->buttons of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
400
    }
401
402
    /**
403
     * Build the root form
404
     *
405
     * @param Form $form
406
     *
407
     * @return RootElementInterface
408
     */
409 5
    private function buildRootForm(Form $form): RootElementInterface
410
    {
411 5
        $buttons = [];
412
413 5
        foreach ($this->buttons as $name => $button) {
414 3
            $buttons[$name] = $button->buildButton();
415
        }
416
417 5
        return new RootForm($form, $buttons, $this->propertyAccessor, $this->validator);
418
    }
419
}
420