Completed
Push — master ( f29654...874dbd )
by Todd
01:53
created

Schema::merge()   C

Complexity

Conditions 8
Paths 1

Size

Total Lines 24
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 8

Importance

Changes 0
Metric Value
dl 0
loc 24
ccs 14
cts 14
cp 1
rs 5.7377
c 0
b 0
f 0
cc 8
eloc 15
nc 1
nop 1
crap 8
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 143
    public function __construct($schema = []) {
67 143
        $this->schema = $schema;
68 143
    }
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
     */
151 2
    public function merge(Schema $schema) {
152
        $fn = function (array &$target, array $source) use (&$fn) {
153 2
            foreach ($source as $key => $val) {
154 2
                if (is_array($val) && array_key_exists($key, $target) && is_array($target[$key])) {
155 2
                    if (isset($val[0]) || isset($target[$key][0])) {
156
                        // This is a numeric array, so just do a merge.
157 1
                        $merged = array_merge($target[$key], $val);
158 1
                        if (is_string($merged[0])) {
159 1
                            $merged = array_keys(array_flip($merged));
160
                        }
161 1
                        $target[$key] = $merged;
162
                    } else {
163 2
                        $target[$key] = $fn($target[$key], $val);
164
                    }
165
                } else {
166 2
                    $target[$key] = $val;
167
                }
168
            }
169
170 2
            return $target;
171 2
        };
172
173 2
        $fn($this->schema, $schema->getSchemaArray());
174 2
    }
175
176
    /**
177
     * Returns the internal schema array.
178
     *
179
     * @return array
180
     * @see Schema::jsonSerialize()
181
     */
182 11
    public function getSchemaArray() {
183 11
        return $this->schema;
184
    }
185
186
    /**
187
     * Parse a short schema and return the associated schema.
188
     *
189
     * @param array $arr The schema array.
190
     * @return Schema Returns a new schema.
191
     */
192 138
    public static function parse(array $arr) {
193 138
        $schema = new Schema();
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...
194 138
        $schema->schema = $schema->parseInternal($arr);
195 138
        return $schema;
196
    }
197
198
    /**
199
     * Parse a schema in short form into a full schema array.
200
     *
201
     * @param array $arr The array to parse into a schema.
202
     * @return array The full schema array.
203
     * @throws \InvalidArgumentException Throws an exception when an item in the schema is invalid.
204
     */
205 138
    protected function parseInternal(array $arr) {
206 138
        if (empty($arr)) {
207
            // An empty schema validates to anything.
208 5
            return [];
209 134
        } elseif (isset($arr['type'])) {
210
            // This is a long form schema and can be parsed as the root.
211
            return $this->parseNode($arr);
212
        } else {
213
            // Check for a root schema.
214 134
            $value = reset($arr);
215 134
            $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...
216 134
            if (is_int($key)) {
217 83
                $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...
218 83
                $value = null;
219
            }
220 134
            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...
221 134
            if (empty($name)) {
222 50
                return $this->parseNode($param, $value);
223
            }
224
        }
225
226
        // If we are here then this is n object schema.
227 86
        list($properties, $required) = $this->parseProperties($arr);
228
229
        $result = [
230 86
            'type' => 'object',
231 86
            'properties' => $properties,
232 86
            'required' => $required
233
        ];
234
235 86
        return array_filter($result);
236
    }
237
238
    /**
239
     * Parse a schema node.
240
     *
241
     * @param array $node The node to parse.
242
     * @param mixed $value Additional information from the node.
243
     * @return array Returns a JSON schema compatible node.
244
     */
