Completed
Push — master ( 8580a3...087750 )
by grégoire
12s
created

ParameterJuicer::validate()   B

Complexity

Conditions 5
Paths 8

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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