Completed
Pull Request — master (#15)
by Viacheslav
09:20
created

Schema::getMeta()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 7
rs 9.4285
cc 2
eloc 4
nc 2
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\Properties;
9
use Swaggest\JsonSchema\Constraint\Type;
10
use Swaggest\JsonSchema\Constraint\UniqueItems;
11
use Swaggest\JsonSchema\Exception\ArrayException;
12
use Swaggest\JsonSchema\Exception\ConstException;
13
use Swaggest\JsonSchema\Exception\EnumException;
14
use Swaggest\JsonSchema\Exception\LogicException;
15
use Swaggest\JsonSchema\Exception\NumericException;
16
use Swaggest\JsonSchema\Exception\ObjectException;
17
use Swaggest\JsonSchema\Exception\StringException;
18
use Swaggest\JsonSchema\Exception\TypeException;
19
use Swaggest\JsonSchema\Meta\Meta;
20
use Swaggest\JsonSchema\Meta\MetaHolder;
21
use Swaggest\JsonSchema\Structure\ClassStructure;
22
use Swaggest\JsonSchema\Structure\Egg;
23
use Swaggest\JsonSchema\Structure\ObjectItem;
24
use Swaggest\JsonSchema\Structure\ObjectItemContract;
25
26
/**
27
 * Class Schema
28
 * @package Swaggest\JsonSchema
29
 */
30
class Schema extends JsonSchema implements MetaHolder
31
{
32
    const CONST_PROPERTY = 'const';
33
34
    const DEFAULT_MAPPING = 'default';
35
36
    const VERSION_AUTO = 'a';
37
    const VERSION_DRAFT_04 = 4;
38
    const VERSION_DRAFT_06 = 6;
39
40
    const SCHEMA_DRAFT_04_URL = 'http://json-schema.org/draft-04/schema';
41
42
    const REF = '$ref';
43
    const ID = '$id';
44
45
46
    /*
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...
47
    public $__seqId;
48
    public static $seq = 0;
49
50
    public function __construct()
51
    {
52
        self::$seq++;
53
        $this->__seqId = self::$seq;
54
    }
55
    //*/
56
57
    // Object
58
    /** @var Properties|Schema[]|Schema */
59
    public $properties;
60
    /** @var Schema|bool */
61
    public $additionalProperties;
62
    /** @var Schema[] */
63
    public $patternProperties;
64
    /** @var string[][]|Schema[]|\stdClass */
65
    public $dependencies;
66
67
    // Array
68
    /** @var null|Schema|Schema[] */
69
    public $items;
70
    /** @var null|Schema|bool */
71
    public $additionalItems;
72
73
    const FORMAT_DATE_TIME = 'date-time'; // todo implement
74
75
76
    /** @var Schema[] */
77
    public $allOf;
78
    /** @var Schema */
79
    public $not;
80
    /** @var Schema[] */
81
    public $anyOf;
82
    /** @var Schema[] */
83
    public $oneOf;
84
85
    public $objectItemClass;
86
    private $useObjectAsArray = false;
87
88
    private $__dataToProperty = array();
89
    private $__propertyToData = array();
90
91
92
    public function addPropertyMapping($dataName, $propertyName, $mapping = self::DEFAULT_MAPPING)
93
    {
94
        $this->__dataToProperty[$mapping][$dataName] = $propertyName;
95
        $this->__propertyToData[$mapping][$propertyName] = $dataName;
96
        return $this;
97
    }
98
99
    private function preProcessReferences($data, Context $options, $nestingLevel = 0)
100
    {
101
        if ($nestingLevel > 200) {
102
            throw new Exception('Too deep nesting level', Exception::DEEP_NESTING);
103
        }
104
        if (is_array($data)) {
105
            foreach ($data as $key => $item) {
106
                $this->preProcessReferences($item, $options, $nestingLevel + 1);
107
            }
108
        } elseif ($data instanceof \stdClass) {
109
            /** @var JsonSchema $data */
110
            if (
111
                isset($data->id)
112
                && is_string($data->id)
113
                && ($options->version === self::VERSION_DRAFT_04 || $options->version === self::VERSION_AUTO)
114
            ) {
115
                $prev = $options->refResolver->setupResolutionScope($data->id, $data);
116
                /** @noinspection PhpUnusedLocalVariableInspection */
117
                $_ = 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...
118
                    $options->refResolver->setResolutionScope($prev);
119
                });
120
            }
121
122
            if (isset($data->{self::ID})
123
                && is_string($data->{self::ID})
124
                && ($options->version >= self::VERSION_DRAFT_06 || $options->version === self::VERSION_AUTO)
125
            ) {
126
                $prev = $options->refResolver->setupResolutionScope($data->{self::ID}, $data);
127
                /** @noinspection PhpUnusedLocalVariableInspection */
128
                $_ = 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...
129
                    $options->refResolver->setResolutionScope($prev);
130
                });
131
            }
