Completed
Pull Request — master (#5)
by Viacheslav
08:29
created

Schema::setAdditionalProperties()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
dl 0
loc 5
rs 9.4285
c 1
b 0
f 1
cc 1
eloc 3
nc 1
nop 1
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
100
    public function import($data, DataPreProcessor $preProcessor = null)
101
    {
102
        return $this->process($data, true, $preProcessor);
103
    }
104
105
    public function export($data, DataPreProcessor $preProcessor = null)
106
    {
107
        return $this->process($data, false, $preProcessor);
108
    }
109
110
    private function process($data, $import = true, DataPreProcessor $preProcessor = null, $path = '#')
111
    {
112
        if (!$import && $data instanceof ObjectItem) {
113
            $data = $data->jsonSerialize();
114
        }
115
        if (null !== $preProcessor) {
116
            $data = $preProcessor->process($data, $this, $import);
117
        }
118
119
        $result = $data;
120
        if ($this->ref !== null) {
121
            // https://github.com/json-schema-org/JSON-Schema-Test-Suite/pull/129
122
            return $this->ref->getSchema()->process($data, $import, $preProcessor, $path . '->' . $this->ref->ref);
123
        }
124
125
        if ($this->type !== null) {
126
            if (!$this->type->isValid($data)) {
127
                $this->fail(new TypeException(ucfirst(implode(', ', $this->type->types) . ' expected, ' . json_encode($data) . ' received')), $path);
128
            }
129
        }
130
131
        if ($this->enum !== null) {
132
            $enumOk = false;
133
            foreach ($this->enum as $item) {
134
                if ($item === $data) { // todo support complex structures here
135
                    $enumOk = true;
136
                    break;
137
                }
138
            }
139
            if (!$enumOk) {
140
                $this->fail(new EnumException('Enum failed'), $path);
141
            }
142
        }
143
144
        if ($this->not !== null) {
145
            $exception = false;
146
            try {
147
                $this->not->process($data, $import, $preProcessor, $path . '->not');
148
            } catch (InvalidValue $exception) {
149
                // Expected exception
150
            }
151
            if ($exception === false) {
152
                $this->fail(new LogicException('Failed due to logical constraint: not'), $path);
153
            }
154
        }
155
156
        if ($this->oneOf !== null) {
157
            $successes = 0;
158
            foreach ($this->oneOf as $index => $item) {
159
                try {
160
                    $result = $item->process($data, $import, $preProcessor, $path . '->oneOf:' . $index);
161
                    $successes++;
162
                    if ($successes > 1) {
163
                        break;
164
                    }
165
                } catch (InvalidValue $exception) {
166
                    // Expected exception
167
                }
168
            }
169
            if ($successes !== 1) {
170
                $this->fail(new LogicException('Failed due to logical constraint: oneOf'), $path);
171
            }
172
        }
173
174
        if ($this->anyOf !== null) {
175
            $successes = 0;
176
            foreach ($this->anyOf as $index => $item) {
177
                try {
178
                    $result = $item->process($data, $import, $preProcessor, $path . '->anyOf:' . $index);
179
                    $successes++;
180
                    if ($successes) {
181
                        break;
182
                    }
183
                } catch (InvalidValue $exception) {
184
                    // Expected exception
185
                }
186
            }
187
            if (!$successes) {
188
                $this->fail(new LogicException('Failed due to logical constraint: anyOf'), $path);
189
            }
190
        }
191
192
        if ($this->allOf !== null) {
193
            foreach ($this->allOf as $index => $item) {
194
                $result = $item->process($data, $import, $preProcessor, $path . '->allOf' . $index);
195
            }
196
        }
197
198
199
        if (is_string($data)) {
200
            if ($this->minLength !== null) {
201
                if (mb_strlen($data, 'UTF-8') < $this->minLength) {
202
                    $this->fail(new StringException('String is too short', StringException::TOO_SHORT), $path);
203
                }
204
            }
205
            if ($this->maxLength !== null) {
206
                if (mb_strlen($data, 'UTF-8') > $this->maxLength) {
207
                    $this->fail(new StringException('String is too long', StringException::TOO_LONG), $path);
208
                }
209
            }
210
            if ($this->pattern !== null) {
211
                if (0 === preg_match($this->pattern, $data)) {
212
                    $this->fail(new StringException('Does not match to '
213
                        . $this->pattern, StringException::PATTERN_MISMATCH), $path);
214
                }
215
            }
216
        }
217
218
        if (is_int($data) || is_float($data)) {
219
            if ($this->multipleOf !== null) {
220
                $div = $data / $this->multipleOf;
221
                if ($div != (int)$div) {
222
                    $this->fail(new NumericException($data . ' is not multiple of ' . $this->multipleOf, NumericException::MULTIPLE_OF), $path);
223
                }
224
            }
225
226
            if ($this->maximum !== null) {
227
                if ($this->exclusiveMaximum === true) {
228
                    if ($data >= $this->maximum) {
229
                        $this->fail(new NumericException(
230
                            'Value less or equal than ' . $this->minimum . ' expected, ' . $data . ' received',
231
                            NumericException::MAXIMUM), $path);
232
                    }
233
                } else {
234
                    if ($data > $this->maximum) {
235
                        $this->fail(new NumericException(
236
                            'Value less than ' . $this->minimum . ' expected, ' . $data . ' received',
237
                            NumericException::MAXIMUM), $path);
238
                    }
239
                }
240
            }
241
242
            if ($this->minimum !== null) {
243
                if ($this->exclusiveMinimum === true) {
244
                    if ($data <= $this->minimum) {
245
                        $this->fail(new NumericException(
246
                            'Value more or equal than ' . $this->minimum . ' expected, ' . $data . ' received',
247
                            NumericException::MINIMUM), $path);
248
                    }
249
                } else {
250
                    if ($data < $this->minimum) {
251
                        $this->fail(new NumericException(
252
                            'Value more than ' . $this->minimum . ' expected, ' . $data . ' received',
253
                            NumericException::MINIMUM), $path);
254
                    }
255
                }
256
            }
257
258
259
        }
260
261
        if ($data instanceof \stdClass) {
262
            if ($this->required !== null) {
263
                foreach ($this->required as $item) {
264
                    if (!property_exists($data, $item)) {
265
                        $this->fail(new ObjectException('Required property missing: ' . $item, ObjectException::REQUIRED), $path);
266
                    }
267
                }
268
            }
269
270
            if ($import && !$result instanceof ObjectItem) {
271
                $result = $this->makeObjectItem();
272
273
                if ($result instanceof ClassStructure) {
274
                    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...
275
                        $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...
276
                        /** @noinspection PhpUnusedLocalVariableInspection */
277
                        $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...
278
                            $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...
279
                        });
280
                    }
281
                }
282
            }
