Completed
Push — master ( fd5c7c...e49e29 )
by grégoire
02:27
created

ParameterJuicer::validateFields()   B

Complexity

Conditions 7
Paths 7

Size

Total Lines 21
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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