Completed
Branch master (b7a0b6)
by Adrian
04:26 queued 02:31
created

AbstractRule::getMessage()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 14
ccs 9
cts 9
cp 1
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 8
nc 2
nop 0
crap 2
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
    /**
63
     * Options map in case the options are passed as list instead of associative array
64
     * 
65
     * @var array
66
     */
67
    protected $optionsIndexMap = array();
68
69 142
    public function __construct($options = array())
70
    {
71 142
        $options = $this->normalizeOptions($options);
72 141
        if (is_array($options) && ! empty($options)) {
73 55
            foreach ($options as $k => $v) {
74 55
                $this->setOption($k, $v);
75 55
            }
76 55
        }
77 141
    }
78
79
    /**
80
     * Method that parses the option variable and converts it into an array
81
     * You can pass anything to a validator like:
82
     * - a query string: 'min=3&max=5'
83
     * - a JSON string: '{"min":3,"max":5}'
84
     * - a CSV string: '5,true' (for this scenario the 'optionsIndexMap' property is required)
85
     *
86
     * @param mixed $options
87
     *
88
     * @return array
89
     * @throws \InvalidArgumentException
90
     */
91 142
    protected function normalizeOptions($options)
92
    {
93 142
        if (! $options) {
94 100
            return array();
95
        }
96
97 56
        if (is_array($options) && $this->arrayIsAssoc($options)) {
98 45
            return $options;
99
        }
100
101 12
        $result = $options;
102 12
        if ($options && is_string($options)) {
103 11
            $startChar = substr($options, 0, 1);
104 11
            if ($startChar == '{') {
105 7
                $result = json_decode($options, true);
106 11
            } elseif (strpos($options, '=') !== false) {
107 5
                $result = $this->parseHttpQueryString($options);
108 5
            } else {
109 1
                $result = $this->parseCsvString($options);
110
            }
111 11
        }
112
113 12
        if (! is_array($result)) {
114 1
            throw new \InvalidArgumentException('Validator options should be an array, JSON string or query string');
115
        }
116
117 11
        return $result;
118
    }
119
120
    /**
121
     * Converts a HTTP query string to an array
122
     *
123
     * @param $str
124
     *
125
     * @return array
126
     */
127 5
    protected function parseHttpQueryString($str)
128
    {
129 5
        parse_str($str, $arr);
130
131 5
        return $this->convertBooleanStrings($arr);
132
    }
133
134
    /**
135
     * Converts 'true' and 'false' strings to TRUE and FALSE
136
     *
137
     * @param $v
138
     *
139
     * @return bool|array
140
     */
141 6
    protected function convertBooleanStrings($v)
142
    {
143 6
        if (is_array($v)) {
144 6
            return array_map(array( $this, 'convertBooleanStrings' ), $v);
145
        }
146 6
        if ($v === 'true') {
147 2
            return true;
148
        }
149 6
        if ($v === 'false') {
150 2
            return false;
151
        }
152
153 6
        return $v;
154
    }
155
156
    /**
157
     * Parses a CSV string and converts the result into an "options" array
158
     * (an associative array that contains the options for the validation rule)
159
     *
160
     * @param $str
161
     *
162
     * @return array
163
     */
164 1
    protected function parseCsvString($str)
165
    {
166 1
        if (! isset($this->optionsIndexMap) || ! is_array($this->optionsIndexMap) || empty($this->optionsIndexMap)) {
167
            throw new \InvalidArgumentException(sprintf(
168
                'Class %s is missing the `optionsIndexMap` property',
169
                get_class($this)
170
            ));
171
        }
172
173 1
        $options = explode(',', $str);
174 1
        $result  = array();
175 1
        foreach ($options as $k => $v) {
176 1
            if (! isset($this->optionsIndexMap[$k])) {
177
                throw new \InvalidArgumentException(sprintf(
178
                    'Class %s does not have the index %d configured in the `optionsIndexMap` property',
179
                    get_class($this),
180
                    $k
181
                ));
182
            }
183 1
            $result[$this->optionsIndexMap[$k]] = $v;
184 1
        }
185
186 1
        return $this->convertBooleanStrings($result);
187
    }