245 134
    private function parseNode($node, $value = null) {
246 134
        if (is_array($value)) {
247
            // The value describes a bit more about the schema.
248 50
            switch ($node['type']) {
249 50
                case 'array':
250 7
                    if (isset($value['items'])) {
251
                        // The value includes array schema information.
252 1
                        $node = array_replace($node, $value);
253
                    } else {
254 6
                        $node['items'] = $this->parseInternal($value);
255
                    }
256 7
                    break;
257 43
                case 'object':
258
                    // The value is a schema of the object.
259 9
                    if (isset($value['properties'])) {
260
                        list($node['properties']) = $this->parseProperties($value['properties']);
261
                    } else {
262 9
                        list($node['properties'], $required) = $this->parseProperties($value);
263 9
                        if (!empty($required)) {
264 9
                            $node['required'] = $required;
265
                        }
266
                    }
267 9
                    break;
268
                default:
269 34
                    $node = array_replace($node, $value);
270 50
                    break;
271
            }
272 101
        } elseif (is_string($value)) {
273 78
            if ($node['type'] === 'array' && $arrType = $this->getType($value)) {
274 2
                $node['items'] = ['type' => $arrType];
275
            } elseif (!empty($value)) {
276 22
                $node['description'] = $value;
277
            }
278 26
        } elseif ($value === null) {
279
            // Parse child elements.
280 24
            if ($node['type'] === 'array' && isset($node['items'])) {
281
                // The value includes array schema information.
282
                $node['items'] = $this->parseInternal($node['items']);
283 24
            } elseif ($node['type'] === 'object' && isset($node['properties'])) {
284
                list($node['properties']) = $this->parseProperties($node['properties']);
285
286
            }
287
        }
288
289
290 134
        return $node;
291
    }
292
293
    /**
294
     * Parse the schema for an object's properties.
295
     *
296
     * @param array $arr An object property schema.
297
     * @return array Returns a schema array suitable to be placed in the **properties** key of a schema.
298
     */
299 86
    private function parseProperties(array $arr) {
300 86
        $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...
301 86
        $requiredProperties = [];
302 86
        foreach ($arr as $key => $value) {
303
            // Fix a schema specified as just a value.
304 86
            if (is_int($key)) {
305 62
                if (is_string($value)) {
306 62
                    $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...
307 62
                    $value = '';
308
                } else {
309
                    throw new \InvalidArgumentException("Schema at position $key is not a valid parameter.", 500);
310
                }
311
            }
312
313
            // The parameter is defined in the key.
314 86
            list($name, $param, $required) = $this->parseShortParam($key, $value);
315
316 86
            $node = $this->parseNode($param, $value);
317
318 86
            $properties[$name] = $node;
319 86
            if ($required) {
320 44
                $requiredProperties[] = $name;
321
            }
322
        }
323 86
        return array($properties, $requiredProperties);
324
    }
325
326
    /**
327
     * Parse a short parameter string into a full array parameter.
328
     *
329
     * @param string $key The short parameter string to parse.
330
     * @param array $value An array of other information that might help resolve ambiguity.
331
     * @return array Returns an array in the form `[string name, array param, bool required]`.
332
     * @throws \InvalidArgumentException Throws an exception if the short param is not in the correct format.
333
     */
334 134
    public function parseShortParam($key, $value = []) {
335
        // Is the parameter optional?
336 134
        if (substr($key, -1) === '?') {
337 60
            $required = false;
338 60
            $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...
339
        } else {
340 92
            $required = true;
341
        }
342
343
        // Check for a type.
344 134
        $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...
345 134
        $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...
346 134
        $allowNull = false;
347 134
        if (!empty($parts[1])) {
348 133
            $types = explode('|', $parts[1]);
349 133
            foreach ($types as $alias) {
350 133
                $found = $this->getType($alias);
351 133
                if ($found === null) {
352
                    throw new \InvalidArgumentException("Unknown type '$alias'", 500);
353 133
                } elseif ($found === 'null') {
354 9
                    $allowNull = true;
355
                } else {
356 133
                    $type = $found;
357
                }
358
            }
359
        } else {
360 2
            $type = null;
361
        }
362
363 134
        if ($value instanceof Schema) {
364 2
            if ($type === 'array') {
365 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...
366
            } else {
367 1
                $param = $value;
368
            }
369 134
        } elseif (isset($value['type'])) {
370
            $param = $value;
371
372
            if (!empty($type) && $type !== $param['type']) {
373
                throw new \InvalidArgumentException("Type mismatch between $type and {$param['type']} for field $name.", 500);
374
            }
375
        } else {
376 134
            if (empty($type) && !empty($parts[1])) {
377
                throw new \InvalidArgumentException("Invalid type {$parts[1]} for field $name.", 500);
378
            }
379 134
            $param = ['type' => $type];
380
381
            // Parsed required strings have a minimum length of 1.
382 134
            if ($type === 'string' && !empty($name) && $required && (!isset($value['default']) || $value['default'] !== '')) {
383 29
                $param['minLength'] = 1;
384
            }
385
        }
386 134
        if ($allowNull) {
387 9
            $param['allowNull'] = true;
388
        }
389
390 134
        return [$name, $param, $required];
391
    }
