Completed
Pull Request — master (#13)
by Viacheslav
01:43
created

Schema::preProcessReferences()   C

Complexity

Conditions 8
Paths 8

Size

Total Lines 24
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

Loading history...
38
    public $__seqId;
39
    public static $seq = 0;
40
41
    public function __construct()
42
    {
43
        self::$seq++;
44
        $this->__seqId = self::$seq;
45
    }
46
    //*/
47
48
    // Object
49
    /** @var Properties|Schema[]|Schema */
50
    public $properties;
51
    /** @var Schema|bool */
52
    public $additionalProperties;
53
    /** @var Schema[] */
54
    public $patternProperties;
55
    /** @var string[][]|Schema[] */
56
    public $dependencies;
57
58
    // Array
59
    /** @var Schema|Schema[] */
60
    public $items;
61
    /** @var Schema|bool */
62
    public $additionalItems;
63
64
    const FORMAT_DATE_TIME = 'date-time'; // todo implement
65
66
67
    /** @var Schema[] */
68
    public $allOf;
69
    /** @var Schema */
70
    public $not;
71
    /** @var Schema[] */
72
    public $anyOf;
73
    /** @var Schema[] */
74
    public $oneOf;
75
76
    public $objectItemClass;
77
    private $useObjectAsArray = false;
78
79
    private $__dataToProperty = array();
80
    private $__propertyToData = array();
81
82
83
    public function addPropertyMapping($dataName, $propertyName, $mapping = self::DEFAULT_MAPPING)
84
    {
85
        $this->__dataToProperty[$mapping][$dataName] = $propertyName;
86
        $this->__propertyToData[$mapping][$propertyName] = $dataName;
87
        return $this;
88
    }
89
90
    private function preProcessReferences($data, Context $options = null, $nestingLevel = 0)
91
    {
92
        if ($nestingLevel > 200) {
93
            throw new Exception('Too deep nesting level', Exception::DEEP_NESTING);
94
        }
95
        if (is_array($data)) {
96
            foreach ($data as $key => $item) {
97
                $this->preProcessReferences($item, $options, $nestingLevel + 1);
98
            }
99
        } elseif ($data instanceof \stdClass) {
100
            /** @var JsonSchema $data */
101
            if (isset($data->id) && is_string($data->id)) {
102
                $prev = $options->refResolver->setupResolutionScope($data->id, $data);
103
                /** @noinspection PhpUnusedLocalVariableInspection */
104
                $_ = new ScopeExit(function () use ($prev, $options) {
0 ignored issues
show
Unused Code introduced by
$_ is not used, you could remove the assignment.

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

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

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

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

Loading history...
105
                    $options->refResolver->setResolutionScope($prev);
106
                });
107
            }
108
109
            foreach ((array)$data as $key => $value) {
110
                $this->preProcessReferences($value, $options, $nestingLevel + 1);
111
            }
112
        }
113
    }
114
115
    public function in($data, Context $options = null)
116
    {
117
        if ($options === null) {
118
            $options = new Context();
119
        }
120
121
        $options->import = true;
122
123
        $options->refResolver = new RefResolver($data);
124
        if ($options->remoteRefProvider) {
125
            $options->refResolver->setRemoteRefProvider($options->remoteRefProvider);
126
        }
127
128
        if ($options->import) {
129
            $this->preProcessReferences($data, $options);
130
        }
131
132
        return $this->process($data, $options, '#');
133
    }
134
135
136
    /**
137
     * @param mixed $data
138
     * @param Context|null $options
139
     * @return array|mixed|null|object|\stdClass
140
     * @throws InvalidValue
141
     */
142
    public function out($data, Context $options = null)
143
    {
144
        if ($options === null) {
145
            $options = new Context();
146
        }
147
148
        $options->circularReferences = new \SplObjectStorage();
149
        $options->import = false;
150
        return $this->process($data, $options);
151
    }
152
153
    /**
154
     * @param mixed $data
155
     * @param Context $options
156
     * @param string $path
157
     * @param null $result
158
     * @return array|mixed|null|object|\stdClass
159
     * @throws InvalidValue
160
     * @throws \Exception
161
     */
162
    public function process($data, Context $options, $path = '#', $result = null)
163
    {
164
165
        $import = $options->import;
166
        //$pathTrace = explode('->', $path);
0 ignored issues
show
Unused Code Comprehensibility introduced by
59% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
167
168
        if (!$import && $data instanceof ObjectItemContract) {
169
            $result = new \stdClass();
170
            if ($options->circularReferences->contains($data)) {
171
                /** @noinspection PhpIllegalArrayKeyTypeInspection */
172
                $path = $options->circularReferences[$data];
173
                // @todo $path is not a valid json pointer $ref
174
                $result->{self::REF} = $path;
175
                return $result;
176
//                return $options->circularReferences[$data];
0 ignored issues
show
Unused Code Comprehensibility introduced by
70% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

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

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

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

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

Loading history...
180
181
            $data = $data->jsonSerialize();
182
        }
183
        if (!$import && is_array($data) && $this->useObjectAsArray) {
184
            $data = (object)$data;
185
        }
186
187
        if (null !== $options->dataPreProcessor) {
188
            $data = $options->dataPreProcessor->process($data, $this, $import);
189
        }
190
191
        if ($result === null) {
192
            $result = $data;
193
        }
194
195
        if ($options->skipValidation) {
196
            goto skipValidation;
197
        }
198
199
        if ($this->type !== null) {
200
            if ($options->tolerateStrings && is_string($data)) {
201
                $valid = Type::readString($this->type, $data);
202
            } else {
203
                $valid = Type::isValid($this->type, $data);
204
            }
205
            if (!$valid) {
206
                $this->fail(new TypeException(ucfirst(
207
                        implode(', ', is_array($this->type) ? $this->type : array($this->type))
208
                        . ' expected, ' . json_encode($data) . ' received')
209
                ), $path);
210
            }
211
        }
212
213
        if ($this->enum !== null) {
214
            $enumOk = false;
215
            foreach ($this->enum as $item) {
216
                if ($item === $data) { // todo support complex structures here
217
                    $enumOk = true;
218
                    break;
219
                }
220
            }
221
            if (!$enumOk) {
222
                $this->fail(new EnumException('Enum failed'), $path);
223
            }
224
        }
225
226
        if ($this->not !== null) {
227
            $exception = false;
228
            try {
229
                $this->not->process($data, $options, $path . '->not');
230
            } catch (InvalidValue $exception) {
231
                // Expected exception
232
            }
233
            if ($exception === false) {
234
                $this->fail(new LogicException('Failed due to logical constraint: not'), $path);
235
            }
236
        }
237
238
        if (is_string($data)) {
239
            if ($this->minLength !== null) {
240
                if (mb_strlen($data, 'UTF-8') < $this->minLength) {
241
                    $this->fail(new StringException('String is too short', StringException::TOO_SHORT), $path);
242
                }
243
            }
244
            if ($this->maxLength !== null) {
245
                if (mb_strlen($data, 'UTF-8') > $this->maxLength) {
246
                    $this->fail(new StringException('String is too long', StringException::TOO_LONG), $path);
247
                }
248
            }
249
            if ($this->pattern !== null) {
250
                if (0 === preg_match(Helper::toPregPattern($this->pattern), $data)) {
251
                    $this->fail(new StringException(json_encode($data) . ' does not match to '
252
                        . $this->pattern, StringException::PATTERN_MISMATCH), $path);
253
                }
254
            }
255
        }
256
257
        if (is_int($data) || is_float($data)) {
258
            if ($this->multipleOf !== null) {
259
                $div = $data / $this->multipleOf;
260
                if ($div != (int)$div) {
261
                    $this->fail(new NumericException($data . ' is not multiple of ' . $this->multipleOf, NumericException::MULTIPLE_OF), $path);
262
                }
263
            }
264
265
            if ($this->maximum !== null) {
266
                if ($this->exclusiveMaximum === true) {
267
                    if ($data >= $this->maximum) {
268
                        $this->fail(new NumericException(
269
                            'Value less or equal than ' . $this->minimum . ' expected, ' . $data . ' received',
270
                            NumericException::MAXIMUM), $path);
271
                    }
272
                } else {
273
                    if ($data > $this->maximum) {
274
                        $this->fail(new NumericException(
275
                            'Value less than ' . $this->minimum . ' expected, ' . $data . ' received',
276
                            NumericException::MAXIMUM), $path);
277
                    }
278
                }
279
            }
280
281
            if ($this->minimum !== null) {
282
                if ($this->exclusiveMinimum === true) {
283
                    if ($data <= $this->minimum) {
284
                        $this->fail(new NumericException(
285
                            'Value more or equal than ' . $this->minimum . ' expected, ' . $data . ' received',
286
                            NumericException::MINIMUM), $path);
287
                    }
288
                } else {
289
                    if ($data < $this->minimum) {
290
                        $this->fail(new NumericException(
291
                            'Value more than ' . $this->minimum . ' expected, ' . $data . ' received',
292
                            NumericException::MINIMUM), $path);
293
                    }
294
                }
295
            }
296
        }
297
298
        skipValidation:
299
300
        if ($this->oneOf !== null) {
301
            $successes = 0;
302
            $failures = '';
303
            $skipValidation = false;
304
            if ($options->skipValidation) {
305
                $skipValidation = true;
306
                $options->skipValidation = false;
307
            }
308
309
            foreach ($this->oneOf as $index => $item) {
310
                try {
311
                    $result = $item->process($data, $options, $path . '->oneOf:' . $index);
312
                    $successes++;
313
                    if ($successes > 1 || $options->skipValidation) {
314
                        break;
315
                    }
316
                } catch (InvalidValue $exception) {
317
                    $failures .= ' ' . $index . ': ' . Helper::padLines(' ', $exception->getMessage()) . "\n";
318
                    // Expected exception
319
                }
320
            }
321
            if ($skipValidation) {
322
                $options->skipValidation = true;
323
                if ($successes === 0) {
324
                    $result = $this->oneOf[0]->process($data, $options, $path . '->oneOf:' . 0);
325
                }
326
            }
327
328
            if (!$options->skipValidation) {
329
                if ($successes === 0) {
330
                    $this->fail(new LogicException('Failed due to logical constraint: no valid results for oneOf {' . "\n" . substr($failures, 0, -1) . "\n}"), $path);
331
                } elseif ($successes > 1) {
332
                    $this->fail(new LogicException('Failed due to logical constraint: '
333
                        . $successes . '/' . count($this->oneOf) . ' valid results for oneOf'), $path);
334
                }
335
            }
336
        }
337
338
        if ($this->anyOf !== null) {
339
            $successes = 0;
340
            $failures = '';
341
            foreach ($this->anyOf as $index => $item) {
342
                try {
343
                    $result = $item->process($data, $options, $path . '->anyOf:' . $index);
344
                    $successes++;
345
                    if ($successes) {
346
                        break;
347
                    }
348
                } catch (InvalidValue $exception) {
349
                    $failures .= ' ' . $index . ': ' . $exception->getMessage() . "\n";
350
                    // Expected exception
351
                }
352
            }
353
            if (!$successes && !$options->skipValidation) {
354
                $this->fail(new LogicException('Failed due to logical constraint: no valid results for anyOf {' . "\n" . substr(Helper::padLines(' ', $failures), 0, -1) . "\n}"), $path);
355
            }
356
        }
357
358
        if ($this->allOf !== null) {
359
            foreach ($this->allOf as $index => $item) {
360
                $result = $item->process($data, $options, $path . '->allOf' . $index);
361
            }
362
        }
363
364
        if ($data instanceof \stdClass) {
365
            if (!$options->skipValidation && $this->required !== null) {
366
367
                if (isset($this->__dataToProperty[$options->mapping])) {
368
                    if ($import) {
369
                        foreach ($this->required as $item) {
370
                            if (isset($this->__propertyToData[$options->mapping][$item])) {
371
                                $item = $this->__propertyToData[$options->mapping][$item];
372
                            }
373
                            if (!property_exists($data, $item)) {
374
                                $this->fail(new ObjectException('Required property missing: ' . $item, ObjectException::REQUIRED), $path);
375
                            }
376
                        }
377
                    } else {
378
                        foreach ($this->required as $item) {
379
                            if (isset($this->__dataToProperty[$options->mapping][$item])) {
380
                                $item = $this->__dataToProperty[$options->mapping][$item];
381
                            }
382
                            if (!property_exists($data, $item)) {
383
                                $this->fail(new ObjectException('Required property missing: ' . $item, ObjectException::REQUIRED), $path);
384
                            }
385
                        }
386
                    }
387
388
                } else {
389
                    foreach ($this->required as $item) {
390
                        if (!property_exists($data, $item)) {
391
                            $this->fail(new ObjectException('Required property missing: ' . $item, ObjectException::REQUIRED), $path);
392
                        }
393
                    }
394
                }
395
396
            }
397
398
            if ($import) {
399
                if ($this->useObjectAsArray) {
400
                    $result = array();
401
                } elseif (!$result instanceof ObjectItemContract) {
402
                    $result = $this->makeObjectItem($options);
403
404
                    if ($result instanceof ClassStructure) {
405
                        if ($result->__validateOnSet) {
406
                            $result->__validateOnSet = false;
407
                            /** @noinspection PhpUnusedLocalVariableInspection */
408
                            $validateOnSetHandler = new ScopeExit(function () use ($result) {
1 ignored issue
show
Unused Code introduced by
$validateOnSetHandler is not used, you could remove the assignment.

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

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

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

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

Loading history...
409
                                $result->__validateOnSet = true;
410
                            });
411
                        }
412
                    }
413
414
                    if ($result instanceof ObjectItemContract) {
415
                        $result->setDocumentPath($path);
416
                    }
417
                }
418
            }
419
420
            if ($import) {
421
                try {
422
                    while (
423
                        isset($data->{self::REF})
424
                        && is_string($data->{self::REF})
425
                        && !isset($this->properties[self::REF])
426
                    ) {
427
                        $refString = $data->{self::REF};
428
                        // TODO consider process # by reference here ?
429
                        $refResolver = $options->refResolver;
430
                        $preRefScope = $refResolver->getResolutionScope();
431
                        /** @noinspection PhpUnusedLocalVariableInspection */
432
                        $deferRefScope = new ScopeExit(function () use ($preRefScope, $refResolver) {
1 ignored issue
show
Unused Code introduced by
$deferRefScope is not used, you could remove the assignment.

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

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

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

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

Loading history...
433
                            $refResolver->setResolutionScope($preRefScope);
434
                        });
435
                        $ref = $refResolver->resolveReference($refString);
436
                        if ($ref->isImported()) {
437
                            $refResult = $ref->getImported();
438
                            return $refResult;
439
                        }
440
                        $data = $ref->getData();
441
                        if ($result instanceof ObjectItemContract) {
442
                            $result->setFromRef($refString);
443
                        }
444
                        $ref->setImported($result);
445
                        $refResult = $this->process($data, $options, $path . '->ref:' . $refString, $result);
446
                        $ref->setImported($refResult);
447
                        return $refResult;
448
                    }
449
                } catch (InvalidValue $exception) {
450
                    $this->fail($exception, $path);
451
                }
452
            }
453
454
            // @todo better check for schema id
455
456
            if ($import && isset($data->id) && is_string($data->id) /*&& (!isset($this->properties['id']))/* && $this->isMetaSchema($data)*/) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
72% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
457
                $id = $data->id;
458
                $refResolver = $options->refResolver;
459
                $parentScope = $refResolver->updateResolutionScope($id);
460
                /** @noinspection PhpUnusedLocalVariableInspection */
461
                $defer = new ScopeExit(function () use ($parentScope, $refResolver) {
1 ignored issue
show
Unused Code introduced by
$defer is not used, you could remove the assignment.

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

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

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

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

Loading history...
462
                    $refResolver->setResolutionScope($parentScope);
463
                });
464
            }
465
466
            if ($this->properties !== null) {
467
                /** @var Schema[] $properties */
468
                $properties = &$this->properties->toArray(); // TODO check performance of pointer
469
                if ($this->properties instanceof Properties) {
470
                    $nestedProperties = $this->properties->getNestedProperties();
471
                } else {
472
                    $nestedProperties = array();
473
                }
474
            }
475
476
            $array = array();
477
            if (!empty($this->__dataToProperty[$options->mapping])) {
478
                foreach ((array)$data as $key => $value) {
479
                    if ($import) {
480
                        if (isset($this->__dataToProperty[$options->mapping][$key])) {
481
                            $key = $this->__dataToProperty[$options->mapping][$key];
482
                        }
483
                    } else {
484
                        if (isset($this->__propertyToData[$options->mapping][$key])) {
485
                            $key = $this->__propertyToData[$options->mapping][$key];
486
                        }
487
                    }
488
                    $array[$key] = $value;
489
                }
490
            } else {
491
                $array = (array)$data;
492
            }
493
494
            if (!$options->skipValidation) {
495
                if ($this->minProperties !== null && count($array) < $this->minProperties) {
496
                    $this->fail(new ObjectException("Not enough properties", ObjectException::TOO_FEW), $path);
497
                }
498
                if ($this->maxProperties !== null && count($array) > $this->maxProperties) {
499
                    $this->fail(new ObjectException("Too many properties", ObjectException::TOO_MANY), $path);
500
                }
501
            }
502
503
            foreach ($array as $key => $value) {
504
                if ($key === '' && PHP_VERSION_ID < 71000) {
505
                    $this->fail(new InvalidValue('Empty property name'), $path);
506
                }
507
508
                $found = false;
509
510
                if (!$options->skipValidation && !empty($this->dependencies)) {
511
                    $deps = $this->dependencies;
512
                    if (isset($deps->$key)) {
513
                        $dependencies = $deps->$key;
514
                        if ($dependencies instanceof Schema) {
515
                            $dependencies->process($data, $options, $path . '->dependencies:' . $key);
516
                        } else {
517
                            foreach ($dependencies as $item) {
518
                                if (!property_exists($data, $item)) {
519
                                    $this->fail(new ObjectException('Dependency property missing: ' . $item,
520
                                        ObjectException::DEPENDENCY_MISSING), $path);
521
                                }
522
                            }
523
                        }
524
                    }
525
                }
526
527
                $propertyFound = false;
528
                if (isset($properties[$key])) {
529
                    $prop = $properties[$key];
530
                    $propertyFound = true;
531
                    $found = true;
532
                    $value = $prop->process($value, $options, $path . '->properties:' . $key);
533
                }
534
535
                /** @var Egg[] $nestedEggs */
536
                $nestedEggs = null;
537
                if (isset($nestedProperties[$key])) {
538
                    $found = true;
539
                    $nestedEggs = $nestedProperties[$key];
540
                    // todo iterate all nested props?
541
                    $value = $nestedEggs[0]->propertySchema->process($value, $options, $path . '->nestedProperties:' . $key);
542
                }
543
544
                if ($this->patternProperties !== null) {
545
                    foreach ($this->patternProperties as $pattern => $propertySchema) {
546
                        if (preg_match(Helper::toPregPattern($pattern), $key)) {
547
                            $found = true;
548
                            $value = $propertySchema->process($value, $options,
549
                                $path . '->patternProperties[' . $pattern . ']:' . $key);
550
                            if ($import) {
551
                                $result->addPatternPropertyName($pattern, $key);
552
                            }
553
                            //break; // todo manage multiple import data properly (pattern accessor)
554
                        }
555
                    }
556
                }
557
                if (!$found && $this->additionalProperties !== null) {
558
                    if (!$options->skipValidation && $this->additionalProperties === false) {
559
                        $this->fail(new ObjectException('Additional properties not allowed'), $path . ':' . $key);
560
                    }
561
562
                    if ($this->additionalProperties !== false) {
563
                        $value = $this->additionalProperties->process($value, $options, $path . '->additionalProperties:' . $key);
564
                    }
565
566
                    if ($import && !$this->useObjectAsArray) {
567
                        $result->addAdditionalPropertyName($key);
568
                    }
569
                }
570
571
                if ($nestedEggs && $import) {
572
                    foreach ($nestedEggs as $nestedEgg) {
573
                        $result->setNestedProperty($key, $value, $nestedEgg);
574
                    }
575
                    if ($propertyFound) {
576
                        $result->$key = $value;
577
                    }
578
                } else {
579
                    if ($this->useObjectAsArray && $import) {
580
                        $result[$key] = $value;
581
                    } else {
582
                        if ($found || !$import) {
583
                            $result->$key = $value;
584
                        } elseif (!isset($result->$key)) {
585
                            $result->$key = $value;
586
                        }
587
                    }
588
                }
589
            }
590
591
        }
592
593
        if (is_array($data)) {
594
595
            if (!$options->skipValidation) {
596
                if ($this->minItems !== null && count($data) < $this->minItems) {
597
                    $this->fail(new ArrayException("Not enough items in array"), $path);
598
                }
599
600
                if ($this->maxItems !== null && count($data) > $this->maxItems) {
601
                    $this->fail(new ArrayException("Too many items in array"), $path);
602
                }
603
            }
604
605
            $pathItems = 'items';
606
            if ($this->items instanceof Schema) {
607
                $items = array();
608
                $additionalItems = $this->items;
609
            } elseif ($this->items === null) { // items defaults to empty schema so everything is valid
610
                $items = array();
611
                $additionalItems = true;
612
            } else { // listed items
613
                $items = $this->items;
614
                $additionalItems = $this->additionalItems;
615
                $pathItems = 'additionalItems';
616
            }
617
618
            if ($items !== null || $additionalItems !== null) {
619
                $itemsLen = is_array($items) ? count($items) : 0;
620
                $index = 0;
621
                foreach ($result as $key => $value) {
622
                    if ($index < $itemsLen) {
623
                        $result[$key] = $items[$index]->process($value, $options, $path . '->items:' . $index);
624
                    } else {
625
                        if ($additionalItems instanceof Schema) {
626
                            $result[$key] = $additionalItems->process($value, $options, $path . '->' . $pathItems
627
                                . '[' . $index . ']');
628
                        } elseif (!$options->skipValidation && $additionalItems === false) {
629
                            $this->fail(new ArrayException('Unexpected array item'), $path);
630
                        }
631
                    }
632
                    ++$index;
633
                }
634
            }
635
636
            if (!$options->skipValidation && $this->uniqueItems) {
637
                if (!UniqueItems::isValid($data)) {
638
                    $this->fail(new ArrayException('Array is not unique'), $path);
639
                }
640
            }
641
        }
642
643
        return $result;
644
    }
645
646
    /**
647
     * @param boolean $useObjectAsArray
648
     * @return Schema
649
     */
650
    public
651
    function setUseObjectAsArray($useObjectAsArray)
652
    {
653
        $this->useObjectAsArray = $useObjectAsArray;
654
        return $this;
655
    }
656
657
    private function fail(InvalidValue $exception, $path)
658
    {
659
        if ($path !== '#') {
660
            $exception->addPath($path);
661
        }
662
        throw $exception;
663
    }
664
665
    public static function integer()
666
    {
667
        $schema = new static();
668
        $schema->type = Type::INTEGER;
669
        return $schema;
670
    }
671
672
    public static function number()
673
    {
674
        $schema = new static();
675
        $schema->type = Type::NUMBER;
676
        return $schema;
677
    }
678
679
    public static function string()
680
    {
681
        $schema = new static();
682
        $schema->type = Type::STRING;
683
        return $schema;
684
    }
685
686
    public static function boolean()
687
    {
688
        $schema = new static();
689
        $schema->type = Type::BOOLEAN;
690
        return $schema;
691
    }
692
693
    public static function object()
694
    {
695
        $schema = new static();
696
        $schema->type = Type::OBJECT;
697
        return $schema;
698
    }
699
700
    public static function arr()
701
    {
702
        $schema = new static();
703
        $schema->type = Type::ARR;
704
        return $schema;
705
    }
706
707
    public static function null()
708
    {
709
        $schema = new static();
710
        $schema->type = Type::NULL;
711
        return $schema;
712
    }
713
714
715
    /**
716
     * @param Properties $properties
717
     * @return Schema
718
     */
719
    public function setProperties($properties)
720
    {
721
        $this->properties = $properties;
722
        return $this;
723
    }
724
725
    /**
726
     * @param string $name
727
     * @param Schema $schema
728
     * @return $this
729
     */
730
    public function setProperty($name, $schema)
731
    {
732
        if (null === $this->properties) {
733
            $this->properties = new Properties();
734
        }
735
        $this->properties->__set($name, $schema);
736
        return $this;
737
    }
738
739
    /** @var Meta[] */
740
    private $metaItems = array();
741
742
    public function addMeta(Meta $meta)
743
    {
744
        $this->metaItems[get_class($meta)] = $meta;
745
        return $this;
746
    }
747
748
    public function getMeta($className)
749
    {
750
        if (isset($this->metaItems[$className])) {
751
            return $this->metaItems[$className];
752
        }
753
        return null;
754
    }
755
756
    /**
757
     * @param Context $options
758
     * @return ObjectItemContract
759
     */
760
    public function makeObjectItem(Context $options = null)
761
    {
762
        if (null === $this->objectItemClass) {
763
            return new ObjectItem();
764
        } else {
765
            $className = $this->objectItemClass;
766
            if ($options !== null) {
767
                if (isset($options->objectItemClassMapping[$className])) {
768
                    $className = $options->objectItemClassMapping[$className];
769
                }
770
            }
771
            return new $className;
772
        }
773
    }
774
}
775