Completed
Push — master ( 90d214...d1cfb9 )
by Todd
27s queued 22s
created

Schema::setFlag()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

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

This check looks for the bodies of elseif 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 elseif bodies can be removed. If you have an empty elseif but statements in the else branch, consider inverting the condition.

Loading history...
216
                // Do nothing, we aren't replacing.
217 3
            } else {
218 5
                $target[$key] = $val;
219
            }
220 6
        }
221
222 6
        if (isset($required)) {
223 4
            if (empty($required)) {
224 1
                unset($target['required']);
225 1
            } else {
226 4
                $target['required'] = $required;
227
            }
228 4
        }
229
230 6
        return $target;
231
    }
232
233
//    public function overlay(Schema $schema )
0 ignored issues
show
Unused Code Comprehensibility introduced by
42% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
234
235
    /**
236
     * Returns the internal schema array.
237
     *
238
     * @return array
239
     * @see Schema::jsonSerialize()
240
     */
241 15
    public function getSchemaArray() {
242 15
        return $this->schema;
243
    }
244
245
    /**
246
     * Parse a short schema and return the associated schema.
247
     *
248
     * @param array $arr The schema array.
249
     * @param mixed ...$args Constructor arguments for the schema instance.
250
     * @return static Returns a new schema.
251
     */
252 150
    public static function parse(array $arr, ...$args) {
253 150
        $schema = new static([], ...$args);
0 ignored issues
show
Unused Code introduced by
The call to Schema::__construct() has too many arguments starting with $args.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
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...
254 150
        $schema->schema = $schema->parseInternal($arr);
255 150
        return $schema;
256
    }
257
258
    /**
259
     * Parse a schema in short form into a full schema array.
260
     *
261
     * @param array $arr The array to parse into a schema.
262
     * @return array The full schema array.
263
     * @throws \InvalidArgumentException Throws an exception when an item in the schema is invalid.
264
     */
265 150
    protected function parseInternal(array $arr) {
266 150
        if (empty($arr)) {
267
            // An empty schema validates to anything.
268 7
            return [];
269 144
        } elseif (isset($arr['type'])) {
270
            // This is a long form schema and can be parsed as the root.
271
            return $this->parseNode($arr);
272
        } else {
273
            // Check for a root schema.
274 144
            $value = reset($arr);
275 144
            $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...
276 144
            if (is_int($key)) {
277 90
                $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...
278 90
                $value = null;
279 90
            }
280 144
            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...
281 144
            if (empty($name)) {
282 52
                return $this->parseNode($param, $value);
283
            }
284
        }
285
286
        // If we are here then this is n object schema.
287 94
        list($properties, $required) = $this->parseProperties($arr);
288
289
        $result = [
290 94
            'type' => 'object',
291 94
            'properties' => $properties,
292
            'required' => $required
293 94
        ];
294
295 94
        return array_filter($result);
296
    }
297
298
    /**
299
     * Parse a schema node.
300
     *
301
     * @param array $node The node to parse.
302
     * @param mixed $value Additional information from the node.
303
     * @return array Returns a JSON schema compatible node.
304
     */
305 144
    private function parseNode($node, $value = null) {
306 144
        if (is_array($value)) {
307
            // The value describes a bit more about the schema.
308 53
            switch ($node['type']) {
309 53
                case 'array':
310 7
                    if (isset($value['items'])) {
311
                        // The value includes array schema information.
312 1
                        $node = array_replace($node, $value);
313 1
                    } else {
314 6
                        $node['items'] = $this->parseInternal($value);
315
                    }
316 7
                    break;
317 46
                case 'object':
318
                    // The value is a schema of the object.
319 10
                    if (isset($value['properties'])) {
320
                        list($node['properties']) = $this->parseProperties($value['properties']);
321
                    } else {
322 10
                        list($node['properties'], $required) = $this->parseProperties($value);
323 10
                        if (!empty($required)) {
324 10
                            $node['required'] = $required;
325 10
                        }
326
                    }
327 10
                    break;
328 36
                default:
329 36
                    $node = array_replace($node, $value);
330 36
                    break;
331 53
            }
332 144
        } elseif (is_string($value)) {
333 86
            if ($node['type'] === 'array' && $arrType = $this->getType($value)) {
334 2
                $node['items'] = ['type' => $arrType];
335 86
            } elseif (!empty($value)) {
336 23
                $node['description'] = $value;
337 23
            }
338 109
        } elseif ($value === null) {
339
            // Parse child elements.
340 24
            if ($node['type'] === 'array' && isset($node['items'])) {
341
                // The value includes array schema information.
342
                $node['items'] = $this->parseInternal($node['items']);
343 24
            } elseif ($node['type'] === 'object' && isset($node['properties'])) {
344
                list($node['properties']) = $this->parseProperties($node['properties']);
345
346
            }
347 24
        }
348
349 144
        if (is_array($node) && $node['type'] === null) {
350 3
            unset($node['type']);
351 3
        }
352
353 144
        return $node;
354
    }
