Completed
Push — master ( 705db9...4595b8 )
by Todd
50:14
created

Validation::getTranslateFieldNames()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
crap 1
1
<?php
2
/**
3
 * @author Todd Burry <[email protected]>
4
 * @copyright 2009-2017 Vanilla Forums Inc.
5
 * @license MIT
6
 */
7
8
namespace Garden\Schema;
9
10
/**
11
 * An class for collecting validation errors.
12
 */
13
class Validation {
14
    /**
15
     * @var array
16
     */
17
    private $errors = [];
18
19
    /**
20
     * @var string
21
     */
22
    private $mainMessage = '';
23
24
    /**
25
     * @var int
26
     */
27
    private $mainStatus = 0;
28
29
    /**
30
     * @var bool Whether or not fields should be translated.
31
     */
32
    private $translateFieldNames = false;
33
34
    /**
35
     * Add an error.
36
     *
37
     * @param string $field The name and path of the field to add or an empty string if this is a global error.
38
     * @param string $error The message code.
39
     * @param int|array $options An array of additional information to add to the error entry or a numeric error code.
40
     * @return $this
41
     */
42 60
    public function addError($field, $error, $options = []) {
43 60
        if (empty($error)) {
44
            throw new \InvalidArgumentException('The error code cannot be empty.', 500);
45
        }
46
47 60
        $fieldKey = $field;
48 60
        $row = ['field' => null, 'code' => null, 'path' => null, 'index' => null];
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 6 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
49
50
        // Split the field out into a path, field, and possible index.
51 60
        if (($pos = strrpos($field, '.')) !== false) {
52 5
            $row['path'] = substr($field, 0, $pos);
53 5
            $field = substr($field, $pos + 1);
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 7 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
54 5
        }
55 60
        if (preg_match('`^([^[]+)\[(\d+)\]$`', $field, $m)) {
56 3
            $row['index'] = (int)$m[2];
57 3
            $field = $m[1];
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 8 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
58 3
        }
59 60
        $row['field'] = $field;
60 60
        $row['code'] = $error;
1 ignored issue
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 2 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
61
62
        $row = array_filter($row, function ($v) {
63 60
            return $v !== null;
64 60
        });
65
66 60
        if (is_array($options)) {
67 57
            $row += $options;
68 60
        } elseif (is_int($options)) {
69 4
            $row['status'] = $options;
70 4
        }
71
72 60
        $this->errors[$fieldKey][] = $row;
73
74 60
        return $this;
75
    }
76
77
    /**
78
     * Get or set the error status code.
79
     *
80
     * The status code is an http response code and should be of the 4xx variety.
81
     *
82
     * @return int Returns the current status code.
83
     */
84 53
    public function getStatus() {
85 53
        if ($status = $this->getMainStatus()) {
86 1
            return $status;
87
        }
88
89 52
        if ($this->isValid()) {
90 1
            return 200;
91
        }
92
93
        // There was no status so loop through the errors and look for the highest one.
94 51
        $maxStatus = 0;
95 51
        foreach ($this->getRawErrors() as $error) {
96 51
            if (isset($error['status']) && $error['status'] > $maxStatus) {
97 46
                $maxStatus = $error['status'];
98 46
            }
99 51
        }
100
101 51
        return $maxStatus?: 400;
102
    }
103
104
    /**
105
     * Get the message for this exception.
106
     *
107
     * @return string Returns the exception message.
108
     */
109 54
    public function getMessage() {
110 54
        if ($message = $this->getMainMessage()) {
111 1
            return $message;
112
        }
113
114 53
        $sentence = $this->translate('%s.');
115
116
        // Generate the message by concatenating all of the errors together.
117 53
        $messages = [];
118 53
        foreach ($this->getRawErrors() as $error) {
119 53
            $message = $this->getErrorMessage($error);
120 53
            if (preg_match('`\PP$`u', $message)) {
121 2
                $message = sprintf($sentence, $message);
122 2
            }
123 53
            $messages[] = $message;
124 53
        }
125 53
        return implode(' ', $messages);
126
    }
127
128
    /**
129
     * Gets all of the errors as a flat array.
130
     *
131
     * The errors are internally stored indexed by field. This method flattens them for final error returns.
132
     *
133
     * @return array Returns all of the errors.
134
     */
135 4
    public function getErrors() {
136 4
        $result = [];
137 4
        foreach ($this->getRawErrors() as $error) {
138 4
            $result[] = $this->formatError($error);
139 4
        }
140 4
        return $result;
141
    }
142
143
    /**
144
     * Get the errors for a specific field.
145
     *
146
     * @param string $field The full path to the field.
147
     * @return array Returns an array of errors.
148
     */
149 6
    public function getFieldErrors($field) {
150 6
        if (empty($this->errors[$field])) {
151
            return [];
152
        } else {
153 6
            $result = [];
154 6
            foreach ($this->errors[$field] as $error) {
155 6
                $result[] = $this->formatError($error);
156 6
            }
157 6
            return $result;
158
        }
159
    }
160
161
    /**
162
     * Gets all of the errors as a flat array.
163
     *
164
     * The errors are internally stored indexed by field. This method flattens them for final error returns.
165
     *
166
     * @return \Traversable Returns all of the errors.
167
     */
168 57
    protected function getRawErrors() {
169 57
        foreach ($this->errors as $errors) {
170 57
            foreach ($errors as $error) {
171 57
                yield $error;
172 57
            }
173 57
        }
174 57
    }
175
176
    /**
177
     * Check whether or not the validation is free of errors.
178
     *
179
     * @return bool Returns true if there are no errors, false otherwise.
180
     */
181 92
    public function isValid() {
182 92
        return empty($this->errors);
183
    }
184
185
    /**
186
     * Check whether or not a particular field is has errors.
187
     *
188
     * @param string $field The name of the field to check for validity.
189
     * @return bool Returns true if the field has no errors, false otherwise.
190
     */
191 34
    public function isValidField($field) {
192 34
        $result = empty($this->errors[$field]);
193 34
        return $result;
194
    }
195
196
    /**
197
     * Get the error message for an error row.
198
     *
199
     * @param array $error The error row.
200
     * @return string Returns a formatted/translated error message.
201
     */
202 55
    private function getErrorMessage(array $error) {
203 55
        if (isset($error['messageCode'])) {
204 49
            $messageCode = $error['messageCode'];
205 55
        } elseif (isset($error['message'])) {
206
            return $error['message'];
207
        } else {
208 7
            $messageCode = $error['code'];
209
        }
210
211
        // Massage the field name for better formatting.
212 55
        if (!$this->getTranslateFieldNames()) {
213 54
            $field = (!empty($error['path']) ? ($error['path'][0] !== '[' ?: 'item').$error['path'].'.' : '').$error['field'];
214 54
            $field = $field ?: (isset($error['index']) ? 'item' : 'value');
215 54
            if (isset($error['index'])) {
216 3
                $field .= '['.$error['index'].']';
217 3
            }
218 54
            $error['field'] = '@'.$field;
219 55
        } elseif (isset($error['index'])) {
220
            if (empty($error['field'])) {
221
                $error['field'] = '@'.$this->formatMessage('item {index}', $error);
222
            } else {
223
                $error['field'] = '@'.$this->formatMessage('{field} at position {index}', $error);
224
            }
225 2
        } elseif (empty($error['field'])) {
226 1
            $error['field'] = 'value';
227 1
        }
228
229 55
        $msg = $this->formatMessage($messageCode, $error);
230 55
        return $msg;
231
    }
232
233
    /**
234
     * Expand and translate a message format against an array of values.
235
     *
236
     * @param string $format The message format.
237
     * @param array $context The context arguments to apply to the message.
238
     * @return string Returns a formatted string.
239
     */
240 55
    private function formatMessage($format, $context = []) {
241 55
        $format = $this->translate($format);
242
243 55
        $msg = preg_replace_callback('`({[^{}]+})`', function ($m) use ($context) {
244 52
            $args = array_filter(array_map('trim', explode(',', trim($m[1], '{}'))));
1 ignored issue
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 2 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
245 52
            $field = array_shift($args);
246 52
            return $this->formatField(isset($context[$field]) ? $context[$field] : null, $args);
247 55
        }, $format);
248 55
        return $msg;
249
    }
250
251
    /**
252
     * Translate an argument being placed in an error message.
253
     *
254
     * @param mixed $value The argument to translate.
255
     * @param array $args Formatting arguments.
256
     * @return string Returns the translated string.
257
     */
258 52
    private function formatField($value, array $args = []) {
259 52
        if (is_string($value)) {
260 51
            $r = $this->translate($value);
261 52
        } elseif (is_numeric($value)) {
262 5
            $r = $value;
263 9
        } elseif (is_array($value)) {
264 4
            $argArray = array_map([$this, 'formatField'], $value);
265 4
            $r = implode(', ', $argArray);
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 8 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
266 4
        } elseif ($value instanceof \DateTimeInterface) {
267
            $r = $value->format('c');
268
        } else {
269
            $r = $value;
270
        }
271
272 52
        $format = array_shift($args);
273
        switch ($format) {
274 52
            case 'plural':
275 6
                $singular = array_shift($args);
276 6
                $plural = array_shift($args) ?: $singular.'s';
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 3 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
277 6
                $count = is_array($value) ? count($value) : $value;
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 4 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
278 6
                $r = $count == 1 ? $singular : $plural;
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 8 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
279 6
                break;
280
        }
281
282 52
        return (string)$r;
283
    }
284
285
    /**
286
     * Translate a string.
287
     *
288
     * This method doesn't do any translation itself, but is meant for subclasses wanting to add translation ability to
289
     * this class.
290
     *
291
     * @param string $str The string to translate.
292
     * @return string Returns the translated string.
293
     */
294 56
    public function translate($str) {
295 56
        if (substr($str, 0, 1) === '@') {
296
            // This is a literal string that bypasses translation.
297 49
            return substr($str, 1);
298
        } else {
299 55
            return $str;
300
        }
301
    }
302
303
    /**
304
     * Merge another validation object with this one.
305
     *
306
     * @param Validation $validation The validation object to merge.
307
     * @param string $name The path to merge to. Use this parameter when the validation object is meant to be a subset of this one.
308
     * @return $this
309
     */
310 1
    public function merge(Validation $validation, $name = '') {
311 1
        $paths = $validation->errors;
312
313 1
        foreach ($paths as $path => $errors) {
314 1
            foreach ($errors as $error) {
315 1
                if (!empty($name)) {
316 1
                    $fullPath = "{$name}.{$path}";
317 1
                    $this->addError($fullPath, $error['code'], $error);
318 1
                }
319 1
            }
320 1
        }
321 1
        return $this;
322
    }
323
324
    /**
325
     * Get the main error message.
326
     *
327
     * If set, this message will be returned as the error message. Otherwise the message will be set from individual
328
     * errors.
329
     *
330
     * @return string Returns the main message.
331
     */
332 54
    public function getMainMessage() {
333 54
        return $this->mainMessage;
334
    }
335
336
    /**
337
     * Set the main error message.
338
     *
339
     * @param string $message The new message.
340
     * @return $this
341
     */
342 1
    public function setMainMessage($message) {
343 1
        $this->mainMessage = $message;
344 1
        return $this;
345
    }
346
347
    /**
348
     * Get the main status.
349
     *
350
     * @return int Returns an HTTP response code or zero to indicate it should be calculated.
351
     */
352 53
    public function getMainStatus() {
353 53
        return $this->mainStatus;
354
    }
355
356
    /**
357
     * Set the main status.
358
     *
359
     * @param int $status An HTTP response code or zero.
360
     * @return $this
361
     */
362 1
    public function setMainStatus($status) {
363 1
        $this->mainStatus = $status;
364 1
        return $this;
365
    }
366
367
    /**
368
     * Whether or not fields should be translated.
369
     *
370
     * @return bool Returns **true** if field names are translated or **false** otherwise.
371
     */
372 55
    public function getTranslateFieldNames() {
373 55
        return $this->translateFieldNames;
374
    }
375
376
    /**
377
     * Set whether or not fields should be translated.
378
     *
379
     * @param bool $translate Whether or not fields should be translated.
380
     * @return $this
381
     */
382 2
    public function setTranslateFieldNames($translate) {
383 2
        $this->translateFieldNames = $translate;
384 2
        return $this;
385
    }
386
387
    /**
388
     * @param $error
389
     * @return array
390
     */
391 10
    private function formatError($error) {
392 10
        $row = array_intersect_key(
393 10
            $error,
394 10
            ['field' => 1, 'path' => 1, 'index' => 1, 'code' => 1]
395 10
        );
396
397 10
        $row['message'] = $this->getErrorMessage($error);
398 10
        return $row;
399
    }
400
}
401