Completed
Push — master ( 0a3381...51a727 )
by Todd
01:47
created

Schema::val()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 2
eloc 2
nc 2
nop 3
crap 2
1
<?php
2
/**
3
 * @author Todd Burry <[email protected]>
4
 * @copyright 2009-2017 Vanilla Forums Inc.
5
 * @license GPLv2
6
 */
7
8
namespace Garden\Schema;
9
10
/**
11
 * A class for defining and validating data schemas.
12
 */
13
class Schema implements \JsonSerializable {
14
    /// Constants ///
15
16
    /**
17
     * Throw a notice when extraneous properties are encountered during validation.
18
     */
19
    const FLAG_EXTRA_PROPERTIES_NOTICE = 0x1;
20
21
    /**
22
     * Throw a ValidationException when extraneous properties are encountered during validation.
23
     */
24
    const FLAG_EXTRA_PROPERTIES_EXCEPTION = 0x2;
25
26
    /// Properties ///
27
    protected static $types = [
28
        'a' => 'array',
29
        'o' => 'object',
30
        'i' => 'integer',
31
        'int' => 'integer',
32
        's' => 'string',
33
        'str' => 'string',
34
        'f' => 'float',
35
        'b' => 'boolean',
36
        'bool' => 'boolean',
37
        'ts' => 'timestamp',
38
        'dt' => 'datetime'
39
    ];
40
41
    protected $schema = [];
42
43
    /**
44
     * @var int A bitwise combination of the various **Schema::FLAG_*** constants.
45
     */
46
    protected $flags = 0;
47
48
    /**
49
     * @var array An array of callbacks that will custom validate the schema.
50
     */
51
    protected $validators = [];
52
53
    /// Methods ///
54
55
    /**
56
     * Initialize an instance of a new {@link Schema} class.
57
     *
58
     * @param array $schema The array schema to validate against.
59
     */
60 96
    public function __construct($schema = []) {
61 96
        $this->schema = $this->parse($schema);
62 96
    }
63
64
    /**
65
     * Grab the schema's current description.
66
     *
67
     * @return string
68
     */
69 1
    public function getDescription() {
70 1
        return isset($this->schema['description']) ? $this->schema['description'] : '';
71
    }
72
73
    /**
74
     * Set the description for the schema.
75
     *
76
     * @param string $description The new description.
77
     * @throws \InvalidArgumentException Throws an exception when the provided description is not a string.
78
     * @return Schema
79
     */
80 2
    public function setDescription($description) {
81 2
        if (is_string($description)) {
82 1
            $this->schema['description'] = $description;
83 1
        } else {
84 1
            throw new \InvalidArgumentException("The description is not a valid string.", 500);
85
        }
86
87 1
        return $this;
88
    }
89
90
    /**
91
     * Return the validation flags.
92
     *
93
     * @return int Returns a bitwise combination of flags.
94
     */
95 1
    public function getFlags() {
96 1
        return $this->flags;
97
    }
98
99
    /**
100
     * Set the validation flags.
101
     *
102
     * @param int $flags One or more of the **Schema::FLAG_*** constants.
103
     * @return Schema Returns the current instance for fluent calls.
104
     */
105 8
    public function setFlags($flags) {
106 8
        if (!is_int($flags)) {
107 1
            throw new \InvalidArgumentException('Invalid flags.', 500);
108
        }
109 7
        $this->flags = $flags;
110
111 7
        return $this;
112
    }
113
114
    /**
115
     * Whether or not the schema has a flag (or combination of flags).
116
     *
117
     * @param int $flag One or more of the **Schema::VALIDATE_*** constants.
118
     * @return bool Returns **true** if all of the flags are set or **false** otherwise.
119
     */
120 8
    public function hasFlag($flag) {
121 8
        return ($this->flags & $flag) === $flag;
122
    }
123
124
    /**
125
     * Set a flag.
126
     *
127
     * @param int $flag One or more of the **Schema::VALIDATE_*** constants.
128
     * @param bool $value Either true or false.
129
     * @return $this
130
     */
131 1
    public function setFlag($flag, $value) {
132 1
        if ($value) {
133 1
            $this->flags = $this->flags | $flag;
134 1
        } else {
135 1
            $this->flags = $this->flags & ~$flag;
136
        }
137 1
        return $this;
138
    }
139
140
    /**
141
     * Merge a schema with this one.
142
     *
143
     * @param Schema $schema A scheme instance. Its parameters will be merged into the current instance.
144
     */
145 2
    public function merge(Schema $schema) {
146
        $fn = function (array &$target, array $source) use (&$fn) {
147 2
            foreach ($source as $key => $val) {
148 2
                if (is_array($val) && array_key_exists($key, $target) && is_array($target[$key])) {
149 2
                    if (isset($val[0]) || isset($target[$key][0])) {
150
                        // This is a numeric array, so just do a merge.
151 1
                        $merged = array_merge($target[$key], $val);
152 1
                        if (is_string($merged[0])) {
153 1
                            $merged = array_keys(array_flip($merged));
154 1
                        }
155 1
                        $target[$key] = $merged;
156 1
                    } else {
157 2
                        $target[$key] = $fn($target[$key], $val);
158
                    }
159 2
                } else {
160 2
                    $target[$key] = $val;
161
                }
162 2
            }
163
164 2
            return $target;
165 2
        };
166
167 2
        $fn($this->schema, $schema->jsonSerialize());
168 2
    }
169
170
    /**
171
     * Parse a schema in short form into a full schema array.
172
     *
173
     * @param array $arr The array to parse into a schema.
174
     * @return array The full schema array.
175
     * @throws \InvalidArgumentException Throws an exception when an item in the schema is invalid.
176
     */
177 96
    public function parse(array $arr) {
178 96
        if (empty($arr)) {
179
            // An empty schema validates to anything.
180 6
            return [];
181 91
        } elseif (isset($arr['type'])) {
182
            // This is a long form schema and can be parsed as the root.
183 2
            return $this->parseNode($arr);
184
        } else {
185
            // Check for a root schema.
186 91
            $value = reset($arr);
187 91
            $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...
188 91
            if (is_int($key)) {
189 72
                $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...
190 72
                $value = null;
191 72
            }
192 91
            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...
193 91
            if (empty($name)) {
194 25
                return $this->parseNode($param, $value);
195
            }
196
        }
197
198
        // If we are here then this is n object schema.
199 68
        list($properties, $required) = $this->parseProperties($arr);
200
201
        $result = [
202 68
            'type' => 'object',
203 68
            'properties' => $properties,
204
            'required' => $required
205 68
        ];
206
207 68
        return array_filter($result);
208
    }
209
210
    /**
211
     * @param array $node
212
     * @param mixed $value
213
     * @return array
214
     */
215 91
    private function parseNode($node, $value = null) {
216 91
        if (is_array($value)) {
217
            // The value describes a bit more about the schema.
218 17
            switch ($node['type']) {
219 17
                case 'array':
220 6
                    if (isset($value['items'])) {
221
                        // The value includes array schema information.
222 1
                        $node = array_replace($node, $value);
223 1
                    } else {
224 5
                        $node['items'] = $this->parse($value);
225
                    }
226 6
                    break;
227 11
                case 'object':
228
                    // The value is a schema of the object.
229 9
                    if (isset($value['properties'])) {
230
                        list($node['properties']) = $this->parseProperties($value['properties']);
231
                    } else {
232 9
                        list($node['properties'], $required) = $this->parseProperties($value);
233 9
                        if (!empty($required)) {
234 9
                            $node['required'] = $required;
235 9
                        }
236
                    }
237 9
                    break;
238 2
                default:
239 2
                    $node = array_replace($node, $value);
240 2
                    break;
241 17
            }
242 91
        } elseif (is_string($value)) {
243 76
            if ($node['type'] === 'array' && $arrType = $this->getType($value)) {
244 2
                $node['items'] = ['type' => $arrType];
245 76
            } elseif (!empty($value)) {
246 22
                $node['description'] = $value;
247 22
            }
248 76
        }
249
250 91
        return $node;
251
    }
252
253
    /**
254
     * @param array $arr
255
     * @return array
256
     */
257 68
    private function parseProperties(array $arr) {
258 68
        $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...
259 68
        $requiredProperties = [];
260 68
        foreach ($arr as $key => $value) {
261
            // Fix a schema specified as just a value.
262 68
            if (is_int($key)) {
263 60
                if (is_string($value)) {
264 60
                    $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...
265 60
                    $value = '';
266 60
                } else {
267
                    throw new \InvalidArgumentException("Schema at position $key is not a valid parameter.", 500);
268
                }
269 60
            }
270
271
            // The parameter is defined in the key.
272 68
            list($name, $param, $required) = $this->parseShortParam($key, $value);
273
274 68
            $node = $this->parseNode($param, $value);
275
276 68
            $properties[$name] = $node;
277 68
            if ($required) {
278 36
                $requiredProperties[] = $name;
279 36
            }
280 68
        }
281 68
        return array($properties, $requiredProperties);
282
    }
283
284
    /**
285
     * Parse a short parameter string into a full array parameter.
286
     *
287
     * @param string $key The short parameter string to parse.
288
     * @param array $value An array of other information that might help resolve ambiguity.
289
     * @return array Returns an array in the form `[string name, array param, bool required]`.
290
     * @throws \InvalidArgumentException Throws an exception if the short param is not in the correct format.
291
     */
292 91
    public function parseShortParam($key, $value = []) {
293
        // Is the parameter optional?
294 91
        if (substr($key, -1) === '?') {
295 50
            $required = false;
296 50
            $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...
297 50
        } else {
298 59
            $required = true;
299
        }
300
301
        // Check for a type.
302 91
        $parts = explode(':', $key);
303 91
        $name = $parts[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...
304 91
        $type = !empty($parts[1]) && isset(self::$types[$parts[1]]) ? self::$types[$parts[1]] : null;
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...
305
306 91
        if ($value instanceof Schema) {
307 2
            if ($type === 'array') {
308 1
                $param = ['type' => $type, 'items' => $value];
309 1
            } else {
310 1
                $param = $value;
311
            }
312 91
        } elseif (isset($value['type'])) {
313
            $param = $value;
314
315
            if (!empty($type) && $type !== $param['type']) {
316
                throw new \InvalidArgumentException("Type mismatch between $type and {$param['type']} for field $name.", 500);
317
            }
318
        } else {
319 91
            if (empty($type) && !empty($parts[1])) {
320
                throw new \InvalidArgumentException("Invalid type {$parts[1]} for field $name.", 500);
321
            }
322 91
            $param = ['type' => $type];
323
324
            // Parsed required strings have a minimum length of 1.
325 91
            if ($type === 'string' && !empty($name) && $required) {
326 21
                $param['minLength'] = 1;
327 21
            }
328
        }
329
330 91
        return [$name, $param, $required];
331
    }
332
333
    /**
334
     * Add a custom validator to to validate the schema.
335
     *
336
     * @param string $fieldname The name of the field to validate, if any.
337
     *
338
     * If you are adding a validator to a deeply nested field then separate the path with dots.
339
     * @param callable $callback The callback to validate with.
340
     * @return Schema Returns `$this` for fluent calls.
341
     */
342 2
    public function addValidator($fieldname, callable $callback) {
343 2
        $this->validators[$fieldname][] = $callback;
344 2
        return $this;
345
    }
346
347
    /**
348
     * Require one of a given set of fields in the schema.
349
     *
350
     * @param array $required The field names to require.
351
     * @param string $fieldname The name of the field to attach to.
352
     * @param int $count The count of required items.
353
     * @return Schema Returns `$this` for fluent calls.
354
     */
355 1
    public function requireOneOf(array $required, $fieldname = '', $count = 1) {
356 1
        $result = $this->addValidator(
357 1
            $fieldname,
358
            function ($data, $fieldname, Validation $validation) use ($required, $count) {
359 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...
360 1
                $flattened = [];
361
362 1
                foreach ($required as $name) {
363 1
                    $flattened = array_merge($flattened, (array)$name);
364
365 1
                    if (is_array($name)) {
366
                        // This is an array of required names. They all must match.
367 1
                        $hasCountInner = 0;
368 1
                        foreach ($name as $nameInner) {
369 1
                            if (isset($data[$nameInner]) && $data[$nameInner]) {
370 1
                                $hasCountInner++;
371 1
                            } else {
372 1
                                break;
373
                            }
374 1
                        }
375 1
                        if ($hasCountInner >= count($name)) {
376 1
                            $hasCount++;
377 1
                        }
378 1
                    } elseif (isset($data[$name]) && $data[$name]) {
379 1
                        $hasCount++;
380 1
                    }
381
382 1
                    if ($hasCount >= $count) {
383 1
                        return true;
384
                    }
385 1
                }
386
387 1
                if ($count === 1) {
388 1
                    $message = 'One of {required} are required.';
389 1
                } else {
390
                    $message = '{count} of {required} are required.';
391
                }
392
393 1
                $validation->addError($fieldname, 'missingField', [
394 1
                    'messageCode' => $message,
395 1
                    'required' => $required,
396
                    'count' => $count
397 1
                ]);
398 1
                return false;
399
            }
400 1
        );
401
402 1
        return $result;
403
    }
404
405
    /**
406
     * Validate data against the schema.
407
     *
408
     * @param mixed $data The data to validate.
409
     * @param bool $sparse Whether or not this is a sparse validation.
410
     * @return mixed Returns a cleaned version of the data.
411
     * @throws ValidationException Throws an exception when the data does not validate against the schema.
412
     */
413 74
    public function validate($data, $sparse = false) {
414 74
        $validation = new Validation();
415
416 74
        $clean = $this->validateField($data, $this->schema, $validation, '', $sparse);
417
418 72
        if (!$validation->isValid()) {
419 41
            throw new ValidationException($validation);
420
        }
421
422 37
        return $clean;
423
    }
424
425
    /**
426
     * Validate data against the schema and return the result.
427
     *
428
     * @param array &$data The data to validate.
429
     * @param bool $sparse Whether or not to do a sparse validation.
430
     * @return bool Returns true if the data is valid. False otherwise.
431
     */
432 21
    public function isValid(array &$data, $sparse = false) {
433
        try {
434 21
            $this->validate($data, $sparse);
435 12
            return true;
436 11
        } catch (ValidationException $ex) {
437 11
            return false;
438
        }
439
    }
440
441
    /**
442
     * Validate a field.
443
     *
444
     * @param mixed $value The value to validate.
445
     * @param array|Schema $field Parameters on the field.
446
     * @param Validation $validation A validation object to add errors to.
447
     * @param string $name The name of the field being validated or an empty string for the root.
448
     * @param bool $sparse Whether or not this is a sparse validation.
449
     * @return mixed Returns a clean version of the value with all extra fields stripped out.
450
     */
451 74
    private function validateField($value, $field, Validation $validation, $name = '', $sparse = false) {
452 74
        if ($field instanceof Schema) {
453
            try {
454 1
                $value = $field->validate($value, $sparse);
455 1
            } catch (ValidationException $ex) {
456
                // The validation failed, so merge the validations together.
457 1
                $validation->merge($ex->getValidation(), $name);
458
            }
459 1
        } else {
460 74
            $type = isset($field['type']) ? $field['type'] : '';
461
462
            // Validate the field's type.
463 74
            $validType = true;
464
            switch ($type) {
465 74
                case 'boolean':
466 19
                    $validType &= $this->validateBoolean($value, $field);
467 19
                    break;
468 62
                case 'integer':
469 19
                    $validType &= $this->validateInteger($value, $field);
470 19
                    break;
471 62
                case 'float':
472 7
                    $validType &= $this->validateFloat($value, $field);
473 7
                    break;
474 62
                case 'string':
475 19
                    $validType &= $this->validateString($value, $field, $validation);
476 19
                    break;
477 62
                case 'timestamp':
478 6
                    $validType &= $this->validateTimestamp($value, $field, $validation);
479 6
                    break;
480 62
                case 'datetime':
481 6
                    $validType &= $this->validateDatetime($value, $field);
482 6
                    break;
483 60
                case 'array':
484 10
                    $validType &= $this->validateArray($value, $field, $validation, $name, $sparse);
485 10
                    break;
486 59
                case 'object':
487 58
                    $validType &= $this->validateObject($value, $field, $validation, $name, $sparse);
488 56
                    break;
489 2
                case '':
490
                    // No type was specified so we are valid.
491 2
                    $validType = true;
492 2
                    break;
493
                default:
494
                    throw new \InvalidArgumentException("Unrecognized type $type.", 500);
495
            }
496 74
            if (!$validType) {
497 35
                $this->addTypeError($validation, $name, $type);
498 35
            }
499
        }
500
501
        // Validate a custom field validator.
502 74
        $this->callValidators($value, $name, $validation);
503
504 74
        return $value;
505
    }
506
507
    /**
508
     * Add an invalid type error.
509
     *
510
     * @param Validation $validation The validation to add the error to.
511
     * @param string $name The full field name.
512
     * @param string $type The type that was checked.
513
     * @return $this
514
     */
515 35
    protected function addTypeError(Validation $validation, $name, $type) {
516 35
        $validation->addError(
517 35
            $name,
518 35
            'invalid',
519
            [
520 35
                'type' => $type,
521 35
                'messageCode' => '{field} is not a valid {type}.',
522
                'status' => 422
523 35
            ]
524 35
        );
525
526 35
        return $this;
527
    }
528
529
    /**
530
     * Call all of the validators attached to a field.
531
     *
532
     * @param mixed $value The field value being validated.
533
     * @param string $name The full path to the field.
534
     * @param Validation $validation The validation object to add errors.
535
     * @internal param array $field The field schema.
536
     * @internal param bool $sparse Whether this is a sparse validation.
537
     */
538 74
    private function callValidators($value, $name, Validation $validation) {
539
        // Strip array references in the name except for the last one.
540 74
        $key = preg_replace(['`\[\d+\]$`', '`\[\d+\]`'], ['[]', ''], $name);
541 74
        if (!empty($this->validators[$key])) {
542 2
            foreach ($this->validators[$key] as $validator) {
543 2
                call_user_func($validator, $value, $name, $validation);
544 2
            }
545 2
        }
546 74
    }
547
548
    /**
549
     * Validate an array.
550
     *
551
     * @param mixed &$value The value to validate.
552
     * @param array $field The field definition.
553
     * @param Validation $validation The validation results to add.
554
     * @param string $name The name of the field being validated or an empty string for the root.
555
     * @param bool $sparse Whether or not this is a sparse validation.
556
     * @return bool Returns true if {@link $value} is valid or false otherwise.
557
     */
558 10
    private function validateArray(&$value, array $field, Validation $validation, $name = '', $sparse = false) {
559 10
        $validType = true;
560
561 10
        if (!is_array($value) || (count($value) > 0 && !array_key_exists(0, $value))) {
562 6
            $validType = false;
563 6
        } else {
564 5
            if (isset($field['items'])) {
565 4
                $result = [];
566
567
                // Validate each of the types.
568 4
                foreach ($value as $i => &$item) {
569 4
                    $itemName = "{$name}[{$i}]";
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...
570 4
                    $validItem = $this->validateField($item, $field['items'], $validation, $itemName, $sparse);
571 4
                    $result[] = $validItem;
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...
572 4
                }
573 4
            } else {
574
                // Cast the items into a proper numeric array.
575 1
                $result = array_values($value);
576
            }
577
            // Set the value to the clean version of itself.
578 5
            $value = $result;
579
        }
580
581 10
        return $validType;
582
    }
583
584
    /**
585
     * Validate a boolean value.
586
     *
587
     * @param mixed &$value The value to validate.
588
     * @param array $field The field definition.
589
     * @return bool Returns true if the value is valid or false otherwise.
590
     * @internal param Validation $validation The validation results to add.
591
     */
592 19
    private function validateBoolean(&$value, array $field) {
0 ignored issues
show
Unused Code introduced by
The parameter $field is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
593 19
        if (is_bool($value)) {
594 4
            $validType = true;
595 4
        } else {
596
            $bools = [
597 15
                '0' => false, 'false' => false, 'no' => false, 'off' => false, '' => false,
598 15
                '1' => true, 'true' => true, 'yes' => true, 'on' => true
599 15
            ];
600 15
            if ((is_string($value) || is_numeric($value)) && isset($bools[$value])) {
601 12
                $value = $bools[$value];
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...
602 12
                $validType = true;
603 12
            } else {
604 3
                $validType = false;
605
            }
606
        }
607 19
        return $validType;
608
    }
609
610
    /**
611
     * Validate a date time.
612
     *
613
     * @param mixed &$value The value to validate.
614
     * @param array $field The field definition.
615
     * @return bool Returns true if <a href='psi_element://$value'>$value</a> is valid or false otherwise.
616
     * is valid or false otherwise.
617
     * @internal param Validation $validation The validation results to add.
618
     */
619 6
    private function validateDatetime(&$value, array $field) {
0 ignored issues
show
Unused Code introduced by
The parameter $field is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
620 6
        $validType = true;
621 6
        if ($value instanceof \DateTimeInterface) {
622 1
            $validType = true;
623 6
        } elseif (is_string($value) && $value !== '') {
624
            try {
625 2
                $dt = new \DateTimeImmutable($value);
626 1
                if ($dt) {
627 1
                    $value = $dt;
628 1
                } else {
629
                    $validType = false;
630
                }
631 2
            } catch (\Exception $ex) {
632 1
                $validType = false;
633
            }
634 5
        } elseif (is_numeric($value) && $value > 0) {
635 1
            $value = new \DateTimeImmutable('@'.(string)round($value));
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...
636 1
            $validType = true;
637 1
        } else {
638 2
            $validType = false;
639
        }
640 6
        return $validType;
641
    }
642
643
    /**
644
     * Validate a float.
645
     *
646
     * @param mixed &$value The value to validate.
647
     * @param array $field The field definition.
648
     * @return bool Returns true if <a href='psi_element://$value'>$value</a> is valid or false otherwise.
649
     * is valid or false otherwise.
650
     * @internal param Validation $validation The validation results to add.
651
     */
652 7
    private function validateFloat(&$value, array $field) {
0 ignored issues
show
Unused Code introduced by
The parameter $field is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
653 7
        if (is_float($value)) {
654 1
            $validType = true;
655 7
        } elseif (is_numeric($value)) {
656 2
            $value = (float)$value;
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...
657 2
            $validType = true;
658 2
        } else {
659 4
            $validType = false;
660
        }
661 7
        return $validType;
662
    }
663
664
    /**
665
     * Validate and integer.
666
     *
667
     * @param mixed &$value The value to validate.
668
     * @param array $field The field definition.
669
     * @return bool Returns true if <a href='psi_element://$value'>$value</a> is valid or false otherwise.
670
     * is valid or false otherwise.
671
     * @internal param Validation $validation The validation results to add.
672
     */
673 19
    private function validateInteger(&$value, array $field) {
0 ignored issues
show
Unused Code introduced by
The parameter $field is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
674 19
        if (is_int($value)) {
675 15
            $validType = true;
676 19
        } elseif (is_numeric($value)) {
677 2
            $value = (int)$value;
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...
678 2
            $validType = true;
679 2
        } else {
680 7
            $validType = false;
681
        }
682 19
        return $validType;
683
    }
684
685
    /**
686
     * Validate an object.
687
     *
688
     * @param mixed &$value The value to validate.
689
     * @param array $field The field definition.
690
     * @param Validation $validation The validation results to add.
691
     * @param string $name The name of the field being validated or an empty string for the root.
692
     * @param bool $sparse Whether or not this is a sparse validation.
693
     * @return bool Returns true if {@link $value} is valid or false otherwise.
694
     */
695 58
    private function validateObject(&$value, array $field, Validation $validation, $name = '', $sparse = false) {
696 58
        if (!is_array($value) || isset($value[0])) {
697 6
            return false;
698 58
        } elseif (isset($field['properties'])) {
699
            // Validate the data against the internal schema.
700 58
            $value = $this->validateProperties($value, $field, $validation, $name, $sparse);
701 56
        }
702 56
        return true;
703
    }
704
705
    /**
706
     * Validate data against the schema and return the result.
707
     *
708
     * @param array $data The data to validate.
709
     * @param array $field The schema array to validate against.
710
     * @param Validation $validation This argument will be filled with the validation result.
711
     * @param string $name The path to the current path for nested objects.
712
     * @param bool $sparse Whether or not this is a sparse validation.
713
     * @return array Returns a clean array with only the appropriate properties and the data coerced to proper types.
714
     */
715 58
    private function validateProperties(array $data, array $field, Validation $validation, $name = '', $sparse = false) {
716 58
        $properties = $field['properties'];
717 58
        $required = isset($field['required']) ? array_flip($field['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...
718 58
        $keys = array_keys($data);
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 7 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...
719 58
        $keys = array_combine(array_map('strtolower', $keys), $keys);
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 7 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...
720
721
722
        // Loop through the schema fields and validate each one.
723 58
        $clean = [];
724 58
        foreach ($properties as $propertyName => $propertyField) {
725 58
            $fullName = ltrim("$name.$propertyName", '.');
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...
726 58
            $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...
727 58
            $isRequired = isset($required[$propertyName]);
728
729
            // First check for required fields.
730 58
            if (!array_key_exists($lName, $keys)) {
731
                // A sparse validation can leave required fields out.
732 18
                if ($isRequired && !$sparse) {
733 6
                    $validation->addError($fullName, 'missingField', ['messageCode' => '{field} is required.']);
734 6
                }
735 58
            } elseif ($data[$keys[$lName]] === null) {
736 17
                $clean[$propertyName] = null;
737 17
                if ($isRequired) {
738 9
                    $validation->addError($fullName, 'missingField', ['messageCode' => '{field} cannot be null.']);
739 9
                }
740 17
            } else {
741 50
                $clean[$propertyName] = $this->validateField($data[$keys[$lName]], $propertyField, $validation, $fullName, $sparse);
742
            }
743
744 58
            unset($keys[$lName]);
745 58
        }
746
747
        // Look for extraneous properties.
748 58
        if (!empty($keys)) {
749 7
            if ($this->hasFlag(Schema::FLAG_EXTRA_PROPERTIES_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...
750 2
                $msg = sprintf("%s has unexpected field(s): %s.", $name ?: 'value', implode(', ', $keys));
751 2
                trigger_error($msg, E_USER_NOTICE);
752
            }
753
754 5
            if ($this->hasFlag(Schema::FLAG_EXTRA_PROPERTIES_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...
755 2
                $validation->addError($name, 'invalid', [
756 2
                    'messageCode' => '{field} has {extra,plural,an unexpected field,unexpected fields}: {extra}.',
757 2
                    'extra' => array_values($keys),
758
                    'status' => 422
759 2
                ]);
760 2
            }
761 5
        }
762
763 56
        return $clean;
764
    }
765
766
    /**
767
     * Validate a string.
768
     *
769
     * @param mixed &$value The value to validate.
770
     * @param array $field The field definition.
771
     * @param Validation $validation The validation results to add.
772
     * @param string $name The name of the field being validated.
773
     * @return bool Returns true if {@link $value} is valid or false otherwise.
774
     */
775 19
    private function validateString(&$value, array $field, Validation $validation, $name = '') {
776 19
        if (is_string($value)) {
777 17
            $validType = true;
778 19
        } elseif (is_numeric($value)) {
779 2
            $value = (string)$value;
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...
780 2
            $validType = true;
781 2
        } else {
782 3
            return false;
783
        }
784
785 17
        if (($minLength = self::val('minLength', $field, 0)) > 0 && mb_strlen($value) < $minLength) {
786 1
            if ($minLength === 1) {
787 1
                $validation->addError($name, 'missingField', ['messageCode' => '{field} is required.', 'status' => 422]);
788 1
            } else {
789
                $validation->addError(
790
                    $name,
791
                    'minLength',
792
                    [
793
                        'messageCode' => '{field} should be at least {minLength} characters long.',
794
                        'minLength' => $minLength,
795
                        'status' => 422
796
                    ]
797
                );
798
            }
799 1
            return false;
800
        }
801 16
        if (($maxLength = self::val('maxLength', $field, 0)) > 0 && mb_strlen($value) > $maxLength) {
802
            $validation->addError(
803
                $name,
804
                'maxLength',
805
                [
806
                    'messageCode' => '{field} is {overflow} {overflow,plural,characters} too long.',
807
                    'maxLength' => $maxLength,
808
                    'overflow' => mb_strlen($value) - $maxLength,
809
                    'status' => 422
810
                ]
811
            );
812
            return false;
813
        }
814 16
        if ($pattern = self::val('pattern', $field)) {
815
            $regex = '`'.str_replace('`', preg_quote('`', '`'), $pattern).'`';
816
817
            if (!preg_match($regex, $value)) {
818
                $validation->addError(
819
                    $name,
820
                    'invalid',
821
                    [
822
                        'messageCode' => '{field} is in the incorrect format.',
823
                        'status' => 422
824
                    ]
825
                );
826
            }
827
828
            return false;
829
        }
830
831 16
        return $validType;
832
    }
833
834
    /**
835
     * Validate a unix timestamp.
836
     *
837
     * @param mixed &$value The value to validate.
838
     * @param array $field The field definition.
839
     * @param Validation $validation The validation results to add.
840
     * @return bool Returns true if {@link $value} is valid or false otherwise.
841
     */
842 6
    private function validateTimestamp(&$value, array $field, Validation $validation) {
0 ignored issues
show
Unused Code introduced by
The parameter $field is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $validation is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
843 6
        $validType = true;
844 6
        if (is_numeric($value)) {
845 1
            $value = (int)$value;
846 6
        } elseif (is_string($value) && $ts = strtotime($value)) {
847 1
            $value = $ts;
848 1
        } else {
849 4
            $validType = false;
850
        }
851 6
        return $validType;
852
    }
853
854
    /**
855
     * Specify data which should be serialized to JSON.
856
     *
857
     * @link http://php.net/manual/en/jsonserializable.jsonserialize.php
858
     * @return mixed data which can be serialized by <b>json_encode</b>,
859
     * which is a value of any type other than a resource.
860
     */
861 20
    public function jsonSerialize() {
862 20
        $result = $this->schema;
863 20
        array_walk_recursive($result, function (&$value, $key) {
0 ignored issues
show
Unused Code introduced by
The parameter $key is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
864 20
            if ($value instanceof \JsonSerializable) {
865 1
                $value = $value->jsonSerialize();
866 1
            }
867 20
        });
868 20
        return $result;
869
    }
870
871
    /**
872
     * Look up a type based on its alias.
873
     *
874
     * @param string $alias The type alias or type name to lookup.
875
     * @return mixed
876
     */
877 9
    private function getType($alias) {
878 9
        if (isset(self::$types[$alias])) {
879 2
            $type = self::$types[$alias];
880 9
        } elseif (array_search($alias, self::$types) !== false) {
881
            $type = $alias;
882
        } else {
883 8
            $type = null;
884
        }
885 9
        return $type;
886
    }
887
888
    /**
889
     * Look up a value in array.
890
     *
891
     * @param string|int $key The array key.
892
     * @param array $arr The array to search.
893
     * @param mixed $default The default if key is not found.
894
     * @return mixed Returns the array value or the default.
895
     */
896 17
    private static function val($key, array $arr, $default = null) {
897 17
        return isset($arr[$key]) ? $arr[$key] : $default;
898
    }
899
}
900