Passed
Pull Request — master (#20)
by Viacheslav
02:54
created

Schema::getProperties()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
eloc 1
nc 1
nop 0
crap 1
1
<?php
2
3
namespace Swaggest\JsonSchema;
4
5
6
use PhpLang\ScopeExit;
7
use Swaggest\JsonDiff\JsonDiff;
8
use Swaggest\JsonSchema\Constraint\Content;
9
use Swaggest\JsonSchema\Constraint\Format;
10
use Swaggest\JsonSchema\Constraint\Properties;
11
use Swaggest\JsonSchema\Constraint\Type;
12
use Swaggest\JsonSchema\Constraint\UniqueItems;
13
use Swaggest\JsonSchema\Exception\ArrayException;
14
use Swaggest\JsonSchema\Exception\ConstException;
15
use Swaggest\JsonSchema\Exception\EnumException;
16
use Swaggest\JsonSchema\Exception\LogicException;
17
use Swaggest\JsonSchema\Exception\NumericException;
18
use Swaggest\JsonSchema\Exception\ObjectException;
19
use Swaggest\JsonSchema\Exception\StringException;
20
use Swaggest\JsonSchema\Exception\TypeException;
21
use Swaggest\JsonSchema\Meta\Meta;
22
use Swaggest\JsonSchema\Meta\MetaHolder;
23
use Swaggest\JsonSchema\Structure\ClassStructure;
24
use Swaggest\JsonSchema\Structure\Egg;
25
use Swaggest\JsonSchema\Structure\ObjectItem;
26
use Swaggest\JsonSchema\Structure\ObjectItemContract;
27
28
/**
29
 * Class Schema
30
 * @package Swaggest\JsonSchema
31
 */
32
class Schema extends JsonSchema implements MetaHolder, SchemaContract
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 PROP_REF = '$ref';
44
    const PROP_ID = '$id';
45
    const PROP_ID_D4 = 'id';
46
47
    // Object
48
    /** @var null|Properties|Schema[]|Schema */
49
    public $properties;
50
    /** @var Schema|bool */
51
    public $additionalProperties;
52
    /** @var Schema[] */
53
    public $patternProperties;
54
    /** @var string[][]|Schema[]|\stdClass */
55
    public $dependencies;
56
57
    // Array
58
    /** @var null|Schema|Schema[] */
59
    public $items;
60
    /** @var null|Schema|bool */
61
    public $additionalItems;
62
63
    /** @var Schema[] */
64
    public $allOf;
65
    /** @var Schema */
66
    public $not;
67
    /** @var Schema[] */
68
    public $anyOf;
69
    /** @var Schema[] */
70
    public $oneOf;
71
72
    /** @var Schema */
73
    public $if;
74
    /** @var Schema */
75
    public $then;
76
    /** @var Schema */
77
    public $else;
78
79
80
    public $objectItemClass;
81
    private $useObjectAsArray = false;
82
83
    private $__dataToProperty = array();
84
    private $__propertyToData = array();
85
86
    private $__booleanSchema;
87
88 2
    public function addPropertyMapping($dataName, $propertyName, $mapping = self::DEFAULT_MAPPING)
89
    {
90 2
        $this->__dataToProperty[$mapping][$dataName] = $propertyName;
91 2
        $this->__propertyToData[$mapping][$propertyName] = $dataName;
92 2
        return $this;
93
    }
94
95
    /**
96
     * @param mixed $data
97
     * @param Context|null $options
98
     * @return SchemaContract
99
     * @throws Exception
100
     * @throws InvalidValue
101
     * @throws \Exception
102
     */
103 3169
    public static function import($data, Context $options = null)
104
    {
105 3169
        if (null === $options) {
106 19
            $options = new Context();
107
        }
108
109 3169
        $options->applyDefaults = false;
110
111 3169
        if (isset($options->schemasCache) && is_object($data)) {
112
            if ($options->schemasCache->contains($data)) {
113
                return $options->schemasCache->offsetGet($data);
114
            } else {
115
                $schema = parent::import($data, $options);
116
                $options->schemasCache->attach($data, $schema);
117
                return $schema;
118
            }
119
        }
120
121
        // string $data is expected to be $ref uri
122 3169
        if (is_string($data)) {
123 4
            $data = (object)array(self::PROP_REF => $data);
124
        }
125
126 3169
        $data = self::unboolSchema($data);
127 3169
        if ($data instanceof SchemaContract) {
128 72
            return $data;
129
        }
130
131 3097
        return parent::import($data, $options);
132
    }
133
134
    /**
135
     * @param mixed $data
136
     * @param Context|null $options
137
     * @return array|mixed|null|object|\stdClass
138
     * @throws Exception
139
     * @throws InvalidValue
140
     * @throws \Exception
141
     */
142 3191
    public function in($data, Context $options = null)
143
    {
144 3191
        if (null !== $this->__booleanSchema) {
145 72
            if ($this->__booleanSchema) {
146 36
                return $data;
147 36
            } elseif (empty($options->skipValidation)) {
148 18
                $this->fail(new InvalidValue('Denied by false schema'), '#');
149
            }
150
        }
151
152 3137
        if ($options === null) {
153 48
            $options = new Context();
154
        }
155
156 3137
        $options->import = true;
157
158 3137
        if ($options->refResolver === null) {
159 2818
            $options->refResolver = new RefResolver($data);
160
        } else {
161 1693
            $options->refResolver->setRootData($data);
162
        }
163
164 3137
        if ($options->remoteRefProvider) {
165 3077
            $options->refResolver->setRemoteRefProvider($options->remoteRefProvider);
166
        }
167
168 3137
        if ($options->import) {
169 3137
            $options->refResolver->preProcessReferences($data, $options);
170
        }
171
172 3137
        return $this->process($data, $options, '#');
173
    }
174
175
176
    /**
177
     * @param mixed $data
178
     * @param Context|null $options
179
     * @return array|mixed|null|object|\stdClass
180
     * @throws InvalidValue
181
     * @throws \Exception
182
     */
183 2360
    public function out($data, Context $options = null)
184
    {
185 2360
        if ($options === null) {
186 951
            $options = new Context();
187
        }
188
189 2360
        $options->circularReferences = new \SplObjectStorage();
190 2360
        $options->import = false;
191 2360
        return $this->process($data, $options);
192
    }
193
194
    /**
195
     * @param mixed $data
196
     * @param Context $options
197
     * @param string $path
198
     * @throws InvalidValue
199
     * @throws \Exception
200
     */
201 3119
    private function processType($data, Context $options, $path = '#')
202
    {
203 3119
        if ($options->tolerateStrings && is_string($data)) {
204
            $valid = Type::readString($this->type, $data);
205
        } else {
206 3119
            $valid = Type::isValid($this->type, $data, $options->version);
207
        }
208 3119
        if (!$valid) {
209 673
            $this->fail(new TypeException(ucfirst(
210 673
                    implode(', ', is_array($this->type) ? $this->type : array($this->type))
211 673
                    . ' expected, ' . json_encode($data) . ' received')
212 673
            ), $path);
213
        }
214 3117
    }
215
216
    /**
217
     * @param mixed $data
218
     * @param string $path
219
     * @throws InvalidValue
220
     * @throws \Exception
221
     */
222 1457
    private function processEnum($data, $path = '#')
223
    {
224 1457
        $enumOk = false;
225 1457
        foreach ($this->enum as $item) {
226 1457
            if ($item === $data) { // todo support complex structures here
227 1443
                $enumOk = true;
228 1457
                break;
229
            }
230
        }
231 1457
        if (!$enumOk) {
232 89
            $this->fail(new EnumException('Enum failed'), $path);
233
        }
234 1443
    }
235
236
    /**
237
     * @param mixed $data
238
     * @param string $path
239
     * @throws InvalidValue
240
     * @throws \Swaggest\JsonDiff\Exception
241
     */
242 43
    private function processConst($data, $path)
243
    {
244 43
        if ($this->const !== $data) {
245 37
            if ((is_object($this->const) && is_object($data))
246 37
                || (is_array($this->const) && is_array($data))) {
247 15
                $diff = new JsonDiff($this->const, $data,
248 15
                    JsonDiff::STOP_ON_DIFF);
249 15
                if ($diff->getDiffCnt() != 0) {
250 15
                    $this->fail(new ConstException('Const failed'), $path);
251
                }
252
            } else {
253 22
                $this->fail(new ConstException('Const failed'), $path);
254
            }
255
        }
256 20
    }
257
258
    /**
259
     * @param mixed $data
260
     * @param Context $options
261
     * @param string $path
262
     * @throws InvalidValue
263
     * @throws \Exception
264
     * @throws \Swaggest\JsonDiff\Exception
265
     */
266 68
    private function processNot($data, Context $options, $path)
267
    {
268 68
        $exception = false;
269
        try {
270 68
            self::unboolSchema($this->not)->process($data, $options, $path . '->not');
271 15
        } catch (InvalidValue $exception) {
272
            // Expected exception
273
        }
274 68
        if ($exception === false) {
275 55
            $this->fail(new LogicException('Failed due to logical constraint: not'), $path);
276
        }
277 15
    }
278
279
    /**
280
     * @param string $data
281
     * @param string $path
282
     * @throws InvalidValue
283
     */
284 2227
    private function processString($data, $path)
285
    {
286 2227
        if ($this->minLength !== null) {
287 38
            if (mb_strlen($data, 'UTF-8') < $this->minLength) {
288 9
                $this->fail(new StringException('String is too short', StringException::TOO_SHORT), $path);
289
            }
290
        }
291 2221
        if ($this->maxLength !== null) {
292 43
            if (mb_strlen($data, 'UTF-8') > $this->maxLength) {
293 19
                $this->fail(new StringException('String is too long', StringException::TOO_LONG), $path);
294
            }
295
        }
296 2218
        if ($this->pattern !== null) {
297 17
            if (0 === preg_match(Helper::toPregPattern($this->pattern), $data)) {
298 4
                $this->fail(new StringException(json_encode($data) . ' does not match to '
299 4
                    . $this->pattern, StringException::PATTERN_MISMATCH), $path);
300
            }
301
        }
302 2218
        if ($this->format !== null) {
303 401
            $validationError = Format::validationError($this->format, $data);
304 401
            if ($validationError !== null) {
305 137
                if (!($this->format === "uri" && substr($path, -3) === ':id')) {
306 122
                    $this->fail(new StringException($validationError), $path);
307
                }
308
            }
309
        }
310 2218
    }
311
312
    /**
313
     * @param float|int $data
314
     * @param string $path
315
     * @throws InvalidValue
316
     */
317 1040
    private function processNumeric($data, $path)
318
    {
319 1040
        if ($this->multipleOf !== null) {
320 39
            $div = $data / $this->multipleOf;
321 39
            if ($div != (int)$div) {
322 15
                $this->fail(new NumericException($data . ' is not multiple of ' . $this->multipleOf, NumericException::MULTIPLE_OF), $path);
323
            }
324
        }
325
326 1040
        if ($this->exclusiveMaximum !== null && !is_bool($this->exclusiveMaximum)) {
327 32
            if ($data >= $this->exclusiveMaximum) {
328 18
                $this->fail(new NumericException(
329 18
                    'Value less or equal than ' . $this->exclusiveMaximum . ' expected, ' . $data . ' received',
330 18
                    NumericException::MAXIMUM), $path);
331
            }
332
        }
333
334 1040
        if ($this->exclusiveMinimum !== null && !is_bool($this->exclusiveMinimum)) {
335 24
            if ($data <= $this->exclusiveMinimum) {
336 12
                $this->fail(new NumericException(
337 12
                    'Value more or equal than ' . $this->exclusiveMinimum . ' expected, ' . $data . ' received',
338 12
                    NumericException::MINIMUM), $path);
339
            }
340
        }
341
342 1040
        if ($this->maximum !== null) {
343 43
            if ($this->exclusiveMaximum === true) {
344 3
                if ($data >= $this->maximum) {
345 2
                    $this->fail(new NumericException(
346 2
                        'Value less or equal than ' . $this->maximum . ' expected, ' . $data . ' received',
347 3
                        NumericException::MAXIMUM), $path);
348
                }
349
            } else {
350 40
                if ($data > $this->maximum) {
351 13
                    $this->fail(new NumericException(
352 13
                        'Value less than ' . $this->minimum . ' expected, ' . $data . ' received',
353 13
                        NumericException::MAXIMUM), $path);
354
                }
355
            }
356
        }
357
358 1040
        if ($this->minimum !== null) {
359 515
            if ($this->exclusiveMinimum === true) {
360 93
                if ($data <= $this->minimum) {
361 2
                    $this->fail(new NumericException(
362 2
                        'Value more or equal than ' . $this->minimum . ' expected, ' . $data . ' received',
363 93
                        NumericException::MINIMUM), $path);
364
                }
365
            } else {
366 440
                if ($data < $this->minimum) {
367 43
                    $this->fail(new NumericException(
368 43
                        'Value more than ' . $this->minimum . ' expected, ' . $data . ' received',
369 43
                        NumericException::MINIMUM), $path);
370
                }
371
            }
372
        }
373 1039
    }
374
375
    /**
376
     * @param mixed $data
377
     * @param Context $options
378
     * @param string $path
379
     * @return array|mixed|null|object|\stdClass
380
     * @throws InvalidValue
381
     * @throws \Exception
382
     * @throws \Swaggest\JsonDiff\Exception
383
     */
384 99
    private function processOneOf($data, Context $options, $path)
385
    {
386 99
        $successes = 0;
387 99
        $failures = '';
388 99
        $skipValidation = false;
389 99
        if ($options->skipValidation) {
390 41
            $skipValidation = true;
391 41
            $options->skipValidation = false;
392
        }
393
394 99
        $result = $data;
395 99
        foreach ($this->oneOf as $index => $item) {
396
            try {
397 99
                $result = self::unboolSchema($item)->process($data, $options, $path . '->oneOf:' . $index);
398 78
                $successes++;
399 78
                if ($successes > 1 || $options->skipValidation) {
400 78
                    break;
401
                }
402 67
            } catch (InvalidValue $exception) {
403 99
                $failures .= ' ' . $index . ': ' . Helper::padLines(' ', $exception->getMessage()) . "\n";
404
                // Expected exception
405
            }
406
        }
407 99
        if ($skipValidation) {
408 41
            $options->skipValidation = true;
409 41
            if ($successes === 0) {
410 8
                $result = self::unboolSchema($this->oneOf[0])->process($data, $options, $path . '->oneOf:' . 0);
411
            }
412
        }
413
414 99
        if (!$options->skipValidation) {
415 58
            if ($successes === 0) {
416 13
                $this->fail(new LogicException('Failed due to logical constraint: no valid results for oneOf {' . "\n" . substr($failures, 0, -1) . "\n}"), $path);
417 45
            } elseif ($successes > 1) {
418 16
                $this->fail(new LogicException('Failed due to logical constraint: '
419 16
                    . $successes . '/' . count($this->oneOf) . ' valid results for oneOf'), $path);
420
            }
421
        }
422 70
        return $result;
423
    }
424
425
    /**
426
     * @param mixed $data
427
     * @param Context $options
428
     * @param string $path
429
     * @return array|mixed|null|object|\stdClass
430
     * @throws InvalidValue
431
     * @throws \Exception
432
     * @throws \Swaggest\JsonDiff\Exception
433
     */
434 1702
    private function processAnyOf($data, Context $options, $path)
435
    {
436 1702
        $successes = 0;
437 1702
        $failures = '';
438 1702
        $result = $data;
439 1702
        foreach ($this->anyOf as $index => $item) {
440
            try {
441 1702
                $result = self::unboolSchema($item)->process($data, $options, $path . '->anyOf:' . $index);
442 1696
                $successes++;
443 1696
                if ($successes) {
444 1696
                    break;
445
                }
446 428
            } catch (InvalidValue $exception) {
447 428
                $failures .= ' ' . $index . ': ' . $exception->getMessage() . "\n";
448
                // Expected exception
449
            }
450
        }
451 1702
        if (!$successes && !$options->skipValidation) {
452 28
            $this->fail(new LogicException('Failed due to logical constraint: no valid results for anyOf {' . "\n" . substr(Helper::padLines(' ', $failures), 0, -1) . "\n}"), $path);
453
        }
454 1696
        return $result;
455
    }
456
457
    /**
458
     * @param mixed $data
459
     * @param Context $options
460
     * @param string $path
461
     * @return array|mixed|null|object|\stdClass
462
     * @throws InvalidValue
463
     * @throws \Exception
464
     * @throws \Swaggest\JsonDiff\Exception
465
     */
466 351
    private function processAllOf($data, Context $options, $path)
467
    {
468 351
        $result = $data;
469 351
        foreach ($this->allOf as $index => $item) {
470 351
            $result = self::unboolSchema($item)->process($data, $options, $path . '->allOf' . $index);
471
        }
472 313
        return $result;
473
    }
474
475
    /**
476
     * @param mixed $data
477
     * @param Context $options
478
     * @param string $path
479
     * @return array|mixed|null|object|\stdClass
480
     * @throws InvalidValue
481
     * @throws \Exception
482
     * @throws \Swaggest\JsonDiff\Exception
483
     */
484 26
    private function processIf($data, Context $options, $path)
485
    {
486 26
        $valid = true;
487
        try {
488 26
            self::unboolSchema($this->if)->process($data, $options, $path . '->if');
489 13
        } catch (InvalidValue $exception) {
490 13
            $valid = false;
491
        }
492 26
        if ($valid) {
493 18
            if ($this->then !== null) {
494 18
                return self::unboolSchema($this->then)->process($data, $options, $path . '->then');
495
            }
496
        } else {
497 13
            if ($this->else !== null) {
498 6
                return self::unboolSchema($this->else)->process($data, $options, $path . '->else');
499
            }
500
        }
501 10
        return null;
502
    }
503
504
    /**
505
     * @param \stdClass $data
506
     * @param Context $options
507
     * @param string $path
508
     * @throws InvalidValue
509
     */
510 159
    private function processObjectRequired($data, Context $options, $path)
511
    {
512 159
        if (isset($this->__dataToProperty[$options->mapping])) {
513 2
            if ($options->import) {
514 1
                foreach ($this->required as $item) {
515 1
                    if (isset($this->__propertyToData[$options->mapping][$item])) {
516 1
                        $item = $this->__propertyToData[$options->mapping][$item];
517
                    }
518 1
                    if (!property_exists($data, $item)) {
519 1
                        $this->fail(new ObjectException('Required property missing: ' . $item, ObjectException::REQUIRED), $path);
520
                    }
521
                }
522
            } else {
523 2
                foreach ($this->required as $item) {
524 2
                    if (isset($this->__dataToProperty[$options->mapping][$item])) {
525
                        $item = $this->__dataToProperty[$options->mapping][$item];
526
                    }
527 2
                    if (!property_exists($data, $item)) {
528 2
                        $this->fail(new ObjectException('Required property missing: ' . $item, ObjectException::REQUIRED), $path);
529
                    }
530
                }
531
            }
532
533
        } else {
534 158
            foreach ($this->required as $item) {
535 154
                if (!property_exists($data, $item)) {
536 154
                    $this->fail(new ObjectException('Required property missing: ' . $item, ObjectException::REQUIRED), $path);
537
                }
538
            }
539
        }
540 127
    }
541
542
    /**
543
     * @param \stdClass $data
544
     * @param Context $options
545
     * @param string $path
546
     * @param ObjectItemContract|null $result
547
     * @return array|null|ClassStructure|ObjectItemContract
548
     * @throws InvalidValue
549
     * @throws \Exception
550
     * @throws \Swaggest\JsonDiff\Exception
551
     */
552 3132
    private function processObject($data, Context $options, $path, $result = null)
553
    {
554 3132
        $import = $options->import;
555
556 3132
        if (!$options->skipValidation && $this->required !== null) {
557 159
            $this->processObjectRequired($data, $options, $path);
558
        }
559
560 3131
        if ($import) {
561 3119
            if (!$options->validateOnly) {
562
563 3119
                if ($this->useObjectAsArray) {
564 1
                    $result = array();
565 3118
                } elseif (!$result instanceof ObjectItemContract) {
566
                    //* todo check performance impact
567 3118
                    if (null === $this->objectItemClass) {
568 1148
                        $result = new ObjectItem();
569
                    } else {
570 3107
                        $className = $this->objectItemClass;
571 3107
                        if ($options->objectItemClassMapping !== null) {
572
                            if (isset($options->objectItemClassMapping[$className])) {
573
                                $className = $options->objectItemClassMapping[$className];
574
                            }
575
                        }
576 3107
                        $result = new $className;
577
                    }
578
                    //*/
579
580
581 3118
                    if ($result instanceof ClassStructure) {
582 3106
                        if ($result->__validateOnSet) {
0 ignored issues
show
Bug Best Practice introduced by
The property __validateOnSet does not exist on Swaggest\JsonSchema\Structure\ObjectItem. Since you implemented __get, consider adding a @property annotation.
Loading history...
583 3106
                            $result->__validateOnSet = false;
0 ignored issues
show
Bug Best Practice introduced by
The property __validateOnSet does not exist on Swaggest\JsonSchema\Structure\ObjectItem. Since you implemented __set, consider adding a @property annotation.
Loading history...
584
                            /** @noinspection PhpUnusedLocalVariableInspection */
585
                            /* todo check performance impact
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% 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...
586
                            $validateOnSetHandler = new ScopeExit(function () use ($result) {
587
                                $result->__validateOnSet = true;
588
                            });
589
                            //*/
590
                        }
591
                    }
592
593
                    //* todo check performance impact
594 3118
                    if ($result instanceof ObjectItemContract) {
595 3118
                        $result->setDocumentPath($path);
596
                    }
597
                    //*/
598
                }
599
            }
600
        }
601
602
        // @todo better check for schema id
603
604 3131
        if ($import
605 3131
            && isset($data->{Schema::PROP_ID_D4})
606 3131
            && ($options->version === Schema::VERSION_DRAFT_04 || $options->version === Schema::VERSION_AUTO)
607 3131
            && is_string($data->{Schema::PROP_ID_D4})) {
608 29
            $id = $data->{Schema::PROP_ID_D4};
609 29
            $refResolver = $options->refResolver;
610 29
            $parentScope = $refResolver->updateResolutionScope($id);
611
            /** @noinspection PhpUnusedLocalVariableInspection */
612
            $defer = new ScopeExit(function () use ($parentScope, $refResolver) {
0 ignored issues
show
Unused Code introduced by
The assignment to $defer is dead and can be removed.
Loading history...
613 29
                $refResolver->setResolutionScope($parentScope);
614 29
            });
615
        }
616
617 3131
        if ($import
618 3131
            && isset($data->{self::PROP_ID})
619 3131
            && ($options->version >= Schema::VERSION_DRAFT_06 || $options->version === Schema::VERSION_AUTO)
620 3131
            && is_string($data->{self::PROP_ID})) {
621 114
            $id = $data->{self::PROP_ID};
622 114
            $refResolver = $options->refResolver;
623 114
            $parentScope = $refResolver->updateResolutionScope($id);
624
            /** @noinspection PhpUnusedLocalVariableInspection */
625
            $defer = new ScopeExit(function () use ($parentScope, $refResolver) {
626 114
                $refResolver->setResolutionScope($parentScope);
627 114
            });
628
        }
629
630 3131
        if ($import) {
631
            try {
632
                while (
633 3119
                    isset($data->{self::PROP_REF})
634 3119
                    && is_string($data->{self::PROP_REF})
635 3119
                    && !isset($this->properties[self::PROP_REF])
636
                ) {
637 407
                    $refString = $data->{self::PROP_REF};
638
639
                    // todo check performance impact
640 407
                    if ($refString === 'http://json-schema.org/draft-04/schema#'
641 397
                        || $refString === 'http://json-schema.org/draft-06/schema#'
642 407
                        || $refString === 'http://json-schema.org/draft-07/schema#') {
643 26
                        return Schema::schema();
644
                    }
645
646
                    // TODO consider process # by reference here ?
647 381
                    $refResolver = $options->refResolver;
648 381
                    $preRefScope = $refResolver->getResolutionScope();
649
                    /** @noinspection PhpUnusedLocalVariableInspection */
650 381
                    $deferRefScope = new ScopeExit(function () use ($preRefScope, $refResolver) {
0 ignored issues
show
Unused Code introduced by
The assignment to $deferRefScope is dead and can be removed.
Loading history...
651 381
                        $refResolver->setResolutionScope($preRefScope);
652 381
                    });
653
654 381
                    $ref = $refResolver->resolveReference($refString);
655 381
                    $data = self::unboolSchemaData($ref->getData());
656 381
                    if (!$options->validateOnly) {
657 381
                        if ($ref->isImported()) {
658 172
                            $refResult = $ref->getImported();
659 172
                            return $refResult;
660
                        }
661 381
                        if ($result instanceof ObjectItemContract) {
662 381
                            $result->setFromRef($refString);
663
                        }
664 381
                        $ref->setImported($result);
665 381
                        $refResult = $this->process($data, $options, $path . '->ref:' . $refString, $result);
666 381
                        $ref->setImported($refResult);
667 381
                        return $refResult;
668
                    } else {
669
                        $this->process($data, $options, $path . '->ref:' . $refString);
670
                    }
671
                }
672
            } catch (InvalidValue $exception) {
673
                $this->fail($exception, $path);
674
            }
675
        }
676
677
        /** @var Schema[]|null $properties */
678 3131
        $properties = null;
679
680 3131
        $nestedProperties = null;
681 3131
        if ($this->properties !== null) {
682 3117
            $properties = $this->properties->toArray(); // todo call directly
683 3117
            if ($this->properties instanceof Properties) {
684 3117
                $nestedProperties = $this->properties->nestedProperties;
0 ignored issues
show
Bug Best Practice introduced by
The property nestedProperties does not exist on Swaggest\JsonSchema\Schema. Since you implemented __get, consider adding a @property annotation.
Loading history...
685
            } else {
686 572
                $nestedProperties = array();
687
            }
688
        }
689
690 3131
        $array = array();
691 3131
        if (!empty($this->__dataToProperty[$options->mapping])) { // todo skip on $options->validateOnly
692 3099
            foreach ((array)$data as $key => $value) {
693 3095
                if ($import) {
694 3094
                    if (isset($this->__dataToProperty[$options->mapping][$key])) {
695 3094
                        $key = $this->__dataToProperty[$options->mapping][$key];
696
                    }
697
                } else {
698 20
                    if (isset($this->__propertyToData[$options->mapping][$key])) {
699 1
                        $key = $this->__propertyToData[$options->mapping][$key];
700
                    }
701
                }
702 3099
                $array[$key] = $value;
703
            }
704
        } else {
705 1225
            $array = (array)$data;
706
        }
707
708 3131
        if (!$options->skipValidation) {
709 3110
            if ($this->minProperties !== null && count($array) < $this->minProperties) {
710 4
                $this->fail(new ObjectException("Not enough properties", ObjectException::TOO_FEW), $path);
711
            }
712 3110
            if ($this->maxProperties !== null && count($array) > $this->maxProperties) {
713 3
                $this->fail(new ObjectException("Too many properties", ObjectException::TOO_MANY), $path);
714
            }
715 3110
            if ($this->propertyNames !== null) {
716 17
                $propertyNames = self::unboolSchema($this->propertyNames);
717 17
                foreach ($array as $key => $tmp) {
718 10
                    $propertyNames->process($key, $options, $path . '->propertyNames:' . $key);
0 ignored issues
show
introduced by
The method process() does not exist on Swaggest\JsonSchema\JsonSchema. Maybe you want to declare this class abstract? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

718
                    $propertyNames->/** @scrutinizer ignore-call */ 
719
                                    process($key, $options, $path . '->propertyNames:' . $key);
Loading history...
719
                }
720
            }
721
        }
722
723 3131
        $defaultApplied = array();
724 3131
        if ($import
725 3131
            && !$options->validateOnly
726 3131
            && $options->applyDefaults
727 3131
            && $properties !== null
728
        ) {
729 32
            foreach ($properties as $key => $property) {
730
                // todo check when property is \stdClass `{}` here (RefTest)
731 30
                if ($property instanceof SchemaContract && null !== $default = $property->getDefault()) {
732 6
                    if (isset($this->__dataToProperty[$options->mapping][$key])) {
733
                        $key = $this->__dataToProperty[$options->mapping][$key];
734
                    }
735 6
                    if (!array_key_exists($key, $array)) {
736 6
                        $defaultApplied[$key] = true;
737 30
                        $array[$key] = $default;
738
                    }
739
                }
740
            }
741
        }
742
743 3131
        foreach ($array as $key => $value) {
744 3121
            if ($key === '' && PHP_VERSION_ID < 71000) {
745 1
                $this->fail(new InvalidValue('Empty property name'), $path);
746
            }
747
748 3120
            $found = false;
749
750 3120
            if (!$options->skipValidation && !empty($this->dependencies)) {
751 73
                $deps = $this->dependencies;
752 73
                if (isset($deps->$key)) {
753 63
                    $dependencies = $deps->$key;
754 63
                    $dependencies = self::unboolSchema($dependencies);
755 63
                    if ($dependencies instanceof SchemaContract) {
756 29
                        $dependencies->process($data, $options, $path . '->dependencies:' . $key);
757
                    } else {
758 34
                        foreach ($dependencies as $item) {
759 31
                            if (!property_exists($data, $item)) {
760 18
                                $this->fail(new ObjectException('Dependency property missing: ' . $item,
761 31
                                    ObjectException::DEPENDENCY_MISSING), $path);
762
                            }
763
                        }
764
                    }
765
                }
766
            }
767
768 3120
            $propertyFound = false;
769 3120
            if (isset($properties[$key])) {
770
                /** @var Schema[] $properties */
771 3110
                $prop = self::unboolSchema($properties[$key]);
772 3110
                $propertyFound = true;
773 3110
                $found = true;
774 3110
                if ($prop instanceof SchemaContract) {
775 3053
                    $value = $prop->process(
776 3053
                        $value,
777 3053
                        isset($defaultApplied[$key]) ? $options->withDefault() : $options,
778 3053
                        $path . '->properties:' . $key
779
                    );
780
                }
781
            }
782
783
            /** @var Egg[] $nestedEggs */
784 3115
            $nestedEggs = null;
785 3115
            if (isset($nestedProperties[$key])) {
786 6
                $found = true;
787 6
                $nestedEggs = $nestedProperties[$key];
788
                // todo iterate all nested props?
789 6
                $value = self::unboolSchema($nestedEggs[0]->propertySchema)->process($value, $options, $path . '->nestedProperties:' . $key);
790
            }
791
792 3115
            if ($this->patternProperties !== null) {
793 164
                foreach ($this->patternProperties as $pattern => $propertySchema) {
794 164
                    if (preg_match(Helper::toPregPattern($pattern), $key)) {
795 118
                        $found = true;
796 118
                        $value = self::unboolSchema($propertySchema)->process($value, $options,
797 118
                            $path . '->patternProperties[' . $pattern . ']:' . $key);
798 92
                        if (!$options->validateOnly && $import) {
799 143
                            $result->addPatternPropertyName($pattern, $key);
0 ignored issues
show
Bug introduced by
The method addPatternPropertyName() does not exist on Swaggest\JsonSchema\Structure\ObjectItemContract. It seems like you code against a sub-type of said class. However, the method does not exist in Swaggest\JsonSchema\Stru...\ClassStructureContract. Are you sure you never get one of those? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

799
                            $result->/** @scrutinizer ignore-call */ 
800
                                     addPatternPropertyName($pattern, $key);
Loading history...
800
                        }
801
                        //break; // todo manage multiple import data properly (pattern accessor)
802
                    }
803
                }
804
            }
805 3115
            if (!$found && $this->additionalProperties !== null) {
806 986
                if (!$options->skipValidation && $this->additionalProperties === false) {
807 11
                    $this->fail(new ObjectException('Additional properties not allowed'), $path . ':' . $key);
808
                }
809
810 986
                if ($this->additionalProperties instanceof SchemaContract) {
811 986
                    $value = $this->additionalProperties->process($value, $options, $path . '->additionalProperties:' . $key);
812
                }
813
814 979
                if ($import && !$this->useObjectAsArray && !$options->validateOnly) {
815 978
                    $result->addAdditionalPropertyName($key);
816
                }
817
            }
818
819 3111
            if (!$options->validateOnly && $nestedEggs && $import) {
820 5
                foreach ($nestedEggs as $nestedEgg) {
821 5
                    $result->setNestedProperty($key, $value, $nestedEgg);
822
                }
823 5
                if ($propertyFound) {
824 5
                    $result->$key = $value;
825
                }
826
            } else {
827 3110
                if ($this->useObjectAsArray && $import) {
828 1
                    $result[$key] = $value;
829
                } else {
830 3110
                    if ($found || !$import) {
831 3109
                        $result->$key = $value;
832 1132
                    } elseif (!isset($result->$key)) {
833 3111
                        $result->$key = $value;
834
                    }
835
                }
836
            }
837
        }
838
839 3119
        return $result;
840
    }
841
842
    /**
843
     * @param array $data
844
     * @param Context $options
845
     * @param string $path
846
     * @param array $result
847
     * @return mixed
848
     * @throws InvalidValue
849
     * @throws \Exception
850
     * @throws \Swaggest\JsonDiff\Exception
851
     */
852 1200
    private function processArray($data, Context $options, $path, $result)
853
    {
854 1200
        $count = count($data);
855 1200
        if (!$options->skipValidation) {
856 1000
            if ($this->minItems !== null && $count < $this->minItems) {
857 9
                $this->fail(new ArrayException("Not enough items in array"), $path);
858
            }
859
860 994
            if ($this->maxItems !== null && $count > $this->maxItems) {
861 6
                $this->fail(new ArrayException("Too many items in array"), $path);
862
            }
863
        }
864
865 1188
        $pathItems = 'items';
866 1188
        $this->items = self::unboolSchema($this->items);
867 1188
        if ($this->items instanceof SchemaContract) {
868 842
            $items = array();
869 842
            $additionalItems = $this->items;
870 681
        } elseif ($this->items === null) { // items defaults to empty schema so everything is valid
0 ignored issues
show
introduced by
The condition $this->items === null can never be true.
Loading history...
871 681
            $items = array();
872 681
            $additionalItems = true;
873
        } else { // listed items
874 104
            $items = $this->items;
875 104
            $additionalItems = $this->additionalItems;
876 104
            $pathItems = 'additionalItems';
877
        }
878
879
        /**
880
         * @var Schema|Schema[] $items
881
         * @var null|bool|Schema $additionalItems
882
         */
883 1188
        $itemsLen = is_array($items) ? count($items) : 0;
884 1188
        $index = 0;
885 1188
        foreach ($result as $key => $value) {
886 1063
            if ($index < $itemsLen) {
887 94
                $itemSchema = self::unboolSchema($items[$index]);
888 94
                $result[$key] = $itemSchema->process($value, $options, $path . '->items:' . $index);
889
            } else {
890 1063
                if ($additionalItems instanceof SchemaContract) {
891 816
                    $result[$key] = $additionalItems->process($value, $options, $path . '->' . $pathItems
892 816
                        . '[' . $index . ']');
893 565
                } elseif (!$options->skipValidation && $additionalItems === false) {
894 6
                    $this->fail(new ArrayException('Unexpected array item'), $path);
895
                }
896
            }
897 1043
            ++$index;
898
        }
899
900 1164
        if (!$options->skipValidation && $this->uniqueItems) {
901 498
            if (!UniqueItems::isValid($data)) {
902 19
                $this->fail(new ArrayException('Array is not unique'), $path);
903
            }
904
        }
905
906 1145
        if (!$options->skipValidation && $this->contains !== null) {
907
            /** @var Schema|bool $contains */
908 36
            $contains = $this->contains;
909 36
            if ($contains === false) {
910 4
                $this->fail(new ArrayException('Contains is false'), $path);
911
            }
912 32
            if ($count === 0) {
913 7
                $this->fail(new ArrayException('Empty array fails contains constraint'), $path);
914
            }
915 25
            if ($contains === true) {
916 2
                $contains = self::unboolSchema($contains);
917
            }
918 25
            $containsOk = false;
919 25
            foreach ($data as $key => $item) {
920
                try {
921 25
                    $contains->process($item, $options, $path . '->' . $key);
922 17
                    $containsOk = true;
923 17
                    break;
924 21
                } catch (InvalidValue $exception) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
925
                }
926
            }
927 25
            if (!$containsOk) {
928 8
                $this->fail(new ArrayException('Array fails contains constraint'), $path);
929
            }
930
        }
931 1126
        return $result;
932
    }
