ParameterJuicer::launchValidators()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 10
rs 9.9332
c 0
b 0
f 0
cc 3
nc 3
nop 2
1
<?php declare(strict_types=1);
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
    /**
40
     *  discard extra values (default)
41
     */
42
    const STRATEGY_IGNORE_EXTRA_VALUES = 0;
43
    /**
44
     * error when extra values
45
     */
46
    const STRATEGY_REFUSE_EXTRA_VALUES = 1;
47
    /**
48
     * accept extra values CAUTION:
49
     * This could lead to unclean & unvalidated values to leak into
50
     * safe area. Use this only if you know what you are doing.
51
     */
52
    const STRATEGY_ACCEPT_EXTRA_VALUES = 2;
53
    /**
54
     *  do not lauch form validation if field validation fails (default)
55
     */
56
    const FORM_VALIDATORS_CONDITIONAL=0;
57
    /**
58
     *  always launch form validation
59
     */
60
    const FORM_VALIDATORS_ALWAYS=1;
61
62
    /** @var  array     list of validators, must be callables */
63
    protected $validators = [];
64
65
    /** @var  array     list of cleaners, must be callables */
66
    protected $cleaners = [];
67
68
    /** @var  array     list of fields, this gives an information if the field
69
                        is mandatory or optional. */
70
    protected $fields = [];
71
72
    /** @var  array     list of default values */
73
    protected $default_values = [];
74
75
    /** @var  array     list of form cleaners, must be callables */
76
    protected $form_cleaners = [];
77
78
    /** @var  array     list of form validators, must be callables */
79
    protected $form_validators = [];
80
81
    /** @var  int       strategy of this juicer */
82
    public $strategy = self::STRATEGY_IGNORE_EXTRA_VALUES;
83
84
    /** @var  int       is form validation triggered when field validation fails */
85
    public $form_validation_strategy = self::FORM_VALIDATORS_CONDITIONAL;
86
87
    /**
88
     * addField
89
     *
90
     * Declare a new field with no validators nor cleaner. It can be declared
91
     * if the field is optional or mandatory.
92
     * If the field already exists, it is overriden.
93
     */
94
    public function addField(string $name, bool $is_mandatory = true): self
95
    {
96
        $this->fields[$name] = $is_mandatory;
97
98
        return $this;
99
    }
100
101
    /**
102
     * addFields
103
     *
104
     * Declare several fields at once.Existing fields are overriden.
105
     */
106
    public function addFields(array $fields, $are_mandatory = true): self
107
    {
108
        array_merge($this->fields, array_fill_keys($fields, $are_mandatory));
109
110
        return $this;
111
    }
112
113
    /**
114
     * removeField
115
     *
116
     * Remove an existing field with all validators or cleaners associated to
117
     * it if any. It throws an exception if the field does not exist.
118
     *
119
     * @throws \InvalidArgumentException
120
     */
121
    public function removeField(string $name): self
122
    {
123
        $this->checkFieldExists($name);
124
        unset($this->fields[$name]);
125
126
        if (isset($this->validators[$name])) {
127
            unset($this->validators[$name]);
128
        }
129
130
        if (isset($this->cleaners[$name])) {
131
            unset($this->cleaners[$name]);
132
        }
133
134
        return $this;
135
    }
136
137
    /**
138
     * addValidator
139
     *
140
     * Add a new validator associated to a key. If the field is not already
141
     * declared, it is created.
142
     *
143
     * @throws \InvalidArgumentException
144
     */
145
    public function addValidator(string $name, callable $validator): self
146
    {
147
        $this
148
            ->checkFieldExists($name)
149
            ->validators[$name][] = $validator
150
            ;
151
152
        return $this;
153
    }
154
155
    /**
156
     * addCleaner
157
     *
158
     * Add a new cleaner associated to a key.
159
     *
160
     * @throws \InvalidArgumentException
161
     */
162
    public function addCleaner(string $name, callable $cleaner): self
163
    {
164
        $this
165
            ->checkFieldExists($name)
166
            ->cleaners[$name][] = $cleaner
167
            ;
168
169
        return $this;
170
    }