355
356
    /**
357
     * Parse the schema for an object's properties.
358
     *
359
     * @param array $arr An object property schema.
360
     * @return array Returns a schema array suitable to be placed in the **properties** key of a schema.
361
     */
362 94
    private function parseProperties(array $arr) {
363 94
        $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...
364 94
        $requiredProperties = [];
365 94
        foreach ($arr as $key => $value) {
366
            // Fix a schema specified as just a value.
367 94
            if (is_int($key)) {
368 70
                if (is_string($value)) {
369 70
                    $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...
370 70
                    $value = '';
371 70
                } else {
372
                    throw new \InvalidArgumentException("Schema at position $key is not a valid parameter.", 500);
373
                }
374 70
            }
375
376
            // The parameter is defined in the key.
377 94
            list($name, $param, $required) = $this->parseShortParam($key, $value);
378
379 94
            $node = $this->parseNode($param, $value);
380
381 94
            $properties[$name] = $node;
382 94
            if ($required) {
383 48
                $requiredProperties[] = $name;
384 48
            }
385 94
        }
386 94
        return array($properties, $requiredProperties);
387
    }
388
389
    /**
390
     * Parse a short parameter string into a full array parameter.
391
     *
392
     * @param string $key The short parameter string to parse.
393
     * @param array $value An array of other information that might help resolve ambiguity.
394
     * @return array Returns an array in the form `[string name, array param, bool required]`.
395
     * @throws \InvalidArgumentException Throws an exception if the short param is not in the correct format.
396
     */
397 144
    public function parseShortParam($key, $value = []) {
398
        // Is the parameter optional?
399 144
        if (substr($key, -1) === '?') {
400 66
            $required = false;
401 66
            $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...
402 66
        } else {
403 98
            $required = true;
404
        }
405
406
        // Check for a type.
407 144
        $parts = explode(':', $key);
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 5 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...
408 144
        $name = $parts[0];
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...
409 144
        $allowNull = false;
410 144
        if (!empty($parts[1])) {
411 143
            $types = explode('|', $parts[1]);
412 143
            foreach ($types as $alias) {
413 143
                $found = $this->getType($alias);
414 143
                if ($found === null) {
415
                    throw new \InvalidArgumentException("Unknown type '$alias'", 500);
416 143
                } elseif ($found === 'null') {
417 11
                    $allowNull = true;
418 11
                } else {
419 143
                    $type = $found;
420
                }
421 143
            }
422 143
        } else {
423 4
            $type = null;
424
        }
425
426 144
        if ($value instanceof Schema) {
427 2
            if ($type === 'array') {
428 1
                $param = ['type' => $type, 'items' => $value];
0 ignored issues
show
Bug introduced by
The variable $type does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
429 1
            } else {
430 1
                $param = $value;
431
            }
432 144
        } elseif (isset($value['type'])) {
433
            $param = $value;
434
435
            if (!empty($type) && $type !== $param['type']) {
436
                throw new \InvalidArgumentException("Type mismatch between $type and {$param['type']} for field $name.", 500);
437
            }
438
        } else {
439 144
            if (empty($type) && !empty($parts[1])) {
440
                throw new \InvalidArgumentException("Invalid type {$parts[1]} for field $name.", 500);
441
            }
442 144
            $param = ['type' => $type];
443
444
            // Parsed required strings have a minimum length of 1.
445 144
            if ($type === 'string' && !empty($name) && $required && (!isset($value['default']) || $value['default'] !== '')) {
446 29
                $param['minLength'] = 1;
447 29
            }
448
        }
449 144
        if ($allowNull) {
450 11
            $param['allowNull'] = true;
451 11
        }
452
453 144
        return [$name, $param, $required];
454
    }
