Completed
Push — master ( c9cd83...0e31a6 )
by grégoire
01:57
created

ParameterJuicer::addValidator()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 9
rs 9.6666
c 0
b 0
f 0
cc 1
eloc 5
nc 1
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
        $this
157
            ->checkFieldExists($name)
158
            ->default_values[$name] = $value
159
            ;
160
161
        return $this;
162
    }
163
164
    /**
165
     * addJuicer
166
     *
167
     * Add a juicer to clean a validate a subset of data.
168
     *
169
     * @throws \InvalidArgumentException
170
     */
171
    public function addJuicer(string $name, ParameterJuicerInterface $juicer): self
172
    {
173
        return $this
174
            ->addCleaner($name, [$juicer, 'clean'])
175
            ->addValidator($name, [$juicer, 'validate'])
176
            ;
177
    }
178
179
    /**
180
     * setStrategy
181
     *
182
     * Set the extra fields strategy for this juicer.
183
     */
184
    public function setStrategy(int $strategy): self
185
    {
186
        $this->strategy = $strategy;
187
188
        return $this;
189
    }
190
191
    /**
192
     * squash
193
     *
194
     * Clean & validate the given data according to the definition.
195
     */
196
    public function squash(array $values): array
197
    {
198
        $values = $this->clean($values);
199
        $this->validate($values);
200
201
        return $values;
202
    }
203
204
    /**
205
     * validate
206
     *
207
     * Trigger validation on values.
208
     *
209
     * @see     ParameterJuicerInterface
210
     */
211
    public function validate(array $values): ParameterJuicerInterface
212
    {
213
        $exception = new ValidationException("validation failed");
214
215
        if ($this->strategy === self::STRATEGY_REFUSE_EXTRA_VALUES) {
216
            $this->refuseExtraFields($values, $exception);
217
        }
218
        $this->validateFields($values, $exception);
219
220
        if ($exception->hasExceptions()) {
221
            throw $exception;
222
        }
223
224
        return $this;
225
    }
226
227
    /**
228
     * refuseExtraFields
229
     *
230
     * Fill the exception with refused extra fields if any.
231
     */
232
    private function refuseExtraFields(array $values, ValidationException $exception): self
233
    {
234
        $diff = array_keys(array_diff_key($values, $this->fields));
235
236
        foreach ($diff as $field_name) {
237
            $exception->addException(
238
                $field_name,
239
                new ValidationException("extra field is refused by validation strategy")
240
            );
241
        }
242
243
        return $this;
244
    }
245
246
    /**
247
     * validateFields
248
     *
249
     * Check mandatory fields and launch validators.
250
     */
251
    private function validateFields(array $values, ValidationException $exception): self
252
    {
253
        foreach ($this->fields as $field => $is_mandatory) {
254
            $is_set = isset($values[$field]) || array_key_exists($field, $values);
255
256
            if ($is_mandatory && !$is_set) {
257
                $exception->addException(
258
                    $field,
259
                    new ValidationException("missing mandatory field")
260
                );
261
262
            } elseif ($is_set && isset($this->validators[$field])) {
263
                $this->launchValidatorsFor(
264
                    $field,
265
                    $values[$field],
266
                    $exception
267
                );
268
            }
269
        }
270
271
        return $this;
272
    }
273
274
    /**
275
     * setDefaultValues
276
     *
277
     * Apply default values. When a field is not present in the values, the
278
     * default value is set.
279
     */
280
    private function setDefaultValues(array $values): array
281
    {
282
        foreach ($this->default_values as $field => $default_value) {
283
            if (!isset($values[$field]) && !array_key_exists($field, $values)) {
284
                $values[$field] = $default_value;
285
            }
286
        }
287
288
        return $values;
289
    }
290
291
    /**
292
     * clean
293
     *
294
     * Clean and return values.
295
     *
296
     * @see     ParameterJuicerInterface
297
     */
298
    public function clean(array $values): array
299
    {
300
        if ($this->strategy === self::STRATEGY_IGNORE_EXTRA_VALUES) {
301
            $values = array_intersect_key($values, $this->fields);
302
        }
303
304
        return $this->setDefaultValues($this->triggerCleaning($values));
305
    }
306
307
    /**
308
     * triggerCleaning
309
     *
310
     * Launch cleaners on the values.
311
     */
312
    private function triggerCleaning(array $values): array
313
    {
314
        foreach ($this->cleaners as $field_name => $cleaners) {
315
            if (isset($values[$field_name]) || array_key_exists($field_name, $values)) {
316
                foreach ($cleaners as $cleaner) {
317
                    try {
318
                        $values[$field_name] =
319
                            call_user_func($cleaner, $values[$field_name])
320
                            ;
321
                    } catch (CleanerRemoveFieldException $e) {
322
                        unset($values[$field_name]);
323
                    }
324
                }
325
            }
326
        }
327
328
        return $values;
329
    }
330
331
    /**
332
     * checkFieldExists
333
     *
334
     * Throw an exception if the field does not exist.
335
     *
336
     * @throws  \InvalidArgumentException
337
     */
338
    private function checkFieldExists(string $name): self
339
    {
340
        if (!isset($this->fields[$name])) {
341
            throw new \InvalidArgumentException(
342
                sprintf(
343
                    "Field '%s' is not declared, fields are {%s}.",
344
                    $name,
345
                    join(', ', array_keys($this->fields))
346
                )
347
            );
348
        }
349
350
        return $this;
351
    }
352
353
    /**
354
     * launchValidatorsFor
355
     *
356
     * Triger validators for the given field if any.
357
     *
358
     * @throws  \RuntimeException if the callable fails.
359
     */
360
    private function launchValidatorsFor(string $field, $value, ValidationException $exception): self
361
    {
362
        foreach ($this->validators[$field] as $validator) {
363
            try {
364
                if (call_user_func($validator, $value) === false) {
365
                    throw new \RuntimeException(
366
                        sprintf("One of the validators for the field '%s' has a PHP error.", $field)
367
                    );
368
                }
369
            } catch (ValidationException $e) {
370
                $exception->addException($field, $e);
371
            }
372
        }
373
374
        return $this;
375
    }
376
}
377
378