283
284
            if ($this->properties !== null) {
285
                /** @var Schema[] $properties */
286
                $properties = &$this->properties->toArray(); // TODO check performance of pointer
287
                $nestedProperties = $this->properties->getNestedProperties();
288
            }
289
290
            $array = (array)$data;
291
            if ($this->minProperties !== null && count($array) < $this->minProperties) {
292
                $this->fail(new ObjectException("Not enough properties", ObjectException::TOO_FEW), $path);
293
            }
294
            if ($this->maxProperties !== null && count($array) > $this->maxProperties) {
295
                $this->fail(new ObjectException("Too many properties", ObjectException::TOO_MANY), $path);
296
            }
297
            foreach ($array as $key => $value) {
298
                $found = false;
299
                if (isset($this->dependencies[$key])) {
300
                    $dependencies = $this->dependencies[$key];
301
                    if ($dependencies instanceof Schema) {
302
                        $dependencies->process($data, $import, $preProcessor, $path . '->dependencies:' . $key);
303
                    } else {
304
                        foreach ($dependencies as $item) {
305
                            if (!property_exists($data, $item)) {
306
                                $this->fail(new ObjectException('Dependency property missing: ' . $item,
307
                                    ObjectException::DEPENDENCY_MISSING), $path);
308
                            }
309
                        }
310
                    }
311
                }
312
313
                $propertyFound = false;
314
                if (isset($properties[$key])) {
315
                    $propertyFound = true;
316
                    $found = true;
317
                    $value = $properties[$key]->process($value, $import, $preProcessor, $path . '->properties:' . $key);
318
                }
319
320
                /** @var Egg[] $nestedEggs */
321
                $nestedEggs = null;
322
                if (isset($nestedProperties[$key])) {
323
                    $found = true;
324
                    $nestedEggs = $nestedProperties[$key];
325
                    // todo iterate all nested props?
326
                    $value = $nestedEggs[0]->propertySchema->process($value, $import, $preProcessor, $path . '->nestedProperties:' . $key);
327
                }
328
329
                if ($this->patternProperties !== null) {
330
                    foreach ($this->patternProperties as $pattern => $propertySchema) {
331
                        if (preg_match($pattern, $key)) {
332
                            $found = true;
333
                            $value = $propertySchema->process($value, $import, $preProcessor, $path . '->patternProperties:' . $pattern);
334
                            if ($import) {
335
                                $result->addPatternPropertyName($pattern, $key);
336
                            }
337
                            //break; // todo manage multiple import data properly (pattern accessor)
338
                        }
339
                    }
340
                }
341
                if (!$found && $this->additionalProperties !== null) {
342
                    if ($this->additionalProperties === false) {
343
                        $this->fail(new ObjectException('Additional properties not allowed'), $path);
344
                    }
345
346
                    $value = $this->additionalProperties->process($value, $import, $preProcessor, $path . '->additionalProperties');
347
                    if ($import) {
348
                        $result->addAdditionalPropertyName($key);
349
                    }
350
                }
351
352
                if ($nestedEggs && $import) {
353
                    foreach ($nestedEggs as $nestedEgg) {
354
                        $result->setNestedProperty($key, $value, $nestedEgg);
355
                    }
356
                    if ($propertyFound) {
357
                        $result->$key = $value;
358
                    }
359
                } else {
360
                    $result->$key = $value;
361
                }
362
363
            }