933
934
    /**
935
     * @param mixed|string $data
936
     * @param Context $options
937
     * @param string $path
938
     * @return bool|mixed|string
939
     * @throws InvalidValue
940
     */
941 14
    private function processContent($data, Context $options, $path)
942
    {
943
        try {
944 14
            if ($options->unpackContentMediaType) {
945 7
                return Content::process($options, $this->contentEncoding, $this->contentMediaType, $data, $options->import);
946
            } else {
947 7
                Content::process($options, $this->contentEncoding, $this->contentMediaType, $data, true);
948
            }
949 4
        } catch (InvalidValue $exception) {
950 4
            $this->fail($exception, $path);
951
        }
952 7
        return $data;
953
    }
954
955
    /**
956
     * @param mixed $data
957
     * @param Context $options
958
     * @param string $path
959
     * @param mixed|null $result
960
     * @return array|mixed|null|object|\stdClass
961
     * @throws InvalidValue
962
     * @throws \Exception
963
     * @throws \Swaggest\JsonDiff\Exception
964
     */
965 3178
    public function process($data, Context $options, $path = '#', $result = null)
966
    {
967
968 3178
        $import = $options->import;
969
970 3178
        if (!$import && $data instanceof ObjectItemContract) {
971 779
            $result = new \stdClass();
972 779
            if ($options->circularReferences->contains($data)) {
973
                /** @noinspection PhpIllegalArrayKeyTypeInspection */
974 1
                $path = $options->circularReferences[$data];
975
                // @todo $path is not a valid json pointer $ref
976 1
                $result->{self::PROP_REF} = $path;
977 1
                return $result;
978
            }
979 779
            $options->circularReferences->attach($data, $path);
980
981 779
            $data = $data->jsonSerialize();
982
        }
983 3178
        if (!$import && is_array($data) && $this->useObjectAsArray) {
984 1
            $data = (object)$data;
985
        }
986
987 3178
        if (null !== $options->dataPreProcessor) {
988
            $data = $options->dataPreProcessor->process($data, $this, $import);
989
        }
990
991 3178
        if ($result === null) {
992 3178
            $result = $data;
993
        }
994
995 3178
        if ($options->skipValidation) {
996 1416
            goto skipValidation;
997
        }
998
999 3140
        if ($this->type !== null) {
0 ignored issues
show
introduced by
The condition $this->type !== null can never be false.
Loading history...
1000 3119
            $this->processType($data, $options, $path);
1001
        }
1002
1003 3139
        if ($this->enum !== null) {
0 ignored issues
show
introduced by
The condition $this->enum !== null can never be false.
Loading history...
1004 1457
            $this->processEnum($data, $path);
1005
        }
1006
1007 3139
        if (array_key_exists(self::CONST_PROPERTY, $this->__arrayOfData)) {
1008 43
            $this->processConst($data, $path);
1009
        }
1010
1011 3139
        if ($this->not !== null) {
1012 68
            $this->processNot($data, $options, $path);
1013
        }
1014
1015 3139
        if (is_string($data)) {
1016 2227
            $this->processString($data, $path);
1017
        }
1018
1019 3139
        if (is_int($data) || is_float($data)) {
1020 1040
            $this->processNumeric($data, $path);
1021
        }
1022
1023 3139
        if ($this->if !== null) {
1024 26
            $result = $this->processIf($data, $options, $path);
1025
        }
1026
1027
        skipValidation:
1028
1029 3177
        if ($this->oneOf !== null) {
0 ignored issues
show
introduced by
The condition $this->oneOf !== null can never be false.
Loading history...
1030 99
            $result = $this->processOneOf($data, $options, $path);
1031
        }
1032
1033 3177
        if ($this->anyOf !== null) {
0 ignored issues
show
introduced by
The condition $this->anyOf !== null can never be false.
Loading history...
1034 1702
            $result = $this->processAnyOf($data, $options, $path);
1035
        }
1036
1037 3177
        if ($this->allOf !== null) {
0 ignored issues
show
introduced by
The condition $this->allOf !== null can never be false.
Loading history...
1038 351
            $result = $this->processAllOf($data, $options, $path);
1039
        }
1040
1041 3177
        if ($data instanceof \stdClass) {
1042 3132
            $result = $this->processObject($data, $options, $path, $result);
0 ignored issues
show
Bug introduced by
It seems like $result can also be of type mixed and integer and double; however, parameter $result of Swaggest\JsonSchema\Schema::processObject() does only seem to accept null|Swaggest\JsonSchema...ture\ObjectItemContract, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1042
            $result = $this->processObject($data, $options, $path, /** @scrutinizer ignore-type */ $result);
Loading history...
1043
        }
1044
1045 3174
        if (is_array($data)) {
1046 1200
            $result = $this->processArray($data, $options, $path, $result);
1047
        }
1048
1049 3174
        if ($this->contentEncoding !== null || $this->contentMediaType !== null) {
0 ignored issues
show
introduced by
The condition $this->contentEncoding !...ntentMediaType !== null can never be false.
Loading history...
1050 14
            $result = $this->processContent($data, $options, $path);
1051
        }
1052
1053 3174
        return $result;
1054
    }
