ChildBuilder::hydrator()   A
last analyzed

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
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 list<callable(RegistryInterface):\Bdf\Form\Filter\FilterInterface[]>
0 ignored issues
show
Bug introduced by
The type Bdf\Form\Child\list was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
80
     */
81
    private $filtersProviders = [];
82
83
    /**
84
     * @var class-string<ChildInterface>
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<ChildInterface> at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<ChildInterface>.
Loading history...
85
     */
86
    private $childClassName = Child::class;
87
88
    /**
89
     * @var list<callable(ChildParameters):void>
90
     */
91
    private $parametersConfigurators = [];
92
93
    /**
94
     * @var HttpFieldsInterface|null
95
     */
96
    private $fields;
97
98
    /**
99
     * @var ElementBuilderInterface
100
     * @psalm-var B
101
     */
102
    private $elementBuilder;
103
104
    /**
105
     * @var HydratorInterface|null
106
     */
107
    private $hydrator;
108
109
    /**
110
     * @var ExtractorInterface|null
111
     */
112
    private $extractor;
113
114
    /**
115
     * Add the trim filter
116
     *
117
     * @var bool
118
     */
119
    private $trim = true;
120
121
122
    /**
123
     * AbstractChildBuilder constructor.
124
     *
125
     * @param string $name The element name
126
     * @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...
127
     * @param RegistryInterface|null $registry
128
     */
129 174
    public function __construct(string $name, ElementBuilderInterface $elementBuilder, RegistryInterface $registry = null)
130
    {
131 174
        $this->name = $name;
132 174
        $this->elementBuilder = $elementBuilder;
133 174
        $this->registry = $registry ?: new Registry();
134 174
    }
135
136
    /**
137
     * {@inheritdoc}
138
     */
139 93
    final public function hydrator(HydratorInterface $hydrator)
140
    {
141 93
        $this->hydrator = $hydrator;
142
143 93
        return $this;
144
    }
145
146
    /**
147
     * {@inheritdoc}
148
     */
149 90
    final public function extractor(ExtractorInterface $extractor)
150
    {
151 90
        $this->extractor = $extractor;
152
153 90
        return $this;
154
    }
155
156
    /**
157
     * {@inheritdoc}
158
     */
159 3
    final public function filter($filter, bool $append = true)
160
    {
161 3
        if ($append === true) {
162 2
            $this->filters[] = $filter;
163
        } else {
164 1
            array_unshift($this->filters, $filter);
165
        }
166
167 3
        return $this;
168
    }
169
170
    /**
171
     * {@inheritdoc}
172
     */
173 69
    final public function default($default)
174
    {
175 69
        $this->default = $default;
176
177 69
        return $this;
178
    }
179
180
    /**
181
     * {@inheritdoc}
182
     */
183 11
    final public function depends(string ...$inputNames)
184
    {
185 11
        foreach ($inputNames as $inputName) {
186 11
            $this->viewDependencies[$inputName] = $inputName;
187
        }
188
189 11
        return $this;
190
    }
191
192
    /**
193
     * {@inheritdoc}
194
     */
195 2
    public function addParametersConfigurator(callable $configurator)
196
    {
197 2
        $this->parametersConfigurators[] = $configurator;
198
199 2
        return $this;
200
    }
201
202
    /**
203
     * {@inheritdoc}
204
     */
205 172
    final public function buildChild(): ChildInterface
206
    {
207 172
        $parameters = $this->buildParameters();
208
209 172
        foreach ($this->parametersConfigurators as $configurator) {
210 2
            $configurator($parameters);
211
        }
212
213 172
        if ($parameters->child === null) {
214 171
            $parameters->child = new $parameters->className(
215 171
                $this->name,
216 171
                $parameters->element,
217 171
                $parameters->fields,
218 171
                $parameters->filters,
219 171
                $parameters->defaultValue,
220 171
                $parameters->hydrator,
221 171
                $parameters->extractor,
222 171
                $parameters->dependencies,
223 171
                $parameters->modelTransformer
224
            );
225
        }
226
227 172
        foreach ($parameters->factories as $factory) {
228 1
            $parameters->child = $factory($parameters);
229
        }
230
231 172
        return $parameters->child;
232
    }