364
365
        }
366
367
        if (is_array($data)) {
368
369
            if ($this->minItems !== null && count($data) < $this->minItems) {
370
                $this->fail(new ArrayException("Not enough items in array"), $path);
371
            }
372
373
            if ($this->maxItems !== null && count($data) > $this->maxItems) {
374
                $this->fail(new ArrayException("Too many items in array"), $path);
375
            }
376
377
            $pathItems = 'items';
378
            if ($this->items instanceof Schema) {
379
                $items = array();
380
                $additionalItems = $this->items;
381
            } elseif ($this->items === null) { // items defaults to empty schema so everything is valid
382
                $items = array();
383
                $additionalItems = true;
384
            } else { // listed items
385
                $items = $this->items;
386
                $additionalItems = $this->additionalItems;
387
                $pathItems = 'additionalItems';
388
            }
389
390
            if ($items !== null || $additionalItems !== null) {
391
                $itemsLen = is_array($items) ? count($items) : 0;
392
                $index = 0;
393
                foreach ($data as $key => $value) {
394
                    if ($index < $itemsLen) {
395
                        $data[$key] = $items[$index]->process($value, $import, $preProcessor, $path . '->items:' . $index);
396
                    } else {
397
                        if ($additionalItems instanceof Schema) {
398
                            $data[$key] = $additionalItems->process($value, $import, $preProcessor, $path . '->' . $pathItems
399
                                . '[' . $index . ']');
400
                        } elseif ($additionalItems === false) {
401
                            $this->fail(new ArrayException('Unexpected array item'), $path);
402
                        }
403
                    }
404
                    ++$index;
405
                }
406
            }
407
408
            if ($this->uniqueItems) {
409
                if (!UniqueItems::isValid($data)) {
410
                    $this->fail(new ArrayException('Array is not unique'), $path);
411
                }
412
            }
413
414
            $result = $data;
415
        }
416
417
418
        return $result;
419
    }
420
421
    /**
422
     * @param bool|Schema $additionalProperties
423
     * @return Schema
424
     */
425
    public function setAdditionalProperties($additionalProperties)
426
    {
427
        $this->additionalProperties = $additionalProperties;
428
        return $this;
429
    }
430
431
    /**
432
     * @param Schema|Schema[] $items
433
     * @return Schema
434
     */
435
    public function setItems($items)
436
    {
437
        $this->items = $items;
438
        return $this;
439
    }
440
441
442
    private function fail(InvalidValue $exception, $path)
443
    {
444
        if ($path !== '#') {
445
            $exception->addPath($path);
446
        }
447
        throw $exception;
448
    }
449
450
    public static function integer()
451
    {
452
        $schema = new Schema();
453
        $schema->type = new Type(Type::INTEGER);
454
        return $schema;
455
    }
456
457
    public static function number()
458
    {
459
        $schema = new Schema();
460
        $schema->type = new Type(Type::NUMBER);
461
        return $schema;
462
    }
463
464
    public static function string()
465
    {
466
        $schema = new Schema();
467
        $schema->type = new Type(Type::STRING);
468
        return $schema;
469
    }
470
471
    public static function boolean()
472
    {
473
        $schema = new Schema();
474
        $schema->type = new Type(Type::BOOLEAN);
475
        return $schema;
476
    }
477
478
    public static function object()
479
    {
480
        $schema = new Schema();
481
        $schema->type = new Type(Type::OBJECT);
482
        return $schema;
483
    }
484
485
    public static function create()
486
    {
487
        $schema = new Schema();
488
        return $schema;
489
    }
490
491
492
    /**
493
     * @param Properties $properties
494
     * @return Schema
495
     */
496
    public function setProperties($properties)
497
    {
498
        $this->properties = $properties;
499
        return $this;
500
    }
501
502
    public function setProperty($name, Schema $schema)
503
    {
504
        if (null === $this->properties) {
505
            $this->properties = new Properties();
506
        }
507
        $this->properties->__set($name, $schema);
508
        return $this;
509
    }
510
511
    /** @var Meta[] */
512
    private $metaItems = array();
513
    public function meta(Meta $meta)
514
    {
515
        $this->metaItems[get_class($meta)] = $meta;
516
        return $this;
517
    }
518
519
    public function getMeta($className)
520
    {
521
        if (isset($this->metaItems[$className])) {
522
            return $this->metaItems[$className];
523
        }
524
        return null;
525
    }
526
527
    /**
528
     * @return ObjectItem
529
     */
530
    public function makeObjectItem()
531
    {
532
        if (null === $this->objectItemClass) {
533
            return new ObjectItem();
534
        } else {
535
            return new $this->objectItemClass;
536
        }
537
    }
538
}
539