Passed
Push — master ( 779d84...3d5507 )
by Vincent
05:31
created

ChildBuilder   A

Complexity

Total Complexity 22

Size/Duplication

Total Lines 383
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 22
eloc 73
c 1
b 0
f 0
dl 0
loc 383
ccs 73
cts 73
cp 1
rs 10

15 Methods

Rating   Name   Duplication   Size   Complexity  
A prefix() 0 3 1
A __construct() 0 5 2
A default() 0 5 1
A setter() 0 3 1
A depends() 0 7 2
A getElementBuilder() 0 3 1
A buildChild() 0 43 5
A trim() 0 5 1
A httpFields() 0 5 1
A configure() 0 5 1
A extractor() 0 5 1
A childFactory() 0 5 1
A getter() 0 3 1
A hydrator() 0 5 1
A filter() 0 9 2
1
<?php
2
3
namespace Bdf\Form\Child;
4
5
use Bdf\Form\Child\Http\ArrayOffsetHttpFields;
6
use Bdf\Form\Child\Http\HttpFieldsInterface;
7
use Bdf\Form\Child\Http\PrefixedHttpFields;
8
use Bdf\Form\ElementBuilderInterface;
9
use Bdf\Form\Filter\TrimFilter;
10
use Bdf\Form\PropertyAccess\ExtractorInterface;
11
use Bdf\Form\PropertyAccess\Getter;
12
use Bdf\Form\PropertyAccess\HydratorInterface;
13
use Bdf\Form\PropertyAccess\Setter;
14
use Bdf\Form\Registry\Registry;
15
use Bdf\Form\Registry\RegistryInterface;
16
use Bdf\Form\Util\MagicCallForwarding;
17
18
/**
19
 * Base builder for a child
20
 * If a method cannot be found, it'll be delegate to the element builder
21
 *
22
 * <code>
23
 * $builder->add('element', MyElement::class)
24
 *     ->getter()->setter()
25
 *     ->default('foo')
26
 *     ->depends('bar')
27
 *     ->satisfy(new MyConstraint(['field' => 'bar']))
28
 * ;
29
 * </code>
30
 *
31
 * @mixin ElementBuilderInterface
32
 *
33
 * @method $this satisfy($constraint, $options = null, bool $append = true)
34
 * @method $this value($value)
35
 * @method $this transformer($transformer, bool $append = true)
36
 * @method $this required($options = null)
37
 *
38
 * @template B as ElementBuilderInterface
39
 * @implements ChildBuilderInterface<B>
40
 */
