Completed
Push — master ( cce9b0...83b3e8 )
by Todd
02:02
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
    /// 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
    /**
27
     * @var array All the known types.
28
     *
29
     * If this is ever given some sort of public access then remove the static.
30
     */
31
    private static $types = [
32
        'a' => 'array',
33
        'o' => 'object',
34
        'i' => 'integer',
35
        'int' => 'integer',
36
        's' => 'string',
37
        'str' => 'string',
38
        'f' => 'float',
39
        'b' => 'boolean',
40
        'bool' => 'boolean',
41
        'ts' => 'timestamp',
42
        'dt' => 'datetime'
43
    ];
44
45
    private $schema = [];
46
47
    /**
48
     * @var int A bitwise combination of the various **Schema::FLAG_*** constants.
49
     */
50
    private $flags = 0;
51
52
    /**
53
     * @var array An array of callbacks that will custom validate the schema.
54
     */
55
    private $validators = [];
56
57
    /**
58
     * @var string|Validation The name of the class or an instance that will be cloned.
59
     */
60
    private $validationClass = Validation::class;
61
62
63
    /// Methods ///
64
65
    /**
66
     * Initialize an instance of a new {@link Schema} class.
67
     *
68
     * @param array $schema The array schema to validate against.
69
     */
70 97
    public function __construct($schema = []) {
71 97
        $this->schema = $this->parse($schema);
72 97
    }
73
74
    /**
75
     * Grab the schema's current description.
76
     *
77
     * @return string
78
     */
79 1
    public function getDescription() {
80 1
        return isset($this->schema['description']) ? $this->schema['description'] : '';
81
    }
82
83
    /**
84
     * Set the description for the schema.
85
     *
86
     * @param string $description The new description.
87
     * @throws \InvalidArgumentException Throws an exception when the provided description is not a string.
88
     * @return Schema
89
     */
90 2
    public function setDescription($description) {
91 2
        if (is_string($description)) {
92 1
            $this->schema['description'] = $description;
93
        } else {
94 1
            throw new \InvalidArgumentException("The description is not a valid string.", 500);
95
        }
96
97 1
        return $this;
98
    }
99
100
    /**
101
     * Return the validation flags.
102
     *
103
     * @return int Returns a bitwise combination of flags.
104
     */
105 1
    public function getFlags() {
106 1
        return $this->flags;
107
    }
108
109
    /**
110
     * Set the validation flags.
111
     *
112
     * @param int $flags One or more of the **Schema::FLAG_*** constants.
113
     * @return Schema Returns the current instance for fluent calls.
114
     */
115 8
    public function setFlags($flags) {
116 8
        if (!is_int($flags)) {
117 1
            throw new \InvalidArgumentException('Invalid flags.', 500);
118
        }
119 7
        $this->flags = $flags;
120
121 7
        return $this;
122
    }
123
124
    /**
125
     * Whether or not the schema has a flag (or combination of flags).
126
     *
127
     * @param int $flag One or more of the **Schema::VALIDATE_*** constants.
128
     * @return bool Returns **true** if all of the flags are set or **false** otherwise.
129
     */
130 8
    public function hasFlag($flag) {
131 8
        return ($this->flags & $flag) === $flag;
132
    }
133
134
    /**
135
     * Set a flag.
136
     *
137
     * @param int $flag One or more of the **Schema::VALIDATE_*** constants.
138
     * @param bool $value Either true or false.
139
     * @return $this
140
     */
141 1
    public function setFlag($flag, $value) {
142 1
        if ($value) {
143 1
            $this->flags = $this->flags | $flag;
144
        } else {
145 1
            $this->flags = $this->flags & ~$flag;
146
        }
147 1
        return $this;
148
    }
149
150
    /**
151
     * Merge a schema with this one.
152
     *
153
     * @param Schema $schema A scheme instance. Its parameters will be merged into the current instance.
154
     */
