Passed
Push — master ( 5b91c4...ecf04c )
by Vincent
11:13
created

ChildBuilder::getElementBuilder()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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