392
393
    /**
394
     * Add a custom validator to to validate the schema.
395
     *
396
     * @param string $fieldname The name of the field to validate, if any.
397
     *
398
     * If you are adding a validator to a deeply nested field then separate the path with dots.
399
     * @param callable $callback The callback to validate with.
400
     * @return Schema Returns `$this` for fluent calls.
401
     */
402 2
    public function addValidator($fieldname, callable $callback) {
403 2
        $this->validators[$fieldname][] = $callback;
404 2
        return $this;
405
    }
406
407
    /**
408
     * Require one of a given set of fields in the schema.
409
     *
410
     * @param array $required The field names to require.
411
     * @param string $fieldname The name of the field to attach to.
412
     * @param int $count The count of required items.
413
     * @return Schema Returns `$this` for fluent calls.
414
     */
415 1
    public function requireOneOf(array $required, $fieldname = '', $count = 1) {
416 1
        $result = $this->addValidator(
417
            $fieldname,
418
            function ($data, ValidationField $field) use ($required, $count) {
419 1
                $hasCount = 0;
1 ignored issue
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 2 spaces but found 1 space

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

To visualize

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

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

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

will produce no issues.

Loading history...
420 1
                $flattened = [];
421
422 1
                foreach ($required as $name) {
423 1
                    $flattened = array_merge($flattened, (array)$name);
424
425 1
                    if (is_array($name)) {
426
                        // This is an array of required names. They all must match.
427 1
                        $hasCountInner = 0;
428 1
                        foreach ($name as $nameInner) {
429 1
                            if (isset($data[$nameInner]) && $data[$nameInner]) {
430 1
                                $hasCountInner++;
431
                            } else {
432 1
                                break;
433
                            }
434
                        }
435 1
                        if ($hasCountInner >= count($name)) {
436 1
                            $hasCount++;
437
                        }
438 1
                    } elseif (isset($data[$name]) && $data[$name]) {
439 1
                        $hasCount++;
440
                    }
441
442 1
                    if ($hasCount >= $count) {
443 1
                        return true;
444
                    }
445
                }
446
447 1
                if ($count === 1) {
448 1
                    $message = 'One of {required} are required.';
449
                } else {
450
                    $message = '{count} of {required} are required.';
451
                }
452
453 1
                $field->addError('missingField', [
454 1
                    'messageCode' => $message,
455 1
                    'required' => $required,
456 1
                    'count' => $count
457
                ]);
458 1
                return false;
459 1
            }
460
        );
461
462 1
        return $result;
463
    }
464
465
    /**
466
     * Validate data against the schema.
467
     *
468
     * @param mixed $data The data to validate.
469
     * @param bool $sparse Whether or not this is a sparse validation.
470
     * @return mixed Returns a cleaned version of the data.
471
     * @throws ValidationException Throws an exception when the data does not validate against the schema.
472
     */
473 117
    public function validate($data, $sparse = false) {
474 117
        $field = new ValidationField($this->createValidation(), $this->schema, '');
475
476 117
        $clean = $this->validateField($data, $field, $sparse);
477
478 115
        if (Invalid::isInvalid($clean) && $field->isValid()) {
479
            // This really shouldn't happen, but we want to protect against seeing the invalid object.
480
            $field->addError('invalid', ['messageCode' => '{field} is invalid.', 'status' => 422]);
481
        }
482
483 115
        if (!$field->getValidation()->isValid()) {
484 63
            throw new ValidationException($field->getValidation());
485
        }
486
487 71
        return $clean;
488
    }
489
490
    /**
491
     * Validate data against the schema and return the result.
492
     *
493
     * @param mixed $data The data to validate.
494
     * @param bool $sparse Whether or not to do a sparse validation.
495
     * @return bool Returns true if the data is valid. False otherwise.
496
     */
497 33
    public function isValid($data, $sparse = false) {
498
        try {
499 33
            $this->validate($data, $sparse);
500 23
            return true;
501 24
        } catch (ValidationException $ex) {
502 24
            return false;
503
        }
504
    }