455
456
    /**
457
     * Add a custom validator to to validate the schema.
458
     *
459
     * @param string $fieldname The name of the field to validate, if any.
460
     *
461
     * If you are adding a validator to a deeply nested field then separate the path with dots.
462
     * @param callable $callback The callback to validate with.
463
     * @return Schema Returns `$this` for fluent calls.
464
     */
465 2
    public function addValidator($fieldname, callable $callback) {
466 2
        $this->validators[$fieldname][] = $callback;
467 2
        return $this;
468
    }
469
470
    /**
471
     * Require one of a given set of fields in the schema.
472
     *
473
     * @param array $required The field names to require.
474
     * @param string $fieldname The name of the field to attach to.
475
     * @param int $count The count of required items.
476
     * @return Schema Returns `$this` for fluent calls.
477
     */
478 1
    public function requireOneOf(array $required, $fieldname = '', $count = 1) {
479 1
        $result = $this->addValidator(
480 1
            $fieldname,
481
            function ($data, ValidationField $field) use ($required, $count) {
482 1
                $hasCount = 0;
483 1
                $flattened = [];
484
485 1
                foreach ($required as $name) {
486 1
                    $flattened = array_merge($flattened, (array)$name);
487
488 1
                    if (is_array($name)) {
489
                        // This is an array of required names. They all must match.
490 1
                        $hasCountInner = 0;
491 1
                        foreach ($name as $nameInner) {
492 1
                            if (isset($data[$nameInner]) && $data[$nameInner]) {
493 1
                                $hasCountInner++;
494 1
                            } else {
495 1
                                break;
496
                            }
497 1
                        }
498 1
                        if ($hasCountInner >= count($name)) {
499 1
                            $hasCount++;
500 1
                        }
501 1
                    } elseif (isset($data[$name]) && $data[$name]) {
502 1
                        $hasCount++;
503 1
                    }
504
505 1
                    if ($hasCount >= $count) {
506 1
                        return true;
507
                    }
508 1
                }
509
510 1
                if ($count === 1) {
511 1
                    $message = 'One of {required} are required.';
512 1
                } else {
513
                    $message = '{count} of {required} are required.';
514
                }
515
516 1
                $field->addError('missingField', [
517 1
                    'messageCode' => $message,
518 1
                    'required' => $required,
519
                    'count' => $count
520 1
                ]);
521 1
                return false;
522
            }
523 1
        );
524
525 1
        return $result;
526
    }
527
528
    /**
529
     * Validate data against the schema.
530
     *
531
     * @param mixed $data The data to validate.
532
     * @param bool $sparse Whether or not this is a sparse validation.
533
     * @return mixed Returns a cleaned version of the data.
534
     * @throws ValidationException Throws an exception when the data does not validate against the schema.
535
     */
536 123
    public function validate($data, $sparse = false) {
537 123
        $field = new ValidationField($this->createValidation(), $this->schema, '');
538
539 123
        $clean = $this->validateField($data, $field, $sparse);
540
541 121
        if (Invalid::isInvalid($clean) && $field->isValid()) {
542
            // This really shouldn't happen, but we want to protect against seeing the invalid object.
543
            $field->addError('invalid', ['messageCode' => '{field} is invalid.', 'status' => 422]);
544
        }
545
546 121
        if (!$field->getValidation()->isValid()) {
547 55
            throw new ValidationException($field->getValidation());
548
        }
549
550 77
        return $clean;
551
    }
552
553
    /**
554
     * Validate data against the schema and return the result.
555
     *
556
     * @param mixed $data The data to validate.
557
     * @param bool $sparse Whether or not to do a sparse validation.
558
     * @return bool Returns true if the data is valid. False otherwise.
559
     */
560 33
    public function isValid($data, $sparse = false) {
561
        try {
562 33
            $this->validate($data, $sparse);
563 23
            return true;
564 16
        } catch (ValidationException $ex) {
565 16
            return false;
566
        }
567
    }
568
569
    /**
570
     * Validate a field.
571
     *
572
     * @param mixed $value The value to validate.
573
     * @param ValidationField $field A validation object to add errors to.
574
     * @param bool $sparse Whether or not this is a sparse validation.
575
     * @return mixed|Invalid Returns a clean version of the value with all extra fields stripped out or invalid if the value
576
     * is completely invalid.
577
     */
