Completed
Pull Request — master (#1)
by Todd
37:31
created

Schema::validateInteger()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 11
ccs 8
cts 8
cp 1
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 8
nc 3
nop 2
crap 3
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
 * A class for defining and validating data schemas.
12
 */
13
class Schema implements \JsonSerializable {
14
    /// Constants ///
15
16
    /**
17
     * Throw a notice when extraneous properties are encountered during validation.
18
     */
19
    const FLAG_EXTRA_PROPERTIES_NOTICE = 0x1;
20
21
    /**
22
     * Throw a ValidationException when extraneous properties are encountered during validation.
23
     */
24
    const FLAG_EXTRA_PROPERTIES_EXCEPTION = 0x2;
25
26
    /**
27
     * @var array All the known types.
28
     *
29
     * If this is ever given some sort of public access then remove the static.
30
     */
31
    private static $types = [
32
        'a' => 'array',
33
        'o' => 'object',
34
        'i' => 'integer',
35
        'int' => 'integer',
36
        's' => 'string',
37
        'str' => 'string',
38
        'n' => 'number',
39
        'b' => 'boolean',
40
        'bool' => 'boolean',
41
        'ts' => 'timestamp',
42
        'dt' => 'datetime'
43
    ];
44
45
    private $schema = [];
46
47
    /**
48
     * @var int A bitwise combination of the various **Schema::FLAG_*** constants.
49
     */
50
    private $flags = 0;
51
52
    /**
53
     * @var array An array of callbacks that will custom validate the schema.
54
     */
55
    private $validators = [];
56
57
    /**
58
     * @var string|Validation The name of the class or an instance that will be cloned.
59
     */
60
    private $validationClass = Validation::class;
61
62
63
    /// Methods ///
64
65
    /**
66
     * Initialize an instance of a new {@link Schema} class.
67
     *
68
     * @param array $schema The array schema to validate against.
69
     */
70 123
    public function __construct($schema = []) {
71 123
        $this->schema = $this->parse($schema);
72 123
    }
73
74
    /**
75
     * Grab the schema's current description.
76
     *
77
     * @return string
78
     */
79 1
    public function getDescription() {
80 1
        return isset($this->schema['description']) ? $this->schema['description'] : '';
81
    }
82
83
    /**
84
     * Set the description for the schema.
85
     *
86
     * @param string $description The new description.
87
     * @throws \InvalidArgumentException Throws an exception when the provided description is not a string.
88
     * @return Schema
89
     */
90 2
    public function setDescription($description) {
91 2
        if (is_string($description)) {
92 1
            $this->schema['description'] = $description;
93 1
        } else {
94 1
            throw new \InvalidArgumentException("The description is not a valid string.", 500);
95
        }
96
97 1
        return $this;
98
    }
99
100
    /**
101
     * Return the validation flags.
102
     *
103
     * @return int Returns a bitwise combination of flags.
104
     */
105 1
    public function getFlags() {
106 1
        return $this->flags;
107
    }
108
109
    /**
110
     * Set the validation flags.
111
     *
112
     * @param int $flags One or more of the **Schema::FLAG_*** constants.
113
     * @return Schema Returns the current instance for fluent calls.
114
     */
115 8
    public function setFlags($flags) {
116 8
        if (!is_int($flags)) {
117 1
            throw new \InvalidArgumentException('Invalid flags.', 500);
118
        }
119 7
        $this->flags = $flags;
120
121 7
        return $this;
122
    }
123
124
    /**
125
     * Whether or not the schema has a flag (or combination of flags).
126
     *
127
     * @param int $flag One or more of the **Schema::VALIDATE_*** constants.
128
     * @return bool Returns **true** if all of the flags are set or **false** otherwise.
129
     */
130 8
    public function hasFlag($flag) {
131 8
        return ($this->flags & $flag) === $flag;
132
    }
133
134
    /**
135
     * Set a flag.
136
     *
137
     * @param int $flag One or more of the **Schema::VALIDATE_*** constants.
138
     * @param bool $value Either true or false.
139
     * @return $this
140
     */
141 1
    public function setFlag($flag, $value) {
142 1
        if ($value) {
143 1
            $this->flags = $this->flags | $flag;
144 1
        } else {
145 1
            $this->flags = $this->flags & ~$flag;
146
        }
147 1
        return $this;
148
    }
149
150
    /**
151
     * Merge a schema with this one.
152
     *
153
     * @param Schema $schema A scheme instance. Its parameters will be merged into the current instance.
154
     */
155 2
    public function merge(Schema $schema) {
156
        $fn = function (array &$target, array $source) use (&$fn) {
157 2
            foreach ($source as $key => $val) {
158 2
                if (is_array($val) && array_key_exists($key, $target) && is_array($target[$key])) {
159 2
                    if (isset($val[0]) || isset($target[$key][0])) {
160
                        // This is a numeric array, so just do a merge.
161 1
                        $merged = array_merge($target[$key], $val);
162 1
                        if (is_string($merged[0])) {
163 1
                            $merged = array_keys(array_flip($merged));
164 1
                        }
165 1
                        $target[$key] = $merged;
166 1
                    } else {
167 2
                        $target[$key] = $fn($target[$key], $val);
168
                    }
169 2
                } else {
170 2
                    $target[$key] = $val;
171
                }
172 2
            }
173
174 2
            return $target;
175 2
        };
176
177 2
        $fn($this->schema, $schema->jsonSerialize());
178 2
    }
179
180
    /**
181
     * Parse a schema in short form into a full schema array.
182
     *
183
     * @param array $arr The array to parse into a schema.
184
     * @return array The full schema array.
185
     * @throws \InvalidArgumentException Throws an exception when an item in the schema is invalid.
186
     */
187 123
    public function parse(array $arr) {
188 123
        if (empty($arr)) {
189
            // An empty schema validates to anything.
190 6
            return [];
191 118
        } elseif (isset($arr['type'])) {
192
            // This is a long form schema and can be parsed as the root.
193 2
            return $this->parseNode($arr);
194
        } else {
195
            // Check for a root schema.
196 118
            $value = reset($arr);
197 118
            $key = key($arr);
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...
198 118
            if (is_int($key)) {
199 73
                $key = $value;
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...
200 73
                $value = null;
201 73
            }
202 118
            list ($name, $param) = $this->parseShortParam($key, $value);
0 ignored issues
show
Documentation introduced by
$value is of type null|false, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
203 118
            if (empty($name)) {
204 38
                return $this->parseNode($param, $value);
205
            }
206
        }
207
208
        // If we are here then this is n object schema.
209 82
        list($properties, $required) = $this->parseProperties($arr);
210
211
        $result = [
212 82
            'type' => 'object',
213 82
            'properties' => $properties,
214
            'required' => $required
215 82
        ];
216
217 82
        return array_filter($result);
218
    }
219
220
    /**
221
     * Parse a schema node.
222
     *
223
     * @param array $node The node to parse.
224
     * @param mixed $value Additional information from the node.
225
     * @return array Returns a JSON schema compatible node.
226
     */
227 118
    private function parseNode($node, $value = null) {
228 118
        if (is_array($value)) {
229
            // The value describes a bit more about the schema.
230 43
            switch ($node['type']) {
231 43
                case 'array':
232 6
                    if (isset($value['items'])) {
233
                        // The value includes array schema information.
234 1
                        $node = array_replace($node, $value);
235 1
                    } else {
236 5
                        $node['items'] = $this->parse($value);
237
                    }
238 6
                    break;
239 37
                case 'object':
240
                    // The value is a schema of the object.
241 9
                    if (isset($value['properties'])) {
242
                        list($node['properties']) = $this->parseProperties($value['properties']);
243
                    } else {
244 9
                        list($node['properties'], $required) = $this->parseProperties($value);
245 9
                        if (!empty($required)) {
246 9
                            $node['required'] = $required;
247 9
                        }
248
                    }
249 9
                    break;
250 28
                default:
251 28
                    $node = array_replace($node, $value);
252 28
                    break;
253 43
            }
254 118
        } elseif (is_string($value)) {
255 76
            if ($node['type'] === 'array' && $arrType = $this->getType($value)) {
256 2
                $node['items'] = ['type' => $arrType];
257 76
            } elseif (!empty($value)) {
258 22
                $node['description'] = $value;
259 22
            }
260 76
        }
261
262 118
        return $node;
263
    }
264
265
    /**
266
     * Parse the schema for an object's properties.
267
     *
268
     * @param array $arr An object property schema.
269
     * @return array Returns a schema array suitable to be placed in the **properties** key of a schema.
270
     */
271 82
    private function parseProperties(array $arr) {
272 82
        $properties = [];
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 9 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...
273 82
        $requiredProperties = [];
274 82
        foreach ($arr as $key => $value) {
275
            // Fix a schema specified as just a value.
276 82
            if (is_int($key)) {
277 60
                if (is_string($value)) {
278 60
                    $key = $value;
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...
279 60
                    $value = '';
280 60
                } else {
281
                    throw new \InvalidArgumentException("Schema at position $key is not a valid parameter.", 500);
282
                }
283 60
            }
284
285
            // The parameter is defined in the key.
286 82
            list($name, $param, $required) = $this->parseShortParam($key, $value);
287
288 82
            $node = $this->parseNode($param, $value);
289
290 82
            $properties[$name] = $node;
291 82
            if ($required) {
292 42
                $requiredProperties[] = $name;
293 42
            }
294 82
        }
295 82
        return array($properties, $requiredProperties);
296
    }
297
298
    /**
299
     * Parse a short parameter string into a full array parameter.
300
     *
301
     * @param string $key The short parameter string to parse.
302
     * @param array $value An array of other information that might help resolve ambiguity.
303
     * @return array Returns an array in the form `[string name, array param, bool required]`.
304
     * @throws \InvalidArgumentException Throws an exception if the short param is not in the correct format.
305
     */
306 118
    public function parseShortParam($key, $value = []) {
307
        // Is the parameter optional?
308 118
        if (substr($key, -1) === '?') {
309 58
            $required = false;
310 58
            $key = substr($key, 0, -1);
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...
311 58
        } else {
312 78
            $required = true;
313
        }
314
315
        // Check for a type.
316 118
        $parts = explode(':', $key);
317 118
        $name = $parts[0];
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...
318 118
        $type = !empty($parts[1]) && isset(self::$types[$parts[1]]) ? self::$types[$parts[1]] : null;
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...
319
320 118
        if ($value instanceof Schema) {
321 2
            if ($type === 'array') {
322 1
                $param = ['type' => $type, 'items' => $value];
323 1
            } else {
324 1
                $param = $value;
325
            }
326 118
        } elseif (isset($value['type'])) {
327
            $param = $value;
328
329
            if (!empty($type) && $type !== $param['type']) {
330
                throw new \InvalidArgumentException("Type mismatch between $type and {$param['type']} for field $name.", 500);
331
            }
332
        } else {
333 118
            if (empty($type) && !empty($parts[1])) {
334
                throw new \InvalidArgumentException("Invalid type {$parts[1]} for field $name.", 500);
335
            }
336 118
            $param = ['type' => $type];
337
338
            // Parsed required strings have a minimum length of 1.
339 118
            if ($type === 'string' && !empty($name) && $required) {
340 27
                $param['minLength'] = 1;
341 27
            }
342
        }
343
344 118
        return [$name, $param, $required];
345
    }
346
347
    /**
348
     * Add a custom validator to to validate the schema.
349
     *
350
     * @param string $fieldname The name of the field to validate, if any.
351
     *
352
     * If you are adding a validator to a deeply nested field then separate the path with dots.
353
     * @param callable $callback The callback to validate with.
354
     * @return Schema Returns `$this` for fluent calls.
355
     */
356 2
    public function addValidator($fieldname, callable $callback) {
357 2
        $this->validators[$fieldname][] = $callback;
358 2
        return $this;
359
    }
360
361
    /**
362
     * Require one of a given set of fields in the schema.
363
     *
364
     * @param array $required The field names to require.
365
     * @param string $fieldname The name of the field to attach to.
366
     * @param int $count The count of required items.
367
     * @return Schema Returns `$this` for fluent calls.
368
     */
369 1
    public function requireOneOf(array $required, $fieldname = '', $count = 1) {
370 1
        $result = $this->addValidator(
371 1
            $fieldname,
372
            function ($data, ValidationField $field) use ($required, $count) {
373 1
                $hasCount = 0;
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...
374 1
                $flattened = [];
375
376 1
                foreach ($required as $name) {
377 1
                    $flattened = array_merge($flattened, (array)$name);
378
379 1
                    if (is_array($name)) {
380
                        // This is an array of required names. They all must match.
381 1
                        $hasCountInner = 0;
382 1
                        foreach ($name as $nameInner) {
383 1
                            if (isset($data[$nameInner]) && $data[$nameInner]) {
384 1
                                $hasCountInner++;
385 1
                            } else {
386 1
                                break;
387
                            }
388 1
                        }
389 1
                        if ($hasCountInner >= count($name)) {
390 1
                            $hasCount++;
391 1
                        }
392 1
                    } elseif (isset($data[$name]) && $data[$name]) {
393 1
                        $hasCount++;
394 1
                    }
395
396 1
                    if ($hasCount >= $count) {
397 1
                        return true;
398
                    }
399 1
                }
400
401 1
                if ($count === 1) {
402 1
                    $message = 'One of {required} are required.';
403 1
                } else {
404
                    $message = '{count} of {required} are required.';
405
                }
406
407 1
                $field->addError('missingField', [
408 1
                    'messageCode' => $message,
409 1
                    'required' => $required,
410
                    'count' => $count
411 1
                ]);
412 1
                return false;
413
            }
414 1
        );
415
416 1
        return $result;
417
    }
418
419
    /**
420
     * Validate data against the schema.
421
     *
422
     * @param mixed $data The data to validate.
423
     * @param bool $sparse Whether or not this is a sparse validation.
424
     * @return mixed Returns a cleaned version of the data.
425
     * @throws ValidationException Throws an exception when the data does not validate against the schema.
426
     */
427 101
    public function validate($data, $sparse = false) {
428 101
        $validation = new ValidationField($this->createValidation(), $this->schema, '');
429
430 101
        $clean = $this->validateField($data, $validation, $sparse);
431
432 99
        if (!$validation->getValidation()->isValid()) {
433 54
            throw new ValidationException($validation->getValidation());
434
        }
435
436 56
        return $clean;
437
    }
438
439
    /**
440
     * Validate data against the schema and return the result.
441
     *
442
     * @param mixed $data The data to validate.
443
     * @param bool $sparse Whether or not to do a sparse validation.
444
     * @return bool Returns true if the data is valid. False otherwise.
445
     */
446 33
    public function isValid($data, $sparse = false) {
447
        try {
448 33
            $this->validate($data, $sparse);
449 23
            return true;
450 16
        } catch (ValidationException $ex) {
451 16
            return false;
452
        }
453
    }
454
455
    /**
456
     * Validate a field.
457
     *
458
     * @param mixed $value The value to validate.
459
     * @param ValidationField $field A validation object to add errors to.
460
     * @param bool $sparse Whether or not this is a sparse validation.
461
     * @return mixed Returns a clean version of the value with all extra fields stripped out.
462
     */
463 101
    private function validateField($value, ValidationField $field, $sparse = false) {
464 101
        $result = $value;
465 101
        if ($field->getField() instanceof Schema) {
466
            try {
467 1
                $result = $field->getField()->validate($value, $sparse);
468 1
            } catch (ValidationException $ex) {
469
                // The validation failed, so merge the validations together.
470 1
                $field->getValidation()->merge($ex->getValidation(), $field->getName());
471
            }
472 1
        } else {
473
            // Validate the field's type.
474 101
            $type = $field->getType();
475
            switch ($type) {
476 101
                case 'boolean':
477 19
                    $result = $this->validateBoolean($value, $field);
478 19
                    break;
479 89
                case 'integer':
480 20
                    $result = $this->validateInteger($value, $field);
481 20
                    break;
482 88
                case 'number':
483 7
                    $result = $this->validateNumber($value, $field);
484 7
                    break;
485 88
                case 'string':
486 45
                    $result = $this->validateString($value, $field);
487 45
                    break;
488 76
                case 'timestamp':
489 6
                    $result = $this->validateTimestamp($value, $field);
490 6
                    break;
491 76
                case 'datetime':
492 6
                    $result = $this->validateDatetime($value, $field);
493 6
                    break;
494 74
                case 'array':
495 10
                    $result = $this->validateArray($value, $field, $sparse);
496 10
                    break;
497 73
                case 'object':
498 72
                    $result = $this->validateObject($value, $field, $sparse);
499 70
                    break;
500 2
                case null:
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $type of type string|null against null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
501
                    // No type was specified so we are valid.
502 2
                    $result = $value;
503 2
                    break;
504
                default:
505
                    throw new \InvalidArgumentException("Unrecognized type $type.", 500);
506
            }
507 101
            if ($result !== null && !$this->validateEnum($value, $field)) {
508 1
                $result = null;
509 1
            }
510
        }
511
512
        // Validate a custom field validator.
513 101
        if ($result !== null) {
514 100
            $this->callValidators($result, $field);
515 100
        }
516
517 101
        return $result;
518
    }
519
520
    /**
521
     * Call all of the validators attached to a field.
522
     *
523
     * @param mixed $value The field value being validated.
524
     * @param ValidationField $field The validation object to add errors.
525
     */
526 100
    private function callValidators($value, ValidationField $field) {
527
        // Strip array references in the name except for the last one.
528 100
        $key = preg_replace(['`\[\d+\]$`', '`\[\d+\]`'], ['[]', ''], $field->getName());
529 100
        if (!empty($this->validators[$key])) {
530 2
            foreach ($this->validators[$key] as $validator) {
531 2
                call_user_func($validator, $value, $field);
532 2
            }
533 2
        }
534 100
    }
535
536
    /**
537
     * Validate an array.
538
     *
539
     * @param mixed $value The value to validate.
540
     * @param ValidationField $field The validation results to add.
541
     * @param bool $sparse Whether or not this is a sparse validation.
542
     * @return array|null Returns an array or **null** if validation fails.
543
     */
544 10
    private function validateArray($value, ValidationField $field, $sparse = false) {
545 10
        if (!is_array($value) || (count($value) > 0 && !array_key_exists(0, $value))) {
546 6
            $field->addTypeError('array');
547 6
            return null;
548 5
        } elseif (empty($value)) {
549 1
            return [];
550 5
        } elseif ($field->val('items') !== null) {
551 4
            $result = [];
552
553
            // Validate each of the types.
554 4
            $itemValidation = new ValidationField(
555 4
                $field->getValidation(),
556 4
                $field->val('items'),
557
                ''
558 4
            );
559
560 4
            foreach ($value as $i => &$item) {
561 4
                $itemValidation->setName($field->getName()."[{$i}]");
562 4
                $validItem = $this->validateField($item, $itemValidation, $sparse);
563 4
                if ($validItem !== null) {
564 4
                    $result[] = $validItem;
565 4
                }
566 4
            }
567 4
        } else {
568
            // Cast the items into a proper numeric array.
569 1
            $result = array_values($value);
570
        }
571
572 5
        return $result;
573
    }
574
575
    /**
576
     * Validate a boolean value.
577
     *
578
     * @param mixed $value The value to validate.
579
     * @param ValidationField $field The validation results to add.
580
     * @return bool|null Returns the cleaned value or **null** if validation fails.
581
     */
582 19
    private function validateBoolean($value, ValidationField $field) {
583 19
        if (!is_bool($value)) {
584
            $bools = [
585 15
                '0' => false, 'false' => false, 'no' => false, 'off' => false, '' => false,
586 15
                '1' => true, 'true' => true, 'yes' => true, 'on' => true
587 15
            ];
588 15
            if ((is_string($value) || is_numeric($value)) && isset($bools[$value])) {
589 12
                $value = $bools[$value];
590 12
            } else {
591 3
                $field->addTypeError('boolean');
592 3
                $value = null;
593
            }
594 15
        }
595 19
        return $value;
596
    }
597
598
    /**
599
     * Validate a date time.
600
     *
601
     * @param mixed $value The value to validate.
602
     * @param ValidationField $field The validation results to add.
603
     * @return \DateTimeInterface|null Returns the cleaned value or **null** if it isn't valid.
604
     */
605 6
    private function validateDatetime($value, ValidationField $field) {
606 6
        if ($value instanceof \DateTimeInterface) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
607
            // do nothing, we're good
608 6
        } elseif (is_string($value) && $value !== '') {
609
            try {
610 2
                $dt = new \DateTimeImmutable($value);
611 1
                if ($dt) {
612 1
                    $value = $dt;
613 1
                } else {
614
                    $value = null;
615
                }
616 2
            } catch (\Exception $ex) {
617 1
                $value = null;
618
            }
619 5
        } elseif (is_numeric($value) && $value > 0) {
620 1
            $value = new \DateTimeImmutable('@'.(string)round($value));
621 1
        } else {
622 2
            $value = null;
623
        }
624
625 6
        if ($value === null) {
626 3
            $field->addTypeError('datetime');
627 3
        }
628 6
        return $value;
629
    }
630
631
    /**
632
     * Validate a float.
633
     *
634
     * @param mixed $value The value to validate.
635
     * @param ValidationField $field The validation results to add.
636
     * @return float|int|null Returns a number or **null** if validation fails.
637
     */
638 7
    private function validateNumber($value, ValidationField $field) {
639 7
        if (is_float($value) || is_int($value)) {
640 2
            $result = $value;
641 7
        } elseif (is_numeric($value)) {
642 1
            $result = (float)$value;
643 1
        } else {
644 4
            $result = null;
645 4
            $field->addTypeError('number');
646
        }
647 7
        return $result;
648
    }
649
650
    /**
651
     * Validate and integer.
652
     *
653
     * @param mixed $value The value to validate.
654
     * @param ValidationField $field The validation results to add.
655
     * @return int|null Returns the cleaned value or **null** if validation fails.
656
     */
657 20
    private function validateInteger($value, ValidationField $field) {
658 20
        if (is_int($value)) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
659
            // Do nothing, we're good.
660 20
        } elseif (is_numeric($value)) {
661 2
            $value = (int)$value;
662 2
        } else {
663 8
            $value = null;
664 8
            $field->addTypeError('integer');
665
        }
666 20
        return $value;
667
    }
