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

Schema::string()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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