578 123
    protected function validateField($value, ValidationField $field, $sparse = false) {
579 123
        $result = $value;
580
581 123
        if ($field->getField() instanceof Schema) {
582
            try {
583 1
                $result = $field->getField()->validate($value, $sparse);
584 1
            } catch (ValidationException $ex) {
585
                // The validation failed, so merge the validations together.
586 1
                $field->getValidation()->merge($ex->getValidation(), $field->getName());
587
            }
588 123
        } elseif (($value === null || ($value === '' && $field->getType() !== 'string')) && $field->val('allowNull', false)) {
589 11
            $result = null;
590 11
        } else {
591
            // Validate the field's type.
592 123
            $type = $field->getType();
593
            switch ($type) {
594 123
                case 'boolean':
595 21
                    $result = $this->validateBoolean($value, $field);
596 21
                    break;
597 110
                case 'integer':
598 22
                    $result = $this->validateInteger($value, $field);
599 22
                    break;
600 108
                case 'number':
601 8
                    $result = $this->validateNumber($value, $field);
602 8
                    break;
603 107
                case 'string':
604 52
                    $result = $this->validateString($value, $field);
605 52
                    break;
606 90
                case 'timestamp':
607 7
                    $result = $this->validateTimestamp($value, $field);
608 7
                    break;
609 89
                case 'datetime':
610 8
                    $result = $this->validateDatetime($value, $field);
611 8
                    break;
612 86
                case 'array':
613 12
                    $result = $this->validateArray($value, $field, $sparse);
614 12
                    break;
615 84
                case 'object':
616 83
                    $result = $this->validateObject($value, $field, $sparse);
617 81
                    break;
618 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...
619
                    // No type was specified so we are valid.
620 2
                    $result = $value;
621 2
                    break;
622
                default:
623
                    throw new \InvalidArgumentException("Unrecognized type $type.", 500);
624
            }
625 123
            if (Invalid::isValid($result)) {
626 121
                $result = $this->validateEnum($result, $field);
627 121
            }
628
        }
629
630
        // Validate a custom field validator.
631 123
        if (Invalid::isValid($result)) {
632 121
            $this->callValidators($result, $field);
633 121
        }
634
635 123
        return $result;
636
    }
637
638
    /**
639
     * Validate an array.
640
     *
641
     * @param mixed $value The value to validate.
642
     * @param ValidationField $field The validation results to add.
643
     * @param bool $sparse Whether or not this is a sparse validation.
644
     * @return array|Invalid Returns an array or invalid if validation fails.
645
     */
646 12
    protected function validateArray($value, ValidationField $field, $sparse = false) {
647 12
        if ((!is_array($value) || (count($value) > 0 && !array_key_exists(0, $value))) && !$value instanceof \Traversable) {
648 6
            $field->addTypeError('array');
649 6
            return Invalid::value();
650 7
        } elseif ($field->val('items') !== null) {
651 5
            $result = [];
652
653
            // Validate each of the types.
654 5
            $itemValidation = new ValidationField(
655 5
                $field->getValidation(),
656 5
                $field->val('items'),
657
                ''
658 5
            );
659
660 5
            $count = 0;
661 5
            foreach ($value as $i => $item) {
662 5
                $itemValidation->setName($field->getName()."[{$i}]");
663 5
                $validItem = $this->validateField($item, $itemValidation, $sparse);
664 5
                if (Invalid::isValid($validItem)) {
665 5
                    $result[] = $validItem;
666 5
                }
667 5
                $count++;
668 5
            }
669
670 5
            return empty($result) && $count > 0 ? Invalid::value() : $result;
671
        } else {
672
            // Cast the items into a proper numeric array.
673 2
            $result = is_array($value) ? array_values($value) : iterator_to_array($value);
674 2
            return $result;
675
        }
676
    }
677
678
    /**
679
     * Validate a boolean value.
680
     *
681
     * @param mixed $value The value to validate.
682
     * @param ValidationField $field The validation results to add.
683
     * @return bool|Invalid Returns the cleaned value or invalid if validation fails.
684
     */
685 21
    protected function validateBoolean($value, ValidationField $field) {
686 21
        $value = $value === null ? $value : filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
687 21
        if ($value === null) {
688 4
            $field->addTypeError('boolean');
689 4
            return Invalid::value();
690
        }
691 18
        return $value;
692
    }
693
694
    /**
695
     * Validate a date time.
696
     *
697
     * @param mixed $value The value to validate.
698
     * @param ValidationField $field The validation results to add.
699
     * @return \DateTimeInterface|Invalid Returns the cleaned value or **null** if it isn't valid.
700
     */