668
669
    /**
670
     * Validate an object.
671
     *
672
     * @param mixed $value The value to validate.
673
     * @param ValidationField $field The validation results to add.
674
     * @param bool $sparse Whether or not this is a sparse validation.
675
     * @return object|null Returns a clean object or **null** if validation fails.
0 ignored issues
show
Documentation introduced by
Should the return type not be null|array?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
676
     */
677 72
    private function validateObject($value, ValidationField $field, $sparse = false) {
678 72
        if (!is_array($value) || isset($value[0])) {
679 6
            $field->addTypeError('object');
680 6
            return null;
681 72
        } elseif (is_array($field->val('properties'))) {
682
            // Validate the data against the internal schema.
683 72
            $value = $this->validateProperties($value, $field, $sparse);
684 70
        }
685 70
        return $value;
686
    }
687
688
    /**
689
     * Validate data against the schema and return the result.
690
     *
691
     * @param array $data The data to validate.
692
     * @param ValidationField $field This argument will be filled with the validation result.
693
     * @param bool $sparse Whether or not this is a sparse validation.
694
     * @return array Returns a clean array with only the appropriate properties and the data coerced to proper types.
695
     */
696 72
    private function validateProperties(array $data, ValidationField $field, $sparse = false) {
697 72
        $properties = $field->val('properties', []);
698 72
        $required = array_flip($field->val('required', []));
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...
699 72
        $keys = array_keys($data);
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...
700 72
        $keys = array_combine(array_map('strtolower', $keys), $keys);
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...
701
702 72
        $propertyField = new ValidationField($field->getValidation(), [], null);
703
704
        // Loop through the schema fields and validate each one.
705 72
        $clean = [];
706 72
        foreach ($properties as $propertyName => $property) {
707
            $propertyField
708 72
                ->setField($property)
709 72
                ->setName(ltrim($field->getName().".$propertyName", '.'));
710
711 72
            $lName = strtolower($propertyName);
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...
712 72
            $isRequired = isset($required[$propertyName]);
713
714
            // First check for required fields.
715 72
            if (!array_key_exists($lName, $keys)) {
716
                // A sparse validation can leave required fields out.
717 18
                if ($isRequired && !$sparse) {
718 6
                    $propertyField->addError('missingField', ['messageCode' => '{field} is required.']);
719 6
                }
720 72
            } elseif ($data[$keys[$lName]] === null) {
721 17
                if ($isRequired) {
722 9
                    $propertyField->addError('missingField', ['messageCode' => '{field} cannot be null.']);
723 9
                } else {
724 8
                    $clean[$propertyName] = null;
725
                }
726 17
            } else {
727 64
                $clean[$propertyName] = $this->validateField($data[$keys[$lName]], $propertyField, $sparse);
728
            }
729
730 72
            unset($keys[$lName]);
731 72
        }
732
733
        // Look for extraneous properties.
734 72
        if (!empty($keys)) {
735 7
            if ($this->hasFlag(Schema::FLAG_EXTRA_PROPERTIES_NOTICE)) {
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
736 2
                $msg = sprintf("%s has unexpected field(s): %s.", $field->getName() ?: 'value', implode(', ', $keys));
737 2
                trigger_error($msg, E_USER_NOTICE);
738
            }
739
740 5
            if ($this->hasFlag(Schema::FLAG_EXTRA_PROPERTIES_EXCEPTION)) {
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
741 2
                $field->addError('invalid', [
742 2
                    'messageCode' => '{field} has {extra,plural,an unexpected field,unexpected fields}: {extra}.',
743 2
                    'extra' => array_values($keys),
744
                    'status' => 422
745 2
                ]);
746 2
            }
747 5
        }
748
749 70
        return $clean;
750
    }