1055
1056
    /**
1057
     * @param boolean $useObjectAsArray
1058
     * @return Schema
1059
     */
1060 1
    public function setUseObjectAsArray($useObjectAsArray)
1061
    {
1062 1
        $this->useObjectAsArray = $useObjectAsArray;
1063 1
        return $this;
1064
    }
1065
1066
    /**
1067
     * @param InvalidValue $exception
1068
     * @param string $path
1069
     * @throws InvalidValue
1070
     */
1071 1211
    private function fail(InvalidValue $exception, $path)
1072
    {
1073 1211
        if ($path !== '#') {
1074 729
            $exception->addPath($path);
1075
        }
1076 1211
        throw $exception;
1077
    }
1078
1079 13
    public static function integer()
1080
    {
1081 13
        $schema = new static();
1082 13
        $schema->type = Type::INTEGER;
1083 13
        return $schema;
1084
    }
1085
1086 4
    public static function number()
1087
    {
1088 4
        $schema = new static();
1089 4
        $schema->type = Type::NUMBER;
1090 4
        return $schema;
1091
    }
1092
1093 8
    public static function string()
1094
    {
1095 8
        $schema = new static();
1096 8
        $schema->type = Type::STRING;
1097 8
        return $schema;
1098
    }
1099
1100 4
    public static function boolean()