155 2
    public function merge(Schema $schema) {
156
        $fn = function (array &$target, array $source) use (&$fn) {
157 2
            foreach ($source as $key => $val) {
158 2
                if (is_array($val) && array_key_exists($key, $target) && is_array($target[$key])) {
159 2
                    if (isset($val[0]) || isset($target[$key][0])) {
160
                        // This is a numeric array, so just do a merge.
161 1
                        $merged = array_merge($target[$key], $val);
162 1
                        if (is_string($merged[0])) {
163 1
                            $merged = array_keys(array_flip($merged));
164
                        }
165 1
                        $target[$key] = $merged;
166
                    } else {
167 2
                        $target[$key] = $fn($target[$key], $val);
168
                    }
169
                } else {
170 2
                    $target[$key] = $val;
171
                }
172
            }
173
174 2
            return $target;
175 2
        };
176
177 2
        $fn($this->schema, $schema->jsonSerialize());
178 2
    }
179
180
    /**
181
     * Parse a schema in short form into a full schema array.
182
     *
183
     * @param array $arr The array to parse into a schema.
184
     * @return array The full schema array.
185
     * @throws \InvalidArgumentException Throws an exception when an item in the schema is invalid.
186
     */
187 97
    public function parse(array $arr) {
188 97
        if (empty($arr)) {
189
            // An empty schema validates to anything.
190 6
            return [];
191 92
        } elseif (isset($arr['type'])) {
192
            // This is a long form schema and can be parsed as the root.
193 2
            return $this->parseNode($arr);
194
        } else {
195
            // Check for a root schema.
196 92
            $value = reset($arr);
197 92
            $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...
198 92
            if (is_int($key)) {
199 73
                $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...
200 73
                $value = null;
201
            }
202 92
            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...
203 92
            if (empty($name)) {
204 26
                return $this->parseNode($param, $value);
205
            }
206
        }
207
208
        // If we are here then this is n object schema.
209 68
        list($properties, $required) = $this->parseProperties($arr);
210
211
        $result = [
212 68
            'type' => 'object',
213 68
            'properties' => $properties,
214 68
            'required' => $required
215
        ];
216
217 68
        return array_filter($result);
218
    }
219
220
    /**
221
     * Parse a schema node.
222
     *
223
     * @param array $node The node to parse.
224
     * @param mixed $value Additional information from the node.
225
     * @return array Returns a JSON schema compatible node.
226
     */
227 92
    private function parseNode($node, $value = null) {
228 92
        if (is_array($value)) {
229
            // The value describes a bit more about the schema.
230 17
            switch ($node['type']) {
231 17
                case 'array':
232 6
                    if (isset($value['items'])) {
233
                        // The value includes array schema information.
234 1
                        $node = array_replace($node, $value);
235
                    } else {
236 5
                        $node['items'] = $this->parse($value);
237
                    }
238 6
                    break;
239 11
                case 'object':
240
                    // The value is a schema of the object.
241 9
                    if (isset($value['properties'])) {
242
                        list($node['properties']) = $this->parseProperties($value['properties']);
243
                    } else {
244 9
                        list($node['properties'], $required) = $this->parseProperties($value);
245 9
                        if (!empty($required)) {
246 9
                            $node['required'] = $required;
247
                        }
248
                    }
249 9
                    break;
250
                default:
251 2
                    $node = array_replace($node, $value);
252 17
                    break;
253
            }
254 91
        } elseif (is_string($value)) {
255 76
            if ($node['type'] === 'array' && $arrType = $this->getType($value)) {
256 2
                $node['items'] = ['type' => $arrType];
257
            } elseif (!empty($value)) {
258 22
                $node['description'] = $value;
259
            }
260
        }
261
262 92
        return $node;
263
    }
264
265
    /**
266
     * Parse the schema for an object's properties.
267
     *
268
     * @param array $arr An object property schema.
269
     * @return array Returns a schema array suitable to be placed in the **properties** key of a schema.
270
     */
