Passed
Pull Request — master (#24)
by Viacheslav
03:13
created

Schema::processNumeric()   F

Complexity

Conditions 17
Paths 675

Size

Total Lines 53
Code Lines 36

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 35
CRAP Score 17

Importance

Changes 0
Metric Value
dl 0
loc 53
c 0
b 0
f 0
ccs 35
cts 35
cp 1
rs 3.4603
cc 17
eloc 36
nc 675
nop 2
crap 17

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

722
                    $propertyNames->/** @scrutinizer ignore-call */ 
723
                                    process($key, $options, $path . '->propertyNames:' . $key);
Loading history...
723
                }
724
            }
725
        }
726
727 3132
        $defaultApplied = array();
728 3132
        if ($import
729 3132
            && !$options->validateOnly
730 3132
            && $options->applyDefaults
731 3132
            && $properties !== null
732
        ) {
733 32
            foreach ($properties as $key => $property) {
734
                // todo check when property is \stdClass `{}` here (RefTest)
735 30
                if ($property instanceof SchemaContract && null !== $default = $property->getDefault()) {
736 6
                    if (isset($this->__dataToProperty[$options->mapping][$key])) {
737
                        $key = $this->__dataToProperty[$options->mapping][$key];
738
                    }
739 6
                    if (!array_key_exists($key, $array)) {
740 6
                        $defaultApplied[$key] = true;
741 30
                        $array[$key] = $default;
742
                    }
743
                }
744
            }
745
        }
746
747 3132
        foreach ($array as $key => $value) {
748 3122
            if ($key === '' && PHP_VERSION_ID < 71000) {
749 1
                $this->fail(new InvalidValue('Empty property name'), $path);
750
            }
751
752 3121
            $found = false;
753
754 3121
            if (!$options->skipValidation && !empty($this->dependencies)) {
755 73
                $deps = $this->dependencies;
756 73
                if (isset($deps->$key)) {
757 63
                    $dependencies = $deps->$key;
758 63
                    $dependencies = self::unboolSchema($dependencies);
759 63
                    if ($dependencies instanceof SchemaContract) {
760 29
                        $dependencies->process($data, $options, $path . '->dependencies:' . $key);
761
                    } else {
762 34
                        foreach ($dependencies as $item) {
763 31
                            if (!property_exists($data, $item)) {
764 18
                                $this->fail(new ObjectException('Dependency property missing: ' . $item,
765 31
                                    ObjectException::DEPENDENCY_MISSING), $path);
766
                            }
767
                        }
768
                    }
769
                }
770
            }
771
772 3121
            $propertyFound = false;
773 3121
            if (isset($properties[$key])) {
774
                /** @var Schema[] $properties */
775 3111
                $prop = self::unboolSchema($properties[$key]);
776 3111
                $propertyFound = true;
777 3111
                $found = true;
778 3111
                if ($prop instanceof SchemaContract) {
779 3054
                    $value = $prop->process(
780 3054
                        $value,
781 3054
                        isset($defaultApplied[$key]) ? $options->withDefault() : $options,
782 3054
                        $path . '->properties:' . $key
783
                    );
784
                }
785
            }
786
787
            /** @var Egg[] $nestedEggs */
788 3116
            $nestedEggs = null;
789 3116
            if (isset($nestedProperties[$key])) {
790 6
                $found = true;
791 6
                $nestedEggs = $nestedProperties[$key];
792
                // todo iterate all nested props?
793 6
                $value = self::unboolSchema($nestedEggs[0]->propertySchema)->process($value, $options, $path . '->nestedProperties:' . $key);
794
            }
795
796 3116
            if ($this->patternProperties !== null) {
797 165
                foreach ($this->patternProperties as $pattern => $propertySchema) {
798 165
                    if (preg_match(Helper::toPregPattern($pattern), $key)) {
799 119
                        $found = true;
800 119
                        $value = self::unboolSchema($propertySchema)->process($value, $options,
801 119
                            $path . '->patternProperties[' . $pattern . ']:' . $key);
802 93
                        if (!$options->validateOnly && $import) {
803 144
                            $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

803
                            $result->/** @scrutinizer ignore-call */ 
804
                                     addPatternPropertyName($pattern, $key);
Loading history...
804
                        }
805
                        //break; // todo manage multiple import data properly (pattern accessor)
806
                    }
807
                }
808
            }
809 3116
            if (!$found && $this->additionalProperties !== null) {
810 987
                if (!$options->skipValidation && $this->additionalProperties === false) {
811 12
                    $this->fail(new ObjectException('Additional properties not allowed'), $path . ':' . $key);
812
                }
813
814 987
                if ($this->additionalProperties instanceof SchemaContract) {
815 987
                    $value = $this->additionalProperties->process($value, $options, $path . '->additionalProperties:' . $key);
816
                }
817
818 980
                if ($import && !$this->useObjectAsArray && !$options->validateOnly) {
819 979
                    $result->addAdditionalPropertyName($key);
820
                }
821
            }
822
823 3112
            if (!$options->validateOnly && $nestedEggs && $import) {
824 5
                foreach ($nestedEggs as $nestedEgg) {
825 5
                    $result->setNestedProperty($key, $value, $nestedEgg);
826
                }
827 5
                if ($propertyFound) {
828 5
                    $result->$key = $value;
829
                }
830
            } else {
831 3111
                if ($this->useObjectAsArray && $import) {
832 1
                    $result[$key] = $value;
833
                } else {
834 3111
                    if ($found || !$import) {
835 3110
                        $result->$key = $value;
836 1133
                    } elseif (!isset($result->$key)) {
837 3112
                        $result->$key = $value;
838
                    }
839
                }
840
            }
841
        }