132
133
134
            foreach ((array)$data as $key => $value) {
135
                $this->preProcessReferences($value, $options, $nestingLevel + 1);
136
            }
137
        }
138
    }
139
140
    public static function import($data, Context $options = null)
141
    {
142
        $data = self::unboolSchemaData($data);
143
        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...
144
    }
145
146
    public function in($data, Context $options = null)
147
    {
148
        if ($options === null) {
149
            $options = new Context();
150
        }
151
152
        $options->import = true;
153
154
        $options->refResolver = new RefResolver($data);
155
        if ($options->remoteRefProvider) {
156
            $options->refResolver->setRemoteRefProvider($options->remoteRefProvider);
157
        }
158
159
        if ($options->import) {
160
            $this->preProcessReferences($data, $options);
161
        }
162
163
        return $this->process($data, $options, '#');
164
    }
165
166
167
    /**
168
     * @param mixed $data
169
     * @param Context|null $options
170
     * @return array|mixed|null|object|\stdClass
171
     * @throws InvalidValue
172
     */
173
    public function out($data, Context $options = null)
174
    {
175
        if ($options === null) {
176
            $options = new Context();
177
        }
178
179
        $options->circularReferences = new \SplObjectStorage();
180
        $options->import = false;
181
        return $this->process($data, $options);
182
    }
183
184
    /**
185
     * @param mixed $data
186
     * @param Context $options
187
     * @param string $path
188
     * @param null $result
189
     * @return array|mixed|null|object|\stdClass
190
     * @throws InvalidValue
191
     * @throws \Exception
192
     */
193
    public function process($data, Context $options, $path = '#', $result = null)