271 68
    private function parseProperties(array $arr) {
272 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...
273 68
        $requiredProperties = [];
274 68
        foreach ($arr as $key => $value) {
275
            // Fix a schema specified as just a value.
276 68
            if (is_int($key)) {
277 60
                if (is_string($value)) {
278 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...
279 60
                    $value = '';
280
                } else {
281
                    throw new \InvalidArgumentException("Schema at position $key is not a valid parameter.", 500);
282
                }
283
            }
284
285
            // The parameter is defined in the key.
286 68
            list($name, $param, $required) = $this->parseShortParam($key, $value);
287
288 68
            $node = $this->parseNode($param, $value);
289
290 68
            $properties[$name] = $node;
291 68
            if ($required) {
292 36
                $requiredProperties[] = $name;
293
            }
294
        }
295 68
        return array($properties, $requiredProperties);
296
    }
297
298
    /**
299
     * Parse a short parameter string into a full array parameter.
300
     *
301
     * @param string $key The short parameter string to parse.
302
     * @param array $value An array of other information that might help resolve ambiguity.
303
     * @return array Returns an array in the form `[string name, array param, bool required]`.
304
     * @throws \InvalidArgumentException Throws an exception if the short param is not in the correct format.
305
     */
306 92
    public function parseShortParam($key, $value = []) {
307
        // Is the parameter optional?
308 92
        if (substr($key, -1) === '?') {
309 50
            $required = false;
310 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...
311
        } else {
312 60
            $required = true;
313
        }
314
315
        // Check for a type.
316 92
        $parts = explode(':', $key);
317 92
        $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...
318 92
        $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...
319
320 92
        if ($value instanceof Schema) {
321 2
            if ($type === 'array') {
322 1
                $param = ['type' => $type, 'items' => $value];
323
            } else {
324 1
                $param = $value;
325
            }
326 92
        } elseif (isset($value['type'])) {
327
            $param = $value;
328
329
            if (!empty($type) && $type !== $param['type']) {
330
                throw new \InvalidArgumentException("Type mismatch between $type and {$param['type']} for field $name.", 500);
331
            }
332
        } else {
333 92
            if (empty($type) && !empty($parts[1])) {
334
                throw new \InvalidArgumentException("Invalid type {$parts[1]} for field $name.", 500);
335
            }
336 92
            $param = ['type' => $type];
337
338
            // Parsed required strings have a minimum length of 1.
339 92
            if ($type === 'string' && !empty($name) && $required) {
340 21
                $param['minLength'] = 1;
341
            }
342
        }
343
344 92
        return [$name, $param, $required];
345
    }
346
347
    /**
348
     * Add a custom validator to to validate the schema.
349
     *
350
     * @param string $fieldname The name of the field to validate, if any.
351
     *
352
     * If you are adding a validator to a deeply nested field then separate the path with dots.
353
     * @param callable $callback The callback to validate with.
354
     * @return Schema Returns `$this` for fluent calls.
355
     */
356 2
    public function addValidator($fieldname, callable $callback) {
357 2
        $this->validators[$fieldname][] = $callback;
358 2
        return $this;
359
    }
360
361
    /**
362
     * Require one of a given set of fields in the schema.
363
     *
364
     * @param array $required The field names to require.
365
     * @param string $fieldname The name of the field to attach to.
366
     * @param int $count The count of required items.
367
     * @return Schema Returns `$this` for fluent calls.
368
     */
