Completed
Pull Request — master (#9)
by Ryan
02:14
created

Schema::parseInternal()   B

Complexity

Conditions 5
Paths 6

Size

Total Lines 32
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 5.0042

Importance

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