505
506
    /**
507
     * Validate a field.
508
     *
509
     * @param mixed $value The value to validate.
510
     * @param ValidationField $field A validation object to add errors to.
511
     * @param bool $sparse Whether or not this is a sparse validation.
512
     * @return mixed|Invalid Returns a clean version of the value with all extra fields stripped out or invalid if the value
513
     * is completely invalid.
514
     */
515 117
    protected function validateField($value, ValidationField $field, $sparse = false) {
516 117
        $result = $value;
517 117
        if ($field->getField() instanceof Schema) {
518
            try {
519 1
                $result = $field->getField()->validate($value, $sparse);
520 1
            } catch (ValidationException $ex) {
521
                // The validation failed, so merge the validations together.
522 1
                $field->getValidation()->merge($ex->getValidation(), $field->getName());
523
            }
524 117
        } elseif ($value === null && $field->val('allowNull', false)) {
525 9
            $result = $value;
526
        } else {
527
            // Validate the field's type.
528 117
            $type = $field->getType();
529
            switch ($type) {
530 117
                case 'boolean':
531 21
                    $result = $this->validateBoolean($value, $field);
532 21
                    break;
533 104
                case 'integer':
534 23
                    $result = $this->validateInteger($value, $field);
535 23
                    break;
536 102
                case 'number':
537 9
                    $result = $this->validateNumber($value, $field);
538 9
                    break;
539 101
                case 'string':
540 52
                    $result = $this->validateString($value, $field);
541 52
                    break;
542 84
                case 'timestamp':
543 8
                    $result = $this->validateTimestamp($value, $field);
544 8
                    break;
545 83
                case 'datetime':
546 9
                    $result = $this->validateDatetime($value, $field);
547 9
                    break;
548 80
                case 'array':
549 13
                    $result = $this->validateArray($value, $field, $sparse);
550 13
                    break;
551 78
                case 'object':
552 77
                    $result = $this->validateObject($value, $field, $sparse);
553 75
                    break;
554 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...
555
                    // No type was specified so we are valid.
556 2
                    $result = $value;
557 2
                    break;
558
                default:
559
                    throw new \InvalidArgumentException("Unrecognized type $type.", 500);
560
            }
561 117
            if (Invalid::isValid($result)) {
562 115
                $result = $this->validateEnum($result, $field);
563
            }
564
        }
565
566
        // Validate a custom field validator.
567 117
        if (Invalid::isValid($result)) {
568 115
            $this->callValidators($result, $field);
569
        }
570
571 117
        return $result;
572
    }
573
574
    /**
575
     * Validate an array.
576
     *
577
     * @param mixed $value The value to validate.
578
     * @param ValidationField $field The validation results to add.
579
     * @param bool $sparse Whether or not this is a sparse validation.
580
     * @return array|Invalid Returns an array or invalid if validation fails.
581
     */
582 13
    protected function validateArray($value, ValidationField $field, $sparse = false) {
583 13
        if ((!is_array($value) || (count($value) > 0 && !array_key_exists(0, $value))) && !$value instanceof \Traversable) {
584 7
            $field->addTypeError('array');
585 7
            return Invalid::value();
586 7
        } elseif ($field->val('items') !== null) {
587 5
            $result = [];
588
589
            // Validate each of the types.
590 5
            $itemValidation = new ValidationField(
591 5
                $field->getValidation(),
592 5
                $field->val('items'),
593 5
                ''
594
            );
595
596 5
            $count = 0;
597 5
            foreach ($value as $i => $item) {
598 5
                $itemValidation->setName($field->getName()."[{$i}]");
599 5
                $validItem = $this->validateField($item, $itemValidation, $sparse);
600 5
                if (Invalid::isValid($validItem)) {
601 5
                    $result[] = $validItem;
602
                }
603 5
                $count++;
604
            }
605
606 5
            return empty($result) && $count > 0 ? Invalid::value() : $result;
607
        } else {
608
            // Cast the items into a proper numeric array.
609 2
            $result = is_array($value) ? array_values($value) : iterator_to_array($value);
610 2
            return $result;
611
        }
612
    }
613
614
    /**
615
     * Validate a boolean value.
616
     *
617
     * @param mixed $value The value to validate.
618
     * @param ValidationField $field The validation results to add.
619
     * @return bool|Invalid Returns the cleaned value or invalid if validation fails.
620
     */