194
    {
195
196
        $import = $options->import;
197
        //$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...
198
199
        if (!$import && $data instanceof ObjectItemContract) {
200
            $result = new \stdClass();
201
            if ($options->circularReferences->contains($data)) {
202
                /** @noinspection PhpIllegalArrayKeyTypeInspection */
203
                $path = $options->circularReferences[$data];
204
                // @todo $path is not a valid json pointer $ref
205
                $result->{self::REF} = $path;
206
                return $result;
207
//                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...
208
            }
209
            $options->circularReferences->attach($data, $path);
210
            //$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...
211
212
            $data = $data->jsonSerialize();
213
        }
214
        if (!$import && is_array($data) && $this->useObjectAsArray) {
215
            $data = (object)$data;
216
        }
217
218
        if (null !== $options->dataPreProcessor) {
219
            $data = $options->dataPreProcessor->process($data, $this, $import);
220
        }
221
222
        if ($result === null) {
223
            $result = $data;
224
        }
225
226
        if ($options->skipValidation) {
227
            goto skipValidation;
228
        }
229
230
        if ($this->type !== null) {
231
            if ($options->tolerateStrings && is_string($data)) {
232
                $valid = Type::readString($this->type, $data);
233
            } else {
234
                $valid = Type::isValid($this->type, $data);
235
            }
236
            if (!$valid) {
237
                $this->fail(new TypeException(ucfirst(
238
                        implode(', ', is_array($this->type) ? $this->type : array($this->type))
239
                        . ' expected, ' . json_encode($data) . ' received')
240
                ), $path);
241
            }
242
        }
243
244
        if ($this->enum !== null) {
245
            $enumOk = false;
246
            foreach ($this->enum as $item) {
247
                if ($item === $data) { // todo support complex structures here
248
                    $enumOk = true;
249
                    break;
250
                }
251
            }
252
            if (!$enumOk) {
253
                $this->fail(new EnumException('Enum failed'), $path);
254
            }
255
        }
256
257
        if (array_key_exists(self::CONST_PROPERTY, $this->__arrayOfData)) {
258
            if ($this->const !== $data) {
259
                if ((is_object($this->const) && is_object($data))
260
                    || (is_array($this->const) && is_array($data))) {
261
                    $diff = new JsonDiff($this->const, $data,
262
                        JsonDiff::SKIP_REARRANGE_ARRAY + JsonDiff::STOP_ON_DIFF);
263
                    if ($diff->getDiffCnt() != 0) {
264
                        $this->fail(new ConstException('Const failed'), $path);
265
                    }
266
                } else {
267
                    $this->fail(new ConstException('Const failed'), $path);
268
                }
269
            }
270
        }
271
272
        if ($this->not !== null) {
273
            $exception = false;
274
            try {
275
                self::unboolSchema($this->not)->process($data, $options, $path . '->not');
276
            } catch (InvalidValue $exception) {
277
                // Expected exception
278
            }
279
            if ($exception === false) {
280
                $this->fail(new LogicException('Failed due to logical constraint: not'), $path);
281
            }
282
        }
283
284
        if (is_string($data)) {
285
            if ($this->minLength !== null) {
286
                if (mb_strlen($data, 'UTF-8') < $this->minLength) {
287
                    $this->fail(new StringException('String is too short', StringException::TOO_SHORT), $path);
288
                }
289
            }
290
            if ($this->maxLength !== null) {
291
                if (mb_strlen($data, 'UTF-8') > $this->maxLength) {
292
                    $this->fail(new StringException('String is too long', StringException::TOO_LONG), $path);
293
                }
294
            }
295
            if ($this->pattern !== null) {
296
                if (0 === preg_match(Helper::toPregPattern($this->pattern), $data)) {
297
                    $this->fail(new StringException(json_encode($data) . ' does not match to '
298
                        . $this->pattern, StringException::PATTERN_MISMATCH), $path);
299
                }
300
            }
301
        }
302
303
        if (is_int($data) || is_float($data)) {
304
            if ($this->multipleOf !== null) {
305
                $div = $data / $this->multipleOf;
306
                if ($div != (int)$div) {
307
                    $this->fail(new NumericException($data . ' is not multiple of ' . $this->multipleOf, NumericException::MULTIPLE_OF), $path);
308
                }
309
            }
310
311
            if ($this->exclusiveMaximum !== null && !is_bool($this->exclusiveMaximum)) {
312
                if ($data >= $this->exclusiveMaximum) {
313
                    $this->fail(new NumericException(
314
                        'Value less or equal than ' . $this->exclusiveMaximum . ' expected, ' . $data . ' received',
315
                        NumericException::MAXIMUM), $path);
316
                }
317
            }
318
319
            if ($this->exclusiveMinimum !== null && !is_bool($this->exclusiveMinimum)) {
320
                if ($data <= $this->exclusiveMinimum) {
321
                    $this->fail(new NumericException(
322
                        'Value more or equal than ' . $this->exclusiveMinimum . ' expected, ' . $data . ' received',
323
                        NumericException::MINIMUM), $path);
324
                }
325
            }
326
327
            if ($this->maximum !== null) {
328
                if ($this->exclusiveMaximum === true) {
329
                    if ($data >= $this->maximum) {
330
                        $this->fail(new NumericException(
331
                            'Value less or equal than ' . $this->maximum . ' expected, ' . $data . ' received',
332
                            NumericException::MAXIMUM), $path);
333
                    }
334
                } else {
335
                    if ($data > $this->maximum) {
336
                        $this->fail(new NumericException(
337
                            'Value less than ' . $this->minimum . ' expected, ' . $data . ' received',
338
                            NumericException::MAXIMUM), $path);
339
                    }
340
                }
341
            }
342
343
            if ($this->minimum !== null) {
344
                if ($this->exclusiveMinimum === true) {
345
                    if ($data <= $this->minimum) {
346
                        $this->fail(new NumericException(
347
                            'Value more or equal than ' . $this->minimum . ' expected, ' . $data . ' received',
348
                            NumericException::MINIMUM), $path);
349
                    }
350
                } else {
351
                    if ($data < $this->minimum) {
352
                        $this->fail(new NumericException(
353
                            'Value more than ' . $this->minimum . ' expected, ' . $data . ' received',
354
                            NumericException::MINIMUM), $path);
355
                    }
356
                }
357
            }
358
        }
359
360
        skipValidation:
361
362
        if ($this->oneOf !== null) {
363
            $successes = 0;
364
            $failures = '';
365
            $skipValidation = false;
366
            if ($options->skipValidation) {
367
                $skipValidation = true;
368
                $options->skipValidation = false;
369
            }
370
371
            foreach ($this->oneOf as $index => $item) {
372
                try {
373
                    $result = self::unboolSchema($item)->process($data, $options, $path . '->oneOf:' . $index);
374
                    $successes++;
375
                    if ($successes > 1 || $options->skipValidation) {
376
                        break;
377
                    }
378
                } catch (InvalidValue $exception) {
379
                    $failures .= ' ' . $index . ': ' . Helper::padLines(' ', $exception->getMessage()) . "\n";
380
                    // Expected exception
381
                }
382
            }
383
            if ($skipValidation) {
384
                $options->skipValidation = true;
385
                if ($successes === 0) {
386
                    $result = self::unboolSchema($this->oneOf[0])->process($data, $options, $path . '->oneOf:' . 0);
387
                }
388
            }
389
390
            if (!$options->skipValidation) {
391
                if ($successes === 0) {
392
                    $this->fail(new LogicException('Failed due to logical constraint: no valid results for oneOf {' . "\n" . substr($failures, 0, -1) . "\n}"), $path);
393
                } elseif ($successes > 1) {
394
                    $this->fail(new LogicException('Failed due to logical constraint: '
395
                        . $successes . '/' . count($this->oneOf) . ' valid results for oneOf'), $path);
396
                }
397
            }
398
        }
399
400
        if ($this->anyOf !== null) {
401
            $successes = 0;
402
            $failures = '';
403
            foreach ($this->anyOf as $index => $item) {
404
                try {
405
                    $result = self::unboolSchema($item)->process($data, $options, $path . '->anyOf:' . $index);
406
                    $successes++;
407
                    if ($successes) {
408
                        break;
409
                    }
410
                } catch (InvalidValue $exception) {
411
                    $failures .= ' ' . $index . ': ' . $exception->getMessage() . "\n";
412
                    // Expected exception
413
                }
414
            }
415
            if (!$successes && !$options->skipValidation) {
416
                $this->fail(new LogicException('Failed due to logical constraint: no valid results for anyOf {' . "\n" . substr(Helper::padLines(' ', $failures), 0, -1) . "\n}"), $path);
417
            }
418
        }
419
420
        if ($this->allOf !== null) {
421
            foreach ($this->allOf as $index => $item) {
422
                $result = self::unboolSchema($item)->process($data, $options, $path . '->allOf' . $index);
423
            }
424
        }
425
426
        if ($data instanceof \stdClass) {
427
            if (!$options->skipValidation && $this->required !== null) {
428
429
                if (isset($this->__dataToProperty[$options->mapping])) {
430
                    if ($import) {
431
                        foreach ($this->required as $item) {
432
                            if (isset($this->__propertyToData[$options->mapping][$item])) {
433
                                $item = $this->__propertyToData[$options->mapping][$item];
434
                            }
435
                            if (!property_exists($data, $item)) {
436
                                $this->fail(new ObjectException('Required property missing: ' . $item, ObjectException::REQUIRED), $path);
437
                            }
438
                        }
439
                    } else {
440
                        foreach ($this->required as $item) {
441
                            if (isset($this->__dataToProperty[$options->mapping][$item])) {
442
                                $item = $this->__dataToProperty[$options->mapping][$item];
443
                            }
444
                            if (!property_exists($data, $item)) {
445
                                $this->fail(new ObjectException('Required property missing: ' . $item, ObjectException::REQUIRED), $path);
446
                            }
447
                        }
448
                    }
449
450
                } else {
451
                    foreach ($this->required as $item) {
452
                        if (!property_exists($data, $item)) {
453
                            $this->fail(new ObjectException('Required property missing: ' . $item, ObjectException::REQUIRED), $path);
454
                        }
455
                    }
456
                }
457
458
            }
459
460
            if ($import) {
461
                if ($this->useObjectAsArray) {
462
                    $result = array();
463
                } elseif (!$result instanceof ObjectItemContract) {
464
                    $result = $this->makeObjectItem($options);
465
466
                    if ($result instanceof ClassStructure) {
467
                        if ($result->__validateOnSet) {
468
                            $result->__validateOnSet = false;
469
                            /** @noinspection PhpUnusedLocalVariableInspection */
470
                            $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...
471
                                $result->__validateOnSet = true;
472
                            });
473
                        }
474
                    }
475
476
                    if ($result instanceof ObjectItemContract) {
477
                        $result->setDocumentPath($path);
478
                    }
479
                }
480
            }
