Completed
Pull Request — master (#11)
by Todd
03:52
created

Schema::setID()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 9
ccs 0
cts 6
cp 0
rs 9.6666
c 0
b 0
f 0
cc 2
eloc 6
nc 2
nop 1
crap 6
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 149
    public function __construct($schema = []) {
67 149
        $this->schema = $schema;
68 149
    }
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
     * Get the ID for the schema.
98
     *
99
     * @return string
100
     */
101
    public function getID() {
102
        return isset($this->schema['id']) ? $this->schema['id'] : '';
103
    }
104
105
    /**
106
     * Set the ID for the schema.
107
     *
108
     * @param string $ID The new ID.
109
     * @throws \InvalidArgumentException Throws an exception when the provided ID is not a string.
110
     * @return Schema
111
     */
112
    public function setID($ID) {
113
        if (is_string($ID)) {
114
            $this->schema['ID'] = $ID;
115
        } else {
116
            throw new \InvalidArgumentException("The ID is not a valid string.", 500);
117
        }
118
119
        return $this;
120
    }
121
122
    /**
123
     * Return the validation flags.
124
     *
125
     * @return int Returns a bitwise combination of flags.
126
     */
127 1
    public function getFlags() {
128 1
        return $this->flags;
129
    }
130
131
    /**
132
     * Set the validation flags.
133
     *
134
     * @param int $flags One or more of the **Schema::FLAG_*** constants.
135
     * @return Schema Returns the current instance for fluent calls.
136
     */
137 8
    public function setFlags($flags) {
138 8
        if (!is_int($flags)) {
139 1
            throw new \InvalidArgumentException('Invalid flags.', 500);
140
        }
141 7
        $this->flags = $flags;
142
143 7
        return $this;
144
    }
145
146
    /**
147
     * Whether or not the schema has a flag (or combination of flags).
148
     *
149
     * @param int $flag One or more of the **Schema::VALIDATE_*** constants.
150
     * @return bool Returns **true** if all of the flags are set or **false** otherwise.
151
     */
152 8
    public function hasFlag($flag) {
153 8
        return ($this->flags & $flag) === $flag;
154
    }
155
156
    /**
157
     * Set a flag.
158
     *
159
     * @param int $flag One or more of the **Schema::VALIDATE_*** constants.
160
     * @param bool $value Either true or false.
161
     * @return $this
162
     */
163 1
    public function setFlag($flag, $value) {
164 1
        if ($value) {
165 1
            $this->flags = $this->flags | $flag;
166 1
        } else {
167 1
            $this->flags = $this->flags & ~$flag;
168
        }
169 1
        return $this;
170
    }
171
172
    /**
173
     * Merge a schema with this one.
174
     *
175
     * @param Schema $schema A scheme instance. Its parameters will be merged into the current instance.
176
     * @return $this
177
     */
178 3
    public function merge(Schema $schema) {
179 3
        $this->mergeInternal($this->schema, $schema->getSchemaArray(), true, true);
180 3
        return $this;
181
    }
182
183
    /**
184
     * Add another schema to this one.
185
     *
186
     * Adding schemas together is analogous to array addition. When you add a schema it will only add missing information.
187
     *
188
     * @param Schema $schema The schema to add.
189
     * @param bool $addProperties Whether to add properties that don't exist in this schema.
190
     * @return $this
191
     */
192 3
    public function add(Schema $schema, $addProperties = false) {
193 3
        $this->mergeInternal($this->schema, $schema->getSchemaArray(), false, $addProperties);
194 3
        return $this;
195
    }
196
197
    /**
198
     * The internal implementation of schema merging.
199
     *
200
     * @param array &$target The target of the merge.
201
     * @param array $source The source of the merge.
202
     * @param bool $overwrite Whether or not to replace values.
203
     * @param bool $addProperties Whether or not to add object properties to the target.
204
     * @return array
205
     */
206 6
    private function mergeInternal(array &$target, array $source, $overwrite = true, $addProperties = true) {
207
        // We need to do a fix for required properties here.
208 6
        if (isset($target['properties']) && !empty($source['required'])) {
209 4
            $required = isset($target['required']) ? $target['required'] : [];
210
211 4
            if (isset($source['required']) && $addProperties) {
212 3
                $newProperties = array_diff(array_keys($source['properties']), array_keys($target['properties']));
213 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...
214
215 3
                $required = array_merge($required, $newRequired);
216 3
            }
217 4
        }
218
219
220 6
        foreach ($source as $key => $val) {
221 6
            if (is_array($val) && array_key_exists($key, $target) && is_array($target[$key])) {
222 6
                if ($key === 'properties' && !$addProperties) {
223
                    // We just want to merge the properties that exist in the destination.
224 1
                    foreach ($val as $name => $prop) {
225 1
                        if (isset($target[$key][$name])) {
226 1
                            $this->mergeInternal($target[$key][$name], $prop, $overwrite, $addProperties);
227 1
                        }
228 1
                    }
229 6
                } elseif (isset($val[0]) || isset($target[$key][0])) {
230 4
                    if ($overwrite) {
231
                        // This is a numeric array, so just do a merge.
232 2
                        $merged = array_merge($target[$key], $val);
233 2
                        if (is_string($merged[0])) {
234 2
                            $merged = array_keys(array_flip($merged));
235 2
                        }
236 2
                        $target[$key] = $merged;
237 2
                    }
238 4
                } else {
239 3
                    $target[$key] = $this->mergeInternal($target[$key], $val, $overwrite, $addProperties);
240
                }
241 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...
242
                // Do nothing, we aren't replacing.
243 3
            } else {
244 5
                $target[$key] = $val;
245
            }
246 6
        }
247
248 6
        if (isset($required)) {
249 4
            if (empty($required)) {
250 1
                unset($target['required']);
251 1
            } else {
252 4
                $target['required'] = $required;
253
            }
254 4
        }
255
256 6
        return $target;
257
    }
258
259
//    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...
260
261
    /**
262
     * Returns the internal schema array.
263
     *
264
     * @return array
265
     * @see Schema::jsonSerialize()
266
     */
267 15
    public function getSchemaArray() {
268 15
        return $this->schema;
269
    }
270
271
    /**
272
     * Parse a short schema and return the associated schema.
273
     *
274
     * @param array $arr The schema array.
275
     * @param mixed ...$args Constructor arguments for the schema instance.
276
     * @return static Returns a new schema.
277
     */
278 144
    public static function parse(array $arr, ...$args) {
279 144
        $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...
280 144
        $schema->schema = $schema->parseInternal($arr);
281 144
        return $schema;
282
    }
283
284
    /**
285
     * Parse a schema in short form into a full schema array.
286
     *
287
     * @param array $arr The array to parse into a schema.
288
     * @return array The full schema array.
289
     * @throws \InvalidArgumentException Throws an exception when an item in the schema is invalid.
290
     */
291 144
    protected function parseInternal(array $arr) {
292 144
        if (empty($arr)) {
293
            // An empty schema validates to anything.
294 7
            return [];
295 138
        } elseif (isset($arr['type'])) {
296
            // This is a long form schema and can be parsed as the root.
297
            return $this->parseNode($arr);
298
        } else {
299
            // Check for a root schema.
300 138
            $value = reset($arr);
301 138
            $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...
302 138
            if (is_int($key)) {
303 84
                $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...
304 84
                $value = null;
305 84
            }
306 138
            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...
307 138
            if (empty($name)) {
308 52
                return $this->parseNode($param, $value);
309
            }
310
        }
311
312
        // If we are here then this is n object schema.
313 88
        list($properties, $required) = $this->parseProperties($arr);
314
315
        $result = [
316 88
            'type' => 'object',
317 88
            'properties' => $properties,
318
            'required' => $required
319 88
        ];
320
321 88
        return array_filter($result);
322
    }
323
324
    /**
325
     * Parse a schema node.
326
     *
327
     * @param array $node The node to parse.
328
     * @param mixed $value Additional information from the node.
329
     * @return array Returns a JSON schema compatible node.
330
     */
331 138
    private function parseNode($node, $value = null) {
332 138
        if (is_array($value)) {
333
            // The value describes a bit more about the schema.
334 53
            switch ($node['type']) {
335 53
                case 'array':
336 7
                    if (isset($value['items'])) {
337
                        // The value includes array schema information.
338 1
                        $node = array_replace($node, $value);
339 1
                    } else {
340 6
                        $node['items'] = $this->parseInternal($value);
341
                    }
342 7
                    break;
343 46
                case 'object':
344
                    // The value is a schema of the object.
345 10
                    if (isset($value['properties'])) {
346
                        list($node['properties']) = $this->parseProperties($value['properties']);
347
                    } else {
348 10
                        list($node['properties'], $required) = $this->parseProperties($value);
349 10
                        if (!empty($required)) {
350 10
                            $node['required'] = $required;
351 10
                        }
352
                    }
353 10
                    break;
354 36
                default:
355 36
                    $node = array_replace($node, $value);
356 36
                    break;
357 53
            }
358 138
        } elseif (is_string($value)) {
359 80
            if ($node['type'] === 'array' && $arrType = $this->getType($value)) {
360 2
                $node['items'] = ['type' => $arrType];
361 80
            } elseif (!empty($value)) {
362 23
                $node['description'] = $value;
363 23
            }
364 103
        } elseif ($value === null) {
365
            // Parse child elements.
366 24
            if ($node['type'] === 'array' && isset($node['items'])) {
367
                // The value includes array schema information.
368
                $node['items'] = $this->parseInternal($node['items']);
369 24
            } elseif ($node['type'] === 'object' && isset($node['properties'])) {
370
                list($node['properties']) = $this->parseProperties($node['properties']);
371
372
            }
373 24
        }
374
375 138
        if (is_array($node) && $node['type'] === null) {
376 3
            unset($node['type']);
377 3
        }
378
379 138
        return $node;
380
    }
381
382
    /**
383
     * Parse the schema for an object's properties.
384
     *
385
     * @param array $arr An object property schema.
386
     * @return array Returns a schema array suitable to be placed in the **properties** key of a schema.
387
     */
388 88
    private function parseProperties(array $arr) {
389 88
        $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...
390 88
        $requiredProperties = [];
391 88
        foreach ($arr as $key => $value) {
392
            // Fix a schema specified as just a value.
393 88
            if (is_int($key)) {
394 64
                if (is_string($value)) {
395 64
                    $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...
396 64
                    $value = '';
397 64
                } else {
398
                    throw new \InvalidArgumentException("Schema at position $key is not a valid parameter.", 500);
399
                }
400 64
            }
401
402
            // The parameter is defined in the key.
403 88
            list($name, $param, $required) = $this->parseShortParam($key, $value);
404
405 88
            $node = $this->parseNode($param, $value);
406
407 88
            $properties[$name] = $node;
408 88
            if ($required) {
409 46
                $requiredProperties[] = $name;
410 46
            }
411 88
        }
412 88
        return array($properties, $requiredProperties);
413
    }
414
415
    /**
416
     * Parse a short parameter string into a full array parameter.
417
     *
418
     * @param string $key The short parameter string to parse.
419
     * @param array $value An array of other information that might help resolve ambiguity.
420
     * @return array Returns an array in the form `[string name, array param, bool required]`.
421
     * @throws \InvalidArgumentException Throws an exception if the short param is not in the correct format.
422
     */
423 138
    public function parseShortParam($key, $value = []) {
424
        // Is the parameter optional?
425 138
        if (substr($key, -1) === '?') {
426 62
            $required = false;
427 62
            $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...
428 62
        } else {
429 96
            $required = true;
430
        }
431
432
        // Check for a type.
433 138
        $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...
434 138
        $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...
435 138
        $allowNull = false;
436 138
        if (!empty($parts[1])) {
437 137
            $types = explode('|', $parts[1]);
438 137
            foreach ($types as $alias) {
439 137
                $found = $this->getType($alias);
440 137
                if ($found === null) {
441
                    throw new \InvalidArgumentException("Unknown type '$alias'", 500);
442 137
                } elseif ($found === 'null') {
443 9
                    $allowNull = true;
444 9
                } else {
445 137
                    $type = $found;
446
                }
447 137
            }
448 137
        } else {
449 4
            $type = null;
450
        }
451
452 138
        if ($value instanceof Schema) {
453 2
            if ($type === 'array') {
454 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...
455 1
            } else {
456 1
                $param = $value;
457
            }
458 138
        } elseif (isset($value['type'])) {
459
            $param = $value;
460
461
            if (!empty($type) && $type !== $param['type']) {
462
                throw new \InvalidArgumentException("Type mismatch between $type and {$param['type']} for field $name.", 500);
463
            }
464
        } else {
465 138
            if (empty($type) && !empty($parts[1])) {
466
                throw new \InvalidArgumentException("Invalid type {$parts[1]} for field $name.", 500);
467
            }
468 138
            $param = ['type' => $type];
469
470
            // Parsed required strings have a minimum length of 1.
471 138
            if ($type === 'string' && !empty($name) && $required && (!isset($value['default']) || $value['default'] !== '')) {
472 29
                $param['minLength'] = 1;
473 29
            }
474
        }
475 138
        if ($allowNull) {
476 9
            $param['allowNull'] = true;
477 9
        }
478
479 138
        return [$name, $param, $required];
480
    }
481
482
    /**
483
     * Add a custom validator to to validate the schema.
484
     *
485
     * @param string $fieldname The name of the field to validate, if any.
486
     *
487
     * If you are adding a validator to a deeply nested field then separate the path with dots.
488
     * @param callable $callback The callback to validate with.
489
     * @return Schema Returns `$this` for fluent calls.
490
     */
491 2
    public function addValidator($fieldname, callable $callback) {
492 2
        $this->validators[$fieldname][] = $callback;
493 2
        return $this;
494
    }
495
496
    /**
497
     * Require one of a given set of fields in the schema.
498
     *
499
     * @param array $required The field names to require.
500
     * @param string $fieldname The name of the field to attach to.
501
     * @param int $count The count of required items.
502
     * @return Schema Returns `$this` for fluent calls.
503
     */
504 1
    public function requireOneOf(array $required, $fieldname = '', $count = 1) {
505 1
        $result = $this->addValidator(
506 1
            $fieldname,
507
            function ($data, ValidationField $field) use ($required, $count) {
508 1
                $hasCount = 0;
509 1
                $flattened = [];
510
511 1
                foreach ($required as $name) {
512 1
                    $flattened = array_merge($flattened, (array)$name);
513
514 1
                    if (is_array($name)) {
515
                        // This is an array of required names. They all must match.
516 1
                        $hasCountInner = 0;
517 1
                        foreach ($name as $nameInner) {
518 1
                            if (isset($data[$nameInner]) && $data[$nameInner]) {
519 1
                                $hasCountInner++;
520 1
                            } else {
521 1
                                break;
522
                            }
523 1
                        }
524 1
                        if ($hasCountInner >= count($name)) {
525 1
                            $hasCount++;
526 1
                        }
527 1
                    } elseif (isset($data[$name]) && $data[$name]) {
528 1
                        $hasCount++;
529 1
                    }
530
531 1
                    if ($hasCount >= $count) {
532 1
                        return true;
533
                    }
534 1
                }
535
536 1
                if ($count === 1) {
537 1
                    $message = 'One of {required} are required.';
538 1
                } else {
539
                    $message = '{count} of {required} are required.';
540
                }
541
542 1
                $field->addError('missingField', [
543 1
                    'messageCode' => $message,
544 1
                    'required' => $required,
545
                    'count' => $count
546 1
                ]);
547 1
                return false;
548
            }
549 1
        );
550
551 1
        return $result;
552
    }
553
554
    /**
555
     * Validate data against the schema.
556
     *
557
     * @param mixed $data The data to validate.
558
     * @param bool $sparse Whether or not this is a sparse validation.
559
     * @return mixed Returns a cleaned version of the data.
560
     * @throws ValidationException Throws an exception when the data does not validate against the schema.
561
     */
562 117
    public function validate($data, $sparse = false) {
563 117
        $field = new ValidationField($this->createValidation(), $this->schema, '', $sparse);
564
565 117
        $clean = $this->validateField($data, $field, $sparse);
566
567 115
        if (Invalid::isInvalid($clean) && $field->isValid()) {
568
            // This really shouldn't happen, but we want to protect against seeing the invalid object.
569
            $field->addError('invalid', ['messageCode' => '{field} is invalid.', 'status' => 422]);
570
        }
571
572 115
        if (!$field->getValidation()->isValid()) {
573 63
            throw new ValidationException($field->getValidation());
574
        }
575
576 71
        return $clean;
577
    }
578
579
    /**
580
     * Validate data against the schema and return the result.
581
     *
582
     * @param mixed $data The data to validate.
583
     * @param bool $sparse Whether or not to do a sparse validation.
584
     * @return bool Returns true if the data is valid. False otherwise.
585
     */
586 33
    public function isValid($data, $sparse = false) {
587
        try {
588 33
            $this->validate($data, $sparse);
589 23
            return true;
590 24
        } catch (ValidationException $ex) {
591 24
            return false;
592
        }
593
    }
594
595
    /**
596
     * Validate a field.
597
     *
598
     * @param mixed $value The value to validate.
599
     * @param ValidationField $field A validation object to add errors to.
600
     * @param bool $sparse Whether or not this is a sparse validation.
601
     * @return mixed|Invalid Returns a clean version of the value with all extra fields stripped out or invalid if the value
602
     * is completely invalid.
603
     */
604 117
    protected function validateField($value, ValidationField $field, $sparse = false) {
605 117
        $result = $value;
606 117
        if ($field->getField() instanceof Schema) {
607
            try {
608 1
                $result = $field->getField()->validate($value, $sparse);
609 1
            } catch (ValidationException $ex) {
610
                // The validation failed, so merge the validations together.
611 1
                $field->getValidation()->merge($ex->getValidation(), $field->getName());
612
            }
613 117
        } elseif ($value === null && $field->val('allowNull', false)) {
614 9
            $result = $value;
615 9
        } else {
616
            // Validate the field's type.
617 117
            $type = $field->getType();
618
            switch ($type) {
619 117
                case 'boolean':
620 21
                    $result = $this->validateBoolean($value, $field);
621 21
                    break;
622 104
                case 'integer':
623 23
                    $result = $this->validateInteger($value, $field);
624 23
                    break;
625 102
                case 'number':
626 9
                    $result = $this->validateNumber($value, $field);
627 9
                    break;
628 101
                case 'string':
629 52
                    $result = $this->validateString($value, $field);
630 52
                    break;
631 84
                case 'timestamp':
632 8
                    $result = $this->validateTimestamp($value, $field);
633 8
                    break;
634 83
                case 'datetime':
635 9
                    $result = $this->validateDatetime($value, $field);
636 9
                    break;
637 80
                case 'array':
638 13
                    $result = $this->validateArray($value, $field, $sparse);
639 13
                    break;
640 78
                case 'object':
641 77
                    $result = $this->validateObject($value, $field, $sparse);
642 75
                    break;
643 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...
644
                    // No type was specified so we are valid.
645 2
                    $result = $value;
646 2
                    break;
647
                default:
648
                    throw new \InvalidArgumentException("Unrecognized type $type.", 500);
649
            }
650 117
            if (Invalid::isValid($result)) {
651 115
                $result = $this->validateEnum($result, $field);
652 115
            }
653
        }
654
655
        // Validate a custom field validator.
656 117
        if (Invalid::isValid($result)) {
657 115
            $this->callValidators($result, $field);
658 115
        }
659
660 117
        return $result;
661
    }
662
663
    /**
664
     * Validate an array.
665
     *
666
     * @param mixed $value The value to validate.
667
     * @param ValidationField $field The validation results to add.
668
     * @param bool $sparse Whether or not this is a sparse validation.
669
     * @return array|Invalid Returns an array or invalid if validation fails.
670
     */
671 13
    protected function validateArray($value, ValidationField $field, $sparse = false) {
672 13
        if ((!is_array($value) || (count($value) > 0 && !array_key_exists(0, $value))) && !$value instanceof \Traversable) {
673 7
            $field->addTypeError('array');
674 7
            return Invalid::value();
675 7
        } elseif ($field->val('items') !== null) {
676 5
            $result = [];
677
678
            // Validate each of the types.
679 5
            $itemValidation = new ValidationField(
680 5
                $field->getValidation(),
681 5
                $field->val('items'),
682 5
                '',
683
                $sparse
684 5
            );
685
686 5
            $count = 0;
687 5
            foreach ($value as $i => $item) {
688 5
                $itemValidation->setName($field->getName()."[{$i}]");
689 5
                $validItem = $this->validateField($item, $itemValidation, $sparse);
690 5
                if (Invalid::isValid($validItem)) {
691 5
                    $result[] = $validItem;
692 5
                }
693 5
                $count++;
694 5
            }
695
696 5
            return empty($result) && $count > 0 ? Invalid::value() : $result;
697
        } else {
698
            // Cast the items into a proper numeric array.
699 2
            $result = is_array($value) ? array_values($value) : iterator_to_array($value);
700 2
            return $result;
701
        }
702
    }
703
704
    /**
705
     * Validate a boolean value.
706
     *
707
     * @param mixed $value The value to validate.
708
     * @param ValidationField $field The validation results to add.
709
     * @return bool|Invalid Returns the cleaned value or invalid if validation fails.
710
     */
711 21
    protected function validateBoolean($value, ValidationField $field) {
712 21
        $value = $value === null ? $value : filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
713 21
        if ($value === null) {
714 5
            $field->addTypeError('boolean');
715 5
            return Invalid::value();
716
        }
717 17
        return $value;
718
    }
719
720
    /**
721
     * Validate a date time.
722
     *
723
     * @param mixed $value The value to validate.
724
     * @param ValidationField $field The validation results to add.
725
     * @return \DateTimeInterface|Invalid Returns the cleaned value or **null** if it isn't valid.
726
     */
727 13
    protected function validateDatetime($value, ValidationField $field) {
728 13
        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...
729
            // do nothing, we're good
730 13
        } elseif (is_string($value) && $value !== '') {
731
            try {
732 7
                $dt = new \DateTimeImmutable($value);
733 5
                if ($dt) {
734 5
                    $value = $dt;
735 5
                } else {
736
                    $value = null;
737
                }
738 7
            } catch (\Exception $ex) {
739 2
                $value = Invalid::value();
740
            }
741 11
        } elseif (is_int($value) && $value > 0) {
742 1
            $value = new \DateTimeImmutable('@'.(string)round($value));
743 1
        } else {
744 3
            $value = Invalid::value();
745
        }
746
747 13
        if (Invalid::isInvalid($value)) {
748 5
            $field->addTypeError('datetime');
749 5
        }
750 13
        return $value;
751
    }
