Completed
Pull Request — master (#13)
by Viacheslav
16:42 queued 06:44
created

Schema::process()   F

Complexity

Conditions 149
Paths > 20000

Size

Total Lines 489
Code Lines 296

Duplication

Lines 0
Ratio 0 %

Importance

Changes 6
Bugs 0 Features 0
Metric Value
c 6
b 0
f 0
dl 0
loc 489
rs 2
cc 149
eloc 296
nc 4294967295
nop 4

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