481
482
            if ($import) {
483
                try {
484
                    while (
485
                        isset($data->{self::REF})
486
                        && is_string($data->{self::REF})
487
                        && !isset($this->properties[self::REF])
488
                    ) {
489
                        $refString = $data->{self::REF};
490
                        // TODO consider process # by reference here ?
491
                        $refResolver = $options->refResolver;
492
                        $preRefScope = $refResolver->getResolutionScope();
493
                        /** @noinspection PhpUnusedLocalVariableInspection */
494
                        $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...
495
                            $refResolver->setResolutionScope($preRefScope);
496
                        });
497
                        $ref = $refResolver->resolveReference($refString);
498
                        if ($ref->isImported()) {
499
                            $refResult = $ref->getImported();
500
                            return $refResult;
501
                        }
502
                        $data = self::unboolSchemaData($ref->getData());
503
                        if ($result instanceof ObjectItemContract) {
504
                            $result->setFromRef($refString);
505
                        }
506
                        $ref->setImported($result);
507
                        $refResult = $this->process($data, $options, $path . '->ref:' . $refString, $result);
508
                        $ref->setImported($refResult);
509
                        return $refResult;
510
                    }
511
                } catch (InvalidValue $exception) {
512
                    $this->fail($exception, $path);
513
                }
514
            }