752
753
    /**
754
     * Validate a float.
755
     *
756
     * @param mixed $value The value to validate.
757
     * @param ValidationField $field The validation results to add.
758
     * @return float|Invalid Returns a number or **null** if validation fails.
759
     */
760 9
    protected function validateNumber($value, ValidationField $field) {
761 9
        $result = filter_var($value, FILTER_VALIDATE_FLOAT);
762 9
        if ($result === false) {
763 5
            $field->addTypeError('number');
764 5
            return Invalid::value();
765
        }
766 4
        return $result;
767
    }
768
769
    /**
770
     * Validate and integer.
771
     *
772
     * @param mixed $value The value to validate.
773
     * @param ValidationField $field The validation results to add.
774
     * @return int|Invalid Returns the cleaned value or **null** if validation fails.
775
     */
776 23
    protected function validateInteger($value, ValidationField $field) {
777 23
        $result = filter_var($value, FILTER_VALIDATE_INT);
778
779 23
        if ($result === false) {
780 9
            $field->addTypeError('integer');
781 9
            return Invalid::value();
782
        }
783 17
        return $result;
784
    }
785
786
    /**
787
     * Validate an object.
788
     *
789
     * @param mixed $value The value to validate.
790
     * @param ValidationField $field The validation results to add.
791
     * @param bool $sparse Whether or not this is a sparse validation.
792
     * @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...
793
     */