751
752
    /**
753
     * Validate a string.
754
     *
755
     * @param mixed $value The value to validate.
756
     * @param ValidationField $field The validation results to add.
757
     * @return string|null Returns the valid string or **null** if validation fails.
758
     */
759 45
    private function validateString($value, ValidationField $field) {
760 45
        if (is_string($value) || is_numeric($value)) {
761 43
            $value = $result = (string)$value;
762 43
        } else {
763 3
            $field->addTypeError('string');
764 3
            return null;
765
        }
766
767 43
        if (($minLength = $field->val('minLength', 0)) > 0 && mb_strlen($value) < $minLength) {
768 4
            if (!empty($field->getName()) && $minLength === 1) {
769 2
                $field->addError('missingField', ['messageCode' => '{field} is required.', 'status' => 422]);
770 2
            } else {
771 2
                $field->addError(
772 2
                    'minLength',
773
                    [
774 2
                        'messageCode' => '{field} should be at least {minLength} {minLength,plural,character} long.',
775 2
                        'minLength' => $minLength,
776
                        'status' => 422
777 2
                    ]
778 2
                );
779
            }
780 4
            $result = null;
781 4
        }
782 43
        if (($maxLength = $field->val('maxLength', 0)) > 0 && mb_strlen($value) > $maxLength) {
783 1
            $field->addError(
784 1
                'maxLength',
785
                [
786 1
                    'messageCode' => '{field} is {overflow} {overflow,plural,characters} too long.',
787 1
                    'maxLength' => $maxLength,
788 1
                    'overflow' => mb_strlen($value) - $maxLength,
789
                    'status' => 422
790 1
                ]
791 1
            );
792 1
            $result = null;
793 1
        }
794 43
        if ($pattern = $field->val('pattern')) {
795 4
            $regex = '`'.str_replace('`', preg_quote('`', '`'), $pattern).'`';
796
797 4
            if (!preg_match($regex, $value)) {
798 2
                $field->addError(
799 2
                    'invalid',
800
                    [
801 2
                        'messageCode' => '{field} is in the incorrect format.',
802
                        'status' => 422
803 2
                    ]
804 2
                );
805 2
            }
806 4
            $result = null;
807 4
        }
808 43
        if ($format = $field->val('format')) {
809 11
            $type = $format;
810
            switch ($format) {
811 11
                case 'email':
812 1
                    $result = filter_var($result, FILTER_VALIDATE_EMAIL);
813 1
                    break;
814 10
                case 'ipv4':
815 1
                    $type = 'IPv4 address';
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...
816 1
                    $result = filter_var($result, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4);
817 1
                    break;
818 9
                case 'ipv6':
819 1
                    $type = 'IPv6 address';
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...
820 1
                    $result = filter_var($result, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6);
821 1
                    break;
822 8
                case 'ip':
823 1
                    $type = 'IP address';
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...
824 1
                    $result = filter_var($result, FILTER_VALIDATE_IP);
825 1
                    break;
826 7
                case 'uri':
827 7
                    $type = 'URI';
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...
828 7
                    $result = filter_var($result, FILTER_VALIDATE_URL, FILTER_FLAG_HOST_REQUIRED | FILTER_FLAG_SCHEME_REQUIRED);
829 7
                    break;
830
                default:
831
                    trigger_error("Unrecognized format '$format'.", E_USER_NOTICE);
832
            }
833 11
            if ($result === false) {
834 5
                $field->addTypeError($type);
835 5
            }
836 11
        }
837
838 43
        return $result;
839
    }
