Passed
Push — master ( 2c8dca...5bdfe8 )
by Vincent
04:30
created

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