794 77
    protected function validateObject($value, ValidationField $field, $sparse = false) {
795 77
        if (!$this->isArray($value) || isset($value[0])) {
796 7
            $field->addTypeError('object');
797 7
            return Invalid::value();
798 77
        } elseif (is_array($field->val('properties'))) {
799
            // Validate the data against the internal schema.
800 76
            $value = $this->validateProperties($value, $field, $sparse);
801 75
        } elseif (!is_array($value)) {
802
            $value = $this->toArray($value);
803
        }
804 75
        return $value;
805
    }
806
807
    /**
808
     * Validate data against the schema and return the result.
809
     *
810
     * @param array|\ArrayAccess $data The data to validate.
811
     * @param ValidationField $field This argument will be filled with the validation result.
812
     * @param bool $sparse Whether or not this is a sparse validation.
813
     * @return array|Invalid Returns a clean array with only the appropriate properties and the data coerced to proper types.
814
     * or invalid if there are no valid properties.
815
     */
816 76
    protected function validateProperties($data, ValidationField $field, $sparse = false) {
817 76
        $properties = $field->val('properties', []);
818 76
        $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...
819
820 76
        if (is_array($data)) {
821 75
            $keys = array_keys($data);
822 75
        } else {
823 1
            $keys = array_keys(iterator_to_array($data));
824
        }
825 76
        $keys = array_combine(array_map('strtolower', $keys), $keys);
826
827 76
        $propertyField = new ValidationField($field->getValidation(), [], null, $sparse);
828
829
        // Loop through the schema fields and validate each one.
830 76
        $clean = [];
831 76
        foreach ($properties as $propertyName => $property) {
832
            $propertyField
833 76
                ->setField($property)
834 76
                ->setName(ltrim($field->getName().".$propertyName", '.'));
835
836 76
            $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...
837 76
            $isRequired = isset($required[$propertyName]);
838
839
            // First check for required fields.
840 76
            if (!array_key_exists($lName, $keys)) {
841 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...
842
                    // Sparse validation can leave required fields out.
843 20
                } elseif ($propertyField->hasVal('default')) {
844 2
                    $clean[$propertyName] = $propertyField->val('default');
845 20
                } elseif ($isRequired) {
846 6
                    $propertyField->addError('missingField', ['messageCode' => '{field} is required.']);
847 6
                }
848 20
            } else {
849 74
                $clean[$propertyName] = $this->validateField($data[$keys[$lName]], $propertyField, $sparse);
850
            }
851
852 76
            unset($keys[$lName]);
853 76
        }
