Completed
Push — master ( 8da5f1...2589ae )
by Viacheslav
8s
created

Schema::addMeta()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 5
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 3
nc 1
nop 1
1
<?php
2
3
namespace Swaggest\JsonSchema;
4
5
6
use PhpLang\ScopeExit;
7
use Swaggest\JsonDiff\JsonDiff;
8
use Swaggest\JsonSchema\Constraint\Content;
9
use Swaggest\JsonSchema\Constraint\Format;
10
use Swaggest\JsonSchema\Constraint\Properties;
11
use Swaggest\JsonSchema\Constraint\Type;
12
use Swaggest\JsonSchema\Constraint\UniqueItems;
13
use Swaggest\JsonSchema\Exception\ArrayException;
14
use Swaggest\JsonSchema\Exception\ConstException;
15
use Swaggest\JsonSchema\Exception\EnumException;
16
use Swaggest\JsonSchema\Exception\LogicException;
17
use Swaggest\JsonSchema\Exception\NumericException;
18
use Swaggest\JsonSchema\Exception\ObjectException;
19
use Swaggest\JsonSchema\Exception\StringException;
20
use Swaggest\JsonSchema\Exception\TypeException;
21
use Swaggest\JsonSchema\Meta\Meta;
22
use Swaggest\JsonSchema\Meta\MetaHolder;
23
use Swaggest\JsonSchema\Structure\ClassStructure;
24
use Swaggest\JsonSchema\Structure\Egg;
25
use Swaggest\JsonSchema\Structure\ObjectItem;
26
use Swaggest\JsonSchema\Structure\ObjectItemContract;
27
28
/**
29
 * Class Schema
30
 * @package Swaggest\JsonSchema
31
 */
