Completed
Pull Request — master (#1)
by Viacheslav
06:08
created

Schema::meta()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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