854
855
        // Look for extraneous properties.
856 76
        if (!empty($keys)) {
857 7
            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...
858 2
                $msg = sprintf("%s has unexpected field(s): %s.", $field->getName() ?: 'value', implode(', ', $keys));
859 2
                trigger_error($msg, E_USER_NOTICE);
860
            }
861
862 5
            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...
863 2
                $field->addError('invalid', [
864 2
                    'messageCode' => '{field} has {extra,plural,an unexpected field,unexpected fields}: {extra}.',
865 2
                    'extra' => array_values($keys),
866
                    'status' => 422
867 2
                ]);
868 2
            }
869 5
        }
870
871 74
        return $clean;
872
    }
873
874
    /**
875
     * Validate a string.
876
     *
877
     * @param mixed $value The value to validate.
878
     * @param ValidationField $field The validation results to add.
879
     * @return string|Invalid Returns the valid string or **null** if validation fails.
880
     */
881 52
    protected function validateString($value, ValidationField $field) {
882 52
        if (is_string($value) || is_numeric($value)) {
883 49
            $value = $result = (string)$value;
884 49
        } else {
885 6
            $field->addTypeError('string');
886 6
            return Invalid::value();
887
        }
888
889 49
        $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...
890 49
        if (($minLength = $field->val('minLength', 0)) > 0 && mb_strlen($value) < $minLength) {
891 4
            if (!empty($field->getName()) && $minLength === 1) {
892 2
                $field->addError('missingField', ['messageCode' => '{field} is required.', 'status' => 422]);
893 2
            } else {
894 2
                $field->addError(
895 2
                    'minLength',
896
                    [
897 2
                        'messageCode' => '{field} should be at least {minLength} {minLength,plural,character} long.',
898 2
                        'minLength' => $minLength,
899
                        'status' => 422
900 2
                    ]
901 2
                );
902
            }
903 4
        }
904 49
        if (($maxLength = $field->val('maxLength', 0)) > 0 && mb_strlen($value) > $maxLength) {
905 1
            $field->addError(
906 1
                'maxLength',
907
                [
908 1
                    'messageCode' => '{field} is {overflow} {overflow,plural,characters} too long.',
909 1
                    'maxLength' => $maxLength,
910 1
                    'overflow' => mb_strlen($value) - $maxLength,
911
                    'status' => 422
912 1
                ]
913 1
            );
914 1
        }
915 49
        if ($pattern = $field->val('pattern')) {
916 4
            $regex = '`'.str_replace('`', preg_quote('`', '`'), $pattern).'`';
917
918 4
            if (!preg_match($regex, $value)) {
919 2
                $field->addError(
920 2
                    'invalid',
921
                    [
922 2
                        'messageCode' => '{field} is in the incorrect format.',
923
                        'status' => 422
924 2
                    ]
925 2
                );
926 2
            }
927 4
        }
928 49
        if ($format = $field->val('format')) {
929 15
            $type = $format;
930
            switch ($format) {
931 15
                case 'date-time':
932 4
                    $result = $this->validateDatetime($result, $field);
933 4
                    if ($result instanceof \DateTimeInterface) {
934 4
                        $result = $result->format(\DateTime::RFC3339);
935 4
                    }
936 4
                    break;
937 11
                case 'email':
938 1
                    $result = filter_var($result, FILTER_VALIDATE_EMAIL);
939 1
                    break;
940 10
                case 'ipv4':
941 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...
942 1
                    $result = filter_var($result, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4);
943 1
                    break;
944 9
                case 'ipv6':
945 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...
946 1
                    $result = filter_var($result, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6);
947 1
                    break;
948 8
                case 'ip':
949 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...
950 1
                    $result = filter_var($result, FILTER_VALIDATE_IP);
951 1
                    break;
952 7
                case 'uri':
953 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...
954 7
                    $result = filter_var($result, FILTER_VALIDATE_URL, FILTER_FLAG_HOST_REQUIRED | FILTER_FLAG_SCHEME_REQUIRED);
955 7
                    break;
956
                default:
957
                    trigger_error("Unrecognized format '$format'.", E_USER_NOTICE);
958
            }
959 15
            if ($result === false) {
960 5
                $field->addTypeError($type);
961 5
            }
962 15
        }
963
964 49
        if ($field->isValid()) {
965 41
            return $result;
966
        } else {
967 12
            return Invalid::value();
968
        }
969
    }