701 12
    protected function validateDatetime($value, ValidationField $field) {
702 12
        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...
703
            // do nothing, we're good
704 12
        } elseif (is_string($value) && $value !== '') {
705
            try {
706 7
                $dt = new \DateTimeImmutable($value);
707 5
                if ($dt) {
708 5
                    $value = $dt;
709 5
                } else {
710
                    $value = null;
711
                }
712 7
            } catch (\Exception $ex) {
713 2
                $value = Invalid::value();
714
            }
715 10
        } elseif (is_int($value) && $value > 0) {
716 1
            $value = new \DateTimeImmutable('@'.(string)round($value));
717 1
        } else {
718 2
            $value = Invalid::value();
719
        }
720
721 12
        if (Invalid::isInvalid($value)) {
722 4
            $field->addTypeError('datetime');
723 4
        }
724 12
        return $value;
725
    }
726
727
    /**
728
     * Validate a float.
729
     *
730
     * @param mixed $value The value to validate.
731
     * @param ValidationField $field The validation results to add.
732
     * @return float|Invalid Returns a number or **null** if validation fails.
733
     */
734 8
    protected function validateNumber($value, ValidationField $field) {
735 8
        $result = filter_var($value, FILTER_VALIDATE_FLOAT);
736 8
        if ($result === false) {
737 4
            $field->addTypeError('number');
738 4
            return Invalid::value();
739
        }
740 4
        return $result;
741
    }
742
743
    /**
744
     * Validate and integer.
745
     *
746
     * @param mixed $value The value to validate.
747
     * @param ValidationField $field The validation results to add.
748
     * @return int|Invalid Returns the cleaned value or **null** if validation fails.
749
     */
750 22
    protected function validateInteger($value, ValidationField $field) {
751 22
        $result = filter_var($value, FILTER_VALIDATE_INT);
752
753 22
        if ($result === false) {
754 8
            $field->addTypeError('integer');
755 8
            return Invalid::value();
756
        }
757 17
        return $result;
758
    }
759
760
    /**
761
     * Validate an object.
762
     *
763
     * @param mixed $value The value to validate.
764
     * @param ValidationField $field The validation results to add.
765
     * @param bool $sparse Whether or not this is a sparse validation.
766
     * @return object|Invalid Returns a clean object or **null** if validation fails.
0 ignored issues
show
Documentation introduced by
Should the return type not be Invalid|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...
767
     */
768 83
    protected function validateObject($value, ValidationField $field, $sparse = false) {
769 83
        if (!$this->isArray($value) || isset($value[0])) {
770 6
            $field->addTypeError('object');
771 6
            return Invalid::value();
772 83
        } elseif (is_array($field->val('properties'))) {
773
            // Validate the data against the internal schema.
774 82
            $value = $this->validateProperties($value, $field, $sparse);
775 81
        } elseif (!is_array($value)) {
776
            $value = $this->toArray($value);
777
        }
778 81
        return $value;
779
    }
780
781
    /**
782
     * Validate data against the schema and return the result.
783
     *
784
     * @param array|\ArrayAccess $data The data to validate.
785
     * @param ValidationField $field This argument will be filled with the validation result.
786
     * @param bool $sparse Whether or not this is a sparse validation.
787
     * @return array|Invalid Returns a clean array with only the appropriate properties and the data coerced to proper types.
788
     * or invalid if there are no valid properties.
789
     */
