Passed
Push — master ( 719d42...a49ea7 )
by Vincent
05:10
created

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