970
971
    /**
972
     * Validate a unix timestamp.
973
     *
974
     * @param mixed $value The value to validate.
975
     * @param ValidationField $field The field being validated.
976
     * @return int|Invalid Returns a valid timestamp or invalid if the value doesn't validate.
977
     */
978 8
    protected function validateTimestamp($value, ValidationField $field) {
979 8
        if (is_numeric($value) && $value > 0) {
980 2
            $result = (int)$value;
981 8
        } elseif (is_string($value) && $ts = strtotime($value)) {
982 1
            $result = $ts;
983 1
        } else {
984 5
            $field->addTypeError('timestamp');
985 5
            $result = Invalid::value();
986
        }
987 8
        return $result;
988
    }
989
990
    /**
991
     * Validate a null value.
992
     *
993
     * @param mixed $value The value to validate.
994
     * @param ValidationField $field The error collector for the field.
995
     * @return null|Invalid Returns **null** or invalid.
996
     */
997
    protected function validateNull($value, ValidationField $field) {
998
        if ($value === null) {
999
            return null;
1000
        }
1001
        $field->addError('invalid', ['messageCode' => '{field} should be null.', 'status' => 422]);
1002
        return Invalid::value();
1003
    }
1004
1005
    /**
1006
     * Validate a value against an enum.
1007
     *
1008
     * @param mixed $value The value to test.
1009
     * @param ValidationField $field The validation object for adding errors.
1010
     * @return mixed|Invalid Returns the value if it is one of the enumerated values or invalid otherwise.
1011
     */