171
172
    /**
173
     * setDefaultValue
174
     *
175
     * Set a default value for a field. If the field is not set or its value is
176
     * null, this value will be set instead. This is triggered AFTER the
177
     * cleaners which is useful because some cleanders can return null and then
178
     * default value is applied.
179
     *
180
     * @throws \InvalidArgumentException
181
     */
182
    public function setDefaultValue(string $name, $value): self
183
    {
184
        if (false === is_callable($value)) {
185
            $value = function () use ($value) {
186
                return $value;
187
            };
188
        }
189
190
        $this
191
            ->checkFieldExists($name)
192
            ->default_values[$name] = $value
193
            ;
194
195
        return $this;
196
    }
197
198
    /**
199
     * addFormCleaner
200
     *
201
     * Add a new cleaner associated to the whole set of values.
202
     */
203
    public function addFormCleaner(callable $cleaner): self
204
    {
205
        $this->form_cleaners[] = $cleaner;
206
207
        return $this;
208
    }
209
210
    /**
211
     * addFormValidator
212
     *
213
     * Add a new validator to the whole set of values.
214
     */
215
    public function addFormValidator(callable $validator): self
216
    {
217
        $this->form_validators[] = $validator;
218
219
        return $this;
220
    }
221
222
    /**
223
     * addJuicer
224
     *
225
     * Add a juicer to clean a validate a subset of data.
226
     *
227
     * @throws \InvalidArgumentException
228
     */
229
    public function addJuicer(string $name, ParameterJuicerInterface $juicer): self
230
    {
231
        return $this
232
            ->addCleaner($name, [$juicer, 'squash'])
233
            ;
234
    }
235
236
    /**
237
     * setStrategy
238
     *
239
     * Set the extra fields strategy for this juicer.
240
     */
241
    public function setStrategy(int $strategy): self
242
    {
243
        $this->strategy = $strategy;
244
245
        return $this;
246
    }
247
248
    /**
249
     * setFormValidationStrategy
250
     *
251
     * Set the form validators strategy
252
     */
253
    public function setFormValidationStrategy(int $strategy): self
254
    {
255
        $this->form_validation_strategy = $strategy;
256
257
        return $this;
258
    }
259
260
    /**
261
     * squash
262
     *
263
     * Clean & validate the given data according to the definition.
264
     */
265
    public function squash(array $values): array
266
    {
267
        $values = $this->clean($values);
268
        $this->validate($values);
269
270
        return $values;
271
    }
272
273
    /**
274
     * validate
275
     *
276
     * Trigger validation on values.
277
     *
278
     * @see     ParameterJuicerInterface
279
     */
280
    public function validate(array $values)
281
    {
282
        $exception = new ValidationException("validation failed");
283
284
        if ($this->strategy === self::STRATEGY_REFUSE_EXTRA_VALUES) {
285
            $this->refuseExtraFields($values, $exception);
286
        }
287
        $this->validateFields($values, $exception);
288
289
        if (!$exception->hasExceptions() || $this->form_validation_strategy === self::FORM_VALIDATORS_ALWAYS) {
290
            $this->validateForm($values, $exception);
291
        }
292
293
        if ($exception->hasExceptions()) {
294
            throw $exception;
295
        }
296
    }
297
298
    /**
299
     * refuseExtraFields
300
     *
301
     * Fill the exception with refused extra fields if any.
302
     */
303
    private function refuseExtraFields(array $values, ValidationException $exception): self
304
    {
305
        $diff = array_keys(array_diff_key($values, $this->fields));
306
307
        foreach ($diff as $field_name) {
308
            $exception->addException(
309
                $field_name,
310
                new ValidationException("extra field is refused by validation strategy")
311
            );
312
        }
313
314
        return $this;
315
    }
316
317
    /**
318
     * validateFields
319
     *
320
     * Check mandatory fields and launch validators.
321
     */
322
    private function validateFields(array $values, ValidationException $exception): self
