Completed
Push — master ( ce18a8...486e29 )
by Andres
04:16
created

AbstractValidator::__invoke()   D

Complexity

Conditions 9
Paths 33

Size

Total Lines 33
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 6
Bugs 1 Features 3
Metric Value
cc 9
eloc 20
c 6
b 1
f 3
nc 33
nop 2
dl 0
loc 33
rs 4.909
1
<?php
2
namespace Comfort\Validator;
3
4
use Comfort\Comfort;
5
use Comfort\Error;
6
use Comfort\Exception\DiscomfortException;
7
use Comfort\Exception\ValidationException;
8
use Comfort\ValidationError;
9
10
/**
11
 * Class AbstractValidator
12
 * @package Comfort\Validator
13
 */
14
abstract class AbstractValidator
15
{
16
    /**
17
     * @var \Closure[]
18
     */
19
    protected $validationStack = [];
20
    /**
21
     * @var Comfort
22
     */
23
    private $comfort;
24
    /**
25
     * @var boolean
26
     */
27
    private $toBool = false;
28
    /**
29
     * @var bool
30
     */
31
    private $optional = true;
32
    /**
33
     * @var mixed
34
     */
35
    private $defaultValue;
36
    /**
37
     * @var array
38
     */
39
    protected $errorHandlers = [
40
        'default' => [
41
            'message' => 'There was a validation error'
42
        ],
43
        'required' => [
44
            'message' => '%s is required',
45
            'default' => 'value'
46
        ]
47
    ];
48
49
    public function __construct(Comfort $comfort)
50
    {
51
52
        $this->comfort = $comfort;
53
    }
54
55
    /**
56
     * Execute validation stack
57
     *
58
     * @param mixed $value
59
     * @param null|string $key
60
     * @return bool|ValidationError
61
     */
62
    public function __invoke($value, $key = null)
63
    {
64
        if (is_null($value) && $this->optional) {
65
            if (is_null($this->defaultValue)) {
66
                return;
67
            } else {
68
                $value = $this->defaultValue;
69
            }
70
        }
71
72
        try {
73
            reset($this->validationStack);
74
75
            do {
76
                /** @var callable $validator */
77
                $validator = current($this->validationStack);
78
                $retVal = $validator($value, $key);
79
                $value = $retVal === null ? $value : $retVal;
80
            } while (next($this->validationStack));
81
82
            if ($this->toBool) {
83
                return true;
84
            }
85
86
            return $value;
87
        } catch (ValidationException $validationException) {
88
            if ($this->toBool) {
89
                return false;
90
            }
91
92
            return ValidationError::fromException($validationException);
93
        }
94
    }
95
96
    public function __call($name, $arguments)
97
    {
98
        return call_user_func_array([$this->comfort, $name], $arguments);
99
    }
100
101
    /**
102
     * Declare the value as being required
103
     *
104
     * @return $this
105
     */
106
    public function required()
107
    {
108
        $this->optional = false;
109
110
        $this->add(function($value, $nameKey) {
111
            if (is_null($value)) {
112
                $this->createError('required', $value, $nameKey);
113
            }
114
        });
115
116
117
        return $this;
118
    }
119
120
121
    /**
122
     * Declare data is optional
123
     *
124
     * @return $this
125
     */
126
    public function optional()
127
    {
128
        $this->optional = true;
129
130
        return $this;
131
    }
132
133
    /**
134
     * Add adhoc validator to validation stack
135
     *
136
     * @param callable $validation
137
     * @return $this
138
     */
139
    public function add(callable $validation)
140
    {
141
        $this->validationStack[] = $validation;
142
143
        return $this;
144
    }
145
146
    /**
147
     * On validation failure whether to return false or a validation error
148
     *
149
     * @param bool $val
150
     * @return $this
151
     */
152
    public function toBool($val = true)
153
    {
154
        $this->toBool = (boolean)$val;
155
156
        return $this;
157
    }
158
159
    /**
160
     * Set default value
161
     *
162
     * @param $value
163
     * @return $this
164
     */
165
    public function defaultValue($value)
166
    {
167
        $this->defaultValue = $value;
168
169
        return $this;
170
    }
171
172
    /**
173
     * Provide conditional validation
174
     *
175
     * @param $conditions
176
     * @return $this
177
     */
178
    public function alternatives($conditions)
179
    {
180
        $this->add(function($value, $nameKey) use($conditions) {
181
            foreach ($conditions as $condition) {
182
183
                if (!isset($condition['is'])) {
184
                    return $this->createError('alternatives.missing_is', $value, $nameKey);
185
                }
186
187
                if (!$condition['is'] instanceof AbstractValidator) {
188
                    return $this->createError('alternatives.invalid_is', $value, $nameKey);
189
                }
190
191
                /** @var AbstractValidator $is */
192
                $is = $condition['is'];
193
                $is->toBool(true);
194
195
                if (!isset($condition['then'])) {
196
                    return $this->createError('alternatives.missing_then', $value, $nameKey);
197
                }
198
199
                if ($is($value)) {
200 View Code Duplication
                    if ($condition['then'] instanceof AbstractValidator) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
201
                        $validationStack = $condition['then']->validationStack;
202
                        foreach ($validationStack as $validator) {
203
                            $this->validationStack[] = $validator;
204
                        }
205
                    } elseif (!is_null($condition['then'])) {
206
                        return $condition['then'];
207
                    }
208 View Code Duplication
                } elseif (isset($condition['else'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
209
                    if ($condition['else'] instanceof AbstractValidator) {
210
                        $validationStack = $condition['else']->validationStack;
211
                        foreach ($validationStack as $validator) {
212
                            $this->validationStack[] = $validator;
213
                        }
214
                    } elseif (!is_null($condition['else'])) {
215
                        return $condition['else'];
216
                    }
217
                }
218
            }
219
        });
220
221
        return $this;
222
    }
223
224
    /**
225
     * Set error messages
226
     *
227
     * @param array $errorMessages
228
     * @return $this
229
     */
230
    public function errorMessages(array $errorMessages)
231
    {
232
        $errorMessages = array_map(function($errorMessage) {
233
            if (is_string($errorMessage)) {
234
                $errorMessage = ['message' => $errorMessage];
235
            }
236
237
            return $errorMessage;
238
        }, $errorMessages);
239
240
        $this->errorHandlers = array_merge($this->errorHandlers, $errorMessages);
241
        return $this;
242
    }
243
244
    /**
245
     * Create an error with a formatted message
246
     *
247
     * @param string $key
248
     * @param null|string $value
249
     * @param null|string $valueKey
250
     * @param mixed $validationValue
251
     * @throws DiscomfortException
252
     * @throws ValidationException
253
     */
254
    protected function createError($key, $value = null, $valueKey = null, $validationValue = null)
0 ignored issues
show
Unused Code introduced by
The parameter $validationValue is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
255
    {
256
        if (!array_key_exists($key, $this->errorHandlers)) {
257
            throw new ValidationException(
258
                $key,
259
                $this->errorHandlers['default']['message']
260
            );
261
        }
262
263
        $errorHandler = $this->errorHandlers[$key];
264
        if (!array_key_exists('message_formatter', $errorHandler)) {
265
            $messageFormatter = function($template, $value, $validationValue = null) {
266
                return sprintf($template, $value, $validationValue);
267
            };
268
        } else {
269
            $messageFormatter = $errorHandler['message_formatter'];
270
            if (!is_callable($messageFormatter)) {
271
                throw new DiscomfortException('"message_formatter" must be callable');
272
            }
273
        }
274
275
        if (!is_null($valueKey)) {
276
            $templateValue = $valueKey;
277
        } else {
278
            if (!is_string($value)) {
279
                $valueType = gettype($value);
280
                $templateValue = "'{$valueType}'";
281
            } else {
282
                $templateValue = "'{$value}'";
283
            }
284
        }
285
286
        $errorMessage = $messageFormatter(
287
            $errorHandler['message'],
288
            $templateValue ?: $errorHandler['value']
289
        );
290
291
        throw new ValidationException($key, $errorMessage);
292
    }
293
}