32
class Schema extends JsonSchema implements MetaHolder
33
{
34
    const CONST_PROPERTY = 'const';
35
36
    const DEFAULT_MAPPING = 'default';
37
38
    const VERSION_AUTO = 'a';
39
    const VERSION_DRAFT_04 = 4;
40
    const VERSION_DRAFT_06 = 6;
41
    const VERSION_DRAFT_07 = 7;
42
43
    const SCHEMA_DRAFT_04_URL = 'http://json-schema.org/draft-04/schema';
44
45
    const REF = '$ref';
46
    const ID = '$id';
47
    const ID_D4 = 'id';
48
49
50
    /*
0 ignored issues
show
Unused Code Comprehensibility introduced by
55% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
51
    public $__seqId;
52
    public static $seq = 0;
53
54
    public function __construct()
55
    {
56
        self::$seq++;
57
        $this->__seqId = self::$seq;
58
    }
59
    //*/
60
61
    // Object
62
    /** @var Properties|Schema[]|Schema */
63
    public $properties;
64
    /** @var Schema|bool */
65
    public $additionalProperties;
66
    /** @var Schema[] */
67
    public $patternProperties;
68
    /** @var string[][]|Schema[]|\stdClass */
69
    public $dependencies;
70
71
    // Array
72
    /** @var null|Schema|Schema[] */
73
    public $items;
74
    /** @var null|Schema|bool */
75
    public $additionalItems;
76
77
    const FORMAT_DATE_TIME = 'date-time'; // todo implement
78
79
80
    /** @var Schema[] */
81
    public $allOf;
82
    /** @var Schema */
83
    public $not;
84
    /** @var Schema[] */
85
    public $anyOf;
86
    /** @var Schema[] */
87
    public $oneOf;
88
89
    /** @var Schema */
90
    public $if;
91
    /** @var Schema */
92
    public $then;
93
    /** @var Schema */
94
    public $else;
95
96
97
    public $objectItemClass;
98
    private $useObjectAsArray = false;
99
100
    private $__dataToProperty = array();
101
    private $__propertyToData = array();
102
103
104
    public function addPropertyMapping($dataName, $propertyName, $mapping = self::DEFAULT_MAPPING)
105
    {
106
        $this->__dataToProperty[$mapping][$dataName] = $propertyName;
107
        $this->__propertyToData[$mapping][$propertyName] = $dataName;
108
        return $this;
109
    }
110
111
    private function preProcessReferences($data, Context $options, $nestingLevel = 0)
112
    {
113
        if ($nestingLevel > 200) {
114
            throw new Exception('Too deep nesting level', Exception::DEEP_NESTING);
115
        }
116
        if (is_array($data)) {
117
            foreach ($data as $key => $item) {
118
                $this->preProcessReferences($item, $options, $nestingLevel + 1);
119
            }
120
        } elseif ($data instanceof \stdClass) {
121
            /** @var JsonSchema $data */
122
            if (
123
                isset($data->{Schema::ID_D4})
124
                && is_string($data->{Schema::ID_D4})
125
                && (($options->version === self::VERSION_AUTO) || $options->version === self::VERSION_DRAFT_04)
126
            ) {
127
                $prev = $options->refResolver->setupResolutionScope($data->{Schema::ID_D4}, $data);
128
                /** @noinspection PhpUnusedLocalVariableInspection */
129
                $_ = new ScopeExit(function () use ($prev, $options) {
0 ignored issues
show
Unused Code introduced by
$_ is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
130
                    $options->refResolver->setResolutionScope($prev);
131
                });
132
            }
133
134
            if (isset($data->{self::ID})
135
                && is_string($data->{self::ID})
136
                && (($options->version === self::VERSION_AUTO) || $options->version >= self::VERSION_DRAFT_06)
137
            ) {
138
                $prev = $options->refResolver->setupResolutionScope($data->{self::ID}, $data);
139
                /** @noinspection PhpUnusedLocalVariableInspection */
140
                $_ = new ScopeExit(function () use ($prev, $options) {
0 ignored issues
show
Unused Code introduced by
$_ is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
141
                    $options->refResolver->setResolutionScope($prev);
142
                });
143
            }
144
145
146
            foreach ((array)$data as $key => $value) {
147
                $this->preProcessReferences($value, $options, $nestingLevel + 1);
148
            }
149
        }
150
    }
151
152
    public static function import($data, Context $options = null)
153
    {
154
        // string $data is expected to be $ref uri
155
        if (is_string($data)) {
156
            $data = (object)array(self::REF => $data);
157
        }
158
159
        $data = self::unboolSchemaData($data);
160
        return parent::import($data, $options);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return parent::import($data, $options); (Swaggest\JsonSchema\Schema) is incompatible with the return type of the parent method Swaggest\JsonSchema\Stru...\ClassStructure::import of type Swaggest\JsonSchema\Structure\ClassStructureTrait.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
161
    }
162
163
    public function in($data, Context $options = null)
164
    {
165
        if ($options === null) {
166
            $options = new Context();
167
        }
168
169
        $options->import = true;
170
171
        $options->refResolver = new RefResolver($data);
172
        if ($options->remoteRefProvider) {
173
            $options->refResolver->setRemoteRefProvider($options->remoteRefProvider);
174
        }
175
176
        if ($options->import) {
177
            $this->preProcessReferences($data, $options);
178
        }
179
180
        return $this->process($data, $options, '#');
181
    }
182
183
184
    /**
185
     * @param mixed $data
186
     * @param Context|null $options
187
     * @return array|mixed|null|object|\stdClass
188
     * @throws InvalidValue
189
     * @throws \Exception
190
     */
191
    public function out($data, Context $options = null)
192
    {
193
        if ($options === null) {
194
            $options = new Context();
195
        }
196
197
        $options->circularReferences = new \SplObjectStorage();
198
        $options->import = false;
199
        return $this->process($data, $options);
200
    }
201
202
    /**
203
     * @param mixed $data
204
     * @param Context $options
205
     * @param string $path
206
     * @param null $result
207
     * @return array|mixed|null|object|\stdClass
208
     * @throws InvalidValue
209
     * @throws \Exception
210
     */
211
    public function process($data, Context $options, $path = '#', $result = null)
212
    {
213
214
        $import = $options->import;
215
        //$pathTrace = explode('->', $path);
0 ignored issues
show
Unused Code Comprehensibility introduced by
59% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
216
217
        if (!$import && $data instanceof ObjectItemContract) {
218
            $result = new \stdClass();
219
            if ($options->circularReferences->contains($data)) {
220
                /** @noinspection PhpIllegalArrayKeyTypeInspection */
221
                $path = $options->circularReferences[$data];
222
                // @todo $path is not a valid json pointer $ref
223
                $result->{self::REF} = $path;
224
                return $result;
225
//                return $options->circularReferences[$data];
0 ignored issues
show
Unused Code Comprehensibility introduced by
70% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
226
            }
227
            $options->circularReferences->attach($data, $path);
228
            //$options->circularReferences->attach($data, $result);
0 ignored issues
show
Unused Code Comprehensibility introduced by
75% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
229
230
            $data = $data->jsonSerialize();
231
        }
232
        if (!$import && is_array($data) && $this->useObjectAsArray) {
233
            $data = (object)$data;
234
        }
235
236
        if (null !== $options->dataPreProcessor) {
237
            $data = $options->dataPreProcessor->process($data, $this, $import);
238
        }
239
240
        if ($result === null) {
241
            $result = $data;
242
        }
243
244
        if ($options->skipValidation) {
245
            goto skipValidation;
246
        }
247
248
        if ($this->type !== null) {
249
            if ($options->tolerateStrings && is_string($data)) {
250
                $valid = Type::readString($this->type, $data);
251
            } else {
252
                $valid = Type::isValid($this->type, $data, $options->version);
253
            }
254
            if (!$valid) {
255
                $this->fail(new TypeException(ucfirst(
256
                        implode(', ', is_array($this->type) ? $this->type : array($this->type))
257
                        . ' expected, ' . json_encode($data) . ' received')
258
                ), $path);
259
            }
260
        }
261
262
        if ($this->enum !== null) {
263
            $enumOk = false;
264
            foreach ($this->enum as $item) {
265
                if ($item === $data) { // todo support complex structures here
266
                    $enumOk = true;
267
                    break;
268
                }
269
            }
270
            if (!$enumOk) {
271
                $this->fail(new EnumException('Enum failed'), $path);
272
            }
273
        }
274
275
        if (array_key_exists(self::CONST_PROPERTY, $this->__arrayOfData)) {
276
            if ($this->const !== $data) {
277
                if ((is_object($this->const) && is_object($data))
278
                    || (is_array($this->const) && is_array($data))) {
279
                    $diff = new JsonDiff($this->const, $data,
280
                        JsonDiff::SKIP_REARRANGE_ARRAY + JsonDiff::STOP_ON_DIFF);
281
                    if ($diff->getDiffCnt() != 0) {
282
                        $this->fail(new ConstException('Const failed'), $path);
283
                    }
284
                } else {
285
                    $this->fail(new ConstException('Const failed'), $path);
286
                }
287
            }
288
        }
289
290
        if ($this->not !== null) {
291
            $exception = false;
292
            try {
293
                self::unboolSchema($this->not)->process($data, $options, $path . '->not');
294
            } catch (InvalidValue $exception) {
295
                // Expected exception
296
            }
297
            if ($exception === false) {
298
                $this->fail(new LogicException('Failed due to logical constraint: not'), $path);
299
            }
300
        }
301
302
        if (is_string($data)) {
303
            if ($this->minLength !== null) {
304
                if (mb_strlen($data, 'UTF-8') < $this->minLength) {
305
                    $this->fail(new StringException('String is too short', StringException::TOO_SHORT), $path);
306
                }
307
            }
308
            if ($this->maxLength !== null) {
309
                if (mb_strlen($data, 'UTF-8') > $this->maxLength) {
310
                    $this->fail(new StringException('String is too long', StringException::TOO_LONG), $path);
311
                }
312
            }
313
            if ($this->pattern !== null) {
314
                if (0 === preg_match(Helper::toPregPattern($this->pattern), $data)) {
315
                    $this->fail(new StringException(json_encode($data) . ' does not match to '
316
                        . $this->pattern, StringException::PATTERN_MISMATCH), $path);
317
                }
318
            }
319
            if ($this->format !== null) {
320
                $validationError = Format::validationError($this->format, $data);
321
                if ($validationError !== null) {
322
                    if ($this->format === "uri" && substr($path, -3) === ':id') {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
323
                    } else {
324
                        $this->fail(new StringException($validationError), $path);
325
                    }
326
                }
327
            }
328
        }
329
330
        if (is_int($data) || is_float($data)) {
331
            if ($this->multipleOf !== null) {
332
                $div = $data / $this->multipleOf;
333
                if ($div != (int)$div) {
334
                    $this->fail(new NumericException($data . ' is not multiple of ' . $this->multipleOf, NumericException::MULTIPLE_OF), $path);
335
                }
336
            }
337
338
            if ($this->exclusiveMaximum !== null && !is_bool($this->exclusiveMaximum)) {
339
                if ($data >= $this->exclusiveMaximum) {
340
                    $this->fail(new NumericException(
341
                        'Value less or equal than ' . $this->exclusiveMaximum . ' expected, ' . $data . ' received',
342
                        NumericException::MAXIMUM), $path);
343
                }
344
            }
345
346
            if ($this->exclusiveMinimum !== null && !is_bool($this->exclusiveMinimum)) {
347
                if ($data <= $this->exclusiveMinimum) {
348
                    $this->fail(new NumericException(
349
                        'Value more or equal than ' . $this->exclusiveMinimum . ' expected, ' . $data . ' received',
350
                        NumericException::MINIMUM), $path);
351
                }
352
            }
353
354
            if ($this->maximum !== null) {
355
                if ($this->exclusiveMaximum === true) {
356
                    if ($data >= $this->maximum) {
357
                        $this->fail(new NumericException(
358
                            'Value less or equal than ' . $this->maximum . ' expected, ' . $data . ' received',
359
                            NumericException::MAXIMUM), $path);
360
                    }
361
                } else {
362
                    if ($data > $this->maximum) {
363
                        $this->fail(new NumericException(
364
                            'Value less than ' . $this->minimum . ' expected, ' . $data . ' received',
365
                            NumericException::MAXIMUM), $path);
366
                    }
367
                }
368
            }
369
370
            if ($this->minimum !== null) {
371
                if ($this->exclusiveMinimum === true) {
372
                    if ($data <= $this->minimum) {
373
                        $this->fail(new NumericException(
374
                            'Value more or equal than ' . $this->minimum . ' expected, ' . $data . ' received',
375
                            NumericException::MINIMUM), $path);
376
                    }
377
                } else {
378
                    if ($data < $this->minimum) {
379
                        $this->fail(new NumericException(
380
                            'Value more than ' . $this->minimum . ' expected, ' . $data . ' received',
381
                            NumericException::MINIMUM), $path);
382
                    }
383
                }
384
            }
385
        }
386
387
        skipValidation:
388
389
        if ($this->oneOf !== null) {
390
            $successes = 0;
391
            $failures = '';
392
            $skipValidation = false;
393
            if ($options->skipValidation) {
394
                $skipValidation = true;
395
                $options->skipValidation = false;
396
            }
397
398
            foreach ($this->oneOf as $index => $item) {
399
                try {
400
                    $result = self::unboolSchema($item)->process($data, $options, $path . '->oneOf:' . $index);
401
                    $successes++;
402
                    if ($successes > 1 || $options->skipValidation) {
403
                        break;
404
                    }
405
                } catch (InvalidValue $exception) {
406
                    $failures .= ' ' . $index . ': ' . Helper::padLines(' ', $exception->getMessage()) . "\n";
407
                    // Expected exception
408
                }
409
            }
410
            if ($skipValidation) {
411
                $options->skipValidation = true;
412
                if ($successes === 0) {
413
                    $result = self::unboolSchema($this->oneOf[0])->process($data, $options, $path . '->oneOf:' . 0);
414
                }
415
            }
416
417
            if (!$options->skipValidation) {
418
                if ($successes === 0) {
419
                    $this->fail(new LogicException('Failed due to logical constraint: no valid results for oneOf {' . "\n" . substr($failures, 0, -1) . "\n}"), $path);
420
                } elseif ($successes > 1) {
421
                    $this->fail(new LogicException('Failed due to logical constraint: '
422
                        . $successes . '/' . count($this->oneOf) . ' valid results for oneOf'), $path);
423
                }
424
            }
425
        }
426
427
        if ($this->anyOf !== null) {
428
            $successes = 0;
429
            $failures = '';
430
            foreach ($this->anyOf as $index => $item) {
431
                try {
432
                    $result = self::unboolSchema($item)->process($data, $options, $path . '->anyOf:' . $index);
433
                    $successes++;
434
                    if ($successes) {
435
                        break;
436
                    }
437
                } catch (InvalidValue $exception) {
438
                    $failures .= ' ' . $index . ': ' . $exception->getMessage() . "\n";
439
                    // Expected exception
440
                }
441
            }
442
            if (!$successes && !$options->skipValidation) {
443
                $this->fail(new LogicException('Failed due to logical constraint: no valid results for anyOf {' . "\n" . substr(Helper::padLines(' ', $failures), 0, -1) . "\n}"), $path);
444
            }
445
        }
446
447
        if ($this->allOf !== null) {
448
            foreach ($this->allOf as $index => $item) {
449
                $result = self::unboolSchema($item)->process($data, $options, $path . '->allOf' . $index);
450
            }
451
        }
452
453
        if ($this->if !== null) {
454
            $valid = true;
455
            try {
456
                self::unboolSchema($this->if)->process($data, $options, $path . '->if');
457
            } catch (InvalidValue $exception) {
458
                $valid = false;
459
            }
460
            if ($valid) {
461
                if ($this->then !== null) {
462
                    $result = self::unboolSchema($this->then)->process($data, $options, $path . '->then');
463
                }
464
            } else {
465
                if ($this->else !== null) {
466
                    $result = self::unboolSchema($this->else)->process($data, $options, $path . '->else');
467
                }
468
            }
469
        }
470
471
        if ($data instanceof \stdClass) {
472
            if (!$options->skipValidation && $this->required !== null) {
473
474
                if (isset($this->__dataToProperty[$options->mapping])) {
475
                    if ($import) {
476
                        foreach ($this->required as $item) {
477
                            if (isset($this->__propertyToData[$options->mapping][$item])) {
478
                                $item = $this->__propertyToData[$options->mapping][$item];
479
                            }
480
                            if (!property_exists($data, $item)) {
481
                                $this->fail(new ObjectException('Required property missing: ' . $item, ObjectException::REQUIRED), $path);
482
                            }
483
                        }
484
                    } else {
485
                        foreach ($this->required as $item) {
486
                            if (isset($this->__dataToProperty[$options->mapping][$item])) {
487
                                $item = $this->__dataToProperty[$options->mapping][$item];
488
                            }
489
                            if (!property_exists($data, $item)) {
490
                                $this->fail(new ObjectException('Required property missing: ' . $item, ObjectException::REQUIRED), $path);
491
                            }
492
                        }
493
                    }
494
495
                } else {
496
                    foreach ($this->required as $item) {
497
                        if (!property_exists($data, $item)) {
498
                            $this->fail(new ObjectException('Required property missing: ' . $item, ObjectException::REQUIRED), $path);
499
                        }
500
                    }
501
                }
502
503
            }
504
505
            if ($import) {
506
                if ($this->useObjectAsArray) {
507
                    $result = array();
508
                } elseif (!$result instanceof ObjectItemContract) {
509
                    $result = $this->makeObjectItem($options);
510
511
                    if ($result instanceof ClassStructure) {
512
                        if ($result->__validateOnSet) {
513
                            $result->__validateOnSet = false;
514
                            /** @noinspection PhpUnusedLocalVariableInspection */
515
                            $validateOnSetHandler = new ScopeExit(function () use ($result) {
1 ignored issue
show
Unused Code introduced by
$validateOnSetHandler is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
516
                                $result->__validateOnSet = true;
517
                            });
518
                        }
519
                    }
520
521
                    if ($result instanceof ObjectItemContract) {
522
                        $result->setDocumentPath($path);
523
                    }
524
                }
525
            }
526
527
            // @todo better check for schema id
528
529
            if ($import
530
                && isset($data->{Schema::ID_D4})
531
                && ($options->version === Schema::VERSION_DRAFT_04 || $options->version === Schema::VERSION_AUTO)
532
                && is_string($data->{Schema::ID_D4}) /*&& (!isset($this->properties['id']))/* && $this->isMetaSchema($data)*/) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
72% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
533
                $id = $data->{Schema::ID_D4};
534
                $refResolver = $options->refResolver;
535
                $parentScope = $refResolver->updateResolutionScope($id);
536
                /** @noinspection PhpUnusedLocalVariableInspection */
537
                $defer = new ScopeExit(function () use ($parentScope, $refResolver) {
1 ignored issue
show
Unused Code introduced by
$defer is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
538
                    $refResolver->setResolutionScope($parentScope);
539
                });
540
            }
541
542
            if ($import
543
                && isset($data->{self::ID})
544
                && ($options->version >= Schema::VERSION_DRAFT_06 || $options->version === Schema::VERSION_AUTO)
545
                && is_string($data->{self::ID}) /*&& (!isset($this->properties['id']))/* && $this->isMetaSchema($data)*/) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
72% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
546
                $id = $data->{self::ID};
547
                $refResolver = $options->refResolver;
548
                $parentScope = $refResolver->updateResolutionScope($id);
549
                /** @noinspection PhpUnusedLocalVariableInspection */
550
                $defer = new ScopeExit(function () use ($parentScope, $refResolver) {
1 ignored issue
show
Unused Code introduced by
$defer is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
551
                    $refResolver->setResolutionScope($parentScope);
552
                });
553
            }
554
555
            if ($import) {
556
                try {
557
                    while (
558
                        isset($data->{self::REF})
559
                        && is_string($data->{self::REF})
560
                        && !isset($this->properties[self::REF])
561
                    ) {
562
                        $refString = $data->{self::REF};
563
                        // TODO consider process # by reference here ?
564
                        $refResolver = $options->refResolver;
565
                        $preRefScope = $refResolver->getResolutionScope();
566
                        /** @noinspection PhpUnusedLocalVariableInspection */
567
                        $deferRefScope = new ScopeExit(function () use ($preRefScope, $refResolver) {
1 ignored issue
show
Unused Code introduced by
$deferRefScope is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
568
                            $refResolver->setResolutionScope($preRefScope);
569
                        });
570
                        $ref = $refResolver->resolveReference($refString);
571
                        if ($ref->isImported()) {
572
                            $refResult = $ref->getImported();
573
                            return $refResult;
574
                        }
575
                        $data = self::unboolSchemaData($ref->getData());
576
                        if ($result instanceof ObjectItemContract) {
577
                            $result->setFromRef($refString);
578
                        }
579
                        $ref->setImported($result);
580
                        $refResult = $this->process($data, $options, $path . '->ref:' . $refString, $result);
581
                        $ref->setImported($refResult);
582
                        return $refResult;
583
                    }
584
                } catch (InvalidValue $exception) {
585
                    $this->fail($exception, $path);
586
                }
587
            }
588
589
            /** @var Schema[] $properties */
590
            $properties = null;
591
592
            $nestedProperties = null;
593
            if ($this->properties !== null) {
594
                $properties = &$this->properties->toArray(); // TODO check performance of pointer
595
                if ($this->properties instanceof Properties) {
596
                    $nestedProperties = $this->properties->getNestedProperties();
597
                } else {
598
                    $nestedProperties = array();
599
                }
600
            }
601
602
            $array = array();
603
            if (!empty($this->__dataToProperty[$options->mapping])) {
604
                foreach ((array)$data as $key => $value) {
605
                    if ($import) {
606
                        if (isset($this->__dataToProperty[$options->mapping][$key])) {
607
                            $key = $this->__dataToProperty[$options->mapping][$key];
608
                        }
609
                    } else {
610
                        if (isset($this->__propertyToData[$options->mapping][$key])) {
611
                            $key = $this->__propertyToData[$options->mapping][$key];
612
                        }
613
                    }
614
                    $array[$key] = $value;
615
                }
616
            } else {
617
                $array = (array)$data;
618
            }
619
620
            if (!$options->skipValidation) {
621
                if ($this->minProperties !== null && count($array) < $this->minProperties) {
622
                    $this->fail(new ObjectException("Not enough properties", ObjectException::TOO_FEW), $path);
623
                }
624
                if ($this->maxProperties !== null && count($array) > $this->maxProperties) {
625
                    $this->fail(new ObjectException("Too many properties", ObjectException::TOO_MANY), $path);
626
                }
627
                if ($this->propertyNames !== null) {
628
                    $propertyNames = self::unboolSchema($this->propertyNames);
629
                    foreach ($array as $key => $tmp) {
630
                        $propertyNames->process($key, $options, $path . '->propertyNames:' . $key);
631
                    }
632
                }
633
            }
634
635
            foreach ($array as $key => $value) {
636
                if ($key === '' && PHP_VERSION_ID < 71000) {
637
                    $this->fail(new InvalidValue('Empty property name'), $path);
638
                }
639
640
                $found = false;
641
642
                if (!$options->skipValidation && !empty($this->dependencies)) {
643
                    $deps = $this->dependencies;
644
                    if (isset($deps->$key)) {
645
                        $dependencies = $deps->$key;
646
                        $dependencies = self::unboolSchema($dependencies);
647
                        if ($dependencies instanceof Schema) {
648
                            $dependencies->process($data, $options, $path . '->dependencies:' . $key);
649
                        } else {
650
                            foreach ($dependencies as $item) {
651
                                if (!property_exists($data, $item)) {
652
                                    $this->fail(new ObjectException('Dependency property missing: ' . $item,
653
                                        ObjectException::DEPENDENCY_MISSING), $path);
654
                                }
655
                            }
656
                        }
657
                    }
658
                }
659
660
                $propertyFound = false;
661
                if (isset($properties[$key])) {
662
                    /** @var Schema[] $properties */
663
                    $prop = self::unboolSchema($properties[$key]);
664
                    $propertyFound = true;
665
                    $found = true;
666
                    if ($prop instanceof Schema) {
667
                        $value = $prop->process($value, $options, $path . '->properties:' . $key);
668
                    }
669
                    // @todo process $prop === false
670
                }
671
672
                /** @var Egg[] $nestedEggs */
673
                $nestedEggs = null;
674
                if (isset($nestedProperties[$key])) {
675
                    $found = true;
676
                    $nestedEggs = $nestedProperties[$key];
677
                    // todo iterate all nested props?
678
                    $value = self::unboolSchema($nestedEggs[0]->propertySchema)->process($value, $options, $path . '->nestedProperties:' . $key);
679
                }
680
681
                if ($this->patternProperties !== null) {
682
                    foreach ($this->patternProperties as $pattern => $propertySchema) {
683
                        if (preg_match(Helper::toPregPattern($pattern), $key)) {
684
                            $found = true;
685
                            $value = self::unboolSchema($propertySchema)->process($value, $options,
686
                                $path . '->patternProperties[' . $pattern . ']:' . $key);
687
                            if ($import) {
688
                                $result->addPatternPropertyName($pattern, $key);
689
                            }
690
                            //break; // todo manage multiple import data properly (pattern accessor)
691
                        }
692
                    }
693
                }
694
                if (!$found && $this->additionalProperties !== null) {
695
                    if (!$options->skipValidation && $this->additionalProperties === false) {
696
                        $this->fail(new ObjectException('Additional properties not allowed'), $path . ':' . $key);
697
                    }
698
699
                    if ($this->additionalProperties instanceof Schema) {
700
                        $value = $this->additionalProperties->process($value, $options, $path . '->additionalProperties:' . $key);
701
                    }
702
703
                    if ($import && !$this->useObjectAsArray) {
704
                        $result->addAdditionalPropertyName($key);
705
                    }
706
                }
707
708
                if ($nestedEggs && $import) {
709
                    foreach ($nestedEggs as $nestedEgg) {
710
                        $result->setNestedProperty($key, $value, $nestedEgg);
711
                    }
712
                    if ($propertyFound) {
713
                        $result->$key = $value;
714
                    }
715
                } else {
716
                    if ($this->useObjectAsArray && $import) {
717
                        $result[$key] = $value;
718
                    } else {
719
                        if ($found || !$import) {
720
                            $result->$key = $value;
721
                        } elseif (!isset($result->$key)) {
722
                            $result->$key = $value;
723
                        }
724
                    }
725
                }
726
            }
727
728
        }
