Completed
Push — master ( 93596b...2db245 )
by grégoire
04:12
created

ParameterJuicer::addFormValidator()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 6
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 3
nc 1
nop 1
1
<?php
2
/*
3
 * This file is part of Chanmix51’s ParameterJuicer package.
4
 *
5
 * (c) 2017 Grégoire HUBERT <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
namespace Chanmix51\ParameterJuicer;
11
12
use Chanmix51\ParameterJuicer\Exception\ValidationException;
13
use Chanmix51\ParameterJuicer\Exception\CleanerRemoveFieldException;
14
use Chanmix51\ParameterJuicer\ParameterJuicerInterface;
15
16
/**
17
 * ParameterJuicer
18
 *
19
 * Cleaner and validator for data set.
20
 *
21
 * A "cleaner" is a callable that takes a data and returns the data tranformed.
22
 * It can throw a CleanerRemoveFieldException if the field is to be unset.
23
 *
24
 * A "default value" is set when the field is NOT PRESENT. In the case the
25
 * field exists and has no value (null), the default value does not apply.
26
 *
27
 * A "validator" is a callable that throws a ValidationException when the given
28
 * data is detected as invalid.
29
 *
30
 * @package   ParameterJuicer
31
 * @copyright 2017 Grégoire HUBERT
32
 * @author    Grégoire HUBERT <[email protected]>
33
 * @license   X11 {@link http://opensource.org/licenses/mit-license.php}
34
 *
35
 * @see       ParameterJuicerInterface
36
 */