621 21
    protected function validateBoolean($value, ValidationField $field) {
622 21
        $value = $value === null ? $value : filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
623 21
        if ($value === null) {
624 5
            $field->addTypeError('boolean');
625 5
            return Invalid::value();
626
        }
627 17
        return $value;
628
    }
629
630
    /**
631
     * Validate a date time.
632
     *
633
     * @param mixed $value The value to validate.
634
     * @param ValidationField $field The validation results to add.
635
     * @return \DateTimeInterface|Invalid Returns the cleaned value or **null** if it isn't valid.
636
     */
637 13
    protected function validateDatetime($value, ValidationField $field) {
638 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...
639
            // do nothing, we're good
640 11
        } elseif (is_string($value) && $value !== '') {
641
            try {
642 7
                $dt = new \DateTimeImmutable($value);
643 5
                if ($dt) {
644 5
                    $value = $dt;
645
                } else {
646
                    $value = null;
647
                }
648 2
            } catch (\Exception $ex) {
649 2
                $value = Invalid::value();
650
            }
651 4
        } elseif (is_int($value) && $value > 0) {
652 1
            $value = new \DateTimeImmutable('@'.(string)round($value));
653
        } else {
654 3
            $value = Invalid::value();
655
        }
656
657 13
        if (Invalid::isInvalid($value)) {
658 5
            $field->addTypeError('datetime');
659
        }
660 13
        return $value;
661
    }
662
663
    /**
664
     * Validate a float.
665
     *
666
     * @param mixed $value The value to validate.
667
     * @param ValidationField $field The validation results to add.
668
     * @return float|Invalid Returns a number or **null** if validation fails.
669
     */
670 9
    protected function validateNumber($value, ValidationField $field) {
671 9
        $result = filter_var($value, FILTER_VALIDATE_FLOAT);
672 9
        if ($result === false) {
673 5
            $field->addTypeError('number');
674 5
            return Invalid::value();
675
        }
676 4
        return $result;
677
    }
678
679
    /**
680
     * Validate and integer.
681
     *
682
     * @param mixed $value The value to validate.
683
     * @param ValidationField $field The validation results to add.
684
     * @return int|Invalid Returns the cleaned value or **null** if validation fails.
685
     */
686 23
    protected function validateInteger($value, ValidationField $field) {
687 23
        $result = filter_var($value, FILTER_VALIDATE_INT);
688
689 23
        if ($result === false) {
690 9
            $field->addTypeError('integer');
691 9
            return Invalid::value();
692
        }
693 17
        return $result;
694
    }
695
696
    /**
697
     * Validate an object.
698
     *
699
     * @param mixed $value The value to validate.
700
     * @param ValidationField $field The validation results to add.
701
     * @param bool $sparse Whether or not this is a sparse validation.
702
     * @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...
703
     */
704 77
    protected function validateObject($value, ValidationField $field, $sparse = false) {
705 77
        if (!$this->isArray($value) || isset($value[0])) {
706 7
            $field->addTypeError('object');
707 7
            return Invalid::value();
708 77
        } elseif (is_array($field->val('properties'))) {
709
            // Validate the data against the internal schema.
710 76
            $value = $this->validateProperties($value, $field, $sparse);
711 1
        } elseif (!is_array($value)) {
712
            $value = $this->toArray($value);
713
        }
714 75
        return $value;
715
    }
716
717
    /**
718
     * Validate data against the schema and return the result.
719
     *
720
     * @param array|\ArrayAccess $data The data to validate.
721
     * @param ValidationField $field This argument will be filled with the validation result.
722
     * @param bool $sparse Whether or not this is a sparse validation.
723
     * @return array|Invalid Returns a clean array with only the appropriate properties and the data coerced to proper types.
724
     * or invalid if there are no valid properties.
725
     */
