Passed
Branch master (3daac1)
by Vincent
07:53
created

ChildBuilder::__construct()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 3
c 1
b 0
f 0
dl 0
loc 5
ccs 4
cts 4
cp 1
rs 10
cc 2
nc 1
nop 3
crap 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
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
class ChildBuilder implements ChildBuilderInterface
38
{
39
    /**
40
     * @var string
41
     */
42
    private $name;
43
44
    /**
45
     * @var RegistryInterface
46
     */
47
    private $registry;
48
49
    /**
50
     * The list of input dependencies
51
     *
52
     * @var string[]
53
     */
54
    private $viewDependencies = [];
55
56
    /**
57
     * @var mixed
58
     */
59
    private $default;
60
61
    /**
62
     * @var array
63
     */
64
    private $filters = [];
65
66
    /**
67
     * @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...
68
     */
69
    private $factory = Child::class;
70
71
    /**
72
     * @var HttpFieldsInterface|null
73
     */
74
    private $fields;
75
76
    /**
77
     * @var ElementBuilderInterface
78
     */
79
    private $elementBuilder;
80
81
    /**
82
     * @var HydratorInterface|null
83
     */
84
    private $hydrator;
85
86
    /**
87
     * @var ExtractorInterface|null
88
     */
89
    private $extractor;
90
91
    /**
92
     * Add the trim filter
93
     *
94
     * @var bool
95
     */
96
    private $trim = true;
97
98
99
    /**
100
     * AbstractChildBuilder constructor.
101
     *
102
     * @param string $name The element name
103
     * @param ElementBuilderInterface $elementBuilder
104
     * @param RegistryInterface|null $registry
105
     */
106 132
    public function __construct(string $name, ElementBuilderInterface $elementBuilder, RegistryInterface $registry = null)
107
    {
108 132
        $this->name = $name;
109 132
        $this->elementBuilder = $elementBuilder;
110 132
        $this->registry = $registry ?: new Registry();
111 132
    }
112
113
    /**
114
     * {@inheritdoc}
115
     */
116 62
    final public function hydrator(HydratorInterface $hydrator)
117
    {
118 62
        $this->hydrator = $hydrator;
119
120 62
        return $this;
121
    }
122
123
    /**
124
     * {@inheritdoc}
125
     */
126 60
    final public function extractor(ExtractorInterface $extractor)
127
    {
128 60
        $this->extractor = $extractor;
129
130 60
        return $this;
131
    }
132
133
    /**
134
     * {@inheritdoc}
135
     */
136 3
    final public function filter($filter, bool $append = true)
137
    {
138 3
        if ($append === true) {
139 2
            $this->filters[] = $filter;
140
        } else {
141 1
            array_unshift($this->filters, $filter);
142
        }
143
144 3
        return $this;
145
    }
146
147
    /**
148
     * {@inheritdoc}
149
     */
150 61
    final public function default($default)
151
    {
152 61
        $this->default = $default;
153
154 61
        return $this;
155
    }
156
157
    /**
158
     * {@inheritdoc}
159
     */
160 10
    final public function depends(string ...$inputNames)
161
    {
162 10
        foreach ($inputNames as $inputName) {
163 10
            $this->viewDependencies[$inputName] = $inputName;
164
        }
165
166 10
        return $this;
167
    }
168
169
    /**
170
     * {@inheritdoc}
171
     */
172 132
    final public function buildChild(): ChildInterface
173
    {
174 132
        $filters = array_map([$this->registry, 'filter'], $this->filters);
175
176 132
        if ($this->trim) {
177 132
            $filters[] = new TrimFilter();
178
        }
179
180 132
        $fields = $this->fields ?: new ArrayOffsetHttpFields($this->name);
181 132
        $element = $this->elementBuilder->buildElement();
182
183
        // Apply element transformation to the default value
184 132
        if ($this->default !== null) {
185 4
            $lastValue = $element->value();
186 4
            $default = $element->import($this->default)->httpValue();
187 4
            $element->import($lastValue);
188
        } else {
189 128
            $default = null;
190
        }
191
192 132
        if (is_string($this->factory)) {
0 ignored issues
show
introduced by
The condition is_string($this->factory) is always true.
Loading history...
193
            /** @var ChildInterface */
194 131
            return new $this->factory(
195 131
                $this->name,
196 131
                $element,
197 131
                $fields,
198 131
                $filters,
199 131
                $default,
200 131
                $this->hydrator,
201 131
                $this->extractor,
202 131
                $this->viewDependencies
203
            );
204
        }
205
206 1
        return ($this->factory)(
207 1
            $this->name,
208 1
            $element,
209 1
            $fields,
210 1
            $filters,
211 1
            $default,
212 1
            $this->hydrator,
213 1
            $this->extractor,
214 1
            $this->viewDependencies
215
        );
216
    }
217
218
    /**
219
     * Define extractor using a getter
220
     * The getter is used by `import()` method, which get the value from the model to fill the form element value
221
     *
222
     * Prototypes :
223
     *   function getter(): this - Add a getter extractor, using the child name as property name
224
     *   function getter(string $propertyName): this - Add a getter extractor, using $propertyName as property name
225
     *   function getter(callable $transformer): this - Add a getter extractor, with a value transformer
226
     *   function getter(?string $propertyName, ?callable $transformer, ?callable $customAccessor): this - Add a getter extractor, with a value transformer and a custom accessor
227
     *
228
     * <code>
229
     * $builder->string('foo')->getter(); // import() from the "foo" property
230
     * $builder->string('foo')->getter('bar'); // import() from the "bar" property
231
     *
232
     * // import() from the "foo" property, and apply a transformer to the value
233
     * // First parameter is the model property value
234
     * // Second parameter is the current child instance
235
     * $builder->string('foo')->getter(function ($value, ChildInterface $input) {
236
     *     return $this->normalizeFoo($value);
237
     * });
238
     *
239
     * // Same as above, but use the "bar" property instead of "foo"
240
     * $builder->string('foo')->getter('bar', function ($value, ChildInterface $input) {
241
     *     return $this->normalizeFoo($value);
242
     * });
243
     *
244
     * // Define a custom accessor
245
     * // First parameter is the import()'ed entity
246
     * // Second is always null
247
     * // Third is the mode : always ExtractorInterface::EXTRACTION
248
     * // Fourth is the Getter instance
249
     * $builder->string('foo')->getter(null, null, function ($entity, $_, string $mode, Getter $getter) {
250
     *     return $entity->myCustomGetter();
251
     * });
252
     * </code>
253
     *
254
     * @param string|callable|null $propertyName The property name. If null use the child name.
255
     * @param callable|null $transformer The value transformer (transform model value to input value)
256
     * @param callable|null $customAccessor Custom getter function. If null use the symfony property accessor
257
     *
258
     * @return $this
259
     *
260
     * @see Getter
261
     * @see ChildBuilderInterface::extractor()
262
     * @see ChildInterface::import()
263
     */
264 52
    final public function getter($propertyName = null, ?callable $transformer = null, ?callable $customAccessor = null): self
265
    {
266 52
        return $this->extractor(new Getter($propertyName, $transformer, $customAccessor));
267
    }
268
269
    /**
270
     * Define hydrator using a setter
271
     * The setter is used by `fill()` method, which get the value from the element to fill the entity property
272
     *
273
     * Prototypes :
274
     *   function setter(): this - Add a setter hydrator, using the child name as property name
275
     *   function setter(string $propertyName): this - Add a setter hydrator, using $propertyName as property name
276
     *   function setter(callable $transformer): this - Add a setter hydrator, with a value transformer
277
     *   function setter(?string $propertyName, ?callable $transformer, ?callable $customAccessor): this - Add a setter hydrator, with a value transformer and a custom accessor
278
     *
279
     * <code>
280
     * $builder->string('foo')->setter(); // fill() the "foo" property
281
     * $builder->string('foo')->setter('bar'); // fill() the "bar" property
282
     *
283
     * // fill() the "foo" property, and apply a transformer to the value
284
     * // First parameter is the model property value
285
     * // Second parameter is the current child instance
286
     * $builder->string('foo')->setter(function ($value, ChildInterface $input) {
287
     *     return $this->parseFoo($value);
288
     * });
289
     *
290
     * // Same as above, but use the "bar" property instead of "foo"
291
     * $builder->string('foo')->setter('bar', function ($value, ChildInterface $input) {
292
     *     return $this->parseFoo($value);
293
     * });
294
     *
295
     * // Define a custom accessor
296
     * // First parameter is the fill()'ed entity
297
     * // Second is the element value
298
     * // Third is the mode : always ExtractorInterface::HYDRATION
299
     * // Fourth is the Setter instance
300
     * $builder->string('foo')->setter(null, null, function ($entity, $value, string $mode, Setter $setter) {
301
     *     return $entity->myCustomSetter($value);
302
     * });
303
     * </code>
304
     *
305
     * @param string|callable|null $propertyName The property name. If null use the child name.
306
     * @param callable|null $transformer The value transformer (transform input [i.e. http] value to model value)
307
     * @param callable|null $customAccessor Custom setter function. If null use the symfony property accessor
308
     *
309
     * @return $this
310
     *
311
     * @see Setter
312
     * @see ChildBuilderInterface::hydrator()
313
     * @see ChildInterface::fill()
314
     */
315 54
    final public function setter($propertyName = null, ?callable $transformer = null, ?callable $customAccessor = null): self
316
    {
317 54
        return $this->hydrator(new Setter($propertyName, $transformer, $customAccessor));
318
    }
319
320
    /**
321
     * Define the child creation factory or class name
322
     *
323
     * @param ChildCreationStrategyInterface|callable|class-string<ChildInterface> $factory The factory, or child class name
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...
324
     *
325
     * @return $this
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 5
    final public function httpFields(HttpFieldsInterface $fields): self
342
    {
343 5
        $this->fields = $fields;
344
345 5
        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 4
    final public function prefix(?string $prefix = null): self
375
    {
376 4
        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
     * Forward calls to the element builder
397
     *
398
     * @param string $name
399
     * @param array $arguments
400
     *
401
     * @return $this|mixed
402
     */
403 81
    final public function __call($name, $arguments)
404
    {
405 81
        $return = $this->elementBuilder->$name(...$arguments);
406
407 81
        return $return === $this->elementBuilder ? $this : $return;
408
    }
409
}
410