369 1
    public function requireOneOf(array $required, $fieldname = '', $count = 1) {
370 1
        $result = $this->addValidator(
371
            $fieldname,
372
            function ($data, $fieldname, Validation $validation) use ($required, $count) {
373 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...
374 1
                $flattened = [];
375
376 1
                foreach ($required as $name) {
377 1
                    $flattened = array_merge($flattened, (array)$name);
378
379 1
                    if (is_array($name)) {
380
                        // This is an array of required names. They all must match.
381 1
                        $hasCountInner = 0;
382 1
                        foreach ($name as $nameInner) {
383 1
                            if (isset($data[$nameInner]) && $data[$nameInner]) {
384 1
                                $hasCountInner++;
385
                            } else {
386 1
                                break;
387
                            }
388
                        }
389 1
                        if ($hasCountInner >= count($name)) {
390 1
                            $hasCount++;
391
                        }
392 1
                    } elseif (isset($data[$name]) && $data[$name]) {
393 1
                        $hasCount++;
394
                    }
395
396 1
                    if ($hasCount >= $count) {
397 1
                        return true;
398
                    }
399
                }
400
401 1
                if ($count === 1) {
402 1
                    $message = 'One of {required} are required.';
403
                } else {
404
                    $message = '{count} of {required} are required.';
405
                }
406
407 1
                $validation->addError($fieldname, 'missingField', [
408 1
                    'messageCode' => $message,
409 1
                    'required' => $required,
410 1
                    'count' => $count
411
                ]);
412 1
                return false;
413 1
            }
414
        );
415
416 1
        return $result;
417
    }
418
419
    /**
420
     * Validate data against the schema.
421
     *
422
     * @param mixed $data The data to validate.
423
     * @param bool $sparse Whether or not this is a sparse validation.
424
     * @return mixed Returns a cleaned version of the data.
425
     * @throws ValidationException Throws an exception when the data does not validate against the schema.
426
     */
427 75
    public function validate($data, $sparse = false) {
428 75
        $validation = $this->createValidation();
429
430 75
        $clean = $this->validateField($data, $this->schema, $validation, '', $sparse);
431
432 73
        if (!$validation->isValid()) {
433 42
            throw new ValidationException($validation);
434
        }
435
436 37
        return $clean;
437
    }
438
439
    /**
440
     * Validate data against the schema and return the result.
441
     *
442
     * @param array &$data The data to validate.
443
     * @param bool $sparse Whether or not to do a sparse validation.
444
     * @return bool Returns true if the data is valid. False otherwise.
445
     */
446 21
    public function isValid(array &$data, $sparse = false) {
447
        try {
448 21
            $this->validate($data, $sparse);
449 12
            return true;
450 11
        } catch (ValidationException $ex) {
451 11
            return false;
452
        }
453
    }
454
455
    /**
456
     * Validate a field.
457
     *
458
     * @param mixed $value The value to validate.
459
     * @param array|Schema $field Parameters on the field.
460
     * @param Validation $validation A validation object to add errors to.
461
     * @param string $name The name of the field being validated or an empty string for the root.
462
     * @param bool $sparse Whether or not this is a sparse validation.
463
     * @return mixed Returns a clean version of the value with all extra fields stripped out.
464
     */
465 75
    private function validateField($value, $field, Validation $validation, $name = '', $sparse = false) {
466 75
        if ($field instanceof Schema) {
467
            try {
468 1
                $value = $field->validate($value, $sparse);
469 1
            } catch (ValidationException $ex) {
470
                // The validation failed, so merge the validations together.
471 1
                $validation->merge($ex->getValidation(), $name);
472
            }
473
        } else {
474 75
            $type = isset($field['type']) ? $field['type'] : '';
475
476
            // Validate the field's type.
477 75
            $validType = true;
478
            switch ($type) {
479 75
                case 'boolean':
480 19
                    $validType &= $this->validateBoolean($value, $field);
481 19
                    break;
482 63
                case 'integer':
483 20
                    $validType &= $this->validateInteger($value, $field);
484 20
                    break;
485 62
                case 'float':
486 7
                    $validType &= $this->validateFloat($value, $field);
487 7
                    break;
488 62
                case 'string':
489 19
                    $validType &= $this->validateString($value, $field, $validation);
490 19
                    break;
491 62
                case 'timestamp':
492 6
                    $validType &= $this->validateTimestamp($value, $field, $validation);
493 6
                    break;
494 62
                case 'datetime':
495 6
                    $validType &= $this->validateDatetime($value, $field);
496 6
                    break;
497 60
                case 'array':
498 10
                    $validType &= $this->validateArray($value, $field, $validation, $name, $sparse);
499 10
                    break;
500 59
                case 'object':
501 58
                    $validType &= $this->validateObject($value, $field, $validation, $name, $sparse);
502 56
                    break;
503 2
                case '':
504
                    // No type was specified so we are valid.
505 2
                    $validType = true;
506 2
                    break;
507
                default:
508
                    throw new \InvalidArgumentException("Unrecognized type $type.", 500);
509
            }
510 75
            if (!$validType) {
511 36
                $this->addTypeError($validation, $name, $type);
512
            }
513
        }
514
515
        // Validate a custom field validator.
516 75
        $this->callValidators($value, $name, $validation);
517
518 75
        return $value;
519
    }