1012 115
    protected function validateEnum($value, ValidationField $field) {
1013 115
        $enum = $field->val('enum');
1014 115
        if (empty($enum)) {
1015 114
            return $value;
1016
        }
1017
1018 1
        if (!in_array($value, $enum, true)) {
1019 1
            $field->addError(
1020 1
                'invalid',
1021
                [
1022 1
                    'messageCode' => '{field} must be one of: {enum}.',
1023 1
                    'enum' => $enum,
1024
                    'status' => 422
1025 1
                ]
1026 1
            );
1027 1
            return Invalid::value();
1028
        }
1029 1
        return $value;
1030
    }
1031
1032
    /**
1033
     * Call all of the validators attached to a field.
1034
     *
1035
     * @param mixed $value The field value being validated.
1036
     * @param ValidationField $field The validation object to add errors.
1037
     */
1038 115
    protected function callValidators($value, ValidationField $field) {
1039 115
        $valid = true;
1040
1041
        // Strip array references in the name except for the last one.
1042 115
        $key = preg_replace(['`\[\d+\]$`', '`\[\d+\]`'], ['[]', ''], $field->getName());
1043 115
        if (!empty($this->validators[$key])) {
1044 2
            foreach ($this->validators[$key] as $validator) {
1045 2
                $r = call_user_func($validator, $value, $field);
1046
1047 2
                if ($r === false || Invalid::isInvalid($r)) {
1048 1
                    $valid = false;
1049 1
                }
1050 2
            }
1051 2
        }
1052
1053
        // Add an error on the field if the validator hasn't done so.
1054 115
        if (!$valid && $field->isValid()) {
1055
            $field->addError('invalid', ['messageCode' => '{field} is invalid.', 'status' => 422]);
1056
        }
1057 115
    }