842
843 3120
        return $result;
844
    }
845
846
    /**
847
     * @param array $data
848
     * @param Context $options
849
     * @param string $path
850
     * @param array $result
851
     * @return mixed
852
     * @throws InvalidValue
853
     * @throws \Exception
854
     * @throws \Swaggest\JsonDiff\Exception
855
     */
856 1201
    private function processArray($data, Context $options, $path, $result)
857
    {
858 1201
        $count = count($data);
859 1201
        if (!$options->skipValidation) {
860 1001
            if ($this->minItems !== null && $count < $this->minItems) {
861 9
                $this->fail(new ArrayException("Not enough items in array"), $path);
862
            }
863
864 995
            if ($this->maxItems !== null && $count > $this->maxItems) {
865 6
                $this->fail(new ArrayException("Too many items in array"), $path);
866
            }
867
        }
868
869 1189
        $pathItems = 'items';
870 1189
        $this->items = self::unboolSchema($this->items);
871 1189
        if ($this->items instanceof SchemaContract) {
872 843
            $items = array();
873 843
            $additionalItems = $this->items;
874 682
        } elseif ($this->items === null) { // items defaults to empty schema so everything is valid
875 682
            $items = array();
876 682
            $additionalItems = true;
877
        } else { // listed items
878 104
            $items = $this->items;
879 104
            $additionalItems = $this->additionalItems;
880 104
            $pathItems = 'additionalItems';
881
        }
882
883
        /**
884
         * @var Schema|Schema[] $items
885
         * @var null|bool|Schema $additionalItems
886
         */
887 1189
        $itemsLen = is_array($items) ? count($items) : 0;
888 1189
        $index = 0;
889 1189
        foreach ($result as $key => $value) {
890 1064
            if ($index < $itemsLen) {
891 94
                $itemSchema = self::unboolSchema($items[$index]);
892 94
                $result[$key] = $itemSchema->process($value, $options, $path . '->items:' . $index);
893
            } else {
894 1064
                if ($additionalItems instanceof SchemaContract) {
895 817
                    $result[$key] = $additionalItems->process($value, $options, $path . '->' . $pathItems
896 817
                        . '[' . $index . ']:' . $index);
897 566
                } elseif (!$options->skipValidation && $additionalItems === false) {
898 6
                    $this->fail(new ArrayException('Unexpected array item'), $path);
899
                }
900
            }
901 1044
            ++$index;
902
        }
903
904 1165
        if (!$options->skipValidation && $this->uniqueItems) {
905 499
            if (!UniqueItems::isValid($data)) {
906 19
                $this->fail(new ArrayException('Array is not unique'), $path);
907
            }
908
        }
909
910 1146
        if (!$options->skipValidation && $this->contains !== null) {
911
            /** @var Schema|bool $contains */
912 36
            $contains = $this->contains;
913 36
            if ($contains === false) {
914 4
                $this->fail(new ArrayException('Contains is false'), $path);
915
            }
916 32
            if ($count === 0) {
917 7
                $this->fail(new ArrayException('Empty array fails contains constraint'), $path);
918
            }
919 25
            if ($contains === true) {
920 2
                $contains = self::unboolSchema($contains);
921
            }
922 25
            $containsOk = false;
923 25
            foreach ($data as $key => $item) {
924
                try {
925 25
                    $contains->process($item, $options, $path . '->' . $key);
926 17
                    $containsOk = true;
927 17
                    break;
928 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...
929
                }
930
            }
931 25
            if (!$containsOk) {
932 8
                $this->fail(new ArrayException('Array fails contains constraint'), $path);
933
            }
934
        }
935 1127
        return $result;
936
    }