233
234
    /**
235
     * Define extractor using a getter
236
     * The getter is used by `import()` method, which get the value from the model to fill the form element value
237
     *
238
     * Prototypes :
239
     *   function getter(): this - Add a getter extractor, using the child name as property name
240
     *   function getter(string $propertyName): this - Add a getter extractor, using $propertyName as property name
241
     *   function getter(callable $transformer): this - Add a getter extractor, with a value transformer
242
     *   function getter(?string $propertyName, ?callable $transformer, ?callable $customAccessor): this - Add a getter extractor, with a value transformer and a custom accessor
243
     *
244
     * <code>
245
     * $builder->string('foo')->getter(); // import() from the "foo" property
246
     * $builder->string('foo')->getter('bar'); // import() from the "bar" property
247
     *
248
     * // import() from the "foo" property, and apply a transformer to the value
249
     * // First parameter is the model property value
250
     * // Second parameter is the current child instance
251
     * $builder->string('foo')->getter(function ($value, ChildInterface $input) {
252
     *     return $this->normalizeFoo($value);
253
     * });
254
     *
255
     * // Same as above, but use the "bar" property instead of "foo"
256
     * $builder->string('foo')->getter('bar', function ($value, ChildInterface $input) {
257
     *     return $this->normalizeFoo($value);
258
     * });
259
     *
260
     * // Define a custom accessor
261
     * // First parameter is the import()'ed entity
262
     * // Second is always null
263
     * // Third is the mode : always ExtractorInterface::EXTRACTION
264
     * // Fourth is the Getter instance
265
     * $builder->string('foo')->getter(null, null, function ($entity, $_, string $mode, Getter $getter) {
266
     *     return $entity->myCustomGetter();
267
     * });
268
     * </code>
269
     *
270
     * @param string|callable|null $propertyName The property name. If null use the child name.
271
     * @param callable|null $transformer The value transformer (transform model value to input value)
272
     * @param callable|null $customAccessor Custom getter function. If null use the symfony property accessor
273
     *
274
     * @return $this
275
     *
276
     * @see Getter
277
     * @see ChildBuilderInterface::extractor()
278
     * @see ChildInterface::import()
279
     */
280 81
    final public function getter($propertyName = null, ?callable $transformer = null, ?callable $customAccessor = null): self
281
    {
282 81
        return $this->extractor(new Getter($propertyName, $transformer, $customAccessor));
283
    }
284
285
    /**
286
     * Define hydrator using a setter
287
     * The setter is used by `fill()` method, which get the value from the element to fill the entity property
288
     *
289
     * Prototypes :
290
     *   function setter(): this - Add a setter hydrator, using the child name as property name
291
     *   function setter(string $propertyName): this - Add a setter hydrator, using $propertyName as property name
292
     *   function setter(callable $transformer): this - Add a setter hydrator, with a value transformer
293
     *   function setter(?string $propertyName, ?callable $transformer, ?callable $customAccessor): this - Add a setter hydrator, with a value transformer and a custom accessor
294
     *
295
     * <code>
296
     * $builder->string('foo')->setter(); // fill() the "foo" property
297
     * $builder->string('foo')->setter('bar'); // fill() the "bar" property
298
     *
299
     * // fill() the "foo" property, and apply a transformer to the value
300
     * // First parameter is the model property value
301
     * // Second parameter is the current child instance
302
     * $builder->string('foo')->setter(function ($value, ChildInterface $input) {
303
     *     return $this->parseFoo($value);
304
     * });
305
     *
306
     * // Same as above, but use the "bar" property instead of "foo"
307
     * $builder->string('foo')->setter('bar', function ($value, ChildInterface $input) {
308
     *     return $this->parseFoo($value);
309
     * });
310
     *
311
     * // Define a custom accessor
312
     * // First parameter is the fill()'ed entity
313
     * // Second is the element value
314
     * // Third is the mode : always ExtractorInterface::HYDRATION
315
     * // Fourth is the Setter instance
316
     * $builder->string('foo')->setter(null, null, function ($entity, $value, string $mode, Setter $setter) {
317
     *     return $entity->myCustomSetter($value);
318
     * });
319
     * </code>
320
     *
321
     * @param string|callable|null $propertyName The property name. If null use the child name.
322
     * @param callable|null $transformer The value transformer (transform input [i.e. http] value to model value)
323
     * @param callable|null $customAccessor Custom setter function. If null use the symfony property accessor
324
     *
325
     * @return $this
326
     *
327
     * @see Setter
328
     * @see ChildBuilderInterface::hydrator()
329
     * @see ChildInterface::fill()
330
     */
331 84
    final public function setter($propertyName = null, ?callable $transformer = null, ?callable $customAccessor = null): self
332
    {
333 84
        return $this->hydrator(new Setter($propertyName, $transformer, $customAccessor));
334
    }
335
336
    /**
337
     * Define the child class name
338
     *
339
     * @param class-string<ChildInterface> $factory The child class name
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<ChildInterface> at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<ChildInterface>.
Loading history...
340
     *
341
     * @return $this
342
     */
343 1
    final public function childClassName(string $factory): self
344
    {
345 1
        $this->childClassName = $factory;
346
347 1
        return $this;
348
    }
349
350
    /**
351
     * Define the child HTTP fields
352
     *
353
     * @param HttpFieldsInterface $fields
354
     *
355
     * @return $this
356
     */
357 7
    final public function httpFields(HttpFieldsInterface $fields): self
358
    {
359 7
        $this->fields = $fields;
360
361 7
        return $this;
362
    }