790 82
    protected function validateProperties($data, ValidationField $field, $sparse = false) {
791 82
        $properties = $field->val('properties', []);
792 82
        $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...
793
794 82
        if (is_array($data)) {
795 81
            $keys = array_keys($data);
796 81
        } else {
797 1
            $keys = array_keys(iterator_to_array($data));
798
        }
799 82
        $keys = array_combine(array_map('strtolower', $keys), $keys);
800
801 82
        $propertyField = new ValidationField($field->getValidation(), [], null);
802
803
        // Loop through the schema fields and validate each one.
804 82
        $clean = [];
805 82
        foreach ($properties as $propertyName => $property) {
806
            $propertyField
807 82
                ->setField($property)
808 82
                ->setName(ltrim($field->getName().".$propertyName", '.'));
809
810 82
            $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...
811 82
            $isRequired = isset($required[$propertyName]);
812
813
            // First check for required fields.
814 82
            if (!array_key_exists($lName, $keys)) {
815 20
                if ($sparse) {
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...
816
                    // Sparse validation can leave required fields out.
817 20
                } elseif ($propertyField->hasVal('default')) {
818 2
                    $clean[$propertyName] = $propertyField->val('default');
819 20
                } elseif ($isRequired) {
820 6
                    $propertyField->addError('missingField', ['messageCode' => '{field} is required.']);
821 6
                }
822 20
            } else {
823 80
                $value = $data[$keys[$lName]];
824
825 80
                if (in_array($value, [null, ''], true) && !$isRequired && !$propertyField->val('allowNull')) {
826 13
                    if ($propertyField->getType() !== 'string' || $value === null) {
827 10
                        continue;
828
                    }
829 3
                }
830
831 70
                $clean[$propertyName] = $this->validateField($value, $propertyField, $sparse);
832
            }
833
834 80
            unset($keys[$lName]);
835 82
        }
836
837
        // Look for extraneous properties.
838 82
        if (!empty($keys)) {
839 17
            if ($this->hasFlag(Schema::VALIDATE_EXTRA_PROPERTY_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...
840 2
                $msg = sprintf("%s has unexpected field(s): %s.", $field->getName() ?: 'value', implode(', ', $keys));
841 2
                trigger_error($msg, E_USER_NOTICE);
842
            }
843
844 15
            if ($this->hasFlag(Schema::VALIDATE_EXTRA_PROPERTY_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...
845 2
                $field->addError('invalid', [
846 2
                    'messageCode' => '{field} has {extra,plural,an unexpected field,unexpected fields}: {extra}.',
847 2
                    'extra' => array_values($keys),
848
                    'status' => 422
849 2
                ]);
850 2
            }
851 15
        }
852
853 80
        return $clean;
854
    }
855
856
    /**
857
     * Validate a string.
858
     *
859
     * @param mixed $value The value to validate.
860
     * @param ValidationField $field The validation results to add.
861
     * @return string|Invalid Returns the valid string or **null** if validation fails.
862
     */
863 52
    protected function validateString($value, ValidationField $field) {
864 52
        if (is_string($value) || is_numeric($value)) {
865 50
            $value = $result = (string)$value;
866 50
        } else {
867 5
            $field->addTypeError('string');
868 5
            return Invalid::value();
869
        }
870
871 50
        $errorCount = $field->getErrorCount();
0 ignored issues
show
Unused Code introduced by
$errorCount is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
872 50
        if (($minLength = $field->val('minLength', 0)) > 0 && mb_strlen($value) < $minLength) {
873 4
            if (!empty($field->getName()) && $minLength === 1) {
874 2
                $field->addError('missingField', ['messageCode' => '{field} is required.', 'status' => 422]);
875 2
            } else {
876 2
                $field->addError(
877 2
                    'minLength',
878
                    [
879 2
                        'messageCode' => '{field} should be at least {minLength} {minLength,plural,character} long.',
880 2
                        'minLength' => $minLength,
881
                        'status' => 422
882 2
                    ]
883 2
                );
884
            }
885 4
        }
886 50
        if (($maxLength = $field->val('maxLength', 0)) > 0 && mb_strlen($value) > $maxLength) {
887 1
            $field->addError(
888 1
                'maxLength',
889
                [
890 1
                    'messageCode' => '{field} is {overflow} {overflow,plural,characters} too long.',
891 1
                    'maxLength' => $maxLength,
892 1
                    'overflow' => mb_strlen($value) - $maxLength,
893
                    'status' => 422
894 1
                ]
895 1
            );
896 1
        }
897 50
        if ($pattern = $field->val('pattern')) {
898 4
            $regex = '`'.str_replace('`', preg_quote('`', '`'), $pattern).'`';
899
900 4
            if (!preg_match($regex, $value)) {
901 2
                $field->addError(
902 2
                    'invalid',
903
                    [
904 2
                        'messageCode' => '{field} is in the incorrect format.',
905
                        'status' => 422
906 2
                    ]
907 2
                );
908 2
            }
909 4
        }
910 50
        if ($format = $field->val('format')) {
911 15
            $type = $format;
912
            switch ($format) {
913 15
                case 'date-time':
914 4
                    $result = $this->validateDatetime($result, $field);
915 4
                    if ($result instanceof \DateTimeInterface) {
916 4
                        $result = $result->format(\DateTime::RFC3339);
917 4
                    }
918 4
                    break;
919 11
                case 'email':
920 1
                    $result = filter_var($result, FILTER_VALIDATE_EMAIL);
921 1
                    break;
922 10
                case 'ipv4':
923 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...
924 1
                    $result = filter_var($result, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4);
925 1
                    break;
926 9
                case 'ipv6':
927 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...
928 1
                    $result = filter_var($result, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6);
929 1
                    break;
930 8
                case 'ip':
931 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...
932 1
                    $result = filter_var($result, FILTER_VALIDATE_IP);
933 1
                    break;
934 7
                case 'uri':
935 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...
936 7
                    $result = filter_var($result, FILTER_VALIDATE_URL, FILTER_FLAG_HOST_REQUIRED | FILTER_FLAG_SCHEME_REQUIRED);
937 7
                    break;
938
                default:
939
                    trigger_error("Unrecognized format '$format'.", E_USER_NOTICE);
940
            }
941 15
            if ($result === false) {
942 5
                $field->addTypeError($type);
943 5
            }
944 15
        }
945
946 50
        if ($field->isValid()) {
947 42
            return $result;
948
        } else {
949 12
            return Invalid::value();
950
        }
951
    }
952
953
    /**
954
     * Validate a unix timestamp.
955
     *
956
     * @param mixed $value The value to validate.
957
     * @param ValidationField $field The field being validated.
958
     * @return int|Invalid Returns a valid timestamp or invalid if the value doesn't validate.
959
     */
960 7
    protected function validateTimestamp($value, ValidationField $field) {
961 7
        if (is_numeric($value) && $value > 0) {
962 2
            $result = (int)$value;
963 7
        } elseif (is_string($value) && $ts = strtotime($value)) {
964 1
            $result = $ts;
965 1
        } else {
966 4
            $field->addTypeError('timestamp');
967 4
            $result = Invalid::value();
968
        }
969 7
        return $result;
970
    }
971
972
    /**
973
     * Validate a null value.
974
     *
975
     * @param mixed $value The value to validate.
976
     * @param ValidationField $field The error collector for the field.
977
     * @return null|Invalid Returns **null** or invalid.
978
     */
979
    protected function validateNull($value, ValidationField $field) {
980
        if ($value === null) {
981
            return null;
982
        }
983
        $field->addError('invalid', ['messageCode' => '{field} should be null.', 'status' => 422]);
984
        return Invalid::value();
985
    }
986
987
    /**
988
     * Validate a value against an enum.
989
     *
990
     * @param mixed $value The value to test.
991
     * @param ValidationField $field The validation object for adding errors.
992
     * @return mixed|Invalid Returns the value if it is one of the enumerated values or invalid otherwise.
993
     */
994 121
    protected function validateEnum($value, ValidationField $field) {
995 121
        $enum = $field->val('enum');
996 121
        if (empty($enum)) {
997 120
            return $value;
998
        }
999
1000 1
        if (!in_array($value, $enum, true)) {
1001 1
            $field->addError(
1002 1
                'invalid',
1003
                [
1004 1
                    'messageCode' => '{field} must be one of: {enum}.',
1005 1
                    'enum' => $enum,
1006
                    'status' => 422
1007 1
                ]
1008 1
            );
1009 1
            return Invalid::value();
1010
        }
1011 1
        return $value;
1012
    }
1013
1014
    /**
1015
     * Call all of the validators attached to a field.
1016
     *
1017
     * @param mixed $value The field value being validated.
1018
     * @param ValidationField $field The validation object to add errors.
1019
     */
1020 121
    protected function callValidators($value, ValidationField $field) {
1021 121
        $valid = true;
1022
1023
        // Strip array references in the name except for the last one.
1024 121
        $key = preg_replace(['`\[\d+\]$`', '`\[\d+\]`'], ['[]', ''], $field->getName());
1025 121
        if (!empty($this->validators[$key])) {
1026 2
            foreach ($this->validators[$key] as $validator) {
1027 2
                $r = call_user_func($validator, $value, $field);
1028
1029 2
                if ($r === false || Invalid::isInvalid($r)) {
1030 1
                    $valid = false;
1031 1
                }
1032 2
            }
1033 2
        }
1034
1035
        // Add an error on the field if the validator hasn't done so.
1036 121
        if (!$valid && $field->isValid()) {
1037
            $field->addError('invalid', ['messageCode' => '{field} is invalid.', 'status' => 422]);
1038
        }
1039 121
    }
1040
1041
    /**
1042
     * Specify data which should be serialized to JSON.
1043
     *
1044
     * This method specifically returns data compatible with the JSON schema format.
1045
     *
1046
     * @return mixed Returns data which can be serialized by **json_encode()**, which is a value of any type other than a resource.
1047
     * @link http://php.net/manual/en/jsonserializable.jsonserialize.php
1048
     * @link http://json-schema.org/
1049
     */
1050
    public function jsonSerialize() {
1051 14
        $fix = function ($schema) use (&$fix) {
1052 14
            if ($schema instanceof Schema) {
1053 1
                return $schema->jsonSerialize();
1054
            }
1055
1056 14
            if (!empty($schema['type'])) {
1057
                // Swap datetime and timestamp to other types with formats.
1058 13
                if ($schema['type'] === 'datetime') {
1059 3
                    $schema['type'] = 'string';
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...
1060 3
                    $schema['format'] = 'date-time';
1061 13
                } elseif ($schema['type'] === 'timestamp') {
1062 3
                    $schema['type'] = 'integer';
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...
1063 3
                    $schema['format'] = 'timestamp';
1064 3
                }
1065 13
            }
1066
1067 14
            if (!empty($schema['items'])) {
1068 4
                $schema['items'] = $fix($schema['items']);
1069 4
            }
1070 14
            if (!empty($schema['properties'])) {
1071 10
                $properties = [];
1072 10
                foreach ($schema['properties'] as $key => $property) {
1073 10
                    $properties[$key] = $fix($property);
1074 10
                }
1075 10
                $schema['properties'] = $properties;
1076 10
            }
1077
1078 14
            return $schema;
1079 14
        };
1080
1081 14
        $result = $fix($this->schema);
1082
1083 14
        return $result;
1084
    }
1085
1086
    /**
1087
     * Look up a type based on its alias.
1088
     *
1089
     * @param string $alias The type alias or type name to lookup.
1090
     * @return mixed
1091
     */
1092 143
    protected function getType($alias) {
1093 143
        if (isset(self::$types[$alias])) {
1094
            return $alias;
1095
        }
1096 143
        foreach (self::$types as $type => $aliases) {
1097 143
            if (in_array($alias, $aliases, true)) {
1098 143
                return $type;
1099
            }
1100 143
        }
1101 8
        return null;
1102
    }
1103
1104
    /**
1105
     * Get the class that's used to contain validation information.
1106
     *
1107
     * @return Validation|string Returns the validation class.
1108
     */
1109 123
    public function getValidationClass() {
1110 123
        return $this->validationClass;
1111
    }
1112
1113
    /**
1114
     * Set the class that's used to contain validation information.
1115
     *
1116
     * @param Validation|string $class Either the name of a class or a class that will be cloned.
1117
     * @return $this
1118
     */
1119 1
    public function setValidationClass($class) {
1120 1
        if (!is_a($class, Validation::class, true)) {
1121
            throw new \InvalidArgumentException("$class must be a subclass of ".Validation::class, 500);
1122
        }
1123
1124 1
        $this->validationClass = $class;
1125 1
        return $this;
1126
    }
1127
1128
    /**
1129
     * Create a new validation instance.
1130
     *
1131
     * @return Validation Returns a validation object.
1132
     */
1133 123
    protected function createValidation() {
1134 123
        $class = $this->getValidationClass();
1135
1136 123
        if ($class instanceof Validation) {
1137 1
            $result = clone $class;
1138 1
        } else {
1139 123
            $result = new $class;
1140
        }
1141 123
        return $result;
1142
    }
1143
1144
    /**
1145
     * Check whether or not a value is an array or accessible like an array.
1146
     *
1147
     * @param mixed $value The value to check.
1148
     * @return bool Returns **true** if the value can be used like an array or **false** otherwise.
1149
     */
1150 83
    private function isArray($value) {
1151 83
        return is_array($value) || ($value instanceof \ArrayAccess && $value instanceof \Traversable);
1152
    }
1153
1154
    /**
1155
     * Cast a value to an array.
1156
     *
1157
     * @param \Traversable $value The value to convert.
1158
     * @return array Returns an array.
1159
     */
1160
    private function toArray(\Traversable $value) {
1161
        if ($value instanceof \ArrayObject) {
1162
            return $value->getArrayCopy();
1163
        }
1164
        return iterator_to_array($value);
1165
    }
1166
}
1167