323
    {
324
        foreach ($this->fields as $field => $is_mandatory) {
325
            $is_set = isset($values[$field]) || array_key_exists($field, $values);
326
327
            if ($is_mandatory && !$is_set) {
328
                $exception->addException(
329
                    $field,
330
                    new ValidationException("missing mandatory field")
331
                );
332
            } elseif ($is_set && isset($this->validators[$field])) {
333
                $this->launchValidatorsFor(
334
                    $field,
335
                    $values[$field],
336
                    $exception
337
                );
338
            }
339
        }
340
341
        return $this;
342
    }
343
344
    /**
345
     * validateForm
346
     *
347
     * form wide validation
348
     */
349
    private function validateForm(array $values, ValidationException $exception): self
350
    {
351
        try {
352
            $this->launchValidators($this->form_validators, $values);
353
        } catch (ValidationException $e) {
354
            $exception->addException('', $e);
355
        }
356
357
        return $this;
358
    }
359
360
    /**
361
     * setDefaultValues
362
     *
363
     * Apply default values. When a field is not present in the values, the
364
     * default value is set.
365
     */
366
    private function setDefaultValues(array $values): array
367
    {
368
        foreach ($this->default_values as $field => $default_value) {
369
            if (!isset($values[$field]) && !array_key_exists($field, $values)) {
370
                $values[$field] = call_user_func($default_value);
371
            }
372
        }
373
374
        return $values;
375
    }
376
377
    /**
378
     * clean
379
     *
380
     * Clean and return values.
381
     *
382
     * @see     ParameterJuicerInterface
383
     */
384
    public function clean(array $values): array
385
    {
386
        if ($this->strategy === self::STRATEGY_IGNORE_EXTRA_VALUES) {
387
            $values = array_intersect_key($values, $this->fields);
388
        }
389
390
        return $this->setDefaultValues($this->triggerCleaning($values));
391
    }
392
393
    /**
394
     * triggerCleaning
395
     *
396
     * Launch cleaners on the values.
397
     */
398
    private function triggerCleaning(array $values): array
399
    {
400
        foreach ($this->cleaners as $field_name => $cleaners) {
401
            if (isset($values[$field_name]) || array_key_exists($field_name, $values)) {
402
                foreach ($cleaners as $cleaner) {
403
                    try {
404
                        $values[$field_name] =
405
                            call_user_func($cleaner, $values[$field_name])
406
                            ;
407
                    } catch (\Exception $e) {
408
                        unset($values[$field_name]);
409
                    }
410
                }
411
            }
412
        }
413
414
        foreach ($this->form_cleaners as $cleaner) {
415
            $values = call_user_func($cleaner, $values);
416
        }
417
418
        return $values;
419
    }
420
421
    /**
422
     * checkFieldExists
423
     *
424
     * Throw an exception if the field does not exist.
425
     *
426
     * @throws  \InvalidArgumentException
427
     */
428
    private function checkFieldExists(string $name): self
429
    {
430
        if (!isset($this->fields[$name])) {
431
            throw new \InvalidArgumentException(
432
                sprintf(
433
                    "Field '%s' is not declared, fields are {%s}.",
434
                    $name,
435
                    join(', ', array_keys($this->fields))
436
                )
437
            );
438
        }
439
440
        return $this;
441
    }
442
443
    /**
444
     * launchValidatorsFor
445
     *
446
     * Triger validators for the given field if any.
447
     */
448
    private function launchValidatorsFor(string $field, $value, ValidationException $exception): self
449
    {
450
        try {
451
            $this->launchValidators($this->validators[$field], $value);
452
        } catch (ValidationException $e) {
453
            $exception->addException($field, $e);
454
        }
455
456
        return $this;
457
    }
458
459
    /**
460
     * launchValidators
461
     *
462
     * Apply validators against the given value.
463
     *
464
     * @throws  \RuntimeException if the callable fails.
465
     */
466
    private function launchValidators(array $validators, $value): self
467
    {
468
        foreach ($validators as $validator) {
469
            if (($return = call_user_func($validator, $value)) !== null) {
470
                throw new ValidationException((string) $return);
471
            }
472
        }
473
474
        return $this;
475
    }
476
}
477