515
516
            // @todo better check for schema id
517
518
            if ($import
519
                && isset($data->id)
520
                && ($options->version === Schema::VERSION_DRAFT_04 || $options->version === Schema::VERSION_AUTO)
521
                && is_string($data->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...
522
                $id = $data->id;
523
                $refResolver = $options->refResolver;
524
                $parentScope = $refResolver->updateResolutionScope($id);
525
                /** @noinspection PhpUnusedLocalVariableInspection */
526
                $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...
527
                    $refResolver->setResolutionScope($parentScope);
528
                });
529
            }
530
531
            if ($import
532
                && isset($data->{self::ID})
533
                && ($options->version >= Schema::VERSION_DRAFT_06 || $options->version === Schema::VERSION_AUTO)
534
                && 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...
535
                $id = $data->{self::ID};
536
                $refResolver = $options->refResolver;
537
                $parentScope = $refResolver->updateResolutionScope($id);
538
                /** @noinspection PhpUnusedLocalVariableInspection */
539
                $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...
540
                    $refResolver->setResolutionScope($parentScope);
541
                });
542
            }
543
544
            /** @var Schema[] $properties */
545
            $properties = null;
546
547
            $nestedProperties = null;
548
            if ($this->properties !== null) {
549
                $properties = &$this->properties->toArray(); // TODO check performance of pointer
550
                if ($this->properties instanceof Properties) {
551
                    $nestedProperties = $this->properties->getNestedProperties();
552
                } else {
553
                    $nestedProperties = array();
554
                }
555
            }
556
557
            $array = array();
