Completed
Pull Request — master (#13)
by Viacheslav
03:57
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
            $properties = 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();
0 ignored issues
show
Unused Code introduced by
$nestedProperties 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...
472
                } else {
473
                    $nestedProperties = array();
0 ignored issues
show
Unused Code introduced by
$nestedProperties 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...
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
                $nestedProperties = null;
539
                if (isset($nestedProperties[$key])) {
540
                    $found = true;
541
                    $nestedEggs = $nestedProperties[$key];
542
                    // todo iterate all nested props?
543
                    $value = $nestedEggs[0]->propertySchema->process($value, $options, $path . '->nestedProperties:' . $key);
544
                }
545
546
                if ($this->patternProperties !== null) {
547
                    foreach ($this->patternProperties as $pattern => $propertySchema) {
548
                        if (preg_match(Helper::toPregPattern($pattern), $key)) {
549
                            $found = true;
550
                            $value = $propertySchema->process($value, $options,
551
                                $path . '->patternProperties[' . $pattern . ']:' . $key);
552
                            if ($import) {
553
                                $result->addPatternPropertyName($pattern, $key);
554
                            }
555
                            //break; // todo manage multiple import data properly (pattern accessor)
556
                        }
557
                    }
558
                }
559
                if (!$found && $this->additionalProperties !== null) {
560
                    if (!$options->skipValidation && $this->additionalProperties === false) {
561
                        $this->fail(new ObjectException('Additional properties not allowed'), $path . ':' . $key);
562
                    }
563
564
                    if ($this->additionalProperties !== false) {
565
                        $value = $this->additionalProperties->process($value, $options, $path . '->additionalProperties:' . $key);
566
                    }
567
568
                    if ($import && !$this->useObjectAsArray) {
569
                        $result->addAdditionalPropertyName($key);
570
                    }
571
                }
572
573
                if ($nestedEggs && $import) {
574
                    foreach ($nestedEggs as $nestedEgg) {
575
                        $result->setNestedProperty($key, $value, $nestedEgg);
576
                    }
577
                    if ($propertyFound) {
578
                        $result->$key = $value;
579
                    }
580
                } else {
581
                    if ($this->useObjectAsArray && $import) {
582
                        $result[$key] = $value;
583
                    } else {
584
                        if ($found || !$import) {
585
                            $result->$key = $value;
586
                        } elseif (!isset($result->$key)) {
587
                            $result->$key = $value;
588
                        }
589
                    }
590
                }
591
            }
592
593
        }
594
595
        if (is_array($data)) {
596
597
            if (!$options->skipValidation) {
598
                if ($this->minItems !== null && count($data) < $this->minItems) {
599
                    $this->fail(new ArrayException("Not enough items in array"), $path);
600
                }
601
602
                if ($this->maxItems !== null && count($data) > $this->maxItems) {
603
                    $this->fail(new ArrayException("Too many items in array"), $path);
604
                }
605
            }
606
607
            $pathItems = 'items';
608
            if ($this->items instanceof Schema) {
609
                $items = array();
610
                $additionalItems = $this->items;
611
            } elseif ($this->items === null) { // items defaults to empty schema so everything is valid
612
                $items = array();
613
                $additionalItems = true;
614
            } else { // listed items
615
                $items = $this->items;
616
                $additionalItems = $this->additionalItems;
617
                $pathItems = 'additionalItems';
618
            }
619
620
            if ($items !== null || $additionalItems !== null) {
621
                $itemsLen = is_array($items) ? count($items) : 0;
622
                $index = 0;
623
                foreach ($result as $key => $value) {
624
                    if ($index < $itemsLen) {
625
                        $result[$key] = $items[$index]->process($value, $options, $path . '->items:' . $index);
626
                    } else {
627
                        if ($additionalItems instanceof Schema) {
628
                            $result[$key] = $additionalItems->process($value, $options, $path . '->' . $pathItems
629
                                . '[' . $index . ']');
630
                        } elseif (!$options->skipValidation && $additionalItems === false) {
631
                            $this->fail(new ArrayException('Unexpected array item'), $path);
632
                        }
633
                    }
634
                    ++$index;
635
                }
636
            }
637
638
            if (!$options->skipValidation && $this->uniqueItems) {
639
                if (!UniqueItems::isValid($data)) {
640
                    $this->fail(new ArrayException('Array is not unique'), $path);
641
                }
642
            }
643
        }
644
645
        return $result;
646
    }
647
648
    /**
649
     * @param boolean $useObjectAsArray
650
     * @return Schema
651
     */
652
    public
653
    function setUseObjectAsArray($useObjectAsArray)
654
    {
655
        $this->useObjectAsArray = $useObjectAsArray;
656
        return $this;
657
    }
658
659
    private function fail(InvalidValue $exception, $path)
660
    {
661
        if ($path !== '#') {
662
            $exception->addPath($path);
663
        }
664
        throw $exception;
665
    }
666
667
    public static function integer()
668
    {
669
        $schema = new static();
670
        $schema->type = Type::INTEGER;
671
        return $schema;
672
    }
673
674
    public static function number()
675
    {
676
        $schema = new static();
677
        $schema->type = Type::NUMBER;
678
        return $schema;
679
    }
680
681
    public static function string()
682
    {
683
        $schema = new static();
684
        $schema->type = Type::STRING;
685
        return $schema;
686
    }
687
688
    public static function boolean()
689
    {
690
        $schema = new static();
691
        $schema->type = Type::BOOLEAN;
692
        return $schema;
693
    }
694
695
    public static function object()
696
    {
697
        $schema = new static();
698
        $schema->type = Type::OBJECT;
699
        return $schema;
700
    }
701
702
    public static function arr()
703
    {
704
        $schema = new static();
705
        $schema->type = Type::ARR;
706
        return $schema;
707
    }
708
709
    public static function null()
710
    {
711
        $schema = new static();
712
        $schema->type = Type::NULL;
713
        return $schema;
714
    }
715
716
717
    /**
718
     * @param Properties $properties
719
     * @return Schema
720
     */
721
    public function setProperties($properties)
722
    {
723
        $this->properties = $properties;
724
        return $this;
725
    }
726
727
    /**
728
     * @param string $name
729
     * @param Schema $schema
730
     * @return $this
731
     */
732
    public function setProperty($name, $schema)
733
    {
734
        if (null === $this->properties) {
735
            $this->properties = new Properties();
736
        }
737
        $this->properties->__set($name, $schema);
738
        return $this;
739
    }
740
741
    /** @var Meta[] */
742
    private $metaItems = array();
743
744
    public function addMeta(Meta $meta)
745
    {
746
        $this->metaItems[get_class($meta)] = $meta;
747
        return $this;
748
    }
749
750
    public function getMeta($className)
751
    {
752
        if (isset($this->metaItems[$className])) {
753
            return $this->metaItems[$className];
754
        }
755
        return null;
756
    }
757
758
    /**
759
     * @param Context $options
760
     * @return ObjectItemContract
761
     */
762
    public function makeObjectItem(Context $options = null)
763
    {
764
        if (null === $this->objectItemClass) {
765
            return new ObjectItem();
766
        } else {
767
            $className = $this->objectItemClass;
768
            if ($options !== null) {
769
                if (isset($options->objectItemClassMapping[$className])) {
770
                    $className = $options->objectItemClassMapping[$className];
771
                }
772
            }
773
            return new $className;
774
        }
775
    }
776
}
777