188
189
    /**
190
     * Checks if an array is associative (ie: the keys are not numbers in sequence)
191
     *
192
     * @param array $arr
193
     *
194
     * @return bool
195
     */
196 45
    protected function arrayIsAssoc($arr)
197
    {
198 45
        return array_keys($arr) !== range(0, count($arr));
199
    }
200
201
202
    /**
203
     * Generates a unique string to identify the validator.
204
     * It is used to compare 2 validators so you don't add the same rule twice in a validator object
205
     *
206
     * @return string
207
     */
208 21
    public function getUniqueId()
209
    {
210 21
        return get_called_class() . '|' . json_encode(ksort($this->options));
211
    }
212
213
    /**
214
     * Set an option for the validator.
215
     *
216
     * The options are also be passed to the error message.
217
     *
218
     * @param string $name
219
     * @param mixed $value
220
     *
221
     * @return \Sirius\Validation\Rule\AbstractRule
222
     */
223 96
    public function setOption($name, $value)
224
    {
225 96
        $this->options[$name] = $value;
226
227 96
        return $this;
228
    }
229
230
    /**
231
     * Get an option for the validator.
232
     *
233
     * @param string $name
234
     *
235
     * @return mixed
236
     */
237 1
    public function getOption($name)
238
    {
239 1
        if (isset($this->options[$name])) {
240 1
            return $this->options[$name];
241
        } else {
242 1
            return null;
243
        }
244
    }
245
246
    /**
247
     * The context of the validator can be used when the validator depends on other values
248
     * that are not known at the moment the validator is constructed
249
     * For example, when you need to validate an email field matches another email field,
250
     * to confirm the email address
251
     *
252
     * @param array|object $context
253
     *
254
     * @throws \InvalidArgumentException
255
     * @return \Sirius\Validation\Rule\AbstractRule
256
     */
257 36
    public function setContext($context = null)
258
    {
259 36
        if ($context === null) {
260 5
            return $this;
261
        }
262 31
        if (is_array($context)) {
263 3
            $context = new ArrayWrapper($context);
264 3
        }
265 31
        if (! is_object($context) || ! $context instanceof WrapperInterface) {
266 1
            throw new \InvalidArgumentException(
267
                'Validator context must be either an array or an instance 
268
                of Sirius\Validator\DataWrapper\WrapperInterface'
269 1
            );
270
        }
271 30
        $this->context = $context;
272
273 30
        return $this;
274
    }
275
276
    /**
277
     * Custom message for this validator to used instead of the the default one
278
     *
279
     * @param string $messageTemplate
280
     *
281
     * @return \Sirius\Validation\Rule\AbstractRule
282
     */
283 22
    public function setMessageTemplate($messageTemplate)
284
    {
285 22
        $this->messageTemplate = $messageTemplate;
286
287 22
        return $this;
288
    }
289
290
    /**
291
     * Retrieves the error message template (either the global one or the custom message)
292
     *
293
     * @return string
294
     */
295 27
    public function getMessageTemplate()
296
    {
297 27
        if ($this->messageTemplate) {
298 21
            return $this->messageTemplate;
299
        }
300 8
        if (isset($this->options['label'])) {
301 1
            return constant(get_class($this) . '::LABELED_MESSAGE');
302
        }
303
304 7
        return constant(get_class($this) . '::MESSAGE');
305
    }
306
307
    /**
308
     * Validates a value
309
     *
310
     * @param mixed $value
311
     * @param null|mixed $valueIdentifier
312
     *
313
     * @return mixed
314
     */
315
    abstract public function validate($value, $valueIdentifier = null);