558
            if (!empty($this->__dataToProperty[$options->mapping])) {
559
                foreach ((array)$data as $key => $value) {
560
                    if ($import) {
561
                        if (isset($this->__dataToProperty[$options->mapping][$key])) {
562
                            $key = $this->__dataToProperty[$options->mapping][$key];
563
                        }
564
                    } else {
565
                        if (isset($this->__propertyToData[$options->mapping][$key])) {
566
                            $key = $this->__propertyToData[$options->mapping][$key];
567
                        }
568
                    }
569
                    $array[$key] = $value;
570
                }
571
            } else {
572
                $array = (array)$data;
573
            }
574
575
            if (!$options->skipValidation) {
576
                if ($this->minProperties !== null && count($array) < $this->minProperties) {
577
                    $this->fail(new ObjectException("Not enough properties", ObjectException::TOO_FEW), $path);
578
                }
579
                if ($this->maxProperties !== null && count($array) > $this->maxProperties) {
580
                    $this->fail(new ObjectException("Too many properties", ObjectException::TOO_MANY), $path);
581
                }
582
                if ($this->propertyNames !== null) {
583
                    $propertyNames = self::unboolSchema($this->propertyNames);
584
                    foreach ($array as $key => $tmp) {
585
                        $propertyNames->process($key, $options, $path . '->propertyNames:' . $key);
586
                    }
587
                }
588
            }
589
590
            foreach ($array as $key => $value) {
591
                if ($key === '' && PHP_VERSION_ID < 71000) {
592
                    $this->fail(new InvalidValue('Empty property name'), $path);
593
                }
594
595
                $found = false;
596
597
                if (!$options->skipValidation && !empty($this->dependencies)) {
598
                    $deps = $this->dependencies;
599
                    if (isset($deps->$key)) {
600
                        $dependencies = $deps->$key;
601
                        $dependencies = self::unboolSchema($dependencies);
602
                        if ($dependencies instanceof Schema) {
603
                            $dependencies->process($data, $options, $path . '->dependencies:' . $key);
604
                        } else {
605
                            foreach ($dependencies as $item) {
606
                                if (!property_exists($data, $item)) {
607
                                    $this->fail(new ObjectException('Dependency property missing: ' . $item,
608
                                        ObjectException::DEPENDENCY_MISSING), $path);
609
                                }
610
                            }
611
                        }
612
                    }
613
                }
614
615
                $propertyFound = false;
616
                if (isset($properties[$key])) {
617
                    /** @var Schema[] $properties */
618
                    $prop = self::unboolSchema($properties[$key]);
619
                    $propertyFound = true;
620
                    $found = true;
621
                    if ($prop instanceof Schema) {
622
                        $value = $prop->process($value, $options, $path . '->properties:' . $key);
623
                    }
624
                    // @todo process $prop === false
625
                }
626
627
                /** @var Egg[] $nestedEggs */
628
                $nestedEggs = null;
629
                if (isset($nestedProperties[$key])) {
630
                    $found = true;
631
                    $nestedEggs = $nestedProperties[$key];
632
                    // todo iterate all nested props?
633
                    $value = self::unboolSchema($nestedEggs[0]->propertySchema)->process($value, $options, $path . '->nestedProperties:' . $key);
634
                }
635
636
                if ($this->patternProperties !== null) {
637
                    foreach ($this->patternProperties as $pattern => $propertySchema) {
638
                        if (preg_match(Helper::toPregPattern($pattern), $key)) {
639
                            $found = true;
640
                            $value = self::unboolSchema($propertySchema)->process($value, $options,
641
                                $path . '->patternProperties[' . $pattern . ']:' . $key);
642
                            if ($import) {
643
                                $result->addPatternPropertyName($pattern, $key);
644
                            }
645
                            //break; // todo manage multiple import data properly (pattern accessor)
646
                        }
647
                    }
648
                }
649
                if (!$found && $this->additionalProperties !== null) {
650
                    if (!$options->skipValidation && $this->additionalProperties === false) {
651
                        $this->fail(new ObjectException('Additional properties not allowed'), $path . ':' . $key);
652
                    }
653
654
                    if ($this->additionalProperties instanceof Schema) {
655
                        $value = $this->additionalProperties->process($value, $options, $path . '->additionalProperties:' . $key);
656
                    }
657
658
                    if ($import && !$this->useObjectAsArray) {
659
                        $result->addAdditionalPropertyName($key);
660
                    }
661
                }
662
663
                if ($nestedEggs && $import) {
664
                    foreach ($nestedEggs as $nestedEgg) {
665
                        $result->setNestedProperty($key, $value, $nestedEgg);
666
                    }
667
                    if ($propertyFound) {
668
                        $result->$key = $value;
669
                    }
670
                } else {
671
                    if ($this->useObjectAsArray && $import) {
672
                        $result[$key] = $value;
673
                    } else {
674
                        if ($found || !$import) {
675
                            $result->$key = $value;
676
                        } elseif (!isset($result->$key)) {
677
                            $result->$key = $value;
678
                        }
679
                    }
680
                }
681
            }