1058
1059
    /**
1060
     * Specify data which should be serialized to JSON.
1061
     *
1062
     * This method specifically returns data compatible with the JSON schema format.
1063
     *
1064
     * @return mixed Returns data which can be serialized by **json_encode()**, which is a value of any type other than a resource.
1065
     * @link http://php.net/manual/en/jsonserializable.jsonserialize.php
1066
     * @link http://json-schema.org/
1067
     */
1068
    public function jsonSerialize() {
1069 14
        $fix = function ($schema) use (&$fix) {
1070 14
            if ($schema instanceof Schema) {
1071 1
                return $schema->jsonSerialize();
1072
            }
1073
1074 14
            if (!empty($schema['type'])) {
1075
                // Swap datetime and timestamp to other types with formats.
1076 13
                if ($schema['type'] === 'datetime') {
1077 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...
1078 3
                    $schema['format'] = 'date-time';
1079 13
                } elseif ($schema['type'] === 'timestamp') {
1080 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...
1081 3
                    $schema['format'] = 'timestamp';
1082 3
                }
1083 13
            }
1084
1085 14
            if (!empty($schema['items'])) {
1086 4
                $schema['items'] = $fix($schema['items']);
1087 4
            }
1088 14
            if (!empty($schema['properties'])) {
1089 10
                $properties = [];
1090 10
                foreach ($schema['properties'] as $key => $property) {
1091 10
                    $properties[$key] = $fix($property);
1092 10
                }
1093 10
                $schema['properties'] = $properties;
1094 10
            }
1095
1096 14
            return $schema;
1097 14
        };
1098
1099 14
        $result = $fix($this->schema);
1100
1101 14
        return $result;
1102
    }