840
841
    /**
842
     * Validate a unix timestamp.
843
     *
844
     * @param mixed $value The value to validate.
845
     * @param ValidationField $field The field being validated.
846
     * @return int|null Returns a valid timestamp or **null** if the value doesn't validate.
847
     */
848 6
    private function validateTimestamp($value, ValidationField $field) {
849 6
        if (is_numeric($value) && $value > 0) {
850 1
            $result = (int)$value;
851 6
        } elseif (is_string($value) && $ts = strtotime($value)) {
852 1
            $result = $ts;
853 1
        } else {
854 4
            $field->addTypeError('timestamp');
855 4
            $result = null;
856
        }
857 6
        return $result;
858
    }
859
860
    /**
861
     * Validate a value against an enum.
862
     *
863
     * @param mixed $value The value to test.
864
     * @param ValidationField $field The validation object for adding errors.
865
     * @return bool Returns **true** if the value one of the enumerated values or **false** otherwise.
866
     */
867 100
    private function validateEnum($value, ValidationField $field) {
868 100
        $enum = $field->val('enum');
869 100
        if (empty($enum)) {
870 99
            return true;
871
        }
872
873 1
        if (!in_array($value, $enum, true)) {
874 1
            $field->addError(
875 1
                'invalid',
876
                [
877 1
                    'messageCode' => '{field} must be one of: {enum}.',
878 1
                    'enum' => $enum,
879
                    'status' => 422
880 1
                ]
881 1
            );
882 1
            return false;
883
        }
884 1
        return true;
885
    }
