Completed
Push — master ( aeb11c...f98da5 )
by Adrian
02:32
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 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 14
ccs 9
cts 9
cp 1
rs 9.4285
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 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);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return array_map(array($...tBooleanStrings'), $v); (array) is incompatible with the return type documented by Sirius\Validation\Rule\A...::convertBooleanStrings of type boolean.

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:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
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)) {
0 ignored issues
show
Bug introduced by
The property optionsIndexMap does not seem to exist. Did you mean options?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
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])) {
0 ignored issues
show
Bug introduced by
The property optionsIndexMap does not seem to exist. Did you mean options?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
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;
0 ignored issues
show
Bug introduced by
The property optionsIndexMap does not seem to exist. Did you mean options?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
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);
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
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