726 76
    protected function validateProperties($data, ValidationField $field, $sparse = false) {
727 76
        $properties = $field->val('properties', []);
728 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...
729
730 76
        if (is_array($data)) {
731 75
            $keys = array_keys($data);
732
        } else {
733 1
            $keys = array_keys(iterator_to_array($data));
734
        }
735 76
        $keys = array_combine(array_map('strtolower', $keys), $keys);
736
737 76
        $propertyField = new ValidationField($field->getValidation(), [], null);
738
739
        // Loop through the schema fields and validate each one.
740 76
        $clean = [];
741 76
        foreach ($properties as $propertyName => $property) {
742
            $propertyField
743 76
                ->setField($property)
744 76
                ->setName(ltrim($field->getName().".$propertyName", '.'));
745
746 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...
747 76
            $isRequired = isset($required[$propertyName]);
748
749
            // First check for required fields.
750 76
            if (!array_key_exists($lName, $keys)) {
751 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...
752
                    // Sparse validation can leave required fields out.
753 20
                } elseif ($propertyField->hasVal('default')) {
754 2
                    $clean[$propertyName] = $propertyField->val('default');
755 18
                } elseif ($isRequired) {
756 6
                    $propertyField->addError('missingField', ['messageCode' => '{field} is required.']);
757
                }
758
            } else {
759 74
                $clean[$propertyName] = $this->validateField($data[$keys[$lName]], $propertyField, $sparse);
760
            }
761
762 76
            unset($keys[$lName]);
763
        }
764
765
        // Look for extraneous properties.
766 76
        if (!empty($keys)) {
767 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...
768 2
                $msg = sprintf("%s has unexpected field(s): %s.", $field->getName() ?: 'value', implode(', ', $keys));
769 2
                trigger_error($msg, E_USER_NOTICE);
770
            }
771
772 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...
773 2
                $field->addError('invalid', [
774 2
                    'messageCode' => '{field} has {extra,plural,an unexpected field,unexpected fields}: {extra}.',
775 2
                    'extra' => array_values($keys),
776 2
                    'status' => 422
777
                ]);
778
            }
779
        }
780
781 74
        return $clean;
782
    }
783
784
    /**
785
     * Validate a string.
786
     *
787
     * @param mixed $value The value to validate.
788
     * @param ValidationField $field The validation results to add.
789
     * @return string|Invalid Returns the valid string or **null** if validation fails.
790
     */
791 52
    protected function validateString($value, ValidationField $field) {
792 52
        if (is_string($value) || is_numeric($value)) {
793 49
            $value = $result = (string)$value;
794
        } else {
795 6
            $field->addTypeError('string');
796 6
            return Invalid::value();
797
        }
798
799 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...
800 49
        if (($minLength = $field->val('minLength', 0)) > 0 && mb_strlen($value) < $minLength) {
801 4
            if (!empty($field->getName()) && $minLength === 1) {
802 2
                $field->addError('missingField', ['messageCode' => '{field} is required.', 'status' => 422]);
803
            } else {
804 2
                $field->addError(
805 2
                    'minLength',
806
                    [
807 2
                        'messageCode' => '{field} should be at least {minLength} {minLength,plural,character} long.',
808 2
                        'minLength' => $minLength,
809 2
                        'status' => 422
810
                    ]
811
                );
812
            }
813
        }
814 49
        if (($maxLength = $field->val('maxLength', 0)) > 0 && mb_strlen($value) > $maxLength) {
815 1
            $field->addError(
816 1
                'maxLength',
817
                [
818 1
                    'messageCode' => '{field} is {overflow} {overflow,plural,characters} too long.',
819 1
                    'maxLength' => $maxLength,
820 1
                    'overflow' => mb_strlen($value) - $maxLength,
821 1
                    'status' => 422
822
                ]
823
            );
824
        }
825 49
        if ($pattern = $field->val('pattern')) {
826 4
            $regex = '`'.str_replace('`', preg_quote('`', '`'), $pattern).'`';
827
828 4
            if (!preg_match($regex, $value)) {
829 2
                $field->addError(
830 2
                    'invalid',
831
                    [
832 2
                        'messageCode' => '{field} is in the incorrect format.',
833
                        'status' => 422
834
                    ]
835
                );
836
            }
837
        }
838 49
        if ($format = $field->val('format')) {
839 15
            $type = $format;
840
            switch ($format) {
841 15
                case 'date-time':
842 4
                    $result = $this->validateDatetime($result, $field);
843 4
                    if ($result instanceof \DateTimeInterface) {
844 4
                        $result = $result->format(\DateTime::RFC3339);
845
                    }
846 4
                    break;
847 11
                case 'email':
848 1
                    $result = filter_var($result, FILTER_VALIDATE_EMAIL);
849 1
                    break;
850 10
                case 'ipv4':
851 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...
852 1
                    $result = filter_var($result, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4);
853 1
                    break;
854 9
                case 'ipv6':
855 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...
856 1
                    $result = filter_var($result, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6);
857 1
                    break;
858 8
                case 'ip':
859 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...
860 1
                    $result = filter_var($result, FILTER_VALIDATE_IP);
861 1
                    break;
862 7
                case 'uri':
863 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...
864 7
                    $result = filter_var($result, FILTER_VALIDATE_URL, FILTER_FLAG_HOST_REQUIRED | FILTER_FLAG_SCHEME_REQUIRED);
865 7
                    break;
866
                default:
867
                    trigger_error("Unrecognized format '$format'.", E_USER_NOTICE);
868
            }
869 15
            if ($result === false) {
870 5
                $field->addTypeError($type);
871
            }
872
        }
873
874 49
        if ($field->isValid()) {
875 41
            return $result;
876
        } else {
877 12
            return Invalid::value();
878
        }
879
    }
