Completed
Pull Request — master (#6)
by Viacheslav
01:49
created

Schema::import()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
c 0
b 0
f 0
rs 10
cc 1
eloc 2
nc 1
nop 2
1
<?php
2
3
namespace Swaggest\JsonSchema;
4
5
6
use PhpLang\ScopeExit;
7
use Swaggest\JsonSchema\Constraint\Properties;
8
use Swaggest\JsonSchema\Constraint\Ref;
9
use Swaggest\JsonSchema\Constraint\Type;
10
use Swaggest\JsonSchema\Constraint\UniqueItems;
11
use Swaggest\JsonSchema\Exception\ArrayException;
12
use Swaggest\JsonSchema\Exception\EnumException;
13
use Swaggest\JsonSchema\Exception\LogicException;
14
use Swaggest\JsonSchema\Exception\NumericException;
15
use Swaggest\JsonSchema\Exception\ObjectException;
16
use Swaggest\JsonSchema\Exception\StringException;
17
use Swaggest\JsonSchema\Exception\TypeException;
18
use Swaggest\JsonSchema\Structure\ClassStructure;
19
use Swaggest\JsonSchema\Structure\Egg;
20
use Swaggest\JsonSchema\Structure\ObjectItem;
21
22
class Schema extends MagicMap
23
{
24
    /** @var Type */
25
    public $type;
26
27
    // Object
28
    /** @var Properties|Schema[] */
29
    public $properties;
30
    /** @var Schema|bool */
31
    public $additionalProperties;
32
    /** @var Schema[] */
33
    public $patternProperties;
34
    /** @var string[] */
35
    public $required;
36
    /** @var string[][]|Schema[] */
37
    public $dependencies;
38
    /** @var int */
39
    public $minProperties;
40
    /** @var int */
41
    public $maxProperties;
42
43
    // Array
44
    /** @var Schema|Schema[] */
45
    public $items;
46
    /** @var Schema|bool */
47
    public $additionalItems;
48
    /** @var bool */
49
    public $uniqueItems;
50
    /** @var int */
51
    public $minItems;
52
    /** @var int */
53
    public $maxItems;
54
55
    // Reference
56
    /** @var Ref */
57
    public $ref;
58
59
    // Enum
60
    /** @var array */
61
    public $enum;
62
63
    // Number
64
    /** @var int */
65
    public $maximum;
66
    /** @var bool */
67
    public $exclusiveMaximum;
68
    /** @var int */
69
    public $minimum;
70
    /** @var bool */
71
    public $exclusiveMinimum;
72
    /** @var float|int */
73
    public $multipleOf;
74
75
76
    // String
77
    /** @var string */
78
    public $pattern;
79
    /** @var int */
80
    public $minLength;
81
    /** @var int */
82
    public $maxLength;
83
    /** @var string */
84
    public $format;
85
86
    const FORMAT_DATE_TIME = 'date-time'; // todo implement
87
88
89
    /** @var Schema[] */
90
    public $allOf;
91
    /** @var Schema */
92
    public $not;
93
    /** @var Schema[] */
94
    public $anyOf;
95
    /** @var Schema[] */
96
    public $oneOf;
97
98
    public $objectItemClass;
99
    private $useObjectAsArray = false;
100
101
    public function import($data, DataPreProcessor $preProcessor = null)
102
    {
103
        return $this->process($data, true, $preProcessor);
104
    }
105
106
    public function export($data, DataPreProcessor $preProcessor = null)
107
    {
108
        return $this->process($data, false, $preProcessor);
109
    }
110
111
    private function process($data, $import = true, DataPreProcessor $preProcessor = null, $path = '#')
112
    {
113
        if (!$import && $data instanceof ObjectItem) {
114
            $data = $data->jsonSerialize();
115
        }
116
        if (!$import && is_array($data) && $this->useObjectAsArray) {
117
            $data = (object)$data;
118
        }
119
120
        if (null !== $preProcessor) {
121
            $data = $preProcessor->process($data, $this, $import);
122
        }
123
124
        $result = $data;
125
        if ($this->ref !== null) {
126
            // https://github.com/json-schema-org/JSON-Schema-Test-Suite/pull/129
127
            return $this->ref->getSchema()->process($data, $import, $preProcessor, $path . '->' . $this->ref->ref);
128
        }
129
130
        if ($this->type !== null) {
131
            if (!$this->type->isValid($data)) {
132
                $this->fail(new TypeException(ucfirst(implode(', ', $this->type->types) . ' expected, ' . json_encode($data) . ' received')), $path);
133
            }
134
        }
135
136
        if ($this->enum !== null) {
137
            $enumOk = false;
138
            foreach ($this->enum as $item) {
139
                if ($item === $data) { // todo support complex structures here
140
                    $enumOk = true;
141
                    break;
142
                }
143
            }
144
            if (!$enumOk) {
145
                $this->fail(new EnumException('Enum failed'), $path);
146
            }
147
        }
148
149
        if ($this->not !== null) {
150
            $exception = false;
151
            try {
152
                $this->not->process($data, $import, $preProcessor, $path . '->not');
153
            } catch (InvalidValue $exception) {
154
                // Expected exception
155
            }
156
            if ($exception === false) {
157
                $this->fail(new LogicException('Failed due to logical constraint: not'), $path);
158
            }
159
        }
160
161
        if ($this->oneOf !== null) {
162
            $successes = 0;
163
            foreach ($this->oneOf as $index => $item) {
164
                try {
165
                    $result = $item->process($data, $import, $preProcessor, $path . '->oneOf:' . $index);
166
                    $successes++;
167
                    if ($successes > 1) {
168
                        break;
169
                    }
170
                } catch (InvalidValue $exception) {
171
                    // Expected exception
172
                }
173
            }
174
            if ($successes !== 1) {
175
                $this->fail(new LogicException('Failed due to logical constraint: oneOf'), $path);
176
            }
177
        }
178
179
        if ($this->anyOf !== null) {
180
            $successes = 0;
181
            foreach ($this->anyOf as $index => $item) {
182
                try {
183
                    $result = $item->process($data, $import, $preProcessor, $path . '->anyOf:' . $index);
184
                    $successes++;
185
                    if ($successes) {
186
                        break;
187
                    }
188
                } catch (InvalidValue $exception) {
189
                    // Expected exception
190
                }
191
            }
192
            if (!$successes) {
193
                $this->fail(new LogicException('Failed due to logical constraint: anyOf'), $path);
194
            }
195
        }
196
197
        if ($this->allOf !== null) {
198
            foreach ($this->allOf as $index => $item) {
199
                $result = $item->process($data, $import, $preProcessor, $path . '->allOf' . $index);
200
            }
201
        }
202
203
204
        if (is_string($data)) {
205
            if ($this->minLength !== null) {
206
                if (mb_strlen($data, 'UTF-8') < $this->minLength) {
207
                    $this->fail(new StringException('String is too short', StringException::TOO_SHORT), $path);
208
                }
209
            }
210
            if ($this->maxLength !== null) {
211
                if (mb_strlen($data, 'UTF-8') > $this->maxLength) {
212
                    $this->fail(new StringException('String is too long', StringException::TOO_LONG), $path);
213
                }
214
            }
215
            if ($this->pattern !== null) {
216
                if (0 === preg_match($this->pattern, $data)) {
217
                    $this->fail(new StringException('Does not match to '
218
                        . $this->pattern, StringException::PATTERN_MISMATCH), $path);
219
                }
220
            }
221
        }
222
223
        if (is_int($data) || is_float($data)) {
224
            if ($this->multipleOf !== null) {
225
                $div = $data / $this->multipleOf;
226
                if ($div != (int)$div) {
227
                    $this->fail(new NumericException($data . ' is not multiple of ' . $this->multipleOf, NumericException::MULTIPLE_OF), $path);
228
                }
229
            }
230
231
            if ($this->maximum !== null) {
232
                if ($this->exclusiveMaximum === true) {
233
                    if ($data >= $this->maximum) {
234
                        $this->fail(new NumericException(
235
                            'Value less or equal than ' . $this->minimum . ' expected, ' . $data . ' received',
236
                            NumericException::MAXIMUM), $path);
237
                    }
238
                } else {
239
                    if ($data > $this->maximum) {
240
                        $this->fail(new NumericException(
241
                            'Value less than ' . $this->minimum . ' expected, ' . $data . ' received',
242
                            NumericException::MAXIMUM), $path);
243
                    }
244
                }
245
            }
246
247
            if ($this->minimum !== null) {
248
                if ($this->exclusiveMinimum === true) {
249
                    if ($data <= $this->minimum) {
250
                        $this->fail(new NumericException(
251
                            'Value more or equal than ' . $this->minimum . ' expected, ' . $data . ' received',
252
                            NumericException::MINIMUM), $path);
253
                    }
254
                } else {
255
                    if ($data < $this->minimum) {
256
                        $this->fail(new NumericException(
257
                            'Value more than ' . $this->minimum . ' expected, ' . $data . ' received',
258
                            NumericException::MINIMUM), $path);
259
                    }
260
                }
261
            }
262
        }
263
264
        if ($data instanceof \stdClass) {
265
            if ($this->required !== null) {
266
                foreach ($this->required as $item) {
267
                    if (!property_exists($data, $item)) {
268
                        $this->fail(new ObjectException('Required property missing: ' . $item, ObjectException::REQUIRED), $path);
269
                    }
270
                }
271
            }
272
273
274
            if ($import) {
275
                if ($this->useObjectAsArray) {
276
                    $result = array();
277
                } elseif (!$result instanceof ObjectItem) {
278
                    $result = $this->makeObjectItem();
279
280
                    if ($result instanceof ClassStructure) {
281
                        if ($result->__validateOnSet) {
0 ignored issues
show
Documentation introduced by
The property $__validateOnSet is declared protected in Swaggest\JsonSchema\Structure\ClassStructure. Since you implemented __get(), maybe consider adding a @property or @property-read annotation. This makes it easier for IDEs to provide auto-completion.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
282
                            $result->__validateOnSet = false;
0 ignored issues
show
Documentation introduced by
The property $__validateOnSet is declared protected in Swaggest\JsonSchema\Structure\ClassStructure. Since you implemented __set(), maybe consider adding a @property or @property-write annotation. This makes it easier for IDEs to provide auto-completion.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
283
                            /** @noinspection PhpUnusedLocalVariableInspection */
284
                            $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...
285
                                $result->__validateOnSet = true;
0 ignored issues
show
Documentation introduced by
The property $__validateOnSet is declared protected in Swaggest\JsonSchema\Structure\ClassStructure. Since you implemented __set(), maybe consider adding a @property or @property-write annotation. This makes it easier for IDEs to provide auto-completion.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
286
                            });
287
                        }
288
                    }
289
                }
290
            }
291
292
            if ($this->properties !== null) {
293
                /** @var Schema[] $properties */
294
                $properties = &$this->properties->toArray(); // TODO check performance of pointer
295
                $nestedProperties = $this->properties->getNestedProperties();
296
            }
297
298
            $array = (array)$data;
299
            if ($this->minProperties !== null && count($array) < $this->minProperties) {
300
                $this->fail(new ObjectException("Not enough properties", ObjectException::TOO_FEW), $path);
301
            }
302
            if ($this->maxProperties !== null && count($array) > $this->maxProperties) {
303
                $this->fail(new ObjectException("Too many properties", ObjectException::TOO_MANY), $path);
304
            }
305
            foreach ($array as $key => $value) {
306
                $found = false;
307
                if (isset($this->dependencies[$key])) {
308
                    $dependencies = $this->dependencies[$key];
309
                    if ($dependencies instanceof Schema) {
310
                        $dependencies->process($data, $import, $preProcessor, $path . '->dependencies:' . $key);
311
                    } else {
312
                        foreach ($dependencies as $item) {
313
                            if (!property_exists($data, $item)) {
314
                                $this->fail(new ObjectException('Dependency property missing: ' . $item,
315
                                    ObjectException::DEPENDENCY_MISSING), $path);
316
                            }
317
                        }
318
                    }
319
                }
320
321
                $propertyFound = false;
322
                if (isset($properties[$key])) {
323
                    $propertyFound = true;
324
                    $found = true;
325
                    $value = $properties[$key]->process($value, $import, $preProcessor, $path . '->properties:' . $key);
326
                }
327
328
                /** @var Egg[] $nestedEggs */
329
                $nestedEggs = null;
330
                if (isset($nestedProperties[$key])) {
331
                    $found = true;
332
                    $nestedEggs = $nestedProperties[$key];
333
                    // todo iterate all nested props?
334
                    $value = $nestedEggs[0]->propertySchema->process($value, $import, $preProcessor, $path . '->nestedProperties:' . $key);
335
                }
336
337
                if ($this->patternProperties !== null) {
338
                    foreach ($this->patternProperties as $pattern => $propertySchema) {
339
                        if (preg_match($pattern, $key)) {
340
                            $found = true;
341
                            $value = $propertySchema->process($value, $import, $preProcessor, $path . '->patternProperties:' . $pattern);
342
                            if ($import) {
343
                                $result->addPatternPropertyName($pattern, $key);
344
                            }
345
                            //break; // todo manage multiple import data properly (pattern accessor)
346
                        }
347
                    }
348
                }
349
                if (!$found && $this->additionalProperties !== null) {
350
                    if ($this->additionalProperties === false) {
351
                        $this->fail(new ObjectException('Additional properties not allowed'), $path);
352
                    }
353
354
                    $value = $this->additionalProperties->process($value, $import, $preProcessor, $path . '->additionalProperties');
355
                    if ($import && !$this->useObjectAsArray) {
356
                        $result->addAdditionalPropertyName($key);
357
                    }
358
                }
359
360
                if ($nestedEggs && $import) {
361
                    foreach ($nestedEggs as $nestedEgg) {
362
                        $result->setNestedProperty($key, $value, $nestedEgg);
363
                    }
364
                    if ($propertyFound) {
365
                        $result->$key = $value;
366
                    }
367
                } else {
368
                    if ($this->useObjectAsArray && $import) {
369
                        $result[$key] = $value;
370
                    } else {
371
                        $result->$key = $value;
372
                    }
373
                }
374
375
            }
376
377
        }
378
379
        if (is_array($data)) {
380
381
            if ($this->minItems !== null && count($data) < $this->minItems) {
382
                $this->fail(new ArrayException("Not enough items in array"), $path);
383
            }
384
385
            if ($this->maxItems !== null && count($data) > $this->maxItems) {
386
                $this->fail(new ArrayException("Too many items in array"), $path);
387
            }
388
389
            $pathItems = 'items';
390
            if ($this->items instanceof Schema) {
391
                $items = array();
392
                $additionalItems = $this->items;
393
            } elseif ($this->items === null) { // items defaults to empty schema so everything is valid
394
                $items = array();
395
                $additionalItems = true;
396
            } else { // listed items
397
                $items = $this->items;
398
                $additionalItems = $this->additionalItems;
399
                $pathItems = 'additionalItems';
400
            }
401
402
            if ($items !== null || $additionalItems !== null) {
403
                $itemsLen = is_array($items) ? count($items) : 0;
404
                $index = 0;
405
                foreach ($data as $key => $value) {
406
                    if ($index < $itemsLen) {
407
                        $data[$key] = $items[$index]->process($value, $import, $preProcessor, $path . '->items:' . $index);
408
                    } else {
409
                        if ($additionalItems instanceof Schema) {
410
                            $data[$key] = $additionalItems->process($value, $import, $preProcessor, $path . '->' . $pathItems
411
                                . '[' . $index . ']');
412
                        } elseif ($additionalItems === false) {
413
                            $this->fail(new ArrayException('Unexpected array item'), $path);
414
                        }
415
                    }
416
                    ++$index;
417
                }
418
            }
419
420
            if ($this->uniqueItems) {
421
                if (!UniqueItems::isValid($data)) {
422
                    $this->fail(new ArrayException('Array is not unique'), $path);
423
                }
424
            }
425
426
            $result = $data;
427
        }
428
429
430
        return $result;
431
    }
