Completed
Pull Request — master (#5)
by Todd
02:05
created

Schema::setValidationClass()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2.032

Importance

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