937
938
    /**
939
     * @param mixed|string $data
940
     * @param Context $options
941
     * @param string $path
942
     * @return bool|mixed|string
943
     * @throws InvalidValue
944
     */
945 14
    private function processContent($data, Context $options, $path)
946
    {
947
        try {
948 14
            if ($options->unpackContentMediaType) {
949 7
                return Content::process($options, $this->contentEncoding, $this->contentMediaType, $data, $options->import);
950
            } else {
951 7
                Content::process($options, $this->contentEncoding, $this->contentMediaType, $data, true);
952
            }
953 4
        } catch (InvalidValue $exception) {
954 4
            $this->fail($exception, $path);
955
        }
956 7
        return $data;
957
    }
958
959
    /**
960
     * @param mixed $data
961
     * @param Context $options
962
     * @param string $path
963
     * @param mixed|null $result
964
     * @return array|mixed|null|object|\stdClass
965
     * @throws InvalidValue
966
     * @throws \Exception
967
     * @throws \Swaggest\JsonDiff\Exception
968
     */
969 3179
    public function process($data, Context $options, $path = '#', $result = null)
970
    {
971 3179
        $import = $options->import;
972
973 3179
        if (!$import && $data instanceof ObjectItemContract) {
974 780
            $result = new \stdClass();
975
976 780
            if ($ref = $data->getFromRef()) {
977 1
                $result->{self::PROP_REF} = $ref;
978 1
                return $result;
979
            }
980
981 780
            if ($options->circularReferences->contains($data)) {
982
                /** @noinspection PhpIllegalArrayKeyTypeInspection */
983 1
                $path = $options->circularReferences[$data];
984 1
                $result->{self::PROP_REF} = PointerUtil::getDataPointer($path);
985 1
                return $result;
986
            }
987 780
            $options->circularReferences->attach($data, $path);
988
989 780
            $data = $data->jsonSerialize();
990
        }
991 3179
        if (!$import && is_array($data) && $this->useObjectAsArray) {
992 1
            $data = (object)$data;
993
        }
994
995 3179
        if (null !== $options->dataPreProcessor) {
996
            $data = $options->dataPreProcessor->process($data, $this, $import);
997
        }
998
999 3179
        if ($result === null) {
1000 3179
            $result = $data;
1001
        }
1002
1003 3179
        if ($options->skipValidation) {
1004 1416
            goto skipValidation;
1005
        }
1006
1007 3141
        if ($this->type !== null) {
1008 3120
            $this->processType($data, $options, $path);
1009
        }
1010
1011 3140
        if ($this->enum !== null) {
1012 1458
            $this->processEnum($data, $path);
1013
        }
1014
1015 3140
        if (array_key_exists(self::CONST_PROPERTY, $this->__arrayOfData)) {
1016 43
            $this->processConst($data, $path);
1017
        }
1018
1019 3140
        if ($this->not !== null) {
1020 69
            $this->processNot($data, $options, $path);
1021
        }
1022
1023 3140
        if (is_string($data)) {
1024 2228
            $this->processString($data, $path);
1025
        }
1026
1027 3140
        if (is_int($data) || is_float($data)) {
1028 1041
            $this->processNumeric($data, $path);
1029
        }
1030
1031 3140
        if ($this->if !== null) {
1032 26
            $result = $this->processIf($data, $options, $path);
1033
        }
1034
1035
        skipValidation:
1036
1037 3178
        if ($this->oneOf !== null) {
1038 100
            $result = $this->processOneOf($data, $options, $path);
1039
        }
1040
1041 3178
        if ($this->anyOf !== null) {
1042 1703
            $result = $this->processAnyOf($data, $options, $path);
1043
        }
1044
1045 3178
        if ($this->allOf !== null) {
1046 352
            $result = $this->processAllOf($data, $options, $path);
1047
        }
1048
1049 3178
        if ($data instanceof \stdClass) {
1050 3133
            $result = $this->processObject($data, $options, $path, $result);
1051
        }
1052
1053 3175
        if (is_array($data)) {
1054 1201
            $result = $this->processArray($data, $options, $path, $result);
1055
        }
1056
1057 3175
        if ($this->contentEncoding !== null || $this->contentMediaType !== null) {
1058 14
            $result = $this->processContent($data, $options, $path);
1059
        }
1060
1061 3175
        return $result;
1062
    }