729
730
        if (is_array($data)) {
731
            $count = count($data);
732
            if (!$options->skipValidation) {
733
                if ($this->minItems !== null && $count < $this->minItems) {
734
                    $this->fail(new ArrayException("Not enough items in array"), $path);
735
                }
736
737
                if ($this->maxItems !== null && $count > $this->maxItems) {
738
                    $this->fail(new ArrayException("Too many items in array"), $path);
739
                }
740
            }
741
742
            $pathItems = 'items';
743
            $this->items = self::unboolSchema($this->items);
744
            if ($this->items instanceof Schema) {
745
                $items = array();
746
                $additionalItems = $this->items;
747
            } elseif ($this->items === null) { // items defaults to empty schema so everything is valid
748
                $items = array();
749
                $additionalItems = true;
750
            } else { // listed items
751
                $items = $this->items;
752
                $additionalItems = $this->additionalItems;
753
                $pathItems = 'additionalItems';
754
            }
755
756
            /**
757
             * @var Schema|Schema[] $items
758
             * @var null|bool|Schema $additionalItems
759
             */
760
            $itemsLen = is_array($items) ? count($items) : 0;
761
            $index = 0;
762
            foreach ($result as $key => $value) {
763
                if ($index < $itemsLen) {
764
                    $itemSchema = self::unboolSchema($items[$index]);
765
                    $result[$key] = $itemSchema->process($value, $options, $path . '->items:' . $index);
766
                } else {
767
                    if ($additionalItems instanceof Schema) {
768
                        $result[$key] = $additionalItems->process($value, $options, $path . '->' . $pathItems
769
                            . '[' . $index . ']');
770
                    } elseif (!$options->skipValidation && $additionalItems === false) {
771
                        $this->fail(new ArrayException('Unexpected array item'), $path);
772
                    }
773
                }
774
                ++$index;
775
            }
776
777
            if (!$options->skipValidation && $this->uniqueItems) {
778
                if (!UniqueItems::isValid($data)) {
779
                    $this->fail(new ArrayException('Array is not unique'), $path);
780
                }
781
            }
782
783
            if (!$options->skipValidation && $this->contains !== null) {
784
                /** @var Schema|bool $contains */
785
                $contains = $this->contains;
786
                if ($contains === false) {
787
                    $this->fail(new ArrayException('Contains is false'), $path);
788
                }
789
                if ($count === 0) {
790
                    $this->fail(new ArrayException('Empty array fails contains constraint'), $path);
791
                }
792
                if ($contains === true) {
793
                    $contains = self::unboolSchema($contains);
794
                }
795
                $containsOk = false;
796
                foreach ($data as $key => $item) {
797
                    try {
798
                        $contains->process($item, $options, $path . '->' . $key);
799
                        $containsOk = true;
800
                        break;
801
                    } catch (InvalidValue $exception) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
802
                    }
803
                }
804
                if (!$containsOk) {
805
                    $this->fail(new ArrayException('Array fails contains constraint'), $path);
806
                }
807
            }
