1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Swaggest\JsonSchema; |
4
|
|
|
|
5
|
|
|
|
6
|
|
|
use PhpLang\ScopeExit; |
7
|
|
|
use Swaggest\JsonSchema\Constraint\Properties; |
8
|
|
|
use Swaggest\JsonSchema\Constraint\Type; |
9
|
|
|
use Swaggest\JsonSchema\Constraint\UniqueItems; |
10
|
|
|
use Swaggest\JsonSchema\Exception\ArrayException; |
11
|
|
|
use Swaggest\JsonSchema\Exception\EnumException; |
12
|
|
|
use Swaggest\JsonSchema\Exception\LogicException; |
13
|
|
|
use Swaggest\JsonSchema\Exception\NumericException; |
14
|
|
|
use Swaggest\JsonSchema\Exception\ObjectException; |
15
|
|
|
use Swaggest\JsonSchema\Exception\StringException; |
16
|
|
|
use Swaggest\JsonSchema\Exception\TypeException; |
17
|
|
|
use Swaggest\JsonSchema\Structure\ClassStructure; |
18
|
|
|
use Swaggest\JsonSchema\Structure\Egg; |
19
|
|
|
use Swaggest\JsonSchema\Structure\ObjectItem; |
20
|
|
|
|
21
|
|
|
/** |
22
|
|
|
* Class Schema |
23
|
|
|
* @package Swaggest\JsonSchema |
24
|
|
|
*/ |
25
|
|
|
class Schema extends ObjectItem |
26
|
|
|
{ |
27
|
|
|
const SCHEMA_DRAFT_04_URL = 'http://json-schema.org/draft-04/schema'; |
28
|
|
|
|
29
|
|
|
/* |
|
|
|
|
30
|
|
|
public $__seqId; |
31
|
|
|
public static $seq = 0; |
32
|
|
|
|
33
|
|
|
public function __construct() |
34
|
|
|
{ |
35
|
|
|
self::$seq++; |
36
|
|
|
$this->__seqId = self::$seq; |
37
|
|
|
} |
38
|
|
|
//*/ |
39
|
|
|
|
40
|
|
|
/** @var Type */ |
41
|
|
|
public $type; |
42
|
|
|
|
43
|
|
|
// Object |
44
|
|
|
/** @var Properties|Schema[] */ |
45
|
|
|
public $properties; |
46
|
|
|
/** @var Schema|bool */ |
47
|
|
|
public $additionalProperties; |
48
|
|
|
/** @var Schema[] */ |
49
|
|
|
public $patternProperties; |
50
|
|
|
/** @var string[] */ |
51
|
|
|
public $required; |
52
|
|
|
/** @var string[][]|Schema[] */ |
53
|
|
|
public $dependencies; |
54
|
|
|
/** @var int */ |
55
|
|
|
public $minProperties; |
56
|
|
|
/** @var int */ |
57
|
|
|
public $maxProperties; |
58
|
|
|
|
59
|
|
|
// Array |
60
|
|
|
/** @var Schema|Schema[] */ |
61
|
|
|
public $items; |
62
|
|
|
/** @var Schema|bool */ |
63
|
|
|
public $additionalItems; |
64
|
|
|
/** @var bool */ |
65
|
|
|
public $uniqueItems; |
66
|
|
|
/** @var int */ |
67
|
|
|
public $minItems; |
68
|
|
|
/** @var int */ |
69
|
|
|
public $maxItems; |
70
|
|
|
|
71
|
|
|
// Reference |
72
|
|
|
/** @var string */ |
73
|
|
|
public $ref; |
74
|
|
|
|
75
|
|
|
// Enum |
76
|
|
|
/** @var array */ |
77
|
|
|
public $enum; |
78
|
|
|
|
79
|
|
|
// Number |
80
|
|
|
/** @var int */ |
81
|
|
|
public $maximum; |
82
|
|
|
/** @var bool */ |
83
|
|
|
public $exclusiveMaximum; |
84
|
|
|
/** @var int */ |
85
|
|
|
public $minimum; |
86
|
|
|
/** @var bool */ |
87
|
|
|
public $exclusiveMinimum; |
88
|
|
|
/** @var float|int */ |
89
|
|
|
public $multipleOf; |
90
|
|
|
|
91
|
|
|
|
92
|
|
|
// String |
93
|
|
|
/** @var string */ |
94
|
|
|
public $pattern; |
95
|
|
|
/** @var int */ |
96
|
|
|
public $minLength; |
97
|
|
|
/** @var int */ |
98
|
|
|
public $maxLength; |
99
|
|
|
/** @var string */ |
100
|
|
|
public $format; |
101
|
|
|
|
102
|
|
|
const FORMAT_DATE_TIME = 'date-time'; // todo implement |
103
|
|
|
|
104
|
|
|
|
105
|
|
|
/** @var Schema[] */ |
106
|
|
|
public $allOf; |
107
|
|
|
/** @var Schema */ |
108
|
|
|
public $not; |
109
|
|
|
/** @var Schema[] */ |
110
|
|
|
public $anyOf; |
111
|
|
|
/** @var Schema[] */ |
112
|
|
|
public $oneOf; |
113
|
|
|
|
114
|
|
|
public $objectItemClass; |
115
|
|
|
private $useObjectAsArray = false; |
116
|
|
|
|
117
|
|
|
private $__dataToProperty = array(); |
118
|
|
|
private $__propertyToData = array(); |
119
|
|
|
|
120
|
|
|
|
121
|
|
|
public function addPropertyMapping($dataName, $propertyName) |
122
|
|
|
{ |
123
|
|
|
$this->__dataToProperty[$dataName] = $propertyName; |
124
|
|
|
$this->__propertyToData[$propertyName] = $dataName; |
125
|
|
|
return $this; |
126
|
|
|
} |
127
|
|
|
|
128
|
|
|
private function preProcessReferences($data, ProcessingOptions $options = null) |
129
|
|
|
{ |
130
|
|
|
if (is_array($data)) { |
131
|
|
|
foreach ($data as $key => $item) { |
132
|
|
|
$this->preProcessReferences($item, $options); |
133
|
|
|
} |
134
|
|
|
} elseif ($data instanceof \stdClass) { |
135
|
|
|
/** @var JsonSchema $data */ |
136
|
|
|
if (isset($data->id) && is_string($data->id)) { |
137
|
|
|
$prev = $options->refResolver->setupResolutionScope($data->id, $data); |
138
|
|
|
/** @noinspection PhpUnusedLocalVariableInspection */ |
139
|
|
|
$_ = new ScopeExit(function () use ($prev, $options) { |
|
|
|
|
140
|
|
|
$options->refResolver->setResolutionScope($prev); |
141
|
|
|
}); |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
foreach ((array)$data as $key => $value) { |
145
|
|
|
$this->preProcessReferences($value, $options); |
146
|
|
|
} |
147
|
|
|
} |
148
|
|
|
} |
149
|
|
|
|
150
|
|
|
public function import($data, ProcessingOptions $options = null) |
151
|
|
|
{ |
152
|
|
|
if ($options === null) { |
153
|
|
|
$options = new ProcessingOptions(); |
154
|
|
|
} |
155
|
|
|
|
156
|
|
|
$options->import = true; |
|
|
|
|
157
|
|
|
|
158
|
|
|
$options->refResolver = new RefResolver($data); |
159
|
|
|
if ($options->remoteRefProvider) { |
160
|
|
|
$options->refResolver->setRemoteRefProvider($options->remoteRefProvider); |
161
|
|
|
} |
162
|
|
|
|
163
|
|
|
if ($options->import) { |
|
|
|
|
164
|
|
|
$this->preProcessReferences($data, $options); |
165
|
|
|
} |
166
|
|
|
|
167
|
|
|
return $this->process($data, $options, '#'); |
168
|
|
|
} |
169
|
|
|
|
170
|
|
|
public function export($data, ProcessingOptions $options = null) |
171
|
|
|
{ |
172
|
|
|
if ($options === null) { |
173
|
|
|
$options = new ProcessingOptions(); |
174
|
|
|
} |
175
|
|
|
|
176
|
|
|
$options->import = false; |
|
|
|
|
177
|
|
|
return $this->process($data, $options); |
178
|
|
|
} |
179
|
|
|
|
180
|
|
|
public function process($data, ProcessingOptions $options, $path = '#', $result = null) |
181
|
|
|
{ |
182
|
|
|
|
183
|
|
|
$import = $options->import; |
|
|
|
|
184
|
|
|
//$pathTrace = explode('->', $path); |
|
|
|
|
185
|
|
|
|
186
|
|
|
if (!$import && $data instanceof ObjectItem) { |
187
|
|
|
$data = $data->jsonSerialize(); |
188
|
|
|
} |
189
|
|
|
if (!$import && is_array($data) && $this->useObjectAsArray) { |
190
|
|
|
$data = (object)$data; |
191
|
|
|
} |
192
|
|
|
|
193
|
|
|
if (null !== $options->dataPreProcessor) { |
194
|
|
|
$data = $options->dataPreProcessor->process($data, $this, $import); |
195
|
|
|
} |
196
|
|
|
|
197
|
|
|
if ($result === null) { |
198
|
|
|
$result = $data; |
199
|
|
|
} |
200
|
|
|
|
201
|
|
|
if ($this->type !== null) { |
202
|
|
|
if (!Type::isValid($this->type, $data)) { |
203
|
|
|
$this->fail(new TypeException(ucfirst( |
204
|
|
|
implode(', ', is_array($this->type) ? $this->type : array($this->type)) |
205
|
|
|
. ' expected, ' . json_encode($data) . ' received') |
206
|
|
|
), $path); |
207
|
|
|
} |
208
|
|
|
} |
209
|
|
|
|
210
|
|
|
if ($this->enum !== null) { |
211
|
|
|
$enumOk = false; |
212
|
|
|
foreach ($this->enum as $item) { |
213
|
|
|
if ($item === $data) { // todo support complex structures here |
214
|
|
|
$enumOk = true; |
215
|
|
|
break; |
216
|
|
|
} |
217
|
|
|
} |
218
|
|
|
if (!$enumOk) { |
219
|
|
|
$this->fail(new EnumException('Enum failed'), $path); |
220
|
|
|
} |
221
|
|
|
} |
222
|
|
|
|
223
|
|
|
if ($this->not !== null) { |
224
|
|
|
$exception = false; |
225
|
|
|
try { |
226
|
|
|
$this->not->process($data, $options, $path . '->not'); |
227
|
|
|
} catch (InvalidValue $exception) { |
228
|
|
|
// Expected exception |
229
|
|
|
} |
230
|
|
|
if ($exception === false) { |
231
|
|
|
$this->fail(new LogicException('Failed due to logical constraint: not'), $path); |
232
|
|
|
} |
233
|
|
|
} |
234
|
|
|
|
235
|
|
|
if ($this->oneOf !== null) { |
236
|
|
|
$successes = 0; |
237
|
|
|
$failures = ''; |
238
|
|
|
foreach ($this->oneOf as $index => $item) { |
239
|
|
|
try { |
240
|
|
|
$result = $item->process($data, $options, $path . '->oneOf:' . $index); |
241
|
|
|
$successes++; |
242
|
|
|
if ($successes > 1) { |
243
|
|
|
break; |
244
|
|
|
} |
245
|
|
|
} catch (InvalidValue $exception) { |
246
|
|
|
$failures .= ' ' . $index . ': ' . $exception->getMessage() . "\n"; |
247
|
|
|
// Expected exception |
248
|
|
|
} |
249
|
|
|
} |
250
|
|
|
if ($successes === 0) { |
251
|
|
|
$this->fail(new LogicException('Failed due to logical constraint: no valid results for oneOf {' . "\n" . substr($failures, 0, -1) . "\n}"), $path); |
252
|
|
|
} elseif ($successes > 1) { |
253
|
|
|
$this->fail(new LogicException('Failed due to logical constraint: ' |
254
|
|
|
. $successes . '/' . count($this->oneOf) . ' valid results for oneOf'), $path); |
255
|
|
|
} |
256
|
|
|
} |
257
|
|
|
|
258
|
|
|
if ($this->anyOf !== null) { |
259
|
|
|
$successes = 0; |
260
|
|
|
$failures = ''; |
261
|
|
|
foreach ($this->anyOf as $index => $item) { |
262
|
|
|
try { |
263
|
|
|
$result = $item->process($data, $options, $path . '->anyOf:' . $index); |
264
|
|
|
$successes++; |
265
|
|
|
if ($successes) { |
266
|
|
|
break; |
267
|
|
|
} |
268
|
|
|
} catch (InvalidValue $exception) { |
269
|
|
|
$failures .= ' ' . $index . ': ' . $exception->getMessage() . "\n"; |
270
|
|
|
// Expected exception |
271
|
|
|
} |
272
|
|
|
} |
273
|
|
|
if (!$successes) { |
274
|
|
|
$this->fail(new LogicException('Failed due to logical constraint: no valid results for anyOf {' . "\n" . substr($failures, 0, -1) . "\n}"), $path); |
275
|
|
|
} |
276
|
|
|
} |
277
|
|
|
|
278
|
|
|
if ($this->allOf !== null) { |
279
|
|
|
foreach ($this->allOf as $index => $item) { |
280
|
|
|
$result = $item->process($data, $options, $path . '->allOf' . $index); |
281
|
|
|
} |
282
|
|
|
} |
283
|
|
|
|
284
|
|
|
|
285
|
|
|
if (is_string($data)) { |
286
|
|
|
if ($this->minLength !== null) { |
287
|
|
|
if (mb_strlen($data, 'UTF-8') < $this->minLength) { |
288
|
|
|
$this->fail(new StringException('String is too short', StringException::TOO_SHORT), $path); |
289
|
|
|
} |
290
|
|
|
} |
291
|
|
|
if ($this->maxLength !== null) { |
292
|
|
|
if (mb_strlen($data, 'UTF-8') > $this->maxLength) { |
293
|
|
|
$this->fail(new StringException('String is too long', StringException::TOO_LONG), $path); |
294
|
|
|
} |
295
|
|
|
} |
296
|
|
|
if ($this->pattern !== null) { |
297
|
|
|
if (0 === preg_match(Helper::toPregPattern($this->pattern), $data)) { |
298
|
|
|
$this->fail(new StringException(json_encode($data) . ' does not match to ' |
299
|
|
|
. $this->pattern, StringException::PATTERN_MISMATCH), $path); |
300
|
|
|
} |
301
|
|
|
} |
302
|
|
|
} |
303
|
|
|
|
304
|
|
|
if (is_int($data) || is_float($data)) { |
305
|
|
|
if ($this->multipleOf !== null) { |
306
|
|
|
$div = $data / $this->multipleOf; |
307
|
|
|
if ($div != (int)$div) { |
308
|
|
|
$this->fail(new NumericException($data . ' is not multiple of ' . $this->multipleOf, NumericException::MULTIPLE_OF), $path); |
309
|
|
|
} |
310
|
|
|
} |
311
|
|
|
|
312
|
|
|
if ($this->maximum !== null) { |
313
|
|
|
if ($this->exclusiveMaximum === true) { |
314
|
|
|
if ($data >= $this->maximum) { |
315
|
|
|
$this->fail(new NumericException( |
316
|
|
|
'Value less or equal than ' . $this->minimum . ' expected, ' . $data . ' received', |
317
|
|
|
NumericException::MAXIMUM), $path); |
318
|
|
|
} |
319
|
|
|
} else { |
320
|
|
|
if ($data > $this->maximum) { |
321
|
|
|
$this->fail(new NumericException( |
322
|
|
|
'Value less than ' . $this->minimum . ' expected, ' . $data . ' received', |
323
|
|
|
NumericException::MAXIMUM), $path); |
324
|
|
|
} |
325
|
|
|
} |
326
|
|
|
} |
327
|
|
|
|
328
|
|
|
if ($this->minimum !== null) { |
329
|
|
|
if ($this->exclusiveMinimum === true) { |
330
|
|
|
if ($data <= $this->minimum) { |
331
|
|
|
$this->fail(new NumericException( |
332
|
|
|
'Value more or equal than ' . $this->minimum . ' expected, ' . $data . ' received', |
333
|
|
|
NumericException::MINIMUM), $path); |
334
|
|
|
} |
335
|
|
|
} else { |
336
|
|
|
if ($data < $this->minimum) { |
337
|
|
|
$this->fail(new NumericException( |
338
|
|
|
'Value more than ' . $this->minimum . ' expected, ' . $data . ' received', |
339
|
|
|
NumericException::MINIMUM), $path); |
340
|
|
|
} |
341
|
|
|
} |
342
|
|
|
} |
343
|
|
|
} |
344
|
|
|
|
345
|
|
|
|
346
|
|
|
if ($data instanceof \stdClass) { |
347
|
|
|
if ($this->required !== null) { |
348
|
|
|
foreach ($this->required as $item) { |
349
|
|
|
if (!property_exists($data, $item)) { |
350
|
|
|
$this->fail(new ObjectException('Required property missing: ' . $item, ObjectException::REQUIRED), $path); |
351
|
|
|
} |
352
|
|
|
} |
353
|
|
|
} |
354
|
|
|
|
355
|
|
|
if ($import) { |
356
|
|
|
if ($this->useObjectAsArray) { |
357
|
|
|
$result = array(); |
358
|
|
|
} elseif (!$result instanceof ObjectItem) { |
359
|
|
|
$result = $this->makeObjectItem(); |
360
|
|
|
|
361
|
|
|
if ($result instanceof ClassStructure) { |
362
|
|
|
if ($result->__validateOnSet) { |
|
|
|
|
363
|
|
|
$result->__validateOnSet = false; |
|
|
|
|
364
|
|
|
/** @noinspection PhpUnusedLocalVariableInspection */ |
365
|
|
|
$validateOnSetHandler = new ScopeExit(function () use ($result) { |
|
|
|
|
366
|
|
|
$result->__validateOnSet = true; |
|
|
|
|
367
|
|
|
}); |
368
|
|
|
} |
369
|
|
|
} |
370
|
|
|
|
371
|
|
|
if ($result instanceof ObjectItem) { |
372
|
|
|
$result->__documentPath = $path; |
373
|
|
|
} |
374
|
|
|
} |
375
|
|
|
} |
376
|
|
|
|
377
|
|
|
if ($import) { |
378
|
|
|
try { |
379
|
|
|
while ( |
380
|
|
|
isset($data->{'$ref'}) |
381
|
|
|
&& is_string($data->{'$ref'}) |
382
|
|
|
&& !isset($this->properties['$ref']) |
383
|
|
|
) { |
384
|
|
|
$refString = $data->{'$ref'}; |
385
|
|
|
$preRefScope = $options->refResolver->getResolutionScope(); |
386
|
|
|
/** @noinspection PhpUnusedLocalVariableInspection */ |
387
|
|
|
$deferRefScope = new ScopeExit(function () use ($preRefScope, $options) { |
|
|
|
|
388
|
|
|
$options->refResolver->setResolutionScope($preRefScope); |
389
|
|
|
}); |
390
|
|
|
$ref = $options->refResolver->resolveReference($refString); |
391
|
|
|
if ($ref->isImported()) { |
392
|
|
|
$refResult = $ref->getImported(); |
393
|
|
|
return $refResult; |
394
|
|
|
} |
395
|
|
|
$data = $ref->getData(); |
396
|
|
|
if ($result instanceof ObjectItem) { |
397
|
|
|
$result->__fromRef = $refString; |
398
|
|
|
} |
399
|
|
|
$ref->setImported($result); |
400
|
|
|
$refResult = $this->process($data, $options, $path . '->ref:' . $refString, $result); |
401
|
|
|
$ref->setImported($refResult); |
402
|
|
|
return $refResult; |
403
|
|
|
} |
404
|
|
|
} catch (InvalidValue $exception) { |
405
|
|
|
$this->fail($exception, $path); |
406
|
|
|
} |
407
|
|
|
} |
408
|
|
|
|
409
|
|
|
// @todo better check for schema id |
410
|
|
|
|
411
|
|
|
if ($import && isset($data->id) && is_string($data->id) /*&& (!isset($this->properties['id']))/* && $this->isMetaSchema($data)*/) { |
|
|
|
|
412
|
|
|
$id = $data->id; |
413
|
|
|
$refResolver = $options->refResolver; |
414
|
|
|
$parentScope = $refResolver->updateResolutionScope($id); |
415
|
|
|
/** @noinspection PhpUnusedLocalVariableInspection */ |
416
|
|
|
$defer = new ScopeExit(function () use ($parentScope, $refResolver) { |
|
|
|
|
417
|
|
|
$refResolver->setResolutionScope($parentScope); |
418
|
|
|
}); |
419
|
|
|
} |
420
|
|
|
|
421
|
|
|
if ($this->properties !== null) { |
422
|
|
|
/** @var Schema[] $properties */ |
423
|
|
|
$properties = &$this->properties->toArray(); // TODO check performance of pointer |
424
|
|
|
if ($this->properties instanceof Properties) { |
425
|
|
|
$nestedProperties = $this->properties->getNestedProperties(); |
426
|
|
|
} else { |
427
|
|
|
$nestedProperties = array(); |
428
|
|
|
} |
429
|
|
|
} |
430
|
|
|
|
431
|
|
|
$array = array(); |
432
|
|
|
if (!empty($this->__dataToProperty)) { |
433
|
|
|
foreach ((array)$data as $key => $value) { |
434
|
|
|
if ($import) { |
435
|
|
|
if (isset($this->__dataToProperty[$key])) { |
436
|
|
|
$key = $this->__dataToProperty[$key]; |
437
|
|
|
} |
438
|
|
|
} else { |
439
|
|
|
if (isset($this->__propertyToData[$key])) { |
440
|
|
|
$key = $this->__propertyToData[$key]; |
441
|
|
|
} |
442
|
|
|
} |
443
|
|
|
$array[$key] = $value; |
444
|
|
|
} |
445
|
|
|
} else { |
446
|
|
|
$array = (array)$data; |
447
|
|
|
} |
448
|
|
|
|
449
|
|
|
if ($this->minProperties !== null && count($array) < $this->minProperties) { |
450
|
|
|
$this->fail(new ObjectException("Not enough properties", ObjectException::TOO_FEW), $path); |
451
|
|
|
} |
452
|
|
|
if ($this->maxProperties !== null && count($array) > $this->maxProperties) { |
453
|
|
|
$this->fail(new ObjectException("Too many properties", ObjectException::TOO_MANY), $path); |
454
|
|
|
} |
455
|
|
|
foreach ($array as $key => $value) { |
456
|
|
|
if ($key === '' && PHP_VERSION_ID < 71000) { |
457
|
|
|
$this->fail(new InvalidValue('Empty property name'), $path); |
458
|
|
|
} |
459
|
|
|
|
460
|
|
|
$found = false; |
461
|
|
|
if (isset($this->dependencies[$key])) { |
462
|
|
|
$dependencies = $this->dependencies[$key]; |
463
|
|
|
if ($dependencies instanceof Schema) { |
464
|
|
|
$dependencies->process($data, $options, $path . '->dependencies:' . $key); |
465
|
|
|
} else { |
466
|
|
|
foreach ($dependencies as $item) { |
467
|
|
|
if (!property_exists($data, $item)) { |
468
|
|
|
$this->fail(new ObjectException('Dependency property missing: ' . $item, |
469
|
|
|
ObjectException::DEPENDENCY_MISSING), $path); |
470
|
|
|
} |
471
|
|
|
} |
472
|
|
|
} |
473
|
|
|
} |
474
|
|
|
|
475
|
|
|
$propertyFound = false; |
476
|
|
|
if (isset($properties[$key])) { |
477
|
|
|
$prop = $properties[$key]; |
478
|
|
|
$propertyFound = true; |
479
|
|
|
$found = true; |
480
|
|
|
$value = $prop->process($value, $options, $path . '->properties:' . $key); |
481
|
|
|
} |
482
|
|
|
|
483
|
|
|
/** @var Egg[] $nestedEggs */ |
484
|
|
|
$nestedEggs = null; |
485
|
|
|
if (isset($nestedProperties[$key])) { |
486
|
|
|
$found = true; |
487
|
|
|
$nestedEggs = $nestedProperties[$key]; |
488
|
|
|
// todo iterate all nested props? |
489
|
|
|
$value = $nestedEggs[0]->propertySchema->process($value, $options, $path . '->nestedProperties:' . $key); |
490
|
|
|
} |
491
|
|
|
|
492
|
|
|
if ($this->patternProperties !== null) { |
493
|
|
|
foreach ($this->patternProperties as $pattern => $propertySchema) { |
494
|
|
|
if (preg_match(Helper::toPregPattern($pattern), $key)) { |
495
|
|
|
$found = true; |
496
|
|
|
$value = $propertySchema->process($value, $options, |
497
|
|
|
$path . '->patternProperties[' . $pattern . ']:' . $key); |
498
|
|
|
if ($import) { |
499
|
|
|
$result->addPatternPropertyName($pattern, $key); |
500
|
|
|
} |
501
|
|
|
//break; // todo manage multiple import data properly (pattern accessor) |
502
|
|
|
} |
503
|
|
|
} |
504
|
|
|
} |
505
|
|
|
if (!$found && $this->additionalProperties !== null) { |
506
|
|
|
if ($this->additionalProperties === false) { |
507
|
|
|
$this->fail(new ObjectException('Additional properties not allowed'), $path . ':' . $key); |
508
|
|
|
} |
509
|
|
|
|
510
|
|
|
$value = $this->additionalProperties->process($value, $options, $path . '->additionalProperties:' . $key); |
511
|
|
|
if ($import && !$this->useObjectAsArray) { |
512
|
|
|
$result->addAdditionalPropertyName($key); |
513
|
|
|
} |
514
|
|
|
} |
515
|
|
|
|
516
|
|
|
if ($nestedEggs && $import) { |
517
|
|
|
foreach ($nestedEggs as $nestedEgg) { |
518
|
|
|
$result->setNestedProperty($key, $value, $nestedEgg); |
519
|
|
|
} |
520
|
|
|
if ($propertyFound) { |
521
|
|
|
$result->$key = $value; |
522
|
|
|
} |
523
|
|
|
} else { |
524
|
|
|
if ($this->useObjectAsArray && $import) { |
525
|
|
|
$result[$key] = $value; |
526
|
|
|
} else { |
527
|
|
|
if ($found || !$import) { |
528
|
|
|
$result->$key = $value; |
529
|
|
|
} elseif (!isset($result->$key)) { |
530
|
|
|
$result->$key = $value; |
531
|
|
|
} |
532
|
|
|
} |
533
|
|
|
} |
534
|
|
|
} |
535
|
|
|
|
536
|
|
|
} |
537
|
|
|
|
538
|
|
|
if (is_array($data)) { |
539
|
|
|
|
540
|
|
|
if ($this->minItems !== null && count($data) < $this->minItems) { |
541
|
|
|
$this->fail(new ArrayException("Not enough items in array"), $path); |
542
|
|
|
} |
543
|
|
|
|
544
|
|
|
if ($this->maxItems !== null && count($data) > $this->maxItems) { |
545
|
|
|
$this->fail(new ArrayException("Too many items in array"), $path); |
546
|
|
|
} |
547
|
|
|
|
548
|
|
|
$pathItems = 'items'; |
549
|
|
|
if ($this->items instanceof Schema) { |
550
|
|
|
$items = array(); |
551
|
|
|
$additionalItems = $this->items; |
552
|
|
|
} elseif ($this->items === null) { // items defaults to empty schema so everything is valid |
553
|
|
|
$items = array(); |
554
|
|
|
$additionalItems = true; |
555
|
|
|
} else { // listed items |
556
|
|
|
$items = $this->items; |
557
|
|
|
$additionalItems = $this->additionalItems; |
558
|
|
|
$pathItems = 'additionalItems'; |
559
|
|
|
} |
560
|
|
|
|
561
|
|
|
if ($items !== null || $additionalItems !== null) { |
562
|
|
|
$itemsLen = is_array($items) ? count($items) : 0; |
563
|
|
|
$index = 0; |
564
|
|
|
foreach ($result as $key => $value) { |
565
|
|
|
if ($index < $itemsLen) { |
566
|
|
|
$result[$key] = $items[$index]->process($value, $options, $path . '->items:' . $index); |
567
|
|
|
} else { |
568
|
|
|
if ($additionalItems instanceof Schema) { |
569
|
|
|
$result[$key] = $additionalItems->process($value, $options, $path . '->' . $pathItems |
570
|
|
|
. '[' . $index . ']'); |
571
|
|
|
} elseif ($additionalItems === false) { |
572
|
|
|
$this->fail(new ArrayException('Unexpected array item'), $path); |
573
|
|
|
} |
574
|
|
|
} |
575
|
|
|
++$index; |
576
|
|
|
} |
577
|
|
|
} |
578
|
|
|
|
579
|
|
|
if ($this->uniqueItems) { |
580
|
|
|
if (!UniqueItems::isValid($data)) { |
581
|
|
|
$this->fail(new ArrayException('Array is not unique'), $path); |
582
|
|
|
} |
583
|
|
|
} |
584
|
|
|
} |
585
|
|
|
|
586
|
|
|
return $result; |
587
|
|
|
} |
588
|
|
|
|
589
|
|
|
/** |
590
|
|
|
* @param boolean $useObjectAsArray |
591
|
|
|
* @return Schema |
592
|
|
|
*/ |
593
|
|
|
public function setUseObjectAsArray($useObjectAsArray) |
594
|
|
|
{ |
595
|
|
|
$this->useObjectAsArray = $useObjectAsArray; |
596
|
|
|
return $this; |
597
|
|
|
} |
598
|
|
|
|
599
|
|
|
/** |
600
|
|
|
* @param bool|Schema $additionalProperties |
601
|
|
|
* @return Schema |
602
|
|
|
*/ |
603
|
|
|
public function setAdditionalProperties($additionalProperties) |
604
|
|
|
{ |
605
|
|
|
$this->additionalProperties = $additionalProperties; |
606
|
|
|
return $this; |
607
|
|
|
} |
608
|
|
|
|
609
|
|
|
/** |
610
|
|
|
* @param Schema|Schema[] $items |
611
|
|
|
* @return Schema |
612
|
|
|
*/ |
613
|
|
|
public function setItems($items) |
614
|
|
|
{ |
615
|
|
|
$this->items = $items; |
616
|
|
|
return $this; |
617
|
|
|
} |
618
|
|
|
|
619
|
|
|
|
620
|
|
|
private function fail(InvalidValue $exception, $path) |
621
|
|
|
{ |
622
|
|
|
if ($path !== '#') { |
623
|
|
|
$exception->addPath($path); |
624
|
|
|
} |
625
|
|
|
throw $exception; |
626
|
|
|
} |
627
|
|
|
|
628
|
|
|
public static function integer() |
629
|
|
|
{ |
630
|
|
|
$schema = new static(); |
631
|
|
|
$schema->type = Type::INTEGER; |
632
|
|
|
return $schema; |
633
|
|
|
} |
634
|
|
|
|
635
|
|
|
public static function number() |
636
|
|
|
{ |
637
|
|
|
$schema = new static(); |
638
|
|
|
$schema->type = Type::NUMBER; |
639
|
|
|
return $schema; |
640
|
|
|
} |
641
|
|
|
|
642
|
|
|
public static function string() |
643
|
|
|
{ |
644
|
|
|
$schema = new static(); |
645
|
|
|
$schema->type = Type::STRING; |
646
|
|
|
return $schema; |
647
|
|
|
} |
648
|
|
|
|
649
|
|
|
public static function boolean() |
650
|
|
|
{ |
651
|
|
|
$schema = new static(); |
652
|
|
|
$schema->type = Type::BOOLEAN; |
653
|
|
|
return $schema; |
654
|
|
|
} |
655
|
|
|
|
656
|
|
|
public static function object() |
657
|
|
|
{ |
658
|
|
|
$schema = new static(); |
659
|
|
|
$schema->type = Type::OBJECT; |
660
|
|
|
return $schema; |
661
|
|
|
} |
662
|
|
|
|
663
|
|
|
public static function arr() |
664
|
|
|
{ |
665
|
|
|
$schema = new static(); |
666
|
|
|
$schema->type = Type::ARR; |
667
|
|
|
return $schema; |
668
|
|
|
} |
669
|
|
|
|
670
|
|
|
public static function null() |
671
|
|
|
{ |
672
|
|
|
$schema = new static(); |
673
|
|
|
$schema->type = Type::NULL; |
674
|
|
|
return $schema; |
675
|
|
|
} |
676
|
|
|
|
677
|
|
|
|
678
|
|
|
public static function create() |
679
|
|
|
{ |
680
|
|
|
$schema = new static(); |
681
|
|
|
return $schema; |
682
|
|
|
} |
683
|
|
|
|
684
|
|
|
|
685
|
|
|
/** |
686
|
|
|
* @param Properties $properties |
687
|
|
|
* @return Schema |
688
|
|
|
*/ |
689
|
|
|
public function setProperties($properties) |
690
|
|
|
{ |
691
|
|
|
$this->properties = $properties; |
692
|
|
|
return $this; |
693
|
|
|
} |
694
|
|
|
|
695
|
|
|
public function setProperty($name, Schema $schema) |
696
|
|
|
{ |
697
|
|
|
if (null === $this->properties) { |
698
|
|
|
$this->properties = new Properties(); |
699
|
|
|
} |
700
|
|
|
$this->properties->__set($name, $schema); |
701
|
|
|
return $this; |
702
|
|
|
} |
703
|
|
|
|
704
|
|
|
/** @var Meta[] */ |
705
|
|
|
private $metaItems = array(); |
706
|
|
|
|
707
|
|
|
public function meta(Meta $meta) |
708
|
|
|
{ |
709
|
|
|
$this->metaItems[get_class($meta)] = $meta; |
710
|
|
|
return $this; |
711
|
|
|
} |
712
|
|
|
|
713
|
|
|
public function getMeta($className) |
714
|
|
|
{ |
715
|
|
|
if (isset($this->metaItems[$className])) { |
716
|
|
|
return $this->metaItems[$className]; |
717
|
|
|
} |
718
|
|
|
return null; |
719
|
|
|
} |
720
|
|
|
|
721
|
|
|
/** |
722
|
|
|
* @return ObjectItem |
723
|
|
|
*/ |
724
|
|
|
public function makeObjectItem() |
725
|
|
|
{ |
726
|
|
|
if (null === $this->objectItemClass) { |
727
|
|
|
return new ObjectItem(); |
728
|
|
|
} else { |
729
|
|
|
return new $this->objectItemClass; |
730
|
|
|
} |
731
|
|
|
} |
732
|
|
|
} |
733
|
|
|
|
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.