520
521
    /**
522
     * Add an invalid type error.
523
     *
524
     * @param Validation $validation The validation to add the error to.
525
     * @param string $name The full field name.
526
     * @param string $type The type that was checked.
527
     * @return $this
528
     */
529 36
    protected function addTypeError(Validation $validation, $name, $type) {
530 36
        $validation->addError(
531
            $name,
532 36
            'invalid',
533
            [
534 36
                'type' => $type,
535 36
                'messageCode' => '{field} is not a valid {type}.',
536 36
                'status' => 422
537
            ]
538
        );
539
540 36
        return $this;
541
    }
542
543
    /**
544
     * Call all of the validators attached to a field.
545
     *
546
     * @param mixed $value The field value being validated.
547
     * @param string $name The full path to the field.
548
     * @param Validation $validation The validation object to add errors.
549
     * @internal param array $field The field schema.
550
     * @internal param bool $sparse Whether this is a sparse validation.
551
     */
552 75
    private function callValidators($value, $name, Validation $validation) {
553
        // Strip array references in the name except for the last one.
554 75
        $key = preg_replace(['`\[\d+\]$`', '`\[\d+\]`'], ['[]', ''], $name);
555 75
        if (!empty($this->validators[$key])) {
556 2
            foreach ($this->validators[$key] as $validator) {
557 2
                call_user_func($validator, $value, $name, $validation);
558
            }
559
        }
560 75
    }
561
562
    /**
563
     * Validate an array.
564
     *
565
     * @param mixed &$value The value to validate.
566
     * @param array $field The field definition.
567
     * @param Validation $validation The validation results to add.
568
     * @param string $name The name of the field being validated or an empty string for the root.
569
     * @param bool $sparse Whether or not this is a sparse validation.
570
     * @return bool Returns true if {@link $value} is valid or false otherwise.
571
     */
572 10
    private function validateArray(&$value, array $field, Validation $validation, $name = '', $sparse = false) {
573 10
        $validType = true;
574
575 10
        if (!is_array($value) || (count($value) > 0 && !array_key_exists(0, $value))) {
576 6
            $validType = false;
577
        } else {
578 5
            if (isset($field['items'])) {
579 4
                $result = [];
580
581
                // Validate each of the types.
582 4
                foreach ($value as $i => &$item) {
583 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...
584 4
                    $validItem = $this->validateField($item, $field['items'], $validation, $itemName, $sparse);
585 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...
586
                }
587
            } else {
588
                // Cast the items into a proper numeric array.
589 1
                $result = array_values($value);
590
            }
591
            // Set the value to the clean version of itself.
592 5
            $value = $result;
593
        }
594
595 10
        return $validType;
596
    }
597
598
    /**
599
     * Validate a boolean value.
600
     *
601
     * @param mixed &$value The value to validate.
602
     * @param array $field The field definition.
603
     * @return bool Returns true if the value is valid or false otherwise.
604
     * @internal param Validation $validation The validation results to add.
605
     */