808
        }
809
810
        if ($this->contentEncoding !== null || $this->contentMediaType !== null) {
811
            try {
812
                if ($options->unpackContentMediaType) {
813
                    $result = Content::process($options, $this->contentEncoding, $this->contentMediaType, $data, $import);
814
                } else {
815
                    Content::process($options, $this->contentEncoding, $this->contentMediaType, $data, true);
816
                }
817
            } catch (InvalidValue $exception) {
818
                $this->fail($exception, $path);
819
            }
820
        }
821
822
        return $result;
823
    }
824
825
    /**
826
     * @param boolean $useObjectAsArray
827
     * @return Schema
828
     */
829
    public
830
    function setUseObjectAsArray($useObjectAsArray)
831
    {
832
        $this->useObjectAsArray = $useObjectAsArray;
833
        return $this;
834
    }
835
836
    private function fail(InvalidValue $exception, $path)
837
    {
838
        if ($path !== '#') {
839
            $exception->addPath($path);
840
        }
841
        throw $exception;
842
    }
843
844
    public static function integer()
845
    {
846
        $schema = new static();
847
        $schema->type = Type::INTEGER;
848
        return $schema;
849
    }
850
851
    public static function number()
852
    {
853
        $schema = new static();
854
        $schema->type = Type::NUMBER;
855
        return $schema;
856
    }
