Completed
Push — master ( b4c757...366bee )
by grégoire
02:04
created

ParameterJuicer   B

Complexity

Total Complexity 42

Size/Duplication

Total Lines 353
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 1

Importance

Changes 0
Metric Value
wmc 42
lcom 1
cbo 1
dl 0
loc 353
c 0
b 0
f 0
rs 8.295

18 Methods

Rating   Name   Duplication   Size   Complexity  
A getName() 0 4 1
A addField() 0 6 1
A addFields() 0 6 1
A removeField() 0 15 3
A addValidator() 0 9 1
A addCleaner() 0 9 1
A setDefaultValue() 0 9 1
A addJuicer() 0 7 1
A setStrategy() 0 6 1
A squash() 0 7 1
A validate() 0 15 3
A refuseExtraFields() 0 12 2
C validateFields() 0 26 7
A setDefaultValues() 0 10 4
A clean() 0 8 2
B triggerCleaning() 0 18 6
A checkFieldExists() 0 14 2
A launchValidatorsFor() 0 16 4

How to fix   Complexity   

Complex Class

Complex classes like ParameterJuicer often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ParameterJuicer, and based on these observations, apply Extract Interface, too.

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