37
class ParameterJuicer implements ParameterJuicerInterface
38
{
39
    const STRATEGY_IGNORE_EXTRA_VALUES = 0;
40
    const STRATEGY_REFUSE_EXTRA_VALUES = 1;
41
    const STRATEGY_ACCEPT_EXTRA_VALUES = 2;
42
43
    /** @var  array     list of validators, must be callables */
44
    protected $validators = [];
45
46
    /** @var  array     list of cleaners, must be callables */
47
    protected $cleaners = [];
48
49
    /** @var  array     list of fields, this gives an information if the field
50
                        is mandatory or optional. */
51
    protected $fields = [];
52
53
    /** @var  array     list of default values */
54
    protected $default_values = [];
55
56
    /** @var  array     list of form cleaners, must be callables */
57
    protected $form_cleaners = [];
58
59
    /** @var  array     list of form validators, must be callables */
60
    protected $form_validators = [];
61
62
    /** @var  int       strategy of this juicer */
63
    public $strategy = self::STRATEGY_IGNORE_EXTRA_VALUES;
64
65
    /**
66
     * addField
67
     *
68
     * Declare a new field with no validators nor cleaner. It can be declared
69
     * if the field is optional or mandatory.
70
     * If the field already exists, it is overriden.
71
     */
72
    public function addField(string $name, bool $is_mandatory = true): self
73
    {
74
        $this->fields[$name] = $is_mandatory;
75
76
        return $this;
77
    }
78
79
    /**
80
     * addFields
81
     *
82
     * Declare several fields at once.Existing fields are overriden.
83
     */
84
    public function addFields(array $fields, $are_mandatory = true): self
85
    {
86
        array_merge($this->fields, array_fill_keys($fields, $are_mandatory));
87
88
        return $this;
89
    }
90
91
    /**
92
     * removeField
93
     *
94
     * Remove an existing field with all validators or cleaners associated to
95
     * it if any. It throws an exception if the field does not exist.
96
     *
97
     * @throws \InvalidArgumentException
98
     */
99
    public function removeField(string $name): self
100
    {
101
        $this->checkFieldExists($name);
102
        unset($this->fields[$name]);
103
104
        if (isset($this->validators[$name])) {
105
            unset($this->validators[$name]);
106
        }
107
108
        if (isset($this->cleaners[$name])) {
109
            unset($this->cleaners[$name]);
110
        }
111
112
        return $this;
113
    }
114
115
    /**
116
     * addValidator
117
     *
118
     * Add a new validator associated to a key. If the field is not already
119
     * declared, it is created.
120
     *
121
     * @throws \InvalidArgumentException
122
     */
123
    public function addValidator(string $name, callable $validator): self
124
    {
125
        $this
126
            ->checkFieldExists($name)
127
            ->validators[$name][] = $validator
128
            ;
129
130
        return $this;
131
    }
132
133
    /**
134
     * addCleaner
135
     *
136
     * Add a new cleaner associated to a key.
137
     *
138
     * @throws \InvalidArgumentException
139
     */
140
    public function addCleaner(string $name, callable $cleaner): self
141
    {
142
        $this
143
            ->checkFieldExists($name)
144
            ->cleaners[$name][] = $cleaner
145
            ;
146
147
        return $this;
148
    }
149
150
    /**
151
     * setDefaultValue
152
     *
153
     * Set a default value for a field. If the field is not set or its value is
154
     * null, this value will be set instead. This is triggered AFTER the
155
     * cleaners which is useful because some cleanders can return null and then
156
     * default value is applied.
157
     *
158
     * @throws \InvalidArgumentException
159
     */
160
    public function setDefaultValue(string $name, $value): self
161
    {
162
        if (false === is_callable($value)) {
163
            $value = function () use ($value) {
164
                return $value;
165
            };
166
        }
167
168
        $this
169
            ->checkFieldExists($name)
170
            ->default_values[$name] = $value
171
            ;
172
173
        return $this;
174
    }
175
176
    /**
177
     * addFormCleaner
178
     *
179
     * Add a new cleaner associated to the whole set of values.
180
     */
181
    public function addFormCleaner(callable $cleaner): self
182
    {
183
        $this->form_cleaners[] = $cleaner;
184
185
        return $this;
186
    }
187
188
    /**
189
     * addFormValidator
190
     *
191
     * Add a new validator to the whole set of values.
192
     */
193
    public function addFormValidator(callable $validator): self
194
    {
195
        $this->form_validators[] = $validator;
196
197
        return $this;
198
    }
199
200
    /**
201
     * addJuicer
202
     *
203
     * Add a juicer to clean a validate a subset of data.
204
     *
205
     * @throws \InvalidArgumentException
206
     */
207
    public function addJuicer(string $name, ParameterJuicerInterface $juicer): self
208
    {
209
        return $this
210
            ->addCleaner($name, [$juicer, 'clean'])
211
            ->addValidator($name, [$juicer, 'validate'])
212
            ;
213
    }
214
215
    /**
216
     * setStrategy
217
     *
218
     * Set the extra fields strategy for this juicer.
219
     */
220
    public function setStrategy(int $strategy): self
221
    {
222
        $this->strategy = $strategy;
223
224
        return $this;
225
    }
226
227
    /**
228
     * squash
229
     *
230
     * Clean & validate the given data according to the definition.
231
     */
232
    public function squash(array $values): array
233
    {
234
        $values = $this->clean($values);
235
        $this->validate($values);
236
237
        return $values;
238
    }
239
240
    /**
241
     * validate
242
     *
243
     * Trigger validation on values.
244
     *
245
     * @see     ParameterJuicerInterface
246
     */
247
    public function validate(array $values)
248
    {
249
        $exception = new ValidationException("validation failed");
250
251
        if ($this->strategy === self::STRATEGY_REFUSE_EXTRA_VALUES) {
252
            $this->refuseExtraFields($values, $exception);
253
        }
254
        $this->validateFields($values, $exception);
255
        $this->validateForm($values, $exception);
256
257
        if ($exception->hasExceptions()) {
258
            throw $exception;
259
        }
260
    }
261
262
    /**
263
     * refuseExtraFields
264
     *
265
     * Fill the exception with refused extra fields if any.
266
     */
267
    private function refuseExtraFields(array $values, ValidationException $exception): self
268
    {
269
        $diff = array_keys(array_diff_key($values, $this->fields));
270
271
        foreach ($diff as $field_name) {
272
            $exception->addException(
273
                $field_name,
274
                new ValidationException("extra field is refused by validation strategy")
275
            );
276
        }
277
278
        return $this;
279
    }
280
281
    /**
282
     * validateFields
283
     *
284
     * Check mandatory fields and launch validators.
285
     */
286
    private function validateFields(array $values, ValidationException $exception): self
287
    {
288
        foreach ($this->fields as $field => $is_mandatory) {
289
            $is_set = isset($values[$field]) || array_key_exists($field, $values);
290
291
            if ($is_mandatory && !$is_set) {
292
                $exception->addException(
293
                    $field,
294
                    new ValidationException("missing mandatory field")
295
                );
296
            } elseif ($is_set && isset($this->validators[$field])) {
297
                $this->launchValidatorsFor(
298
                    $field,
299
                    $values[$field],
300
                    $exception
301
                );
302
            }
303
        }
304
305
        return $this;
306
    }
307
308
    /**
309
     * validateForm
310
     *
311
     * form wide validation
312
     */
313
    private function validateForm(array $values, ValidationException $exception): self
314
    {
315
        return $this->launchValidators($this->form_validators, '', $values, $exception);
316
    }
317
318
    /**
319
     * setDefaultValues
320
     *
321
     * Apply default values. When a field is not present in the values, the
322
     * default value is set.
323
     */
324
    private function setDefaultValues(array $values): array
325
    {
326
        foreach ($this->default_values as $field => $default_value) {
327
            if (!isset($values[$field]) && !array_key_exists($field, $values)) {
328
                $values[$field] = call_user_func($default_value);
329
            }
330
        }
331
332
        return $values;
333
    }
334
335
    /**
336
     * clean
337
     *
338
     * Clean and return values.
339
     *
340
     * @see     ParameterJuicerInterface
341
     */
342
    public function clean(array $values): array
343
    {
344
        if ($this->strategy === self::STRATEGY_IGNORE_EXTRA_VALUES) {
345
            $values = array_intersect_key($values, $this->fields);
346
        }
347
348
        return $this->setDefaultValues($this->triggerCleaning($values));
349
    }
350
351
    /**
352
     * triggerCleaning
353
     *
354
     * Launch cleaners on the values.
355
     */
356
    private function triggerCleaning(array $values): array
357
    {
358
        foreach ($this->cleaners as $field_name => $cleaners) {
359
            if (isset($values[$field_name]) || array_key_exists($field_name, $values)) {
360
                foreach ($cleaners as $cleaner) {
361
                    try {
362
                        $values[$field_name] =
363
                            call_user_func($cleaner, $values[$field_name])
364
                            ;
365
                    } catch (CleanerRemoveFieldException $e) {
366
                        unset($values[$field_name]);
367
                    }
368
                }
369
            }
370
        }
371
372
        foreach ($this->form_cleaners as $cleaner) {
373
            $values = call_user_func($cleaner, $values);
374
        }
375
376
        return $values;
377
    }
378
379
    /**
380
     * checkFieldExists
381
     *
382
     * Throw an exception if the field does not exist.
383
     *
384
     * @throws  \InvalidArgumentException
385
     */
386
    private function checkFieldExists(string $name): self
387
    {
388
        if (!isset($this->fields[$name])) {
389
            throw new \InvalidArgumentException(
390
                sprintf(
391
                    "Field '%s' is not declared, fields are {%s}.",
392
                    $name,
393
                    join(', ', array_keys($this->fields))
394
                )
395
            );
396
        }
397
398
        return $this;
399
    }
400
401
    /**
402
     * launchValidatorsFor
403
     *
404
     * Triger validators for the given field if any.
405
     */
406
    private function launchValidatorsFor(string $field, $value, ValidationException $exception): self
407
    {
408
        try {
409
            $this->launchValidators($this->validators[$field], $field, $value, $exception);
410
        } catch (ValidationException $e) {
411
            $exception->addException($field, $e);
412
        }
413
414
        return $this;
415
    }
416
417
    /**
418
     * launchValidators
419
     *
420
     * Apply validators against the given value.
421
     *
422
     * @throws  \RuntimeException if the callable fails.
423
     */
424
    private function launchValidators(array $validators, string $field, $value, ValidationException $exception): self
0 ignored issues
show
Unused Code introduced by
The parameter $field is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $exception is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
425
    {
426
        foreach ($validators as $validator) {
427
            if (($return = call_user_func($validator, $value)) !== null) {
428
                throw new ValidationException((string) $return);
429
            }
430
        }
431
432
        return $this;
433
    }
434
}
435