363
364
    /**
365
     * Define the child as a prefixed partition of the parent form
366
     *
367
     * Note: The child element must be an aggregation element, like an embedded form, or an array element
368
     *
369
     * <code>
370
     * // For HTTP value ['emb_foo' => 'xxx', 'emb_bar' => 'xxx']
371
     * $builder->embedded('emb', function ($builder) {
372
     *     $builder->string('foo');
373
     *     $builder->string('bar');
374
     * })->prefix();
375
     *
376
     * // For HTTP value ['efoo' => 'xxx', 'ebar' => 'xxx']
377
     * $builder->embedded('emb', function ($builder) {
378
     *     $builder->string('foo');
379
     *     $builder->string('bar');
380
     * })->prefix('e');
381
     * </code>
382
     *
383
     * @param string|null $prefix The HTTP fields prefix. If null, use the name followed by en underscore "_" as prefix.
384
     *                            The prefix may be an empty string for partitioning the parent form without prefixing embedded names
385
     *
386
     * @return $this
387
     *
388
     * @see PrefixedHttpFields
389
     */
390 6
    final public function prefix(?string $prefix = null): self
391
    {
392 6
        return $this->httpFields(new PrefixedHttpFields($prefix ?? $this->name.'_'));
393
    }
394
395
    /**
396
     * Enable or disable trim on input value
397
     *
398
     * Note: trim is active by default
399
     *
400
     * @param bool $active
401
     *
402
     * @return $this
403
     */
404 1
    final public function trim(bool $active = true): self
405
    {
406 1
        $this->trim = $active;
407
408 1
        return $this;
409
    }
410
411
    /**
412
     * Configure the element builder using a callback
413
     *
414
     * <code>
415
     * $builder->string('foo')->configure(function (StringElementBuilder $builder) {
416
     *     $builder->length(['min' => 3]);
417
     * });
418
     * </code>
419
     *
420
     * @param callable(B):void $configurator
421
     *
422
     * @return $this
423
     */
424 1
    final public function configure(callable $configurator): self
425
    {
426 1
        $configurator($this->elementBuilder);
427
428 1
        return $this;
429
    }
430
431
    /**
432
     * Forward call to element builder
433
     *
434
     * @param callable|TransformerInterface|DataTransformerInterface $transformer
435
     * @param bool $append
436
     * @return $this
437
     *
438
     * @see ElementBuilderInterface::transformer()
439
     */
440 2
    public function transformer($transformer, bool $append = true)
441
    {
442 2
        $this->elementBuilder->transformer($transformer, $append);
443
444 2
        return $this;
445
    }
446
447
    /**
448
     * {@inheritdoc}
449
     *
450
     * @return B
451
     */
452 104
    final protected function getElementBuilder(): ElementBuilderInterface
453
    {
454 104
        return $this->elementBuilder;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->elementBuilder returns the type Bdf\Form\ElementBuilderInterface which is incompatible with the documented return type Bdf\Form\Child\B.
Loading history...
455
    }
456
457
    /**
458
     * {@inheritdoc}
459
     */
460 173
    final protected function registry(): RegistryInterface
461
    {
462 173
        return $this->registry;
463
    }
464
465
    /**
466
     * Add a new filter provider
467
     * The filter provider permit to create a filter during the build of the element transformer
468
     * So the filter can be configured by the child builder
469
     *
470
     * @param callable(RegistryInterface):\Bdf\Form\Filter\FilterInterface[] $provider
471
     */
472 6
    final protected function addFilterProvider(callable $provider): void
473
    {
474 6
        $this->filtersProviders[] = $provider;
475 6
    }
476
477
    /**
478
     * Create the child parameters
479
     *
480
     * @return ChildParameters
481
     */
482 172
    private function buildParameters(): ChildParameters
483
    {
484 172
        $parameters = new ChildParameters();
485
486 172
        $parameters->name = $this->name;
487 172
        $parameters->hydrator = $this->hydrator;
488 172
        $parameters->extractor = $this->extractor;
489 172
        $parameters->dependencies = $this->viewDependencies;
490 172
        $parameters->modelTransformer = $this->buildTransformer();
491 172
        $parameters->className = $this->childClassName;
492
493 172
        $parameters->filters = $this->trim ? [TrimFilter::instance()] : [];
494
495 172
        foreach ($this->filters as $filter) {
496 3
            $parameters->filters[] = $this->registry->filter($filter);
497
        }
498
499 172
        foreach ($this->filtersProviders as $provider) {
500 5
            $parameters->filters = array_merge($parameters->filters, $provider($this->registry));
501
        }
502
503 172
        $parameters->fields = $this->fields ?: new ArrayOffsetHttpFields($this->name);
504 172
        $parameters->element = $this->elementBuilder->buildElement();
505
506
        // Apply element transformation to the default value
507 172
        if ($this->default !== null) {
508 4
            $lastValue = $parameters->element->value();
509 4
            $parameters->defaultValue = $parameters->element->import($this->default)->httpValue();
510 4
            $parameters->element->import($lastValue);
511
        } else {
512 168
            $parameters->defaultValue = null;
513
        }
514
515 172
        return $parameters;
516
    }
517
}
518