1063
1064
    /**
1065
     * @param boolean $useObjectAsArray
1066
     * @return Schema
1067
     */
1068 1
    public function setUseObjectAsArray($useObjectAsArray)
1069
    {
1070 1
        $this->useObjectAsArray = $useObjectAsArray;
1071 1
        return $this;
1072
    }
1073
1074
    /**
1075
     * @param InvalidValue $exception
1076
     * @param string $path
1077
     * @throws InvalidValue
1078
     */
1079 1212
    private function fail(InvalidValue $exception, $path)
1080
    {
1081 1212
        if ($path !== '#') {
1082 730
            $exception->addPath($path);
1083
        }
1084 1212
        throw $exception;
1085
    }
1086
1087 13
    public static function integer()
1088
    {
1089 13
        $schema = new static();
1090 13
        $schema->type = Type::INTEGER;
1091 13
        return $schema;
1092
    }
1093
1094 4
    public static function number()
1095
    {
1096 4
        $schema = new static();
1097 4
        $schema->type = Type::NUMBER;
1098 4
        return $schema;
1099
    }
1100
1101 8
    public static function string()
1102
    {
1103 8
        $schema = new static();
1104 8
        $schema->type = Type::STRING;
1105 8
        return $schema;
1106
    }
1107
1108 4
    public static function boolean()
1109
    {
1110 4
        $schema = new static();
1111 4
        $schema->type = Type::BOOLEAN;
1112 4
        return $schema;
1113
    }
1114
1115 6
    public static function object()
1116
    {
1117 6
        $schema = new static();
1118 6
        $schema->type = Type::OBJECT;
1119 6
        return $schema;
1120
    }
1121
1122 1
    public static function arr()
1123
    {
1124 1
        $schema = new static();
1125 1
        $schema->type = Type::ARR;
1126 1
        return $schema;
1127
    }