880
881
    /**
882
     * Validate a unix timestamp.
883
     *
884
     * @param mixed $value The value to validate.
885
     * @param ValidationField $field The field being validated.
886
     * @return int|Invalid Returns a valid timestamp or invalid if the value doesn't validate.
887
     */
888 8
    protected function validateTimestamp($value, ValidationField $field) {
889 8
        if (is_numeric($value) && $value > 0) {
890 2
            $result = (int)$value;
891 6
        } elseif (is_string($value) && $ts = strtotime($value)) {
892 1
            $result = $ts;
893
        } else {
894 5
            $field->addTypeError('timestamp');
895 5
            $result = Invalid::value();
896
        }
897 8
        return $result;
898
    }
899
900
    /**
901
     * Validate a null value.
902
     *
903
     * @param mixed $value The value to validate.
904
     * @param ValidationField $field The error collector for the field.
905
     * @return null|Invalid Returns **null** or invalid.
906
     */
907
    protected function validateNull($value, ValidationField $field) {
908
        if ($value === null) {
909
            return null;
910
        }
911
        $field->addError('invalid', ['messageCode' => '{field} should be null.', 'status' => 422]);
912
        return Invalid::value();
913
    }
914
915
    /**
916
     * Validate a value against an enum.
917
     *
918
     * @param mixed $value The value to test.
919
     * @param ValidationField $field The validation object for adding errors.
920
     * @return mixed|Invalid Returns the value if it is one of the enumerated values or invalid otherwise.
921
     */
922 115
    protected function validateEnum($value, ValidationField $field) {
923 115
        $enum = $field->val('enum');
924 115
        if (empty($enum)) {
925 114
            return $value;
926
        }
927
928 1
        if (!in_array($value, $enum, true)) {
929 1
            $field->addError(
930 1
                'invalid',
931
                [
932 1
                    'messageCode' => '{field} must be one of: {enum}.',
933 1
                    'enum' => $enum,
934 1
                    'status' => 422
935
                ]
936
            );
937 1
            return Invalid::value();
938
        }
939 1
        return $value;
940
    }
941
942
    /**
943
     * Call all of the validators attached to a field.
944
     *
945
     * @param mixed $value The field value being validated.
946
     * @param ValidationField $field The validation object to add errors.
947
     */
948 115
    protected function callValidators($value, ValidationField $field) {
949 115
        $valid = true;
950
951
        // Strip array references in the name except for the last one.
952 115
        $key = preg_replace(['`\[\d+\]$`', '`\[\d+\]`'], ['[]', ''], $field->getName());
953 115
        if (!empty($this->validators[$key])) {
954 2
            foreach ($this->validators[$key] as $validator) {
955 2
                $r = call_user_func($validator, $value, $field);
956
957 2
                if ($r === false || Invalid::isInvalid($r)) {
958 1
                    $valid = false;
959
                }
960
            }
961
        }
962
963
        // Add an error on the field if the validator hasn't done so.
964 115
        if (!$valid && $field->isValid()) {
965
            $field->addError('invalid', ['messageCode' => '{field} is invalid.', 'status' => 422]);
966
        }
967 115
    }