606 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...
607 19
        if (is_bool($value)) {
608 4
            $validType = true;
609
        } else {
610
            $bools = [
611 15
                '0' => false, 'false' => false, 'no' => false, 'off' => false, '' => false,
612
                '1' => true, 'true' => true, 'yes' => true, 'on' => true
613
            ];
614 15
            if ((is_string($value) || is_numeric($value)) && isset($bools[$value])) {
615 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...
616 12
                $validType = true;
617
            } else {
618 3
                $validType = false;
619
            }
620
        }
621 19
        return $validType;
622
    }
623
624
    /**
625
     * Validate a date time.
626
     *
627
     * @param mixed &$value The value to validate.
628
     * @param array $field The field definition.
629
     * @return bool Returns true if <a href='psi_element://$value'>$value</a> is valid or false otherwise.
630
     * is valid or false otherwise.
631
     * @internal param Validation $validation The validation results to add.
632
     */
633 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...
634 6
        $validType = true;
635 6
        if ($value instanceof \DateTimeInterface) {
636 1
            $validType = true;
637 5
        } elseif (is_string($value) && $value !== '') {
638
            try {
639 2
                $dt = new \DateTimeImmutable($value);
640 1
                if ($dt) {
641 1
                    $value = $dt;
642
                } else {
643
                    $validType = false;
644
                }
645 1
            } catch (\Exception $ex) {
646 1
                $validType = false;
647
            }
648 3
        } elseif (is_numeric($value) && $value > 0) {
649 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...
650 1
            $validType = true;
651
        } else {
652 2
            $validType = false;
653
        }
654 6
        return $validType;
655
    }
656
657
    /**
658
     * Validate a float.
659
     *
660
     * @param mixed &$value The value to validate.
661
     * @param array $field The field definition.
662
     * @return bool Returns true if <a href='psi_element://$value'>$value</a> is valid or false otherwise.
663
     * is valid or false otherwise.
664
     * @internal param Validation $validation The validation results to add.
665
     */
666 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...
667 7
        if (is_float($value)) {
668 1
            $validType = true;
669 6
        } elseif (is_numeric($value)) {
670 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...
671 2
            $validType = true;
672
        } else {
673 4
            $validType = false;
674
        }
675 7
        return $validType;
676
    }
677
678
    /**
679
     * Validate and integer.
680
     *
681
     * @param mixed &$value The value to validate.
682
     * @param array $field The field definition.
683
     * @return bool Returns true if <a href='psi_element://$value'>$value</a> is valid or false otherwise.
684
     * is valid or false otherwise.
685
     * @internal param Validation $validation The validation results to add.
686
     */
687 20
    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...
688 20
        if (is_int($value)) {
689 15
            $validType = true;
690 9
        } elseif (is_numeric($value)) {
691 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...
692 2
            $validType = true;
693
        } else {
694 8
            $validType = false;
695
        }
696 20
        return $validType;
697
    }
698
699
    /**
700
     * Validate an object.
701
     *
702
     * @param mixed &$value The value to validate.
703
     * @param array $field The field definition.
704
     * @param Validation $validation The validation results to add.
705
     * @param string $name The name of the field being validated or an empty string for the root.
706
     * @param bool $sparse Whether or not this is a sparse validation.
707
     * @return bool Returns true if {@link $value} is valid or false otherwise.
708
     */
709 58
    private function validateObject(&$value, array $field, Validation $validation, $name = '', $sparse = false) {
710 58
        if (!is_array($value) || isset($value[0])) {
711 6
            return false;
712 58
        } elseif (isset($field['properties'])) {
713
            // Validate the data against the internal schema.
714 58
            $value = $this->validateProperties($value, $field, $validation, $name, $sparse);
715
        }
716 56
        return true;
717
    }
718
719
    /**
720
     * Validate data against the schema and return the result.
721
     *
722
     * @param array $data The data to validate.
723
     * @param array $field The schema array to validate against.
724
     * @param Validation $validation This argument will be filled with the validation result.
725
     * @param string $name The path to the current path for nested objects.
726
     * @param bool $sparse Whether or not this is a sparse validation.
727
     * @return array Returns a clean array with only the appropriate properties and the data coerced to proper types.
728
     */