1101
    {
1102 4
        $schema = new static();
1103 4
        $schema->type = Type::BOOLEAN;
1104 4
        return $schema;
1105
    }
1106
1107 6
    public static function object()
1108
    {
1109 6
        $schema = new static();
1110 6
        $schema->type = Type::OBJECT;
1111 6
        return $schema;
1112
    }
1113
1114 1
    public static function arr()
1115
    {
1116 1
        $schema = new static();
1117 1
        $schema->type = Type::ARR;
1118 1
        return $schema;
1119
    }
1120
1121
    public static function null()
1122
    {
1123
        $schema = new static();
1124
        $schema->type = Type::NULL;
1125
        return $schema;
1126
    }
1127
1128
1129
    /**
1130
     * @param Properties $properties
1131
     * @return Schema
1132
     */
1133 3
    public function setProperties($properties)
1134
    {
1135 3
        $this->properties = $properties;
1136 3
        return $this;
1137
    }
1138
1139
    /**
1140
     * @param string $name
1141
     * @param Schema $schema
1142
     * @return $this
1143
     */
1144 3
    public function setProperty($name, $schema)
1145
    {
1146 3
        if (null === $this->properties) {
1147 3
            $this->properties = new Properties();
1148
        }
1149 3
        $this->properties->__set($name, $schema);
1150 3
        return $this;
1151
    }