432
433
    /**
434
     * @param boolean $useObjectAsArray
435
     * @return Schema
436
     */
437
    public function setUseObjectAsArray($useObjectAsArray)
438
    {
439
        $this->useObjectAsArray = $useObjectAsArray;
440
        return $this;
441
    }
442
443
    /**
444
     * @param bool|Schema $additionalProperties
445
     * @return Schema
446
     */
447
    public function setAdditionalProperties($additionalProperties)
448
    {
449
        $this->additionalProperties = $additionalProperties;
450
        return $this;
451
    }
452
453
    /**
454
     * @param Schema|Schema[] $items
455
     * @return Schema
456
     */
457
    public function setItems($items)
458
    {
459
        $this->items = $items;
460
        return $this;
461
    }
462
463
464
    private function fail(InvalidValue $exception, $path)
465
    {
466
        if ($path !== '#') {
467
            $exception->addPath($path);
468
        }
469
        throw $exception;
470
    }
471
472
    public static function integer()
473
    {
474
        $schema = new Schema();
475
        $schema->type = new Type(Type::INTEGER);
476
        return $schema;
477
    }
478
479
    public static function number()
480
    {
481
        $schema = new Schema();
482
        $schema->type = new Type(Type::NUMBER);
483
        return $schema;
484
    }
