1
|
|
|
<?php |
2
|
|
|
namespace Sirius\Validation\Rule; |
3
|
|
|
|
4
|
|
|
use Sirius\Validation\DataWrapper\ArrayWrapper; |
5
|
|
|
use Sirius\Validation\DataWrapper\WrapperInterface; |
6
|
|
|
use Sirius\Validation\ErrorMessage; |
7
|
|
|
|
8
|
|
|
abstract class AbstractRule |
9
|
|
|
{ |
10
|
|
|
// default error message when there is no LABEL attached |
11
|
|
|
const MESSAGE = 'Value is not valid'; |
12
|
|
|
|
13
|
|
|
// default error message when there is a LABEL attached |
14
|
|
|
const LABELED_MESSAGE = '{label} is not valid'; |
15
|
|
|
|
16
|
|
|
/** |
17
|
|
|
* The validation context |
18
|
|
|
* This is the data set that the data being validated belongs to |
19
|
|
|
* @var \Sirius\Validation\DataWrapper\WrapperInterface |
20
|
|
|
*/ |
21
|
|
|
protected $context; |
22
|
|
|
|
23
|
|
|
/** |
24
|
|
|
* Options for the validator. |
25
|
|
|
* Also passed to the error message for customization. |
26
|
|
|
* |
27
|
|
|
* @var array |
28
|
|
|
*/ |
29
|
|
|
protected $options = array(); |
30
|
|
|
|
31
|
|
|
/** |
32
|
|
|
* Custom error message template for the validator instance |
33
|
|
|
* If you don't agree with the default messages that were provided |
34
|
|
|
* |
35
|
|
|
* @var string |
36
|
|
|
*/ |
37
|
|
|
protected $messageTemplate; |
38
|
|
|
|
39
|
|
|
/** |
40
|
|
|
* Result of the last validation |
41
|
|
|
* |
42
|
|
|
* @var boolean |
43
|
|
|
*/ |
44
|
|
|
protected $success = false; |
45
|
|
|
|
46
|
|
|
/** |
47
|
|
|
* Last value validated with the validator. |
48
|
|
|
* Stored in order to be passed to the errorMessage so that you get error |
49
|
|
|
* messages like '"abc" is not a valid email' |
50
|
|
|
* |
51
|
|
|
* @var mixed |
52
|
|
|
*/ |
53
|
|
|
protected $value; |
54
|
|
|
|
55
|
|
|
/** |
56
|
|
|
* The error message prototype that will be used to generate the error message |
57
|
|
|
* |
58
|
|
|
* @var ErrorMessage |
59
|
|
|
*/ |
60
|
|
|
protected $errorMessagePrototype; |
61
|
|
|
|
62
|
142 |
|
public function __construct($options = array()) |
63
|
|
|
{ |
64
|
142 |
|
$options = $this->normalizeOptions($options); |
65
|
141 |
|
if (is_array($options) && ! empty($options)) { |
66
|
55 |
|
foreach ($options as $k => $v) { |
67
|
55 |
|
$this->setOption($k, $v); |
68
|
55 |
|
} |
69
|
55 |
|
} |
70
|
141 |
|
} |
71
|
|
|
|
72
|
|
|
/** |
73
|
|
|
* Method that parses the option variable and converts it into an array |
74
|
|
|
* You can pass anything to a validator like: |
75
|
|
|
* - a query string: 'min=3&max=5' |
76
|
|
|
* - a JSON string: '{"min":3,"max":5}' |
77
|
|
|
* - a CSV string: '5,true' (for this scenario the 'optionsIndexMap' property is required) |
78
|
|
|
* |
79
|
|
|
* @param mixed $options |
80
|
|
|
* |
81
|
|
|
* @return array |
82
|
|
|
* @throws \InvalidArgumentException |
83
|
|
|
*/ |
84
|
142 |
|
protected function normalizeOptions($options) |
85
|
|
|
{ |
86
|
142 |
|
if ( ! $options) { |
87
|
100 |
|
return array(); |
88
|
|
|
} |
89
|
|
|
|
90
|
56 |
|
if (is_array($options) && $this->arrayIsAssoc($options)) { |
91
|
45 |
|
return $options; |
92
|
|
|
} |
93
|
|
|
|
94
|
12 |
|
$result = $options; |
95
|
12 |
|
if ($options && is_string($options)) { |
96
|
11 |
|
$startChar = substr($options, 0, 1); |
97
|
11 |
|
if ($startChar == '{') { |
98
|
7 |
|
$result = json_decode($options, true); |
99
|
11 |
|
} elseif (strpos($options, '=') !== false) { |
100
|
5 |
|
$result = $this->parseHttpQueryString($options); |
101
|
5 |
|
} else { |
102
|
1 |
|
$result = $this->parseCsvString($options); |
103
|
|
|
} |
104
|
11 |
|
} |
105
|
|
|
|
106
|
12 |
|
if ( ! is_array($result)) { |
107
|
1 |
|
throw new \InvalidArgumentException('Validator options should be an array, JSON string or query string'); |
108
|
|
|
} |
109
|
|
|
|
110
|
11 |
|
return $result; |
111
|
|
|
} |
112
|
|
|
|
113
|
|
|
/** |
114
|
|
|
* Converts a HTTP query string to an array |
115
|
|
|
* |
116
|
|
|
* @param $str |
117
|
|
|
* |
118
|
|
|
* @return array |
119
|
|
|
*/ |
120
|
5 |
|
protected function parseHttpQueryString($str) |
121
|
|
|
{ |
122
|
5 |
|
parse_str($str, $arr); |
123
|
|
|
|
124
|
5 |
|
return $this->convertBooleanStrings($arr); |
125
|
|
|
} |
126
|
|
|
|
127
|
|
|
/** |
128
|
|
|
* Converts 'true' and 'false' strings to TRUE and FALSE |
129
|
|
|
* |
130
|
|
|
* @param $v |
131
|
|
|
* |
132
|
|
|
* @return bool |
133
|
|
|
*/ |
134
|
6 |
|
protected function convertBooleanStrings($v) |
135
|
|
|
{ |
136
|
6 |
|
if (is_array($v)) { |
137
|
6 |
|
return array_map(array( $this, 'convertBooleanStrings' ), $v); |
|
|
|
|
138
|
|
|
} |
139
|
6 |
|
if ($v === 'true') { |
140
|
2 |
|
return true; |
141
|
|
|
} |
142
|
6 |
|
if ($v === 'false') { |
143
|
2 |
|
return false; |
144
|
|
|
} |
145
|
|
|
|
146
|
6 |
|
return $v; |
147
|
|
|
} |
148
|
|
|
|
149
|
|
|
/** |
150
|
|
|
* Parses a CSV string and converts the result into an "options" array |
151
|
|
|
* (an associative array that contains the options for the validation rule) |
152
|
|
|
* |
153
|
|
|
* @param $str |
154
|
|
|
* |
155
|
|
|
* @return array |
156
|
|
|
*/ |
157
|
1 |
|
protected function parseCsvString($str) |
158
|
|
|
{ |
159
|
1 |
|
if ( ! isset($this->optionsIndexMap) || ! is_array($this->optionsIndexMap) || empty($this->optionsIndexMap)) { |
|
|
|
|
160
|
|
|
throw new \InvalidArgumentException(sprintf('Class %s is missing the `optionsIndexMap` property', |
161
|
|
|
get_class($this))); |
162
|
|
|
} |
163
|
|
|
|
164
|
1 |
|
$options = explode(',', $str); |
165
|
1 |
|
$result = array(); |
166
|
1 |
|
foreach ($options as $k => $v) { |
167
|
1 |
|
if ( ! isset($this->optionsIndexMap[$k])) { |
|
|
|
|
168
|
|
|
throw new \InvalidArgumentException(sprintf('Class %s does not have the index %d configured in the `optionsIndexMap` property', |
169
|
|
|
get_class($this), $k)); |
170
|
|
|
} |
171
|
1 |
|
$result[$this->optionsIndexMap[$k]] = $v; |
|
|
|
|
172
|
1 |
|
} |
173
|
|
|
|
174
|
1 |
|
return $this->convertBooleanStrings($result); |
175
|
|
|
} |
176
|
|
|
|
177
|
|
|
/** |
178
|
|
|
* Checks if an array is associative (ie: the keys are not numbers in sequence) |
179
|
|
|
* |
180
|
|
|
* @param array $arr |
181
|
|
|
* |
182
|
|
|
* @return bool |
183
|
|
|
*/ |
184
|
45 |
|
protected function arrayIsAssoc($arr) |
185
|
|
|
{ |
186
|
45 |
|
return array_keys($arr) !== range(0, count($arr)); |
187
|
|
|
} |
188
|
|
|
|
189
|
|
|
|
190
|
|
|
/** |
191
|
|
|
* Generates a unique string to identify the validator. |
192
|
|
|
* It is used to compare 2 validators so you don't add the same rule twice in a validator object |
193
|
|
|
* |
194
|
|
|
* @return string |
195
|
|
|
*/ |
196
|
21 |
|
public function getUniqueId() |
197
|
|
|
{ |
198
|
21 |
|
return get_called_class() . '|' . json_encode(ksort($this->options)); |
199
|
|
|
} |
200
|
|
|
|
201
|
|
|
/** |
202
|
|
|
* Set an option for the validator. |
203
|
|
|
* |
204
|
|
|
* The options are also be passed to the error message. |
205
|
|
|
* |
206
|
|
|
* @param string $name |
207
|
|
|
* @param mixed $value |
208
|
|
|
* |
209
|
|
|
* @return \Sirius\Validation\Rule\AbstractRule |
210
|
|
|
*/ |
211
|
96 |
|
public function setOption($name, $value) |
212
|
|
|
{ |
213
|
96 |
|
$this->options[$name] = $value; |
214
|
|
|
|
215
|
96 |
|
return $this; |
216
|
|
|
} |
217
|
|
|
|
218
|
|
|
/** |
219
|
|
|
* Get an option for the validator. |
220
|
|
|
* |
221
|
|
|
* @param string $name |
222
|
|
|
* |
223
|
|
|
* @return mixed |
224
|
|
|
*/ |
225
|
1 |
|
public function getOption($name) |
226
|
|
|
{ |
227
|
1 |
|
if (isset($this->options[$name])) { |
228
|
1 |
|
return $this->options[$name]; |
229
|
|
|
} else { |
230
|
1 |
|
return null; |
231
|
|
|
} |
232
|
|
|
} |
233
|
|
|
|
234
|
|
|
/** |
235
|
|
|
* The context of the validator can be used when the validator depends on other values |
236
|
|
|
* that are not known at the moment the validator is constructed |
237
|
|
|
* For example, when you need to validate an email field matches another email field, |
238
|
|
|
* to confirm the email address |
239
|
|
|
* |
240
|
|
|
* @param array|object $context |
241
|
|
|
* |
242
|
|
|
* @throws \InvalidArgumentException |
243
|
|
|
* @return \Sirius\Validation\Rule\AbstractRule |
244
|
|
|
*/ |
245
|
36 |
|
public function setContext($context = null) |
246
|
|
|
{ |
247
|
36 |
|
if ($context === null) { |
248
|
5 |
|
return $this; |
249
|
|
|
} |
250
|
31 |
|
if (is_array($context)) { |
251
|
3 |
|
$context = new ArrayWrapper($context); |
252
|
3 |
|
} |
253
|
31 |
|
if ( ! is_object($context) || ! $context instanceof WrapperInterface) { |
254
|
1 |
|
throw new \InvalidArgumentException( |
255
|
|
|
'Validator context must be either an array or an instance of Sirius\Validator\DataWrapper\WrapperInterface' |
256
|
1 |
|
); |
257
|
|
|
} |
258
|
30 |
|
$this->context = $context; |
259
|
|
|
|
260
|
30 |
|
return $this; |
261
|
|
|
} |
262
|
|
|
|
263
|
|
|
/** |
264
|
|
|
* Custom message for this validator to used instead of the the default one |
265
|
|
|
* |
266
|
|
|
* @param string $messageTemplate |
267
|
|
|
* |
268
|
|
|
* @return \Sirius\Validation\Rule\AbstractRule |
269
|
|
|
*/ |
270
|
22 |
|
public function setMessageTemplate($messageTemplate) |
271
|
|
|
{ |
272
|
22 |
|
$this->messageTemplate = $messageTemplate; |
273
|
|
|
|
274
|
22 |
|
return $this; |
275
|
|
|
} |
276
|
|
|
|
277
|
|
|
/** |
278
|
|
|
* Retrieves the error message template (either the global one or the custom message) |
279
|
|
|
* |
280
|
|
|
* @return string |
281
|
|
|
*/ |
282
|
27 |
|
public function getMessageTemplate() |
283
|
|
|
{ |
284
|
27 |
|
if ($this->messageTemplate) { |
285
|
21 |
|
return $this->messageTemplate; |
286
|
|
|
} |
287
|
8 |
|
if (isset($this->options['label'])) { |
288
|
1 |
|
return constant(get_class($this) . '::LABELED_MESSAGE'); |
289
|
|
|
} |
290
|
|
|
|
291
|
7 |
|
return constant(get_class($this) . '::MESSAGE'); |
292
|
|
|
} |
293
|
|
|
|
294
|
|
|
/** |
295
|
|
|
* Validates a value |
296
|
|
|
* |
297
|
|
|
* @param mixed $value |
298
|
|
|
* @param null|mixed $valueIdentifier |
299
|
|
|
* |
300
|
|
|
* @return mixed |
301
|
|
|
*/ |
302
|
|
|
abstract function validate($value, $valueIdentifier = null); |
|
|
|
|
303
|
|
|
|
304
|
|
|
/** |
305
|
|
|
* Sets the error message prototype that will be used when returning the error message |
306
|
|
|
* when validation fails. |
307
|
|
|
* This option can be used when you need translation |
308
|
|
|
* |
309
|
|
|
* @param ErrorMessage $errorMessagePrototype |
310
|
|
|
* |
311
|
|
|
* @throws \InvalidArgumentException |
312
|
|
|
* @return \Sirius\Validation\Rule\AbstractRule |
313
|
|
|
*/ |
314
|
21 |
|
public function setErrorMessagePrototype(ErrorMessage $errorMessagePrototype) |
315
|
|
|
{ |
316
|
21 |
|
$this->errorMessagePrototype = $errorMessagePrototype; |
317
|
|
|
|
318
|
21 |
|
return $this; |
319
|
|
|
} |
320
|
|
|
|
321
|
|
|
/** |
322
|
|
|
* Returns the error message prototype. |
323
|
|
|
* It constructs one if there isn't one. |
324
|
|
|
* |
325
|
|
|
* @return ErrorMessage |
326
|
|
|
*/ |
327
|
28 |
|
public function getErrorMessagePrototype() |
328
|
|
|
{ |
329
|
28 |
|
if ( ! $this->errorMessagePrototype) { |
330
|
9 |
|
$this->errorMessagePrototype = new ErrorMessage(); |
331
|
9 |
|
} |
332
|
|
|
|
333
|
28 |
|
return $this->errorMessagePrototype; |
334
|
|
|
} |
335
|
|
|
|
336
|
|
|
/** |
337
|
|
|
* Retrieve the error message if validation failed |
338
|
|
|
* |
339
|
|
|
* @return NULL|\Sirius\Validation\ErrorMessage |
340
|
|
|
*/ |
341
|
23 |
|
public function getMessage() |
342
|
|
|
{ |
343
|
23 |
|
if ($this->success) { |
344
|
1 |
|
return null; |
345
|
|
|
} |
346
|
22 |
|
$message = $this->getPotentialMessage(); |
347
|
22 |
|
$message->setVariables( |
348
|
|
|
array( |
349
|
22 |
|
'value' => $this->value |
350
|
22 |
|
) |
351
|
22 |
|
); |
352
|
|
|
|
353
|
22 |
|
return $message; |
354
|
|
|
} |
355
|
|
|
|
356
|
|
|
/** |
357
|
|
|
* Retrieve the potential error message. |
358
|
|
|
* Example: when you do client-side validation you need to access the "potential error message" to be displayed |
359
|
|
|
* |
360
|
|
|
* @return ErrorMessage |
361
|
|
|
*/ |
362
|
27 |
|
public function getPotentialMessage() |
363
|
|
|
{ |
364
|
27 |
|
$message = clone ($this->getErrorMessagePrototype()); |
365
|
27 |
|
$message->setTemplate($this->getMessageTemplate()); |
366
|
27 |
|
$message->setVariables($this->options); |
367
|
|
|
|
368
|
27 |
|
return $message; |
369
|
|
|
} |
370
|
|
|
|
371
|
|
|
/** |
372
|
|
|
* Method for determining the path to a related item. |
373
|
|
|
* Eg: for `lines[5][price]` the related item `lines[*][quantity]` |
374
|
|
|
* has the value identifier as `lines[5][quantity]` |
375
|
|
|
* |
376
|
|
|
* @param $valueIdentifier |
377
|
|
|
* @param $relatedItem |
378
|
|
|
* |
379
|
|
|
* @return string|null |
380
|
|
|
*/ |
381
|
11 |
|
protected function getRelatedValueIdentifier($valueIdentifier, $relatedItem) |
382
|
|
|
{ |
383
|
|
|
// in case we don't have a related path |
384
|
11 |
|
if (strpos($relatedItem, '*') === false) { |
385
|
9 |
|
return $relatedItem; |
386
|
|
|
} |
387
|
|
|
|
388
|
|
|
// lines[*][quantity] is converted to ['lines', '*', 'quantity'] |
389
|
3 |
|
$relatedItemParts = explode('[', str_replace(']', '', $relatedItem)); |
390
|
|
|
// lines[5][price] is ['lines', '5', 'price'] |
391
|
3 |
|
$valueIdentifierParts = explode('[', str_replace(']', '', $valueIdentifier)); |
392
|
|
|
|
393
|
3 |
|
if (count($relatedItemParts) !== count($valueIdentifierParts)) { |
394
|
|
|
return $relatedItem; |
395
|
|
|
} |
396
|
|
|
|
397
|
|
|
// the result should be ['lines', '5', 'quantity'] |
398
|
3 |
|
$relatedValueIdentifierParts = array(); |
399
|
3 |
|
foreach ($relatedItemParts as $index => $part) { |
400
|
3 |
|
if ($part === '*' && isset($valueIdentifierParts[$index])) { |
401
|
3 |
|
$relatedValueIdentifierParts[] = $valueIdentifierParts[$index]; |
402
|
3 |
|
} else { |
403
|
3 |
|
$relatedValueIdentifierParts[] = $part; |
404
|
|
|
} |
405
|
3 |
|
} |
406
|
|
|
|
407
|
3 |
|
$relatedValueIdentifier = implode('][', $relatedValueIdentifierParts) . ']'; |
408
|
3 |
|
$relatedValueIdentifier = str_replace($relatedValueIdentifierParts[0] . ']', $relatedValueIdentifierParts[0], |
409
|
3 |
|
$relatedValueIdentifier); |
410
|
|
|
|
411
|
3 |
|
return $relatedValueIdentifier; |
412
|
|
|
} |
413
|
|
|
} |
414
|
|
|
|
If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.
Let’s take a look at an example:
Our function
my_function
expects aPost
object, and outputs the author of the post. The base classPost
returns a simple string and outputting a simple string will work just fine. However, the child classBlogPost
which is a sub-type ofPost
instead decided to return anobject
, and is therefore violating the SOLID principles. If aBlogPost
were passed tomy_function
, PHP would not complain, but ultimately fail when executing thestrtoupper
call in its body.