Completed
Pull Request — master (#17)
by Viacheslav
28:32
created

Schema::addPropertyMapping()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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