485
486
    public static function string()
487
    {
488
        $schema = new Schema();
489
        $schema->type = new Type(Type::STRING);
490
        return $schema;
491
    }
492
493
    public static function boolean()
494
    {
495
        $schema = new Schema();
496
        $schema->type = new Type(Type::BOOLEAN);
497
        return $schema;
498
    }
499
500
    public static function object()
501
    {
502
        $schema = new Schema();
503
        $schema->type = new Type(Type::OBJECT);
504
        return $schema;
505
    }
506
507
    public static function create()
508
    {
509
        $schema = new Schema();
510
        return $schema;
511
    }
512
513
514
    /**
515
     * @param Properties $properties
516
     * @return Schema
517
     */
518
    public function setProperties($properties)
519
    {
520
        $this->properties = $properties;
521
        return $this;
522
    }
523
524
    public function setProperty($name, Schema $schema)
525
    {
526
        if (null === $this->properties) {
527
            $this->properties = new Properties();
528
        }
529
        $this->properties->__set($name, $schema);
530
        return $this;
531
    }
532
533
    /** @var Meta[] */
534
    private $metaItems = array();
535
    public function meta(Meta $meta)
536
    {
537
        $this->metaItems[get_class($meta)] = $meta;
538
        return $this;
539
    }
540
541
    public function getMeta($className)
542
    {
543
        if (isset($this->metaItems[$className])) {
544
            return $this->metaItems[$className];
545
        }
546
        return null;
547
    }
548
549
    /**
550
     * @return ObjectItem
551
     */
552
    public function makeObjectItem()
553
    {
554
        if (null === $this->objectItemClass) {
555
            return new ObjectItem();
556
        } else {
557
            return new $this->objectItemClass;
558
        }
559
    }
560
}
561