Completed
Pull Request — master (#61)
by
unknown
01:35
created

AbstractRule::getRelatedValueIdentifier()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 35

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 6.0045

Importance

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