857
858
    public static function string()
859
    {
860
        $schema = new static();
861
        $schema->type = Type::STRING;
862
        return $schema;
863
    }
864
865
    public static function boolean()
866
    {
867
        $schema = new static();
868
        $schema->type = Type::BOOLEAN;
869
        return $schema;
870
    }
871
872
    public static function object()
873
    {
874
        $schema = new static();
875
        $schema->type = Type::OBJECT;
876
        return $schema;
877
    }
878
879
    public static function arr()
880
    {
881
        $schema = new static();
882
        $schema->type = Type::ARR;
883
        return $schema;
884
    }
885
886
    public static function null()
887
    {
888
        $schema = new static();
889
        $schema->type = Type::NULL;
890
        return $schema;
891
    }
892
893
894
    /**
895
     * @param Properties $properties
896
     * @return Schema
897
     */
898
    public function setProperties($properties)
899
    {
900
        $this->properties = $properties;
901
        return $this;
902
    }
903
904
    /**
905
     * @param string $name
906
     * @param Schema $schema
907
     * @return $this
908
     */
909
    public function setProperty($name, $schema)
910
    {
911
        if (null === $this->properties) {
912
            $this->properties = new Properties();
913
        }
914
        $this->properties->__set($name, $schema);
915
        return $this;
916
    }