1152
1153
    /** @var Meta[] */
1154
    private $metaItems = array();
1155
1156 1
    public function addMeta(Meta $meta)
1157
    {
1158 1
        $this->metaItems[get_class($meta)] = $meta;
1159 1
        return $this;
1160
    }
1161
1162 1
    public function getMeta($className)
1163
    {
1164 1
        if (isset($this->metaItems[$className])) {
1165 1
            return $this->metaItems[$className];
1166
        }
1167
        return null;
1168
    }
1169
1170
    /**
1171
     * @param Context $options
1172
     * @return ObjectItemContract
1173
     */
1174 5
    public function makeObjectItem(Context $options = null)
1175
    {
1176 5
        if (null === $this->objectItemClass) {
1177
            return new ObjectItem();
1178
        } else {
1179 5
            $className = $this->objectItemClass;
1180 5
            if ($options !== null) {
1181
                if (isset($options->objectItemClassMapping[$className])) {
1182
                    $className = $options->objectItemClassMapping[$className];
1183
                }
1184
            }
1185 5
            return new $className;
1186
        }
1187
    }
1188
1189
    /**
1190
     * @param mixed $schema
1191
     * @return mixed|Schema
1192
     */
1193 3190
    private static function unboolSchema($schema)
1194
    {
1195 3190
        static $trueSchema;
1196 3190
        static $falseSchema;
1197
1198 3190
        if (null === $trueSchema) {
1199 1
            $trueSchema = new Schema();
1200 1
            $trueSchema->__booleanSchema = true;
1201 1
            $falseSchema = new Schema();
1202 1
            $falseSchema->not = $trueSchema;
1203 1
            $falseSchema->__booleanSchema = false;
1204
        }
1205
1206 3190
        if ($schema === true) {
1207 108
            return $trueSchema;
1208 3162
        } elseif ($schema === false) {
1209 94
            return $falseSchema;
1210
        } else {
1211 3130
            return $schema;
1212
        }
1213
    }