729 58
    private function validateProperties(array $data, array $field, Validation $validation, $name = '', $sparse = false) {
730 58
        $properties = $field['properties'];
731 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...
732 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...
733 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...
734
735
736
        // Loop through the schema fields and validate each one.
737 58
        $clean = [];
738 58
        foreach ($properties as $propertyName => $propertyField) {
739 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...
740 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...
741 58
            $isRequired = isset($required[$propertyName]);
742
743
            // First check for required fields.
744 58
            if (!array_key_exists($lName, $keys)) {
745
                // A sparse validation can leave required fields out.
746 18
                if ($isRequired && !$sparse) {
747 6
                    $validation->addError($fullName, 'missingField', ['messageCode' => '{field} is required.']);
748
                }
749 58
            } elseif ($data[$keys[$lName]] === null) {
750 17
                $clean[$propertyName] = null;
751 17
                if ($isRequired) {
752 9
                    $validation->addError($fullName, 'missingField', ['messageCode' => '{field} cannot be null.']);
753
                }
754
            } else {
755 50
                $clean[$propertyName] = $this->validateField($data[$keys[$lName]], $propertyField, $validation, $fullName, $sparse);
756
            }
757
758 58
            unset($keys[$lName]);
759
        }
760
761
        // Look for extraneous properties.
762 58
        if (!empty($keys)) {
763 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...
764 2
                $msg = sprintf("%s has unexpected field(s): %s.", $name ?: 'value', implode(', ', $keys));
765 2
                trigger_error($msg, E_USER_NOTICE);
766
            }
767
768 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...
769 2
                $validation->addError($name, 'invalid', [
770 2
                    'messageCode' => '{field} has {extra,plural,an unexpected field,unexpected fields}: {extra}.',
771 2
                    'extra' => array_values($keys),
772 2
                    'status' => 422
773
                ]);
774
            }
775
        }
776
777 56
        return $clean;
778
    }
779
780
    /**
781
     * Validate a string.
782
     *
783
     * @param mixed &$value The value to validate.
784
     * @param array $field The field definition.
785
     * @param Validation $validation The validation results to add.
786
     * @param string $name The name of the field being validated.
787
     * @return bool Returns true if {@link $value} is valid or false otherwise.
788
     */
789 19
    private function validateString(&$value, array $field, Validation $validation, $name = '') {
790 19
        if (is_string($value)) {
791 17
            $validType = true;
792 5
        } elseif (is_numeric($value)) {
793 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...
794 2
            $validType = true;
795
        } else {
796 3
            return false;
797
        }
798
799 17
        if (($minLength = self::val('minLength', $field, 0)) > 0 && mb_strlen($value) < $minLength) {
800 1
            if ($minLength === 1) {
801 1
                $validation->addError($name, 'missingField', ['messageCode' => '{field} is required.', 'status' => 422]);
802
            } else {
803
                $validation->addError(
804
                    $name,
805
                    'minLength',
806
                    [
807
                        'messageCode' => '{field} should be at least {minLength} characters long.',
808
                        'minLength' => $minLength,
809
                        'status' => 422
810
                    ]
811
                );
812
            }
813 1
            return false;
814
        }
815 16
        if (($maxLength = self::val('maxLength', $field, 0)) > 0 && mb_strlen($value) > $maxLength) {
816
            $validation->addError(
817
                $name,
818
                'maxLength',
819
                [
820
                    'messageCode' => '{field} is {overflow} {overflow,plural,characters} too long.',
821
                    'maxLength' => $maxLength,
822
                    'overflow' => mb_strlen($value) - $maxLength,
823
                    'status' => 422
824
                ]
825
            );
826
            return false;
827
        }
828 16
        if ($pattern = self::val('pattern', $field)) {
829
            $regex = '`'.str_replace('`', preg_quote('`', '`'), $pattern).'`';
830
831
            if (!preg_match($regex, $value)) {
832
                $validation->addError(
833
                    $name,
834
                    'invalid',
835
                    [
836
                        'messageCode' => '{field} is in the incorrect format.',
837
                        'status' => 422
838
                    ]
839
                );
840
            }
841
842
            return false;
843
        }
844
845 16
        return $validType;
846
    }