968
969
    /**
970
     * Specify data which should be serialized to JSON.
971
     *
972
     * This method specifically returns data compatible with the JSON schema format.
973
     *
974
     * @return mixed Returns data which can be serialized by **json_encode()**, which is a value of any type other than a resource.
975
     * @link http://php.net/manual/en/jsonserializable.jsonserialize.php
976
     * @link http://json-schema.org/
977
     */
978
    public function jsonSerialize() {
979 15
        $fix = function ($schema) use (&$fix) {
980 15
            if ($schema instanceof Schema) {
981 1
                return $schema->jsonSerialize();
982
            }
983
984 15
            if (!empty($schema['type'])) {
985
                // Swap datetime and timestamp to other types with formats.
986 14
                if ($schema['type'] === 'datetime') {
987 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...
988 3
                    $schema['format'] = 'date-time';
989 13
                } elseif ($schema['type'] === 'timestamp') {
990 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...
991 3
                    $schema['format'] = 'timestamp';
992
                }
993
            }
994
995 15
            if (!empty($schema['items'])) {
996 4
                $schema['items'] = $fix($schema['items']);
997
            }
998 15
            if (!empty($schema['properties'])) {
999 11
                $properties = [];
1000 11
                foreach ($schema['properties'] as $key => $property) {
1001 11
                    $properties[$key] = $fix($property);
1002
                }
1003 11
                $schema['properties'] = $properties;
1004
            }
1005
1006 15
            return $schema;
1007 15
        };
1008
1009 15
        $result = $fix($this->schema);
1010
1011 15
        return $result;
1012
    }
1013
1014
    /**
1015
     * Look up a type based on its alias.
1016
     *
1017
     * @param string $alias The type alias or type name to lookup.
1018
     * @return mixed
1019
     */
1020 133
    protected function getType($alias) {
1021 133
        if (isset(self::$types[$alias])) {
1022
            return $alias;
1023
        }
1024 133
        foreach (self::$types as $type => $aliases) {
1025 133
            if (in_array($alias, $aliases, true)) {
1026 133
                return $type;
1027
            }
1028
        }
1029 8
        return null;
1030
    }
1031
1032
    /**
1033
     * Get the class that's used to contain validation information.
1034
     *
1035
     * @return Validation|string Returns the validation class.
1036
     */
1037 117
    public function getValidationClass() {
1038 117
        return $this->validationClass;
1039
    }
1040
1041
    /**
1042
     * Set the class that's used to contain validation information.
1043
     *
1044
     * @param Validation|string $class Either the name of a class or a class that will be cloned.
1045
     * @return $this
1046
     */
1047 1
    public function setValidationClass($class) {
1048 1
        if (!is_a($class, Validation::class, true)) {
1049
            throw new \InvalidArgumentException("$class must be a subclass of ".Validation::class, 500);
1050
        }
1051
1052 1
        $this->validationClass = $class;
1053 1
        return $this;
1054
    }
1055
1056
    /**
1057
     * Create a new validation instance.
1058
     *
1059
     * @return Validation Returns a validation object.
1060
     */
1061 117
    protected function createValidation() {
1062 117
        $class = $this->getValidationClass();
1063
1064 117
        if ($class instanceof Validation) {
1065 1
            $result = clone $class;
1066
        } else {
1067 117
            $result = new $class;
1068
        }
1069 117
        return $result;
1070
    }
1071
1072
    /**
1073
     * Check whether or not a value is an array or accessible like an array.
1074
     *
1075
     * @param mixed $value The value to check.
1076
     * @return bool Returns **true** if the value can be used like an array or **false** otherwise.
1077
     */
1078 77
    private function isArray($value) {
1079 77
        return is_array($value) || ($value instanceof \ArrayAccess && $value instanceof \Traversable);
1080
    }
1081
1082
    /**
1083
     * Cast a value to an array.
1084
     *
1085
     * @param \Traversable $value The value to convert.
1086
     * @return array Returns an array.
1087
     */
1088
    private function toArray(\Traversable $value) {
1089
        if ($value instanceof \ArrayObject) {
1090
            return $value->getArrayCopy();
1091
        }
1092
        return iterator_to_array($value);
1093
    }
1094
}
1095