1128
1129
    public static function null()
1130
    {
1131
        $schema = new static();
1132
        $schema->type = Type::NULL;
1133
        return $schema;
1134
    }
1135
1136
1137
    /**
1138
     * @param Properties $properties
1139
     * @return Schema
1140
     */
1141 3
    public function setProperties($properties)
1142
    {
1143 3
        $this->properties = $properties;
1144 3
        return $this;
1145
    }
1146
1147
    /**
1148
     * @param string $name
1149
     * @param Schema $schema
1150
     * @return $this
1151
     */
1152 3
    public function setProperty($name, $schema)
1153
    {
1154 3
        if (null === $this->properties) {
1155 3
            $this->properties = new Properties();
1156
        }
1157 3
        $this->properties->__set($name, $schema);
1158 3
        return $this;
1159
    }
1160
1161
    /** @var Meta[] */
1162
    private $metaItems = array();
1163
1164 1
    public function addMeta(Meta $meta)
1165
    {
1166 1
        $this->metaItems[get_class($meta)] = $meta;
1167 1
        return $this;
1168
    }
1169
1170 1
    public function getMeta($className)
1171
    {
1172 1
        if (isset($this->metaItems[$className])) {
1173 1
            return $this->metaItems[$className];
1174
        }
1175
        return null;
1176
    }
1177
1178
    /**
1179
     * @param Context $options
1180
     * @return ObjectItemContract
1181
     */
1182 5
    public function makeObjectItem(Context $options = null)
1183
    {
1184 5
        if (null === $this->objectItemClass) {
1185
            return new ObjectItem();
1186
        } else {
1187 5
            $className = $this->objectItemClass;
1188 5
            if ($options !== null) {
1189
                if (isset($options->objectItemClassMapping[$className])) {
1190
                    $className = $options->objectItemClassMapping[$className];
1191
                }
1192
            }
1193 5
            return new $className;
1194
        }
1195
    }
1196
1197
    /**
1198
     * @param mixed $schema
1199
     * @return mixed|Schema
1200
     */
1201 3191
    private static function unboolSchema($schema)
1202
    {
1203 3191
        static $trueSchema;
1204 3191
        static $falseSchema;
1205
1206 3191
        if (null === $trueSchema) {
1207 1
            $trueSchema = new Schema();
1208 1
            $trueSchema->__booleanSchema = true;
1209 1
            $falseSchema = new Schema();
1210 1
            $falseSchema->not = $trueSchema;
1211 1
            $falseSchema->__booleanSchema = false;
1212
        }
1213
1214 3191
        if ($schema === true) {
1215 108
            return $trueSchema;
1216 3163
        } elseif ($schema === false) {
1217 94
            return $falseSchema;
1218
        } else {
1219 3131
            return $schema;
1220
        }
1221
    }
1222
1223
    /**
1224
     * @param mixed $data
1225
     * @return \stdClass
1226
     */
1227 382
    private static function unboolSchemaData($data)
1228
    {
1229 382
        static $trueSchema;
1230 382
        static $falseSchema;
1231
1232 382
        if (null === $trueSchema) {
1233 1
            $trueSchema = new \stdClass();
1234 1
            $falseSchema = new \stdClass();
1235 1
            $falseSchema->not = $trueSchema;
1236
        }
1237
1238 382
        if ($data === true) {
1239 6
            return $trueSchema;
1240 378
        } elseif ($data === false) {
1241 5
            return $falseSchema;
1242
        } else {
1243 373
            return $data;
1244
        }
1245
    }
1246
1247 30
    public function getDefault()
1248
    {
1249 30
        return $this->default;
1250
    }
1251
1252 33
    public function getProperties()
1253
    {
1254 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...
1255
    }
1256
1257
    public function getObjectItemClass()
1258
    {
1259
        return $this->objectItemClass;
1260
    }
1261
}
1262