682
683
        }
684
685
        if (is_array($data)) {
686
            $count = count($data);
687
            if (!$options->skipValidation) {
688
                if ($this->minItems !== null && $count < $this->minItems) {
689
                    $this->fail(new ArrayException("Not enough items in array"), $path);
690
                }
691
692
                if ($this->maxItems !== null && $count > $this->maxItems) {
693
                    $this->fail(new ArrayException("Too many items in array"), $path);
694
                }
695
            }
696
697
            $pathItems = 'items';
698
            $this->items = self::unboolSchema($this->items);
699
            if ($this->items instanceof Schema) {
700
                $items = array();
701
                $additionalItems = $this->items;
702
            } elseif ($this->items === null) { // items defaults to empty schema so everything is valid
703
                $items = array();
704
                $additionalItems = true;
705
            } else { // listed items
706
                $items = $this->items;
707
                $additionalItems = $this->additionalItems;
708
                $pathItems = 'additionalItems';
709
            }
710
711
            /**
712
             * @var Schema|Schema[] $items
713
             * @var null|bool|Schema $additionalItems
714
             */
715
            $itemsLen = is_array($items) ? count($items) : 0;
716
            $index = 0;
717
            foreach ($result as $key => $value) {
718
                if ($index < $itemsLen) {
719
                    $itemSchema = self::unboolSchema($items[$index]);
720
                    $result[$key] = $itemSchema->process($value, $options, $path . '->items:' . $index);
721
                } else {
722
                    if ($additionalItems instanceof Schema) {
723
                        $result[$key] = $additionalItems->process($value, $options, $path . '->' . $pathItems
724
                            . '[' . $index . ']');
725
                    } elseif (!$options->skipValidation && $additionalItems === false) {
726
                        $this->fail(new ArrayException('Unexpected array item'), $path);
727
                    }
728
                }
729
                ++$index;
730
            }
731
732
            if (!$options->skipValidation && $this->uniqueItems) {
733
                if (!UniqueItems::isValid($data)) {
734
                    $this->fail(new ArrayException('Array is not unique'), $path);
735
                }
736
            }
737
738
            if (!$options->skipValidation && $this->contains !== null) {
739
                /** @var Schema|bool $contains */
740
                $contains = $this->contains;
741
                if ($contains === false) {
742
                    $this->fail(new ArrayException('Contains is false'), $path);
743
                }
744
                if ($count === 0) {
745
                    $this->fail(new ArrayException('Empty array fails contains constraint'), $path);
746
                }
747
                if ($contains === true) {
748
                    $contains = self::unboolSchema($contains);
749
                }
750
                $containsOk = false;
751
                foreach ($data as $key => $item) {
752
                    try {
753
                        $contains->process($item, $options, $path . '->' . $key);
754
                        $containsOk = true;
755
                        break;
756
                    } catch (InvalidValue $exception) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
757
                    }
758
                }
759
                if (!$containsOk) {
760
                    $this->fail(new ArrayException('Array fails contains constraint'), $path);
761
                }
762
            }
763
        }
764
765
        return $result;
766
    }
767
768
    /**
769
     * @param boolean $useObjectAsArray
770
     * @return Schema
771
     */
772
    public
773
    function setUseObjectAsArray($useObjectAsArray)