316
317
    /**
318
     * Sets the error message prototype that will be used when returning the error message
319
     * when validation fails.
320
     * This option can be used when you need translation
321
     *
322
     * @param ErrorMessage $errorMessagePrototype
323
     *
324
     * @throws \InvalidArgumentException
325
     * @return \Sirius\Validation\Rule\AbstractRule
326
     */
327 21
    public function setErrorMessagePrototype(ErrorMessage $errorMessagePrototype)
328
    {
329 21
        $this->errorMessagePrototype = $errorMessagePrototype;
330
331 21
        return $this;
332
    }
333
334
    /**
335
     * Returns the error message prototype.
336
     * It constructs one if there isn't one.
337
     *
338
     * @return ErrorMessage
339
     */
340 28
    public function getErrorMessagePrototype()
341
    {
342 28
        if (! $this->errorMessagePrototype) {
343 9
            $this->errorMessagePrototype = new ErrorMessage();
344 9
        }
345
346 28
        return $this->errorMessagePrototype;
347
    }
348
349
    /**
350
     * Retrieve the error message if validation failed
351
     *
352
     * @return NULL|\Sirius\Validation\ErrorMessage
353
     */
354 23
    public function getMessage()
355
    {
356 23
        if ($this->success) {
357 1
            return null;
358
        }
359 22
        $message = $this->getPotentialMessage();
360 22
        $message->setVariables(
361
            array(
362 22
                'value' => $this->value
363 22
            )
364 22
        );
365
366 22
        return $message;
367
    }
368
369
    /**
370
     * Retrieve the potential error message.
371
     * Example: when you do client-side validation you need to access the "potential error message" to be displayed
372
     *
373
     * @return ErrorMessage
374
     */
375 27
    public function getPotentialMessage()
376
    {
377 27
        $message = clone ($this->getErrorMessagePrototype());
378 27
        $message->setTemplate($this->getMessageTemplate());
379 27
        $message->setVariables($this->options);
380
381 27
        return $message;
382
    }
383
384
    /**
385
     * Method for determining the path to a related item.
386
     * Eg: for `lines[5][price]` the related item `lines[*][quantity]`
387
     * has the value identifier as `lines[5][quantity]`
388
     *
389
     * @param $valueIdentifier
390
     * @param $relatedItem
391
     *
392
     * @return string|null
393
     */
394 11
    protected function getRelatedValueIdentifier($valueIdentifier, $relatedItem)
395
    {
396
        // in case we don't have a related path
397 11
        if (strpos($relatedItem, '*') === false) {
398 9
            return $relatedItem;
399
        }
400
401
        // lines[*][quantity] is converted to ['lines', '*', 'quantity']
402 3
        $relatedItemParts = explode('[', str_replace(']', '', $relatedItem));
403
        // lines[5][price] is ['lines', '5', 'price']
404 3
        $valueIdentifierParts = explode('[', str_replace(']', '', $valueIdentifier));
405
406 3
        if (count($relatedItemParts) !== count($valueIdentifierParts)) {
407
            return $relatedItem;
408
        }
409
410
        // the result should be ['lines', '5', 'quantity']
411 3
        $relatedValueIdentifierParts = array();
412 3
        foreach ($relatedItemParts as $index => $part) {
413 3
            if ($part === '*' && isset($valueIdentifierParts[$index])) {
414 3
                $relatedValueIdentifierParts[] = $valueIdentifierParts[$index];
415 3
            } else {
416 3
                $relatedValueIdentifierParts[] = $part;
417
            }
418 3
        }
419
420 3
        $relatedValueIdentifier = implode('][', $relatedValueIdentifierParts) . ']';
421 3
        $relatedValueIdentifier = str_replace(
422 3
            $relatedValueIdentifierParts[0] . ']',
423 3
            $relatedValueIdentifierParts[0],
424
            $relatedValueIdentifier
425 3
        );
426
427 3
        return $relatedValueIdentifier;
428
    }
429
}
430