1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* Spiral Framework. |
4
|
|
|
* |
5
|
|
|
* @license MIT |
6
|
|
|
* @author Anton Titov (Wolfy-J) |
7
|
|
|
*/ |
8
|
|
|
namespace Spiral\Validation; |
9
|
|
|
|
10
|
|
|
use Interop\Container\ContainerInterface; |
11
|
|
|
use Psr\Log\LoggerAwareInterface; |
12
|
|
|
use Spiral\Core\Component; |
13
|
|
|
use Spiral\Core\Exceptions\SugarException; |
14
|
|
|
use Spiral\Core\Traits\SaturateTrait; |
15
|
|
|
use Spiral\Debug\Traits\LoggerTrait; |
16
|
|
|
use Spiral\Translator\Traits\TranslatorTrait; |
17
|
|
|
use Spiral\Validation\Configs\ValidatorConfig; |
18
|
|
|
use Spiral\Validation\Exceptions\ValidationException; |
19
|
|
|
|
20
|
|
|
/** |
21
|
|
|
* Validator is default implementation of ValidatorInterface. Class support functional rules with |
22
|
|
|
* user parameters. In addition, part of validation rules moved into validation checkers used to |
23
|
|
|
* simplify adding new rules, checkers are resolved using container and can be rebinded in |
24
|
|
|
* application. |
25
|
|
|
* |
26
|
|
|
* Examples: |
27
|
|
|
* |
28
|
|
|
* "status" => [ |
29
|
|
|
* ["notEmpty"], |
30
|
|
|
* ["string::shorter", 10, "error" => "Your string is too short."], |
31
|
|
|
* [["MyClass","myMethod"], "error" => "Custom validation failed."] |
32
|
|
|
* [, |
33
|
|
|
* "email" => [ |
34
|
|
|
* ["notEmpty", "error" => "Please enter your email address."], |
35
|
|
|
* ["email", "error" => "Email is not valid."] |
36
|
|
|
* [, |
37
|
|
|
* "pin" => [ |
38
|
|
|
* ["string::regexp", "/[0-9]{5}/", "error" => "Invalid pin format, if you don't know your |
39
|
|
|
* pin, please skip this field."] |
40
|
|
|
* [, |
41
|
|
|
* "flag" => ["notEmpty", "boolean"] |
42
|
|
|
* |
43
|
|
|
* In cases where you don't need custom message or check parameters you can use simplified |
44
|
|
|
* rule syntax: |
45
|
|
|
* "flag" => ["notEmpty", "boolean"] |
46
|
|
|
*/ |
47
|
|
|
class Validator extends Component implements ValidatorInterface, LoggerAwareInterface |
48
|
|
|
{ |
49
|
|
|
/** |
50
|
|
|
* Validator will translate default errors and throw log messages when validation rule fails. |
51
|
|
|
*/ |
52
|
|
|
use LoggerTrait, TranslatorTrait, SaturateTrait; |
53
|
|
|
|
54
|
|
|
/** |
55
|
|
|
* Return from validation rule to stop any future field validations. Internal contract. |
56
|
|
|
*/ |
57
|
|
|
const STOP_VALIDATION = -99; |
58
|
|
|
|
59
|
|
|
/** |
60
|
|
|
* @var array|\ArrayAccess |
61
|
|
|
*/ |
62
|
|
|
private $data = []; |
63
|
|
|
|
64
|
|
|
/** |
65
|
|
|
* Validation rules, see class title for description. |
66
|
|
|
* |
67
|
|
|
* @var array |
68
|
|
|
*/ |
69
|
|
|
private $rules = []; |
70
|
|
|
|
71
|
|
|
/** |
72
|
|
|
* Error messages raised while validation. |
73
|
|
|
* |
74
|
|
|
* @var array |
75
|
|
|
*/ |
76
|
|
|
private $errors = []; |
77
|
|
|
|
78
|
|
|
/** |
79
|
|
|
* Errors provided from outside. |
80
|
|
|
* |
81
|
|
|
* @var array |
82
|
|
|
*/ |
83
|
|
|
private $registeredErrors = []; |
84
|
|
|
|
85
|
|
|
/** |
86
|
|
|
* If rule has no definer error message this text will be used instead. Localizable. |
87
|
|
|
* |
88
|
|
|
* @invisible |
89
|
|
|
* @var string |
90
|
|
|
*/ |
91
|
|
|
protected $defaultMessage = "[[Condition '{condition}' does not meet.]]"; |
92
|
|
|
|
93
|
|
|
/** |
94
|
|
|
* @invisible |
95
|
|
|
* @var ContainerInterface |
96
|
|
|
*/ |
97
|
|
|
protected $container = null; |
98
|
|
|
|
99
|
|
|
/** |
100
|
|
|
* @invisible |
101
|
|
|
* @var ValidatorConfig |
102
|
|
|
*/ |
103
|
|
|
protected $config = null; |
104
|
|
|
|
105
|
|
|
/** |
106
|
|
|
* {@inheritdoc} |
107
|
|
|
* |
108
|
|
|
* @param ValidatorConfig $config |
109
|
|
|
* @param ContainerInterface $container |
110
|
|
|
* @throws SugarException |
111
|
|
|
*/ |
112
|
|
|
public function __construct( |
113
|
|
|
array $rules = [], |
114
|
|
|
$data = [], |
115
|
|
|
ValidatorConfig $config = null, |
116
|
|
|
ContainerInterface $container = null |
117
|
|
|
) { |
118
|
|
|
$this->data = $data; |
119
|
|
|
$this->rules = $rules; |
120
|
|
|
|
121
|
|
|
//Let's get validation from shared container if none provided |
122
|
|
|
$this->config = $this->saturate($config, ValidatorConfig::class); |
123
|
|
|
|
124
|
|
|
//We can use global container as fallback if no default values were provided |
125
|
|
|
$this->container = $this->saturate($container, ContainerInterface::class); |
126
|
|
|
} |
127
|
|
|
|
128
|
|
|
/** |
129
|
|
|
* {@inheritdoc} |
130
|
|
|
*/ |
131
|
|
|
public function setRules(array $rules) |
132
|
|
|
{ |
133
|
|
|
if ($this->rules == $rules) { |
134
|
|
|
return $this; |
135
|
|
|
} |
136
|
|
|
|
137
|
|
|
$this->rules = $rules; |
138
|
|
|
$this->errors = []; |
139
|
|
|
|
140
|
|
|
return $this; |
141
|
|
|
} |
142
|
|
|
|
143
|
|
|
/** |
144
|
|
|
* {@inheritdoc} |
145
|
|
|
*/ |
146
|
|
|
public function setData($data) |
147
|
|
|
{ |
148
|
|
|
if ($this->data == $data) { |
149
|
|
|
return $this; |
150
|
|
|
} |
151
|
|
|
|
152
|
|
|
$this->data = $data; |
153
|
|
|
$this->errors = []; |
154
|
|
|
|
155
|
|
|
return $this; |
156
|
|
|
} |
157
|
|
|
|
158
|
|
|
/** |
159
|
|
|
* {@inheritdoc} |
160
|
|
|
*/ |
161
|
|
|
public function isValid() |
162
|
|
|
{ |
163
|
|
|
$this->validate(); |
164
|
|
|
|
165
|
|
|
return empty($this->errors); |
166
|
|
|
} |
167
|
|
|
|
168
|
|
|
/** |
169
|
|
|
* {@inheritdoc} |
170
|
|
|
*/ |
171
|
|
|
public function registerError($field, $error) |
172
|
|
|
{ |
173
|
|
|
$this->registeredErrors[$field] = $error; |
174
|
|
|
|
175
|
|
|
return $this; |
176
|
|
|
} |
177
|
|
|
|
178
|
|
|
/** |
179
|
|
|
* {@inheritdoc} |
180
|
|
|
*/ |
181
|
|
|
public function flushRegistered() |
182
|
|
|
{ |
183
|
|
|
$this->registeredErrors = []; |
184
|
|
|
|
185
|
|
|
return $this; |
186
|
|
|
} |
187
|
|
|
|
188
|
|
|
/** |
189
|
|
|
* {@inheritdoc} |
190
|
|
|
*/ |
191
|
|
|
public function hasErrors() |
192
|
|
|
{ |
193
|
|
|
return !$this->isValid(); |
194
|
|
|
} |
195
|
|
|
|
196
|
|
|
/** |
197
|
|
|
* {@inheritdoc} |
198
|
|
|
*/ |
199
|
|
|
public function getErrors() |
200
|
|
|
{ |
201
|
|
|
$this->validate(); |
202
|
|
|
|
203
|
|
|
return $this->registeredErrors + $this->errors; |
204
|
|
|
} |
205
|
|
|
|
206
|
|
|
/** |
207
|
|
|
* Receive field from context data or return default value. |
208
|
|
|
* |
209
|
|
|
* @param string $field |
210
|
|
|
* @param mixed $default |
211
|
|
|
* @return mixed |
212
|
|
|
*/ |
213
|
|
|
public function field($field, $default = null) |
214
|
|
|
{ |
215
|
|
|
$value = isset($this->data[$field]) ? $this->data[$field] : $default; |
216
|
|
|
|
217
|
|
|
return $value instanceof ValueInterface ? $value->serializeData() : $value; |
218
|
|
|
} |
219
|
|
|
|
220
|
|
|
/** |
221
|
|
|
* Validate context data with set of validation rules. |
222
|
|
|
*/ |
223
|
|
|
protected function validate() |
224
|
|
|
{ |
225
|
|
|
$this->errors = []; |
226
|
|
|
foreach ($this->rules as $field => $rules) { |
227
|
|
|
foreach ($rules as $rule) { |
228
|
|
|
if (isset($this->errors[$field])) { |
229
|
|
|
//We are validating field till first error |
230
|
|
|
continue; |
231
|
|
|
} |
232
|
|
|
|
233
|
|
|
//Condition either rule itself or first array element |
234
|
|
|
$condition = is_string($rule) ? $rule : $rule[0]; |
235
|
|
|
|
236
|
|
|
if (empty($this->field($field)) && !$this->config->emptyCondition($condition)) { |
237
|
|
|
//There is no need to validate empty field except for special conditions |
238
|
|
|
break; |
239
|
|
|
} |
240
|
|
|
|
241
|
|
|
$result = $this->check( |
242
|
|
|
$field, |
243
|
|
|
$this->field($field), |
244
|
|
|
$condition, |
245
|
|
|
$arguments = is_string($rule) ? [] : $this->fetchArguments($rule) |
246
|
|
|
); |
247
|
|
|
|
248
|
|
|
if ($result === true) { |
249
|
|
|
//No errors |
250
|
|
|
continue; |
251
|
|
|
} |
252
|
|
|
|
253
|
|
|
if ($result === self::STOP_VALIDATION) { |
254
|
|
|
//Validation has to be stopped per rule request |
255
|
|
|
break; |
256
|
|
|
} |
257
|
|
|
|
258
|
|
|
if ($result instanceof Checker) { |
259
|
|
|
//Failed inside checker, this is implementation agreement |
260
|
|
|
if ($message = $result->getMessage($condition[1])) { |
261
|
|
|
//Checker provides it's own message for condition |
262
|
|
|
$this->addMessage( |
263
|
|
|
$field, |
264
|
|
|
is_string($rule) ? $message : $this->fetchMessage($rule, $message), |
265
|
|
|
$condition, |
266
|
|
|
$arguments |
267
|
|
|
); |
268
|
|
|
|
269
|
|
|
continue; |
270
|
|
|
} |
271
|
|
|
} |
272
|
|
|
|
273
|
|
|
//Default message |
274
|
|
|
$message = $this->say($this->defaultMessage); |
275
|
|
|
|
276
|
|
|
//Recording error message |
277
|
|
|
$this->addMessage( |
278
|
|
|
$field, |
279
|
|
|
is_string($rule) ? $message : $this->fetchMessage($rule, $message), |
280
|
|
|
$condition, |
281
|
|
|
$arguments |
282
|
|
|
); |
283
|
|
|
} |
284
|
|
|
} |
285
|
|
|
} |
286
|
|
|
|
287
|
|
|
/** |
288
|
|
|
* Check field with given condition. Can return instance of Checker (data is not valid) to |
289
|
|
|
* clarify error. |
290
|
|
|
* |
291
|
|
|
* @param string $field |
292
|
|
|
* @param mixed $value |
293
|
|
|
* @param mixed $condition Reference, can be altered if alias exists. |
294
|
|
|
* @param array $arguments Rule arguments if any. |
295
|
|
|
* @return bool|Checker |
296
|
|
|
* @throws ValidationException |
297
|
|
|
*/ |
298
|
|
|
protected function check($field, $value, &$condition, array $arguments = []) |
299
|
|
|
{ |
300
|
|
|
$condition = $this->config->resolveCondition($condition); |
301
|
|
|
|
302
|
|
|
try { |
303
|
|
|
if (strpos($condition, '::')) { |
304
|
|
|
$condition = explode('::', $condition); |
305
|
|
|
if ($this->config->hasChecker($condition[0])) { |
306
|
|
|
$checker = $this->checker($condition[0]); |
307
|
|
|
if (!$result = $checker->check($condition[1], $value, $arguments, $this)) { |
308
|
|
|
//To let validation() method know that message should be handled via Checker |
309
|
|
|
return $checker; |
310
|
|
|
} |
311
|
|
|
|
312
|
|
|
return $result; |
313
|
|
|
} |
314
|
|
|
} |
315
|
|
|
|
316
|
|
|
if (is_array($condition)) { |
317
|
|
|
//We are going to resolve class using constructor |
318
|
|
|
$condition[0] = is_object($condition[0]) |
319
|
|
|
? $condition[0] |
320
|
|
|
: $this->container->get($condition[0]); |
321
|
|
|
} |
322
|
|
|
|
323
|
|
|
//Value always coming first |
324
|
|
|
array_unshift($arguments, $value); |
325
|
|
|
|
326
|
|
|
return call_user_func_array($condition, $arguments); |
327
|
|
|
} catch (\ErrorException $exception) { |
328
|
|
|
$condition = func_get_arg(2); |
329
|
|
View Code Duplication |
if (is_array($condition)) { |
|
|
|
|
330
|
|
|
if (is_object($condition[0])) { |
331
|
|
|
$condition[0] = get_class($condition[0]); |
332
|
|
|
} |
333
|
|
|
|
334
|
|
|
$condition = join('::', $condition); |
335
|
|
|
} |
336
|
|
|
|
337
|
|
|
$this->logger()->error( |
338
|
|
|
"Condition '{condition}' failed with '{exception}' while checking '{field}' field.", |
339
|
|
|
compact('condition', 'field') + ['exception' => $exception->getMessage()] |
340
|
|
|
); |
341
|
|
|
|
342
|
|
|
return false; |
343
|
|
|
} |
344
|
|
|
} |
345
|
|
|
|
346
|
|
|
/** |
347
|
|
|
* Get or create instance of validation checker. |
348
|
|
|
* |
349
|
|
|
* @param string $name |
350
|
|
|
* @return Checker |
351
|
|
|
* @throws ValidationException |
352
|
|
|
*/ |
353
|
|
|
protected function checker($name) |
354
|
|
|
{ |
355
|
|
|
if (!$this->config->hasChecker($name)) { |
356
|
|
|
throw new ValidationException( |
357
|
|
|
"Unable to create validation checker defined by '{$name}' name." |
358
|
|
|
); |
359
|
|
|
} |
360
|
|
|
|
361
|
|
|
return $this->container->get($this->config->checkerClass($name)); |
362
|
|
|
} |
363
|
|
|
|
364
|
|
|
/** |
365
|
|
|
* Fetch validation rule arguments from rule definition. |
366
|
|
|
* |
367
|
|
|
* @param array $rule |
368
|
|
|
* @return array |
369
|
|
|
*/ |
370
|
|
|
private function fetchArguments(array $rule) |
371
|
|
|
{ |
372
|
|
|
unset($rule[0], $rule['message'], $rule['error']); |
373
|
|
|
|
374
|
|
|
return array_values($rule); |
375
|
|
|
} |
376
|
|
|
|
377
|
|
|
/** |
378
|
|
|
* Fetch error message from rule definition or use default message. Method will check "message" |
379
|
|
|
* and "error" properties of definition. |
380
|
|
|
* |
381
|
|
|
* @param array $rule |
382
|
|
|
* @param string $message Default message to use. |
383
|
|
|
* @return mixed |
384
|
|
|
*/ |
385
|
|
|
private function fetchMessage(array $rule, $message) |
386
|
|
|
{ |
387
|
|
|
if (isset($rule['message'])) { |
388
|
|
|
$message = $rule['message']; |
389
|
|
|
} |
390
|
|
|
|
391
|
|
|
if (isset($rule['error'])) { |
392
|
|
|
$message = $rule['error']; |
393
|
|
|
} |
394
|
|
|
|
395
|
|
|
return $message; |
396
|
|
|
} |
397
|
|
|
|
398
|
|
|
/** |
399
|
|
|
* Register error message for specified field. Rule definition will be interpolated into |
400
|
|
|
* message. |
401
|
|
|
* |
402
|
|
|
* @param string $field |
403
|
|
|
* @param string $message |
404
|
|
|
* @param mixed $condition |
405
|
|
|
* @param array $arguments |
406
|
|
|
*/ |
407
|
|
|
private function addMessage($field, $message, $condition, array $arguments = []) |
408
|
|
|
{ |
409
|
|
View Code Duplication |
if (is_array($condition)) { |
|
|
|
|
410
|
|
|
if (is_object($condition[0])) { |
411
|
|
|
$condition[0] = get_class($condition[0]); |
412
|
|
|
} |
413
|
|
|
|
414
|
|
|
$condition = join('::', $condition); |
415
|
|
|
} |
416
|
|
|
|
417
|
|
|
$this->errors[$field] = \Spiral\interpolate( |
418
|
|
|
$message, |
419
|
|
|
compact('field', 'condition') + $arguments |
420
|
|
|
); |
421
|
|
|
} |
422
|
|
|
} |
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.