847
848
    /**
849
     * Validate a unix timestamp.
850
     *
851
     * @param mixed &$value The value to validate.
852
     * @param array $field The field definition.
853
     * @param Validation $validation The validation results to add.
854
     * @return bool Returns true if {@link $value} is valid or false otherwise.
855
     */
856 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...
857 6
        $validType = true;
858 6
        if (is_numeric($value)) {
859 1
            $value = (int)$value;
860 5
        } elseif (is_string($value) && $ts = strtotime($value)) {
861 1
            $value = $ts;
862
        } else {
863 4
            $validType = false;
864
        }
865 6
        return $validType;
866
    }
867
868
    /**
869
     * Specify data which should be serialized to JSON.
870
     *
871
     * @link http://php.net/manual/en/jsonserializable.jsonserialize.php
872
     * @return mixed data which can be serialized by <b>json_encode</b>,
873
     * which is a value of any type other than a resource.
874
     */
875 20
    public function jsonSerialize() {
876 20
        $result = $this->schema;
877 20
        array_walk_recursive($result, function (&$value) {
878 20
            if ($value instanceof \JsonSerializable) {
879 1
                $value = $value->jsonSerialize();
880
            }
881 20
        });
882 20
        return $result;
883
    }
884
885
    /**
886
     * Look up a type based on its alias.
887
     *
888
     * @param string $alias The type alias or type name to lookup.
889
     * @return mixed
890
     */
891 9
    private function getType($alias) {
892 9
        if (isset(self::$types[$alias])) {
893 2
            $type = self::$types[$alias];
894 8
        } elseif (array_search($alias, self::$types) !== false) {
895
            $type = $alias;
896
        } else {
897 8
            $type = null;
898
        }
899 9
        return $type;
900
    }
901
902
    /**
903
     * Look up a value in array.
904
     *
905
     * @param string|int $key The array key.
906
     * @param array $arr The array to search.
907
     * @param mixed $default The default if key is not found.
908
     * @return mixed Returns the array value or the default.
909
     */
910 17
    private static function val($key, array $arr, $default = null) {
911 17
        return isset($arr[$key]) ? $arr[$key] : $default;
912
    }
913
914
    /**
915
     * Get the class that's used to contain validation information.
916
     *
917
     * @return Validation|string Returns the validation class.
918
     */
919 75
    public function getValidationClass() {
920 75
        return $this->validationClass;
921
    }
922
923
    /**
924
     * Set the class that's used to contain validation information.
925
     *
926
     * @param Validation|string $class Either the name of a class or a class that will be cloned.
927
     * @return $this
928
     */
929 1
    public function setValidationClass($class) {
930 1
        if (!is_a($class, Validation::class, true)) {
931
            throw new \InvalidArgumentException("$class must be a subclass of ".Validation::class, 500);
932
        }
933
934 1
        $this->validationClass = $class;
935 1
        return $this;
936
    }
937
938
    /**
939
     * Create a new validation instance.
940
     *
941
     * @return Validation Returns a validation object.
942
     */
943 75
    protected function createValidation() {
944 75
        $class = $this->getValidationClass();
945
946 75
        if (is_string($this->getValidationClass())) {
947 75
            $result = new $class;
948
        } else {
949 1
            $result = clone $class;
0 ignored issues
show
Bug Compatibility introduced by
The expression clone $class; of type string|Garden\Schema\Validation adds the type string to the return on line 951 which is incompatible with the return type documented by Garden\Schema\Schema::createValidation of type Garden\Schema\Validation.
Loading history...
950
        }
951 75
        return $result;
952
    }
953
}
954