1103
1104
    /**
1105
     * Look up a type based on its alias.
1106
     *
1107
     * @param string $alias The type alias or type name to lookup.
1108
     * @return mixed
1109
     */
1110 137
    protected function getType($alias) {
1111 137
        if (isset(self::$types[$alias])) {
1112
            return $alias;
1113
        }
1114 137
        foreach (self::$types as $type => $aliases) {
1115 137
            if (in_array($alias, $aliases, true)) {
1116 137
                return $type;
1117
            }
1118 137
        }
1119 8
        return null;
1120
    }
1121
1122
    /**
1123
     * Get the class that's used to contain validation information.
1124
     *
1125
     * @return Validation|string Returns the validation class.
1126
     */
1127 117
    public function getValidationClass() {
1128 117
        return $this->validationClass;
1129
    }
1130
1131
    /**
1132
     * Set the class that's used to contain validation information.
1133
     *
1134
     * @param Validation|string $class Either the name of a class or a class that will be cloned.
1135
     * @return $this
1136
     */
1137 1
    public function setValidationClass($class) {
1138 1
        if (!is_a($class, Validation::class, true)) {
1139
            throw new \InvalidArgumentException("$class must be a subclass of ".Validation::class, 500);
1140
        }
1141
1142 1
        $this->validationClass = $class;
1143 1
        return $this;
1144
    }
1145
1146
    /**
1147
     * Create a new validation instance.
1148
     *
1149
     * @return Validation Returns a validation object.
1150
     */
1151 117
    protected function createValidation() {
1152 117
        $class = $this->getValidationClass();
1153
1154 117
        if ($class instanceof Validation) {
1155 1
            $result = clone $class;
1156 1
        } else {
1157 117
            $result = new $class;
1158
        }
1159 117
        return $result;
1160
    }
1161
1162
    /**
1163
     * Check whether or not a value is an array or accessible like an array.
1164
     *
1165
     * @param mixed $value The value to check.
1166
     * @return bool Returns **true** if the value can be used like an array or **false** otherwise.
1167
     */
1168 77
    private function isArray($value) {
1169 77
        return is_array($value) || ($value instanceof \ArrayAccess && $value instanceof \Traversable);
1170
    }
1171
1172
    /**
1173
     * Cast a value to an array.
1174
     *
1175
     * @param \Traversable $value The value to convert.
1176
     * @return array Returns an array.
1177
     */
1178
    private function toArray(\Traversable $value) {
1179
        if ($value instanceof \ArrayObject) {
1180
            return $value->getArrayCopy();
1181
        }
1182
        return iterator_to_array($value);
1183
    }
1184
}
1185