917
918
    /** @var Meta[] */
919
    private $metaItems = array();
920
921
    public function addMeta(Meta $meta)
922
    {
923
        $this->metaItems[get_class($meta)] = $meta;
924
        return $this;
925
    }
926
927
    public function getMeta($className)
928
    {
929
        if (isset($this->metaItems[$className])) {
930
            return $this->metaItems[$className];
931
        }
932
        return null;
933
    }
934
935
    /**
936
     * @param Context $options
937
     * @return ObjectItemContract
938
     */
939
    public function makeObjectItem(Context $options = null)
940
    {
941
        if (null === $this->objectItemClass) {
942
            return new ObjectItem();
943
        } else {
944
            $className = $this->objectItemClass;
945
            if ($options !== null) {
946
                if (isset($options->objectItemClassMapping[$className])) {
947
                    $className = $options->objectItemClassMapping[$className];
948
                }
949
            }
950
            return new $className;
951
        }
952
    }
953
954
    /**
955
     * @param mixed $schema
956
     * @return mixed|Schema
957
     */
958
    private static function unboolSchema($schema)
959
    {
960
        static $trueSchema;
961
        static $falseSchema;
962
963
        if (null === $trueSchema) {
964
            $trueSchema = new Schema();
965
            $falseSchema = new Schema();
966
            $falseSchema->not = $trueSchema;
967
        }
968
969
        if ($schema === true) {
970
            return $trueSchema;
971
        } elseif ($schema === false) {
972
            return $falseSchema;
973
        } else {
974
            return $schema;
975
        }
976
    }
977
978
    /**
979
     * @param mixed $data
980
     * @return \stdClass
981
     */
982
    private static function unboolSchemaData($data)
983
    {
984
        static $trueSchema;
985
        static $falseSchema;
986
987
        if (null === $trueSchema) {
988
            $trueSchema = new \stdClass();
989
            $falseSchema = new \stdClass();
990
            $falseSchema->not = $trueSchema;
991
        }
992
993
        if ($data === true) {
994
            return $trueSchema;
995
        } elseif ($data === false) {
996
            return $falseSchema;
997
        } else {
998
            return $data;
999
        }
1000
    }
1001
1002
}
1003