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

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
415
                    }
416
                }
417
            }
418
419
            if ($import) {
420
                try {
421
                    while (
422
                        isset($data->{self::REF})
423
                        && is_string($data->{self::REF})
424
                        && !isset($this->properties[self::REF])
425
                    ) {
426
                        $refString = $data->{self::REF};
427
                        // TODO consider process # by reference here ?
428
                        $refResolver = $options->refResolver;
429
                        $preRefScope = $refResolver->getResolutionScope();
430
                        /** @noinspection PhpUnusedLocalVariableInspection */
431
                        $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...
432
                            $refResolver->setResolutionScope($preRefScope);
433
                        });
434
                        $ref = $refResolver->resolveReference($refString);
435
                        if ($ref->isImported()) {
436
                            $refResult = $ref->getImported();
437
                            return $refResult;
438
                        }
439
                        $data = $ref->getData();
440
                        if ($result instanceof ObjectItemContract) {
441
                            $result->__fromRef = $refString;
0 ignored issues
show
Bug introduced by
Accessing __fromRef on the interface Swaggest\JsonSchema\Structure\ObjectItemContract suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

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