41
class ChildBuilder implements ChildBuilderInterface
42
{
43
    use MagicCallForwarding;
44
45
    /**
46
     * @var string
47
     */
48
    private $name;
49
50
    /**
51
     * @var RegistryInterface
52
     */
53
    private $registry;
54
55
    /**
56
     * The list of input dependencies
57
     *
58
     * @var string[]
59
     */
60
    private $viewDependencies = [];
61
62
    /**
63
     * @var mixed
64
     */
65
    private $default;
66
67
    /**
68
     * @var array
69
     */
70
    private $filters = [];
71
72
    /**
73
     * @var ChildCreationStrategyInterface|callable|class-string<ChildInterface>
0 ignored issues
show
Documentation Bug introduced by
The doc comment ChildCreationStrategyInt...-string<ChildInterface> at position 4 could not be parsed: Unknown type name 'class-string' at position 4 in ChildCreationStrategyInterface|callable|class-string<ChildInterface>.
Loading history...
74
     */
75
    private $factory = Child::class;
76
77
    /**
78
     * @var HttpFieldsInterface|null
79
     */
80
    private $fields;
81
82
    /**
83
     * @var ElementBuilderInterface
84
     * @psalm-var B
85
     */
86
    private $elementBuilder;
87
88
    /**
89
     * @var HydratorInterface|null
90
     */
91
    private $hydrator;
92
93
    /**
94
     * @var ExtractorInterface|null
95
     */
96
    private $extractor;
97
98
    /**
99
     * Add the trim filter
100
     *
101
     * @var bool
102
     */
103
    private $trim = true;
104
105
106
    /**
107
     * AbstractChildBuilder constructor.
108
     *
109
     * @param string $name The element name
110
     * @param B $elementBuilder
0 ignored issues
show
Bug introduced by
The type Bdf\Form\Child\B was not found. Did you mean B? If so, make sure to prefix the type with \.
Loading history...
111
     * @param RegistryInterface|null $registry
112
     */
113 144
    public function __construct(string $name, ElementBuilderInterface $elementBuilder, RegistryInterface $registry = null)
114
    {
115 144
        $this->name = $name;
116 144
        $this->elementBuilder = $elementBuilder;
117 144
        $this->registry = $registry ?: new Registry();
118 144
    }
119
120
    /**
121
     * {@inheritdoc}
122
     */
123 67
    final public function hydrator(HydratorInterface $hydrator)
124
    {
125 67
        $this->hydrator = $hydrator;
126
127 67
        return $this;
128
    }
129
130
    /**
131
     * {@inheritdoc}
132
     */
133 65
    final public function extractor(ExtractorInterface $extractor)
134
    {
135 65
        $this->extractor = $extractor;
136
137 65
        return $this;
138
    }
139
140
    /**
141
     * {@inheritdoc}
142
     */
143 3
    final public function filter($filter, bool $append = true)
144
    {
145 3
        if ($append === true) {
146 2
            $this->filters[] = $filter;
147
        } else {
148 1
            array_unshift($this->filters, $filter);
149
        }
150
151 3
        return $this;
152
    }
153
154
    /**
155
     * {@inheritdoc}
156
     */
157 65
    final public function default($default)
158
    {
159 65
        $this->default = $default;
160
161 65
        return $this;
162
    }
163
164
    /**
165
     * {@inheritdoc}
166
     */
167 10
    final public function depends(string ...$inputNames)
168
    {
169 10
        foreach ($inputNames as $inputName) {
170 10
            $this->viewDependencies[$inputName] = $inputName;
171
        }
172
173 10
        return $this;
174
    }
175
176
    /**
177
     * {@inheritdoc}
178
     */
179 143
    final public function buildChild(): ChildInterface
180
    {
181 143
        $filters = array_map([$this->registry, 'filter'], $this->filters);
182
183 143
        if ($this->trim) {
184 143
            $filters[] = new TrimFilter();
185
        }
186
187 143
        $fields = $this->fields ?: new ArrayOffsetHttpFields($this->name);
188 143
        $element = $this->elementBuilder->buildElement();
189
190
        // Apply element transformation to the default value
191 143
        if ($this->default !== null) {
192 4
            $lastValue = $element->value();
193 4
            $default = $element->import($this->default)->httpValue();
194 4
            $element->import($lastValue);
195
        } else {
196 139
            $default = null;
197
        }
198
199 143
        if (is_string($this->factory)) {
0 ignored issues
show
introduced by
The condition is_string($this->factory) is always true.
Loading history...
200
            /** @var ChildInterface */
201 142
            return new $this->factory(
202 142
                $this->name,
203 142
                $element,
204 142
                $fields,
205 142
                $filters,
206 142
                $default,
207 142
                $this->hydrator,
208 142
                $this->extractor,
209 142
                $this->viewDependencies
210
            );
211
        }
212
213 1
        return ($this->factory)(
214 1
            $this->name,
215 1
            $element,
216 1
            $fields,
217 1
            $filters,
218 1
            $default,
219 1
            $this->hydrator,
220 1
            $this->extractor,
221 1
            $this->viewDependencies
222
        );
223
    }
224
225
    /**
226
     * Define extractor using a getter
227
     * The getter is used by `import()` method, which get the value from the model to fill the form element value
228
     *
229
     * Prototypes :
230
     *   function getter(): this - Add a getter extractor, using the child name as property name
231
     *   function getter(string $propertyName): this - Add a getter extractor, using $propertyName as property name
232
     *   function getter(callable $transformer): this - Add a getter extractor, with a value transformer
233
     *   function getter(?string $propertyName, ?callable $transformer, ?callable $customAccessor): this - Add a getter extractor, with a value transformer and a custom accessor
234
     *
235
     * <code>
236
     * $builder->string('foo')->getter(); // import() from the "foo" property
237
     * $builder->string('foo')->getter('bar'); // import() from the "bar" property
238
     *
239
     * // import() from the "foo" property, and apply a transformer to the value
240
     * // First parameter is the model property value
241
     * // Second parameter is the current child instance
242
     * $builder->string('foo')->getter(function ($value, ChildInterface $input) {
243
     *     return $this->normalizeFoo($value);
244
     * });
245
     *
246
     * // Same as above, but use the "bar" property instead of "foo"
247
     * $builder->string('foo')->getter('bar', function ($value, ChildInterface $input) {
248
     *     return $this->normalizeFoo($value);
249
     * });
250
     *
251
     * // Define a custom accessor
252
     * // First parameter is the import()'ed entity
253
     * // Second is always null
254
     * // Third is the mode : always ExtractorInterface::EXTRACTION
255
     * // Fourth is the Getter instance
256
     * $builder->string('foo')->getter(null, null, function ($entity, $_, string $mode, Getter $getter) {
257
     *     return $entity->myCustomGetter();
258
     * });
259
     * </code>
260
     *
261
     * @param string|callable|null $propertyName The property name. If null use the child name.
262
     * @param callable|null $transformer The value transformer (transform model value to input value)
263
     * @param callable|null $customAccessor Custom getter function. If null use the symfony property accessor
264
     *
265
     * @return $this
266
     *
267
     * @see Getter
268
     * @see ChildBuilderInterface::extractor()
269
     * @see ChildInterface::import()
270
     */
271 56
    final public function getter($propertyName = null, ?callable $transformer = null, ?callable $customAccessor = null): self
272
    {
273 56
        return $this->extractor(new Getter($propertyName, $transformer, $customAccessor));
274
    }
275
276
    /**
277
     * Define hydrator using a setter
278
     * The setter is used by `fill()` method, which get the value from the element to fill the entity property
279
     *
280
     * Prototypes :
281
     *   function setter(): this - Add a setter hydrator, using the child name as property name
282
     *   function setter(string $propertyName): this - Add a setter hydrator, using $propertyName as property name
283
     *   function setter(callable $transformer): this - Add a setter hydrator, with a value transformer
284
     *   function setter(?string $propertyName, ?callable $transformer, ?callable $customAccessor): this - Add a setter hydrator, with a value transformer and a custom accessor
285
     *
286
     * <code>
287
     * $builder->string('foo')->setter(); // fill() the "foo" property
288
     * $builder->string('foo')->setter('bar'); // fill() the "bar" property
289
     *
290
     * // fill() the "foo" property, and apply a transformer to the value
291
     * // First parameter is the model property value
292
     * // Second parameter is the current child instance
293
     * $builder->string('foo')->setter(function ($value, ChildInterface $input) {
294
     *     return $this->parseFoo($value);
295
     * });
296
     *
297
     * // Same as above, but use the "bar" property instead of "foo"
298
     * $builder->string('foo')->setter('bar', function ($value, ChildInterface $input) {
299
     *     return $this->parseFoo($value);
300
     * });
301
     *
302
     * // Define a custom accessor
303
     * // First parameter is the fill()'ed entity
304
     * // Second is the element value
305
     * // Third is the mode : always ExtractorInterface::HYDRATION
306
     * // Fourth is the Setter instance
307
     * $builder->string('foo')->setter(null, null, function ($entity, $value, string $mode, Setter $setter) {
308
     *     return $entity->myCustomSetter($value);
309
     * });
310
     * </code>
311
     *
312
     * @param string|callable|null $propertyName The property name. If null use the child name.
313
     * @param callable|null $transformer The value transformer (transform input [i.e. http] value to model value)
314
     * @param callable|null $customAccessor Custom setter function. If null use the symfony property accessor
315
     *
316
     * @return $this
317
     *
318
     * @see Setter
319
     * @see ChildBuilderInterface::hydrator()
320
     * @see ChildInterface::fill()
321
     */
322 58
    final public function setter($propertyName = null, ?callable $transformer = null, ?callable $customAccessor = null): self
323
    {
324 58
        return $this->hydrator(new Setter($propertyName, $transformer, $customAccessor));
325
    }
326
327
    /**
328
     * {@inheritdoc}
329
     */
330 2
    final public function childFactory($factory): self
331
    {
332 2
        $this->factory = $factory;
333
334 2
        return $this;
335
    }
336
337
    /**
338
     * Define the child HTTP fields
339
     *
340
     * @param HttpFieldsInterface $fields
341
     *
342
     * @return $this
343
     */
344 7
    final public function httpFields(HttpFieldsInterface $fields): self
345
    {
346 7
        $this->fields = $fields;
347
348 7
        return $this;
349
    }
350
351
    /**
352
     * Define the child as a prefixed partition of the parent form
353
     *
354
     * Note: The child element must be an aggregation element, like an embedded form, or an array element
355
     *
356
     * <code>
357
     * // For HTTP value ['emb_foo' => 'xxx', 'emb_bar' => 'xxx']
358
     * $builder->embedded('emb', function ($builder) {
359
     *     $builder->string('foo');
360
     *     $builder->string('bar');
361
     * })->prefix();
362
     *
363
     * // For HTTP value ['efoo' => 'xxx', 'ebar' => 'xxx']
364
     * $builder->embedded('emb', function ($builder) {
365
     *     $builder->string('foo');
366
     *     $builder->string('bar');
367
     * })->prefix('e');
368
     * </code>
369
     *
370
     * @param string|null $prefix The HTTP fields prefix. If null, use the name followed by en underscore "_" as prefix.
371
     *                            The prefix may be an empty string for partitioning the parent form without prefixing embedded names
372
     *
373
     * @return $this
374
     *
375
     * @see PrefixedHttpFields
376
     */
377 6
    final public function prefix(?string $prefix = null): self
378
    {
379 6
        return $this->httpFields(new PrefixedHttpFields($prefix ?? $this->name.'_'));
380
    }
381
382
    /**
383
     * Enable or disable trim on input value
384
     *
385
     * Note: trim is active by default
386
     *
387
     * @param bool $active
388
     *
389
     * @return $this
390
     */
391 1
    final public function trim(bool $active = true): self
392
    {
393 1
        $this->trim = $active;
394
395 1
        return $this;
396
    }
397
398
    /**
399
     * Configure the element builder using a callback
400
     *
401
     * <code>
402
     * $builder->string('foo')->configure(function (StringElementBuilder $builder) {
403
     *     $builder->length(['min' => 3]);
404
     * });
405
     * </code>
406
     *
407
     * @param callable(B):void $configurator
408
     *
409
     * @return $this
410
     */
411 1
    final public function configure(callable $configurator): self
412
    {
413 1
        $configurator($this->elementBuilder);
414
415 1
        return $this;
416
    }
417
418
    /**
419
     * {@inheritdoc}
420
     */
421 87
    final protected function getElementBuilder(): ElementBuilderInterface
422
    {
423 87
        return $this->elementBuilder;
424
    }
425
}
426