886
887
    /**
888
     * Specify data which should be serialized to JSON.
889
     *
890
     * @link http://php.net/manual/en/jsonserializable.jsonserialize.php
891
     * @return mixed data which can be serialized by <b>json_encode</b>,
892
     * which is a value of any type other than a resource.
893
     */
894 20
    public function jsonSerialize() {
895 20
        $result = $this->schema;
896 20
        array_walk_recursive($result, function (&$value) {
897 20
            if ($value instanceof \JsonSerializable) {
898 1
                $value = $value->jsonSerialize();
899 1
            }
900 20
        });
901 20
        return $result;
902
    }
903
904
    /**
905
     * Look up a type based on its alias.
906
     *
907
     * @param string $alias The type alias or type name to lookup.
908
     * @return mixed
909
     */
910 9
    private function getType($alias) {
911 9
        if (isset(self::$types[$alias])) {
912 2
            $type = self::$types[$alias];
913 9
        } elseif (array_search($alias, self::$types) !== false) {
914
            $type = $alias;
915
        } else {
916 8
            $type = null;
917
        }
918 9
        return $type;
919
    }
920
921
    /**
922
     * Get the class that's used to contain validation information.
923
     *
924
     * @return Validation|string Returns the validation class.
925
     */
926 101
    public function getValidationClass() {
927 101
        return $this->validationClass;
928
    }
929
930
    /**
931
     * Set the class that's used to contain validation information.
932
     *
933
     * @param Validation|string $class Either the name of a class or a class that will be cloned.
934
     * @return $this
935
     */
936 1
    public function setValidationClass($class) {
937 1
        if (!is_a($class, Validation::class, true)) {
938
            throw new \InvalidArgumentException("$class must be a subclass of ".Validation::class, 500);
939
        }
940
941 1
        $this->validationClass = $class;
942 1
        return $this;
943
    }
944
945
    /**
946
     * Create a new validation instance.
947
     *
948
     * @return Validation Returns a validation object.
949
     */
950 101
    protected function createValidation() {
951 101
        $class = $this->getValidationClass();
952
953 101
        if ($class instanceof Validation) {
954 1
            $result = clone $class;
955 1
        } else {
956 101
            $result = new $class;
957
        }
958 101
        return $result;
959
    }
960
}
961