1214
1215
    /**
1216
     * @param mixed $data
1217
     * @return \stdClass
1218
     */
1219 381
    private static function unboolSchemaData($data)
1220
    {
1221 381
        static $trueSchema;
1222 381
        static $falseSchema;
1223
1224 381
        if (null === $trueSchema) {
1225 1
            $trueSchema = new \stdClass();
1226 1
            $falseSchema = new \stdClass();
1227 1
            $falseSchema->not = $trueSchema;
1228
        }
1229
1230 381
        if ($data === true) {
1231 6
            return $trueSchema;
1232 377
        } elseif ($data === false) {
1233 5
            return $falseSchema;
1234
        } else {
1235 372
            return $data;
1236
        }
1237
    }
1238
1239 30
    public function getDefault()
1240
    {
1241 30
        return $this->default;
1242
    }
1243
1244 33
    public function getProperties()
1245
    {
1246 33
        return $this->properties;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->properties also could return the type Swaggest\JsonSchema\Schema which is incompatible with the return type mandated by Swaggest\JsonSchema\Sche...ntract::getProperties() of null|Swaggest\JsonSchema...a\Constraint\Properties.
Loading history...
1247
    }
1248
1249
    public function getObjectItemClass()
1250
    {
1251
        return $this->objectItemClass;
1252
    }
1253
}
1254