1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Swaggest\JsonSchema; |
4
|
|
|
|
5
|
|
|
|
6
|
|
|
use PhpLang\ScopeExit; |
7
|
|
|
use Swaggest\JsonDiff\JsonDiff; |
8
|
|
|
use Swaggest\JsonSchema\Constraint\Properties; |
9
|
|
|
use Swaggest\JsonSchema\Constraint\Type; |
10
|
|
|
use Swaggest\JsonSchema\Constraint\UniqueItems; |
11
|
|
|
use Swaggest\JsonSchema\Exception\ArrayException; |
12
|
|
|
use Swaggest\JsonSchema\Exception\ConstException; |
13
|
|
|
use Swaggest\JsonSchema\Exception\EnumException; |
14
|
|
|
use Swaggest\JsonSchema\Exception\LogicException; |
15
|
|
|
use Swaggest\JsonSchema\Exception\NumericException; |
16
|
|
|
use Swaggest\JsonSchema\Exception\ObjectException; |
17
|
|
|
use Swaggest\JsonSchema\Exception\StringException; |
18
|
|
|
use Swaggest\JsonSchema\Exception\TypeException; |
19
|
|
|
use Swaggest\JsonSchema\Meta\Meta; |
20
|
|
|
use Swaggest\JsonSchema\Meta\MetaHolder; |
21
|
|
|
use Swaggest\JsonSchema\Structure\ClassStructure; |
22
|
|
|
use Swaggest\JsonSchema\Structure\Egg; |
23
|
|
|
use Swaggest\JsonSchema\Structure\ObjectItem; |
24
|
|
|
use Swaggest\JsonSchema\Structure\ObjectItemContract; |
25
|
|
|
|
26
|
|
|
/** |
27
|
|
|
* Class Schema |
28
|
|
|
* @package Swaggest\JsonSchema |
29
|
|
|
*/ |
30
|
|
|
class Schema extends JsonSchema implements MetaHolder |
31
|
|
|
{ |
32
|
|
|
const CONST_PROPERTY = 'const'; |
33
|
|
|
|
34
|
|
|
const DEFAULT_MAPPING = 'default'; |
35
|
|
|
|
36
|
|
|
const VERSION_AUTO = 'a'; |
37
|
|
|
const VERSION_DRAFT_04 = 4; |
38
|
|
|
const VERSION_DRAFT_06 = 6; |
39
|
|
|
|
40
|
|
|
const SCHEMA_DRAFT_04_URL = 'http://json-schema.org/draft-04/schema'; |
41
|
|
|
|
42
|
|
|
const REF = '$ref'; |
43
|
|
|
const ID = '$id'; |
44
|
|
|
|
45
|
|
|
|
46
|
|
|
/* |
|
|
|
|
47
|
|
|
public $__seqId; |
48
|
|
|
public static $seq = 0; |
49
|
|
|
|
50
|
|
|
public function __construct() |
51
|
|
|
{ |
52
|
|
|
self::$seq++; |
53
|
|
|
$this->__seqId = self::$seq; |
54
|
|
|
} |
55
|
|
|
//*/ |
56
|
|
|
|
57
|
|
|
// Object |
58
|
|
|
/** @var Properties|Schema[]|Schema */ |
59
|
|
|
public $properties; |
60
|
|
|
/** @var Schema|bool */ |
61
|
|
|
public $additionalProperties; |
62
|
|
|
/** @var Schema[] */ |
63
|
|
|
public $patternProperties; |
64
|
|
|
/** @var string[][]|Schema[]|\stdClass */ |
65
|
|
|
public $dependencies; |
66
|
|
|
|
67
|
|
|
// Array |
68
|
|
|
/** @var null|Schema|Schema[] */ |
69
|
|
|
public $items; |
70
|
|
|
/** @var null|Schema|bool */ |
71
|
|
|
public $additionalItems; |
72
|
|
|
|
73
|
|
|
const FORMAT_DATE_TIME = 'date-time'; // todo implement |
74
|
|
|
|
75
|
|
|
|
76
|
|
|
/** @var Schema[] */ |
77
|
|
|
public $allOf; |
78
|
|
|
/** @var Schema */ |
79
|
|
|
public $not; |
80
|
|
|
/** @var Schema[] */ |
81
|
|
|
public $anyOf; |
82
|
|
|
/** @var Schema[] */ |
83
|
|
|
public $oneOf; |
84
|
|
|
|
85
|
|
|
public $objectItemClass; |
86
|
|
|
private $useObjectAsArray = false; |
87
|
|
|
|
88
|
|
|
private $__dataToProperty = array(); |
89
|
|
|
private $__propertyToData = array(); |
90
|
|
|
|
91
|
|
|
|
92
|
|
|
public function addPropertyMapping($dataName, $propertyName, $mapping = self::DEFAULT_MAPPING) |
93
|
|
|
{ |
94
|
|
|
$this->__dataToProperty[$mapping][$dataName] = $propertyName; |
95
|
|
|
$this->__propertyToData[$mapping][$propertyName] = $dataName; |
96
|
|
|
return $this; |
97
|
|
|
} |
98
|
|
|
|
99
|
|
|
private function preProcessReferences($data, Context $options, $nestingLevel = 0) |
100
|
|
|
{ |
101
|
|
|
if ($nestingLevel > 200) { |
102
|
|
|
throw new Exception('Too deep nesting level', Exception::DEEP_NESTING); |
103
|
|
|
} |
104
|
|
|
if (is_array($data)) { |
105
|
|
|
foreach ($data as $key => $item) { |
106
|
|
|
$this->preProcessReferences($item, $options, $nestingLevel + 1); |
107
|
|
|
} |
108
|
|
|
} elseif ($data instanceof \stdClass) { |
109
|
|
|
/** @var JsonSchema $data */ |
110
|
|
|
if ( |
111
|
|
|
isset($data->id) |
112
|
|
|
&& is_string($data->id) |
113
|
|
|
&& ($options->version === self::VERSION_DRAFT_04 || $options->version === self::VERSION_AUTO) |
114
|
|
|
) { |
115
|
|
|
$prev = $options->refResolver->setupResolutionScope($data->id, $data); |
116
|
|
|
/** @noinspection PhpUnusedLocalVariableInspection */ |
117
|
|
|
$_ = new ScopeExit(function () use ($prev, $options) { |
|
|
|
|
118
|
|
|
$options->refResolver->setResolutionScope($prev); |
119
|
|
|
}); |
120
|
|
|
} |
121
|
|
|
|
122
|
|
|
if (isset($data->{self::ID}) |
123
|
|
|
&& is_string($data->{self::ID}) |
124
|
|
|
&& ($options->version >= self::VERSION_DRAFT_06 || $options->version === self::VERSION_AUTO) |
125
|
|
|
) { |
126
|
|
|
$prev = $options->refResolver->setupResolutionScope($data->{self::ID}, $data); |
127
|
|
|
/** @noinspection PhpUnusedLocalVariableInspection */ |
128
|
|
|
$_ = new ScopeExit(function () use ($prev, $options) { |
|
|
|
|
129
|
|
|
$options->refResolver->setResolutionScope($prev); |
130
|
|
|
}); |
131
|
|
|
} |
132
|
|
|
|
133
|
|
|
|
134
|
|
|
foreach ((array)$data as $key => $value) { |
135
|
|
|
$this->preProcessReferences($value, $options, $nestingLevel + 1); |
136
|
|
|
} |
137
|
|
|
} |
138
|
|
|
} |
139
|
|
|
|
140
|
|
|
public static function import($data, Context $options = null) |
141
|
|
|
{ |
142
|
|
|
$data = self::unboolSchemaData($data); |
143
|
|
|
return parent::import($data, $options); |
|
|
|
|
144
|
|
|
} |
145
|
|
|
|
146
|
|
|
public function in($data, Context $options = null) |
147
|
|
|
{ |
148
|
|
|
if ($options === null) { |
149
|
|
|
$options = new Context(); |
150
|
|
|
} |
151
|
|
|
|
152
|
|
|
$options->import = true; |
153
|
|
|
|
154
|
|
|
$options->refResolver = new RefResolver($data); |
155
|
|
|
if ($options->remoteRefProvider) { |
156
|
|
|
$options->refResolver->setRemoteRefProvider($options->remoteRefProvider); |
157
|
|
|
} |
158
|
|
|
|
159
|
|
|
if ($options->import) { |
160
|
|
|
$this->preProcessReferences($data, $options); |
161
|
|
|
} |
162
|
|
|
|
163
|
|
|
return $this->process($data, $options, '#'); |
164
|
|
|
} |
165
|
|
|
|
166
|
|
|
|
167
|
|
|
/** |
168
|
|
|
* @param mixed $data |
169
|
|
|
* @param Context|null $options |
170
|
|
|
* @return array|mixed|null|object|\stdClass |
171
|
|
|
* @throws InvalidValue |
172
|
|
|
*/ |
173
|
|
|
public function out($data, Context $options = null) |
174
|
|
|
{ |
175
|
|
|
if ($options === null) { |
176
|
|
|
$options = new Context(); |
177
|
|
|
} |
178
|
|
|
|
179
|
|
|
$options->circularReferences = new \SplObjectStorage(); |
180
|
|
|
$options->import = false; |
181
|
|
|
return $this->process($data, $options); |
182
|
|
|
} |
183
|
|
|
|
184
|
|
|
/** |
185
|
|
|
* @param mixed $data |
186
|
|
|
* @param Context $options |
187
|
|
|
* @param string $path |
188
|
|
|
* @param null $result |
189
|
|
|
* @return array|mixed|null|object|\stdClass |
190
|
|
|
* @throws InvalidValue |
191
|
|
|
* @throws \Exception |
192
|
|
|
*/ |
193
|
|
|
public function process($data, Context $options, $path = '#', $result = null) |
194
|
|
|
{ |
195
|
|
|
|
196
|
|
|
$import = $options->import; |
197
|
|
|
//$pathTrace = explode('->', $path); |
|
|
|
|
198
|
|
|
|
199
|
|
|
if (!$import && $data instanceof ObjectItemContract) { |
200
|
|
|
$result = new \stdClass(); |
201
|
|
|
if ($options->circularReferences->contains($data)) { |
202
|
|
|
/** @noinspection PhpIllegalArrayKeyTypeInspection */ |
203
|
|
|
$path = $options->circularReferences[$data]; |
204
|
|
|
// @todo $path is not a valid json pointer $ref |
205
|
|
|
$result->{self::REF} = $path; |
206
|
|
|
return $result; |
207
|
|
|
// return $options->circularReferences[$data]; |
|
|
|
|
208
|
|
|
} |
209
|
|
|
$options->circularReferences->attach($data, $path); |
210
|
|
|
//$options->circularReferences->attach($data, $result); |
|
|
|
|
211
|
|
|
|
212
|
|
|
$data = $data->jsonSerialize(); |
213
|
|
|
} |
214
|
|
|
if (!$import && is_array($data) && $this->useObjectAsArray) { |
215
|
|
|
$data = (object)$data; |
216
|
|
|
} |
217
|
|
|
|
218
|
|
|
if (null !== $options->dataPreProcessor) { |
219
|
|
|
$data = $options->dataPreProcessor->process($data, $this, $import); |
220
|
|
|
} |
221
|
|
|
|
222
|
|
|
if ($result === null) { |
223
|
|
|
$result = $data; |
224
|
|
|
} |
225
|
|
|
|
226
|
|
|
if ($options->skipValidation) { |
227
|
|
|
goto skipValidation; |
228
|
|
|
} |
229
|
|
|
|
230
|
|
|
if ($this->type !== null) { |
231
|
|
|
if ($options->tolerateStrings && is_string($data)) { |
232
|
|
|
$valid = Type::readString($this->type, $data); |
233
|
|
|
} else { |
234
|
|
|
$valid = Type::isValid($this->type, $data); |
235
|
|
|
} |
236
|
|
|
if (!$valid) { |
237
|
|
|
$this->fail(new TypeException(ucfirst( |
238
|
|
|
implode(', ', is_array($this->type) ? $this->type : array($this->type)) |
239
|
|
|
. ' expected, ' . json_encode($data) . ' received') |
240
|
|
|
), $path); |
241
|
|
|
} |
242
|
|
|
} |
243
|
|
|
|
244
|
|
|
if ($this->enum !== null) { |
245
|
|
|
$enumOk = false; |
246
|
|
|
foreach ($this->enum as $item) { |
247
|
|
|
if ($item === $data) { // todo support complex structures here |
248
|
|
|
$enumOk = true; |
249
|
|
|
break; |
250
|
|
|
} |
251
|
|
|
} |
252
|
|
|
if (!$enumOk) { |
253
|
|
|
$this->fail(new EnumException('Enum failed'), $path); |
254
|
|
|
} |
255
|
|
|
} |
256
|
|
|
|
257
|
|
|
if (array_key_exists(self::CONST_PROPERTY, $this->__arrayOfData)) { |
258
|
|
|
if ($this->const !== $data) { |
259
|
|
|
if ((is_object($this->const) && is_object($data)) |
260
|
|
|
|| (is_array($this->const) && is_array($data))) { |
261
|
|
|
$diff = new JsonDiff($this->const, $data, |
262
|
|
|
JsonDiff::SKIP_REARRANGE_ARRAY + JsonDiff::STOP_ON_DIFF); |
263
|
|
|
if ($diff->getDiffCnt() != 0) { |
264
|
|
|
$this->fail(new ConstException('Const failed'), $path); |
265
|
|
|
} |
266
|
|
|
} else { |
267
|
|
|
$this->fail(new ConstException('Const failed'), $path); |
268
|
|
|
} |
269
|
|
|
} |
270
|
|
|
} |
271
|
|
|
|
272
|
|
|
if ($this->not !== null) { |
273
|
|
|
$exception = false; |
274
|
|
|
try { |
275
|
|
|
self::unboolSchema($this->not)->process($data, $options, $path . '->not'); |
276
|
|
|
} catch (InvalidValue $exception) { |
277
|
|
|
// Expected exception |
278
|
|
|
} |
279
|
|
|
if ($exception === false) { |
280
|
|
|
$this->fail(new LogicException('Failed due to logical constraint: not'), $path); |
281
|
|
|
} |
282
|
|
|
} |
283
|
|
|
|
284
|
|
|
if (is_string($data)) { |
285
|
|
|
if ($this->minLength !== null) { |
286
|
|
|
if (mb_strlen($data, 'UTF-8') < $this->minLength) { |
287
|
|
|
$this->fail(new StringException('String is too short', StringException::TOO_SHORT), $path); |
288
|
|
|
} |
289
|
|
|
} |
290
|
|
|
if ($this->maxLength !== null) { |
291
|
|
|
if (mb_strlen($data, 'UTF-8') > $this->maxLength) { |
292
|
|
|
$this->fail(new StringException('String is too long', StringException::TOO_LONG), $path); |
293
|
|
|
} |
294
|
|
|
} |
295
|
|
|
if ($this->pattern !== null) { |
296
|
|
|
if (0 === preg_match(Helper::toPregPattern($this->pattern), $data)) { |
297
|
|
|
$this->fail(new StringException(json_encode($data) . ' does not match to ' |
298
|
|
|
. $this->pattern, StringException::PATTERN_MISMATCH), $path); |
299
|
|
|
} |
300
|
|
|
} |
301
|
|
|
} |
302
|
|
|
|
303
|
|
|
if (is_int($data) || is_float($data)) { |
304
|
|
|
if ($this->multipleOf !== null) { |
305
|
|
|
$div = $data / $this->multipleOf; |
306
|
|
|
if ($div != (int)$div) { |
307
|
|
|
$this->fail(new NumericException($data . ' is not multiple of ' . $this->multipleOf, NumericException::MULTIPLE_OF), $path); |
308
|
|
|
} |
309
|
|
|
} |
310
|
|
|
|
311
|
|
|
if ($this->exclusiveMaximum !== null && !is_bool($this->exclusiveMaximum)) { |
312
|
|
|
if ($data >= $this->exclusiveMaximum) { |
313
|
|
|
$this->fail(new NumericException( |
314
|
|
|
'Value less or equal than ' . $this->exclusiveMaximum . ' expected, ' . $data . ' received', |
315
|
|
|
NumericException::MAXIMUM), $path); |
316
|
|
|
} |
317
|
|
|
} |
318
|
|
|
|
319
|
|
|
if ($this->exclusiveMinimum !== null && !is_bool($this->exclusiveMinimum)) { |
320
|
|
|
if ($data <= $this->exclusiveMinimum) { |
321
|
|
|
$this->fail(new NumericException( |
322
|
|
|
'Value more or equal than ' . $this->exclusiveMinimum . ' expected, ' . $data . ' received', |
323
|
|
|
NumericException::MINIMUM), $path); |
324
|
|
|
} |
325
|
|
|
} |
326
|
|
|
|
327
|
|
|
if ($this->maximum !== null) { |
328
|
|
|
if ($this->exclusiveMaximum === true) { |
329
|
|
|
if ($data >= $this->maximum) { |
330
|
|
|
$this->fail(new NumericException( |
331
|
|
|
'Value less or equal than ' . $this->maximum . ' expected, ' . $data . ' received', |
332
|
|
|
NumericException::MAXIMUM), $path); |
333
|
|
|
} |
334
|
|
|
} else { |
335
|
|
|
if ($data > $this->maximum) { |
336
|
|
|
$this->fail(new NumericException( |
337
|
|
|
'Value less than ' . $this->minimum . ' expected, ' . $data . ' received', |
338
|
|
|
NumericException::MAXIMUM), $path); |
339
|
|
|
} |
340
|
|
|
} |
341
|
|
|
} |
342
|
|
|
|
343
|
|
|
if ($this->minimum !== null) { |
344
|
|
|
if ($this->exclusiveMinimum === true) { |
345
|
|
|
if ($data <= $this->minimum) { |
346
|
|
|
$this->fail(new NumericException( |
347
|
|
|
'Value more or equal than ' . $this->minimum . ' expected, ' . $data . ' received', |
348
|
|
|
NumericException::MINIMUM), $path); |
349
|
|
|
} |
350
|
|
|
} else { |
351
|
|
|
if ($data < $this->minimum) { |
352
|
|
|
$this->fail(new NumericException( |
353
|
|
|
'Value more than ' . $this->minimum . ' expected, ' . $data . ' received', |
354
|
|
|
NumericException::MINIMUM), $path); |
355
|
|
|
} |
356
|
|
|
} |
357
|
|
|
} |
358
|
|
|
} |
359
|
|
|
|
360
|
|
|
skipValidation: |
361
|
|
|
|
362
|
|
|
if ($this->oneOf !== null) { |
363
|
|
|
$successes = 0; |
364
|
|
|
$failures = ''; |
365
|
|
|
$skipValidation = false; |
366
|
|
|
if ($options->skipValidation) { |
367
|
|
|
$skipValidation = true; |
368
|
|
|
$options->skipValidation = false; |
369
|
|
|
} |
370
|
|
|
|
371
|
|
|
foreach ($this->oneOf as $index => $item) { |
372
|
|
|
try { |
373
|
|
|
$result = self::unboolSchema($item)->process($data, $options, $path . '->oneOf:' . $index); |
374
|
|
|
$successes++; |
375
|
|
|
if ($successes > 1 || $options->skipValidation) { |
376
|
|
|
break; |
377
|
|
|
} |
378
|
|
|
} catch (InvalidValue $exception) { |
379
|
|
|
$failures .= ' ' . $index . ': ' . Helper::padLines(' ', $exception->getMessage()) . "\n"; |
380
|
|
|
// Expected exception |
381
|
|
|
} |
382
|
|
|
} |
383
|
|
|
if ($skipValidation) { |
384
|
|
|
$options->skipValidation = true; |
385
|
|
|
if ($successes === 0) { |
386
|
|
|
$result = self::unboolSchema($this->oneOf[0])->process($data, $options, $path . '->oneOf:' . 0); |
387
|
|
|
} |
388
|
|
|
} |
389
|
|
|
|
390
|
|
|
if (!$options->skipValidation) { |
391
|
|
|
if ($successes === 0) { |
392
|
|
|
$this->fail(new LogicException('Failed due to logical constraint: no valid results for oneOf {' . "\n" . substr($failures, 0, -1) . "\n}"), $path); |
393
|
|
|
} elseif ($successes > 1) { |
394
|
|
|
$this->fail(new LogicException('Failed due to logical constraint: ' |
395
|
|
|
. $successes . '/' . count($this->oneOf) . ' valid results for oneOf'), $path); |
396
|
|
|
} |
397
|
|
|
} |
398
|
|
|
} |
399
|
|
|
|
400
|
|
|
if ($this->anyOf !== null) { |
401
|
|
|
$successes = 0; |
402
|
|
|
$failures = ''; |
403
|
|
|
foreach ($this->anyOf as $index => $item) { |
404
|
|
|
try { |
405
|
|
|
$result = self::unboolSchema($item)->process($data, $options, $path . '->anyOf:' . $index); |
406
|
|
|
$successes++; |
407
|
|
|
if ($successes) { |
408
|
|
|
break; |
409
|
|
|
} |
410
|
|
|
} catch (InvalidValue $exception) { |
411
|
|
|
$failures .= ' ' . $index . ': ' . $exception->getMessage() . "\n"; |
412
|
|
|
// Expected exception |
413
|
|
|
} |
414
|
|
|
} |
415
|
|
|
if (!$successes && !$options->skipValidation) { |
416
|
|
|
$this->fail(new LogicException('Failed due to logical constraint: no valid results for anyOf {' . "\n" . substr(Helper::padLines(' ', $failures), 0, -1) . "\n}"), $path); |
417
|
|
|
} |
418
|
|
|
} |
419
|
|
|
|
420
|
|
|
if ($this->allOf !== null) { |
421
|
|
|
foreach ($this->allOf as $index => $item) { |
422
|
|
|
$result = self::unboolSchema($item)->process($data, $options, $path . '->allOf' . $index); |
423
|
|
|
} |
424
|
|
|
} |
425
|
|
|
|
426
|
|
|
if ($data instanceof \stdClass) { |
427
|
|
|
if (!$options->skipValidation && $this->required !== null) { |
428
|
|
|
|
429
|
|
|
if (isset($this->__dataToProperty[$options->mapping])) { |
430
|
|
|
if ($import) { |
431
|
|
|
foreach ($this->required as $item) { |
432
|
|
|
if (isset($this->__propertyToData[$options->mapping][$item])) { |
433
|
|
|
$item = $this->__propertyToData[$options->mapping][$item]; |
434
|
|
|
} |
435
|
|
|
if (!property_exists($data, $item)) { |
436
|
|
|
$this->fail(new ObjectException('Required property missing: ' . $item, ObjectException::REQUIRED), $path); |
437
|
|
|
} |
438
|
|
|
} |
439
|
|
|
} else { |
440
|
|
|
foreach ($this->required as $item) { |
441
|
|
|
if (isset($this->__dataToProperty[$options->mapping][$item])) { |
442
|
|
|
$item = $this->__dataToProperty[$options->mapping][$item]; |
443
|
|
|
} |
444
|
|
|
if (!property_exists($data, $item)) { |
445
|
|
|
$this->fail(new ObjectException('Required property missing: ' . $item, ObjectException::REQUIRED), $path); |
446
|
|
|
} |
447
|
|
|
} |
448
|
|
|
} |
449
|
|
|
|
450
|
|
|
} else { |
451
|
|
|
foreach ($this->required as $item) { |
452
|
|
|
if (!property_exists($data, $item)) { |
453
|
|
|
$this->fail(new ObjectException('Required property missing: ' . $item, ObjectException::REQUIRED), $path); |
454
|
|
|
} |
455
|
|
|
} |
456
|
|
|
} |
457
|
|
|
|
458
|
|
|
} |
459
|
|
|
|
460
|
|
|
if ($import) { |
461
|
|
|
if ($this->useObjectAsArray) { |
462
|
|
|
$result = array(); |
463
|
|
|
} elseif (!$result instanceof ObjectItemContract) { |
464
|
|
|
$result = $this->makeObjectItem($options); |
465
|
|
|
|
466
|
|
|
if ($result instanceof ClassStructure) { |
467
|
|
|
if ($result->__validateOnSet) { |
468
|
|
|
$result->__validateOnSet = false; |
469
|
|
|
/** @noinspection PhpUnusedLocalVariableInspection */ |
470
|
|
|
$validateOnSetHandler = new ScopeExit(function () use ($result) { |
|
|
|
|
471
|
|
|
$result->__validateOnSet = true; |
472
|
|
|
}); |
473
|
|
|
} |
474
|
|
|
} |
475
|
|
|
|
476
|
|
|
if ($result instanceof ObjectItemContract) { |
477
|
|
|
$result->setDocumentPath($path); |
478
|
|
|
} |
479
|
|
|
} |
480
|
|
|
} |
481
|
|
|
|
482
|
|
|
if ($import) { |
483
|
|
|
try { |
484
|
|
|
while ( |
485
|
|
|
isset($data->{self::REF}) |
486
|
|
|
&& is_string($data->{self::REF}) |
487
|
|
|
&& !isset($this->properties[self::REF]) |
488
|
|
|
) { |
489
|
|
|
$refString = $data->{self::REF}; |
490
|
|
|
// TODO consider process # by reference here ? |
491
|
|
|
$refResolver = $options->refResolver; |
492
|
|
|
$preRefScope = $refResolver->getResolutionScope(); |
493
|
|
|
/** @noinspection PhpUnusedLocalVariableInspection */ |
494
|
|
|
$deferRefScope = new ScopeExit(function () use ($preRefScope, $refResolver) { |
|
|
|
|
495
|
|
|
$refResolver->setResolutionScope($preRefScope); |
496
|
|
|
}); |
497
|
|
|
$ref = $refResolver->resolveReference($refString); |
498
|
|
|
if ($ref->isImported()) { |
499
|
|
|
$refResult = $ref->getImported(); |
500
|
|
|
return $refResult; |
501
|
|
|
} |
502
|
|
|
$data = self::unboolSchemaData($ref->getData()); |
503
|
|
|
if ($result instanceof ObjectItemContract) { |
504
|
|
|
$result->setFromRef($refString); |
505
|
|
|
} |
506
|
|
|
$ref->setImported($result); |
507
|
|
|
$refResult = $this->process($data, $options, $path . '->ref:' . $refString, $result); |
508
|
|
|
$ref->setImported($refResult); |
509
|
|
|
return $refResult; |
510
|
|
|
} |
511
|
|
|
} catch (InvalidValue $exception) { |
512
|
|
|
$this->fail($exception, $path); |
513
|
|
|
} |
514
|
|
|
} |
515
|
|
|
|
516
|
|
|
// @todo better check for schema id |
517
|
|
|
|
518
|
|
|
if ($import |
519
|
|
|
&& isset($data->id) |
520
|
|
|
&& ($options->version === Schema::VERSION_DRAFT_04 || $options->version === Schema::VERSION_AUTO) |
521
|
|
|
&& is_string($data->id) /*&& (!isset($this->properties['id']))/* && $this->isMetaSchema($data)*/) { |
|
|
|
|
522
|
|
|
$id = $data->id; |
523
|
|
|
$refResolver = $options->refResolver; |
524
|
|
|
$parentScope = $refResolver->updateResolutionScope($id); |
525
|
|
|
/** @noinspection PhpUnusedLocalVariableInspection */ |
526
|
|
|
$defer = new ScopeExit(function () use ($parentScope, $refResolver) { |
|
|
|
|
527
|
|
|
$refResolver->setResolutionScope($parentScope); |
528
|
|
|
}); |
529
|
|
|
} |
530
|
|
|
|
531
|
|
|
if ($import |
532
|
|
|
&& isset($data->{self::ID}) |
533
|
|
|
&& ($options->version >= Schema::VERSION_DRAFT_06 || $options->version === Schema::VERSION_AUTO) |
534
|
|
|
&& is_string($data->{self::ID}) /*&& (!isset($this->properties['id']))/* && $this->isMetaSchema($data)*/) { |
|
|
|
|
535
|
|
|
$id = $data->{self::ID}; |
536
|
|
|
$refResolver = $options->refResolver; |
537
|
|
|
$parentScope = $refResolver->updateResolutionScope($id); |
538
|
|
|
/** @noinspection PhpUnusedLocalVariableInspection */ |
539
|
|
|
$defer = new ScopeExit(function () use ($parentScope, $refResolver) { |
|
|
|
|
540
|
|
|
$refResolver->setResolutionScope($parentScope); |
541
|
|
|
}); |
542
|
|
|
} |
543
|
|
|
|
544
|
|
|
/** @var Schema[] $properties */ |
545
|
|
|
$properties = null; |
546
|
|
|
|
547
|
|
|
$nestedProperties = null; |
548
|
|
|
if ($this->properties !== null) { |
549
|
|
|
$properties = &$this->properties->toArray(); // TODO check performance of pointer |
550
|
|
|
if ($this->properties instanceof Properties) { |
551
|
|
|
$nestedProperties = $this->properties->getNestedProperties(); |
552
|
|
|
} else { |
553
|
|
|
$nestedProperties = array(); |
554
|
|
|
} |
555
|
|
|
} |
556
|
|
|
|
557
|
|
|
$array = array(); |
558
|
|
|
if (!empty($this->__dataToProperty[$options->mapping])) { |
559
|
|
|
foreach ((array)$data as $key => $value) { |
560
|
|
|
if ($import) { |
561
|
|
|
if (isset($this->__dataToProperty[$options->mapping][$key])) { |
562
|
|
|
$key = $this->__dataToProperty[$options->mapping][$key]; |
563
|
|
|
} |
564
|
|
|
} else { |
565
|
|
|
if (isset($this->__propertyToData[$options->mapping][$key])) { |
566
|
|
|
$key = $this->__propertyToData[$options->mapping][$key]; |
567
|
|
|
} |
568
|
|
|
} |
569
|
|
|
$array[$key] = $value; |
570
|
|
|
} |
571
|
|
|
} else { |
572
|
|
|
$array = (array)$data; |
573
|
|
|
} |
574
|
|
|
|
575
|
|
|
if (!$options->skipValidation) { |
576
|
|
|
if ($this->minProperties !== null && count($array) < $this->minProperties) { |
577
|
|
|
$this->fail(new ObjectException("Not enough properties", ObjectException::TOO_FEW), $path); |
578
|
|
|
} |
579
|
|
|
if ($this->maxProperties !== null && count($array) > $this->maxProperties) { |
580
|
|
|
$this->fail(new ObjectException("Too many properties", ObjectException::TOO_MANY), $path); |
581
|
|
|
} |
582
|
|
|
if ($this->propertyNames !== null) { |
583
|
|
|
$propertyNames = self::unboolSchema($this->propertyNames); |
584
|
|
|
foreach ($array as $key => $tmp) { |
585
|
|
|
$propertyNames->process($key, $options, $path . '->propertyNames:' . $key); |
586
|
|
|
} |
587
|
|
|
} |
588
|
|
|
} |
589
|
|
|
|
590
|
|
|
foreach ($array as $key => $value) { |
591
|
|
|
if ($key === '' && PHP_VERSION_ID < 71000) { |
592
|
|
|
$this->fail(new InvalidValue('Empty property name'), $path); |
593
|
|
|
} |
594
|
|
|
|
595
|
|
|
$found = false; |
596
|
|
|
|
597
|
|
|
if (!$options->skipValidation && !empty($this->dependencies)) { |
598
|
|
|
$deps = $this->dependencies; |
599
|
|
|
if (isset($deps->$key)) { |
600
|
|
|
$dependencies = $deps->$key; |
601
|
|
|
$dependencies = self::unboolSchema($dependencies); |
602
|
|
|
if ($dependencies instanceof Schema) { |
603
|
|
|
$dependencies->process($data, $options, $path . '->dependencies:' . $key); |
604
|
|
|
} else { |
605
|
|
|
foreach ($dependencies as $item) { |
606
|
|
|
if (!property_exists($data, $item)) { |
607
|
|
|
$this->fail(new ObjectException('Dependency property missing: ' . $item, |
608
|
|
|
ObjectException::DEPENDENCY_MISSING), $path); |
609
|
|
|
} |
610
|
|
|
} |
611
|
|
|
} |
612
|
|
|
} |
613
|
|
|
} |
614
|
|
|
|
615
|
|
|
$propertyFound = false; |
616
|
|
|
if (isset($properties[$key])) { |
617
|
|
|
/** @var Schema[] $properties */ |
618
|
|
|
$prop = self::unboolSchema($properties[$key]); |
619
|
|
|
$propertyFound = true; |
620
|
|
|
$found = true; |
621
|
|
|
if ($prop instanceof Schema) { |
622
|
|
|
$value = $prop->process($value, $options, $path . '->properties:' . $key); |
623
|
|
|
} |
624
|
|
|
// @todo process $prop === false |
625
|
|
|
} |
626
|
|
|
|
627
|
|
|
/** @var Egg[] $nestedEggs */ |
628
|
|
|
$nestedEggs = null; |
629
|
|
|
if (isset($nestedProperties[$key])) { |
630
|
|
|
$found = true; |
631
|
|
|
$nestedEggs = $nestedProperties[$key]; |
632
|
|
|
// todo iterate all nested props? |
633
|
|
|
$value = self::unboolSchema($nestedEggs[0]->propertySchema)->process($value, $options, $path . '->nestedProperties:' . $key); |
634
|
|
|
} |
635
|
|
|
|
636
|
|
|
if ($this->patternProperties !== null) { |
637
|
|
|
foreach ($this->patternProperties as $pattern => $propertySchema) { |
638
|
|
|
if (preg_match(Helper::toPregPattern($pattern), $key)) { |
639
|
|
|
$found = true; |
640
|
|
|
$value = self::unboolSchema($propertySchema)->process($value, $options, |
641
|
|
|
$path . '->patternProperties[' . $pattern . ']:' . $key); |
642
|
|
|
if ($import) { |
643
|
|
|
$result->addPatternPropertyName($pattern, $key); |
644
|
|
|
} |
645
|
|
|
//break; // todo manage multiple import data properly (pattern accessor) |
646
|
|
|
} |
647
|
|
|
} |
648
|
|
|
} |
649
|
|
|
if (!$found && $this->additionalProperties !== null) { |
650
|
|
|
if (!$options->skipValidation && $this->additionalProperties === false) { |
651
|
|
|
$this->fail(new ObjectException('Additional properties not allowed'), $path . ':' . $key); |
652
|
|
|
} |
653
|
|
|
|
654
|
|
|
if ($this->additionalProperties instanceof Schema) { |
655
|
|
|
$value = $this->additionalProperties->process($value, $options, $path . '->additionalProperties:' . $key); |
656
|
|
|
} |
657
|
|
|
|
658
|
|
|
if ($import && !$this->useObjectAsArray) { |
659
|
|
|
$result->addAdditionalPropertyName($key); |
660
|
|
|
} |
661
|
|
|
} |
662
|
|
|
|
663
|
|
|
if ($nestedEggs && $import) { |
664
|
|
|
foreach ($nestedEggs as $nestedEgg) { |
665
|
|
|
$result->setNestedProperty($key, $value, $nestedEgg); |
666
|
|
|
} |
667
|
|
|
if ($propertyFound) { |
668
|
|
|
$result->$key = $value; |
669
|
|
|
} |
670
|
|
|
} else { |
671
|
|
|
if ($this->useObjectAsArray && $import) { |
672
|
|
|
$result[$key] = $value; |
673
|
|
|
} else { |
674
|
|
|
if ($found || !$import) { |
675
|
|
|
$result->$key = $value; |
676
|
|
|
} elseif (!isset($result->$key)) { |
677
|
|
|
$result->$key = $value; |
678
|
|
|
} |
679
|
|
|
} |
680
|
|
|
} |
681
|
|
|
} |
682
|
|
|
|
683
|
|
|
} |
684
|
|
|
|
685
|
|
|
if (is_array($data)) { |
686
|
|
|
$count = count($data); |
687
|
|
|
if (!$options->skipValidation) { |
688
|
|
|
if ($this->minItems !== null && $count < $this->minItems) { |
689
|
|
|
$this->fail(new ArrayException("Not enough items in array"), $path); |
690
|
|
|
} |
691
|
|
|
|
692
|
|
|
if ($this->maxItems !== null && $count > $this->maxItems) { |
693
|
|
|
$this->fail(new ArrayException("Too many items in array"), $path); |
694
|
|
|
} |
695
|
|
|
} |
696
|
|
|
|
697
|
|
|
$pathItems = 'items'; |
698
|
|
|
$this->items = self::unboolSchema($this->items); |
699
|
|
|
if ($this->items instanceof Schema) { |
700
|
|
|
$items = array(); |
701
|
|
|
$additionalItems = $this->items; |
702
|
|
|
} elseif ($this->items === null) { // items defaults to empty schema so everything is valid |
703
|
|
|
$items = array(); |
704
|
|
|
$additionalItems = true; |
705
|
|
|
} else { // listed items |
706
|
|
|
$items = $this->items; |
707
|
|
|
$additionalItems = $this->additionalItems; |
708
|
|
|
$pathItems = 'additionalItems'; |
709
|
|
|
} |
710
|
|
|
|
711
|
|
|
/** |
712
|
|
|
* @var Schema|Schema[] $items |
713
|
|
|
* @var null|bool|Schema $additionalItems |
714
|
|
|
*/ |
715
|
|
|
$itemsLen = is_array($items) ? count($items) : 0; |
716
|
|
|
$index = 0; |
717
|
|
|
foreach ($result as $key => $value) { |
718
|
|
|
if ($index < $itemsLen) { |
719
|
|
|
$itemSchema = self::unboolSchema($items[$index]); |
720
|
|
|
$result[$key] = $itemSchema->process($value, $options, $path . '->items:' . $index); |
721
|
|
|
} else { |
722
|
|
|
if ($additionalItems instanceof Schema) { |
723
|
|
|
$result[$key] = $additionalItems->process($value, $options, $path . '->' . $pathItems |
724
|
|
|
. '[' . $index . ']'); |
725
|
|
|
} elseif (!$options->skipValidation && $additionalItems === false) { |
726
|
|
|
$this->fail(new ArrayException('Unexpected array item'), $path); |
727
|
|
|
} |
728
|
|
|
} |
729
|
|
|
++$index; |
730
|
|
|
} |
731
|
|
|
|
732
|
|
|
if (!$options->skipValidation && $this->uniqueItems) { |
733
|
|
|
if (!UniqueItems::isValid($data)) { |
734
|
|
|
$this->fail(new ArrayException('Array is not unique'), $path); |
735
|
|
|
} |
736
|
|
|
} |
737
|
|
|
|
738
|
|
|
if (!$options->skipValidation && $this->contains !== null) { |
739
|
|
|
/** @var Schema|bool $contains */ |
740
|
|
|
$contains = $this->contains; |
741
|
|
|
if ($contains === false) { |
742
|
|
|
$this->fail(new ArrayException('Contains is false'), $path); |
743
|
|
|
} |
744
|
|
|
if ($count === 0) { |
745
|
|
|
$this->fail(new ArrayException('Empty array fails contains constraint'), $path); |
746
|
|
|
} |
747
|
|
|
if ($contains === true) { |
748
|
|
|
$contains = self::unboolSchema($contains); |
749
|
|
|
} |
750
|
|
|
$containsOk = false; |
751
|
|
|
foreach ($data as $key => $item) { |
752
|
|
|
try { |
753
|
|
|
$contains->process($item, $options, $path . '->' . $key); |
754
|
|
|
$containsOk = true; |
755
|
|
|
break; |
756
|
|
|
} catch (InvalidValue $exception) { |
|
|
|
|
757
|
|
|
} |
758
|
|
|
} |
759
|
|
|
if (!$containsOk) { |
760
|
|
|
$this->fail(new ArrayException('Array fails contains constraint'), $path); |
761
|
|
|
} |
762
|
|
|
} |
763
|
|
|
} |
764
|
|
|
|
765
|
|
|
return $result; |
766
|
|
|
} |
767
|
|
|
|
768
|
|
|
/** |
769
|
|
|
* @param boolean $useObjectAsArray |
770
|
|
|
* @return Schema |
771
|
|
|
*/ |
772
|
|
|
public |
773
|
|
|
function setUseObjectAsArray($useObjectAsArray) |
774
|
|
|
{ |
775
|
|
|
$this->useObjectAsArray = $useObjectAsArray; |
776
|
|
|
return $this; |
777
|
|
|
} |
778
|
|
|
|
779
|
|
|
private function fail(InvalidValue $exception, $path) |
780
|
|
|
{ |
781
|
|
|
if ($path !== '#') { |
782
|
|
|
$exception->addPath($path); |
783
|
|
|
} |
784
|
|
|
throw $exception; |
785
|
|
|
} |
786
|
|
|
|
787
|
|
|
public static function integer() |
788
|
|
|
{ |
789
|
|
|
$schema = new static(); |
790
|
|
|
$schema->type = Type::INTEGER; |
791
|
|
|
return $schema; |
792
|
|
|
} |
793
|
|
|
|
794
|
|
|
public static function number() |
795
|
|
|
{ |
796
|
|
|
$schema = new static(); |
797
|
|
|
$schema->type = Type::NUMBER; |
798
|
|
|
return $schema; |
799
|
|
|
} |
800
|
|
|
|
801
|
|
|
public static function string() |
802
|
|
|
{ |
803
|
|
|
$schema = new static(); |
804
|
|
|
$schema->type = Type::STRING; |
805
|
|
|
return $schema; |
806
|
|
|
} |
807
|
|
|
|
808
|
|
|
public static function boolean() |
809
|
|
|
{ |
810
|
|
|
$schema = new static(); |
811
|
|
|
$schema->type = Type::BOOLEAN; |
812
|
|
|
return $schema; |
813
|
|
|
} |
814
|
|
|
|
815
|
|
|
public static function object() |
816
|
|
|
{ |
817
|
|
|
$schema = new static(); |
818
|
|
|
$schema->type = Type::OBJECT; |
819
|
|
|
return $schema; |
820
|
|
|
} |
821
|
|
|
|
822
|
|
|
public static function arr() |
823
|
|
|
{ |
824
|
|
|
$schema = new static(); |
825
|
|
|
$schema->type = Type::ARR; |
826
|
|
|
return $schema; |
827
|
|
|
} |
828
|
|
|
|
829
|
|
|
public static function null() |
830
|
|
|
{ |
831
|
|
|
$schema = new static(); |
832
|
|
|
$schema->type = Type::NULL; |
833
|
|
|
return $schema; |
834
|
|
|
} |
835
|
|
|
|
836
|
|
|
|
837
|
|
|
/** |
838
|
|
|
* @param Properties $properties |
839
|
|
|
* @return Schema |
840
|
|
|
*/ |
841
|
|
|
public function setProperties($properties) |
842
|
|
|
{ |
843
|
|
|
$this->properties = $properties; |
844
|
|
|
return $this; |
845
|
|
|
} |
846
|
|
|
|
847
|
|
|
/** |
848
|
|
|
* @param string $name |
849
|
|
|
* @param Schema $schema |
850
|
|
|
* @return $this |
851
|
|
|
*/ |
852
|
|
|
public function setProperty($name, $schema) |
853
|
|
|
{ |
854
|
|
|
if (null === $this->properties) { |
855
|
|
|
$this->properties = new Properties(); |
856
|
|
|
} |
857
|
|
|
$this->properties->__set($name, $schema); |
858
|
|
|
return $this; |
859
|
|
|
} |
860
|
|
|
|
861
|
|
|
/** @var Meta[] */ |
862
|
|
|
private $metaItems = array(); |
863
|
|
|
|
864
|
|
|
public function addMeta(Meta $meta) |
865
|
|
|
{ |
866
|
|
|
$this->metaItems[get_class($meta)] = $meta; |
867
|
|
|
return $this; |
868
|
|
|
} |
869
|
|
|
|
870
|
|
|
public function getMeta($className) |
871
|
|
|
{ |
872
|
|
|
if (isset($this->metaItems[$className])) { |
873
|
|
|
return $this->metaItems[$className]; |
874
|
|
|
} |
875
|
|
|
return null; |
876
|
|
|
} |
877
|
|
|
|
878
|
|
|
/** |
879
|
|
|
* @param Context $options |
880
|
|
|
* @return ObjectItemContract |
881
|
|
|
*/ |
882
|
|
|
public function makeObjectItem(Context $options = null) |
883
|
|
|
{ |
884
|
|
|
if (null === $this->objectItemClass) { |
885
|
|
|
return new ObjectItem(); |
886
|
|
|
} else { |
887
|
|
|
$className = $this->objectItemClass; |
888
|
|
|
if ($options !== null) { |
889
|
|
|
if (isset($options->objectItemClassMapping[$className])) { |
890
|
|
|
$className = $options->objectItemClassMapping[$className]; |
891
|
|
|
} |
892
|
|
|
} |
893
|
|
|
return new $className; |
894
|
|
|
} |
895
|
|
|
} |
896
|
|
|
|
897
|
|
|
/** |
898
|
|
|
* @param mixed $schema |
899
|
|
|
* @return mixed|Schema |
900
|
|
|
*/ |
901
|
|
|
private static function unboolSchema($schema) |
902
|
|
|
{ |
903
|
|
|
static $trueSchema; |
904
|
|
|
static $falseSchema; |
905
|
|
|
|
906
|
|
|
if (null === $trueSchema) { |
907
|
|
|
$trueSchema = new Schema(); |
908
|
|
|
$falseSchema = new Schema(); |
909
|
|
|
$falseSchema->not = $trueSchema; |
910
|
|
|
} |
911
|
|
|
|
912
|
|
|
if ($schema === true) { |
913
|
|
|
return $trueSchema; |
914
|
|
|
} elseif ($schema === false) { |
915
|
|
|
return $falseSchema; |
916
|
|
|
} else { |
917
|
|
|
return $schema; |
918
|
|
|
} |
919
|
|
|
} |
920
|
|
|
|
921
|
|
|
/** |
922
|
|
|
* @param mixed $data |
923
|
|
|
* @return \stdClass |
924
|
|
|
*/ |
925
|
|
|
private static function unboolSchemaData($data) |
926
|
|
|
{ |
927
|
|
|
static $trueSchema; |
928
|
|
|
static $falseSchema; |
929
|
|
|
|
930
|
|
|
if (null === $trueSchema) { |
931
|
|
|
$trueSchema = new \stdClass(); |
932
|
|
|
$falseSchema = new \stdClass(); |
933
|
|
|
$falseSchema->not = $trueSchema; |
934
|
|
|
} |
935
|
|
|
|
936
|
|
|
if ($data === true) { |
937
|
|
|
return $trueSchema; |
938
|
|
|
} elseif ($data === false) { |
939
|
|
|
return $falseSchema; |
940
|
|
|
} else { |
941
|
|
|
return $data; |
942
|
|
|
} |
943
|
|
|
} |
944
|
|
|
|
945
|
|
|
} |
946
|
|
|
|
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.