774
    {
775
        $this->useObjectAsArray = $useObjectAsArray;
776
        return $this;
777
    }
778
779
    private function fail(InvalidValue $exception, $path)
780
    {
781
        if ($path !== '#') {
782
            $exception->addPath($path);
783
        }
784
        throw $exception;
785
    }
786
787
    public static function integer()
788
    {
789
        $schema = new static();
790
        $schema->type = Type::INTEGER;
791
        return $schema;
792
    }
793
794
    public static function number()
795
    {
796
        $schema = new static();
797
        $schema->type = Type::NUMBER;
798
        return $schema;
799
    }
800
801
    public static function string()
802
    {
803
        $schema = new static();
804
        $schema->type = Type::STRING;
805
        return $schema;
806
    }
807
808
    public static function boolean()
809
    {
810
        $schema = new static();
811
        $schema->type = Type::BOOLEAN;
812
        return $schema;
813
    }
814
815
    public static function object()
816
    {
817
        $schema = new static();
818
        $schema->type = Type::OBJECT;
819
        return $schema;
820
    }
821
822
    public static function arr()
823
    {
824
        $schema = new static();
825
        $schema->type = Type::ARR;
826
        return $schema;
827
    }
828
829
    public static function null()
830
    {
831
        $schema = new static();
832
        $schema->type = Type::NULL;
833
        return $schema;
834
    }
835
836
837
    /**
838
     * @param Properties $properties
839
     * @return Schema
840
     */
841
    public function setProperties($properties)
842
    {
843
        $this->properties = $properties;
844
        return $this;
845
    }
846
847
    /**
848
     * @param string $name
849
     * @param Schema $schema
850
     * @return $this
851
     */
852
    public function setProperty($name, $schema)
853
    {
854
        if (null === $this->properties) {
855
            $this->properties = new Properties();
856
        }
857
        $this->properties->__set($name, $schema);
858
        return $this;
859
    }
860
861
    /** @var Meta[] */
862
    private $metaItems = array();
863
864
    public function addMeta(Meta $meta)
865
    {
866
        $this->metaItems[get_class($meta)] = $meta;
867
        return $this;
868
    }
869
870
    public function getMeta($className)
871
    {
872
        if (isset($this->metaItems[$className])) {
873
            return $this->metaItems[$className];
874
        }
875
        return null;
876
    }
877
878
    /**
879
     * @param Context $options
880
     * @return ObjectItemContract
881
     */
882
    public function makeObjectItem(Context $options = null)
883
    {
884
        if (null === $this->objectItemClass) {
885
            return new ObjectItem();
886
        } else {
887
            $className = $this->objectItemClass;
888
            if ($options !== null) {
889
                if (isset($options->objectItemClassMapping[$className])) {
890
                    $className = $options->objectItemClassMapping[$className];
891
                }
892
            }
893
            return new $className;
894
        }
895
    }
896
897
    /**
898
     * @param mixed $schema
899
     * @return mixed|Schema
900
     */
901
    private static function unboolSchema($schema)
902
    {
903
        static $trueSchema;
904
        static $falseSchema;
905
906
        if (null === $trueSchema) {
907
            $trueSchema = new Schema();
908
            $falseSchema = new Schema();
909
            $falseSchema->not = $trueSchema;
910
        }
911
912
        if ($schema === true) {
913
            return $trueSchema;
914
        } elseif ($schema === false) {
915
            return $falseSchema;
916
        } else {
917
            return $schema;
918
        }
919
    }
920
921
    /**
922
     * @param mixed $data
923
     * @return \stdClass
924
     */
925
    private static function unboolSchemaData($data)
926
    {
927
        static $trueSchema;
928
        static $falseSchema;
929
930
        if (null === $trueSchema) {
931
            $trueSchema = new \stdClass();
932
            $falseSchema = new \stdClass();
933
            $falseSchema->not = $trueSchema;
934
        }
935
936
        if ($data === true) {
937
            return $trueSchema;
938
        } elseif ($data === false) {
939
            return $falseSchema;
940
        } else {
941
            return $data;
942
        }
943
    }
944
945
}
946