1 | <?php |
||||
2 | /** |
||||
3 | * @author Todd Burry <[email protected]> |
||||
4 | * @copyright 2009-2018 Vanilla Forums Inc. |
||||
5 | * @license MIT |
||||
6 | */ |
||||
7 | |||||
8 | namespace Garden\Schema; |
||||
9 | |||||
10 | /** |
||||
11 | * A class for defining and validating data schemas. |
||||
12 | */ |
||||
13 | class Schema implements \JsonSerializable, \ArrayAccess { |
||||
14 | /** |
||||
15 | * Trigger a notice when extraneous properties are encountered during validation. |
||||
16 | */ |
||||
17 | const VALIDATE_EXTRA_PROPERTY_NOTICE = 0x1; |
||||
18 | |||||
19 | /** |
||||
20 | * Throw a ValidationException when extraneous properties are encountered during validation. |
||||
21 | */ |
||||
22 | const VALIDATE_EXTRA_PROPERTY_EXCEPTION = 0x2; |
||||
23 | |||||
24 | /** |
||||
25 | * Validate string lengths as unicode characters instead of bytes. |
||||
26 | */ |
||||
27 | const VALIDATE_STRING_LENGTH_AS_UNICODE = 0x4; |
||||
28 | |||||
29 | /** |
||||
30 | * @var array All the known types. |
||||
31 | * |
||||
32 | * If this is ever given some sort of public access then remove the static. |
||||
33 | */ |
||||
34 | private static $types = [ |
||||
35 | 'array' => ['a'], |
||||
36 | 'object' => ['o'], |
||||
37 | 'integer' => ['i', 'int'], |
||||
38 | 'string' => ['s', 'str'], |
||||
39 | 'number' => ['f', 'float'], |
||||
40 | 'boolean' => ['b', 'bool'], |
||||
41 | |||||
42 | // Psuedo-types |
||||
43 | 'timestamp' => ['ts'], // type: integer, format: timestamp |
||||
44 | 'datetime' => ['dt'], // type: string, format: date-time |
||||
45 | 'null' => ['n'], // Adds nullable: true |
||||
46 | ]; |
||||
47 | |||||
48 | /** |
||||
49 | * @var string The regular expression to strictly determine if a string is a date. |
||||
50 | */ |
||||
51 | private static $DATE_REGEX = '`^\d{4}-\d{2}-\d{2}([ T]\d{2}:\d{2}(:\d{2})?)?`i'; |
||||
52 | |||||
53 | private $schema = []; |
||||
54 | |||||
55 | /** |
||||
56 | * @var int A bitwise combination of the various **Schema::FLAG_*** constants. |
||||
57 | */ |
||||
58 | private $flags = 0; |
||||
59 | |||||
60 | /** |
||||
61 | * @var array An array of callbacks that will filter data in the schema. |
||||
62 | */ |
||||
63 | private $filters = []; |
||||
64 | |||||
65 | /** |
||||
66 | * @var array An array of callbacks that will custom validate the schema. |
||||
67 | */ |
||||
68 | private $validators = []; |
||||
69 | |||||
70 | /** |
||||
71 | * @var string|Validation The name of the class or an instance that will be cloned. |
||||
72 | * @deprecated |
||||
73 | */ |
||||
74 | private $validationClass = Validation::class; |
||||
75 | |||||
76 | /** |
||||
77 | * @var callable A callback is used to create validation objects. |
||||
78 | */ |
||||
79 | private $validationFactory = [Validation::class, 'createValidation']; |
||||
80 | |||||
81 | /** |
||||
82 | * @var callable |
||||
83 | */ |
||||
84 | private $refLookup; |
||||
85 | |||||
86 | /// Methods /// |
||||
87 | |||||
88 | /** |
||||
89 | 298 | * Initialize an instance of a new {@link Schema} class. |
|||
90 | 298 | * |
|||
91 | * @param array $schema The array schema to validate against. |
||||
92 | 276 | * @param callable $refLookup The function used to lookup references. |
|||
93 | */ |
||||
94 | 1 | public function __construct(array $schema = [], callable $refLookup = null) { |
|||
95 | 276 | $this->schema = $schema; |
|||
96 | 298 | ||||
97 | $this->refLookup = $refLookup ?? function (/** @scrutinizer ignore-unused */ |
||||
98 | string $_) { |
||||
99 | return null; |
||||
100 | }; |
||||
101 | } |
||||
102 | |||||
103 | /** |
||||
104 | * Parse a short schema and return the associated schema. |
||||
105 | 179 | * |
|||
106 | 179 | * @param array $arr The schema array. |
|||
107 | 179 | * @param mixed[] $args Constructor arguments for the schema instance. |
|||
108 | 177 | * @return static Returns a new schema. |
|||
109 | */ |
||||
110 | public static function parse(array $arr, ...$args) { |
||||
111 | $schema = new static([], ...$args); |
||||
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||||
112 | $schema->schema = $schema->parseInternal($arr); |
||||
113 | return $schema; |
||||
114 | } |
||||
115 | |||||
116 | /** |
||||
117 | * Parse a schema in short form into a full schema array. |
||||
118 | 179 | * |
|||
119 | 179 | * @param array $arr The array to parse into a schema. |
|||
120 | * @return array The full schema array. |
||||
121 | 6 | * @throws ParseException Throws an exception when an item in the schema is invalid. |
|||
122 | 174 | */ |
|||
123 | protected function parseInternal(array $arr): array { |
||||
124 | 2 | if (empty($arr)) { |
|||
125 | // An empty schema validates to anything. |
||||
126 | return []; |
||||
127 | 173 | } elseif (isset($arr['type'])) { |
|||
128 | 173 | // This is a long form schema and can be parsed as the root. |
|||
129 | 173 | return $this->parseNode($arr); |
|||
0 ignored issues
–
show
|
|||||
130 | 108 | } else { |
|||
131 | 108 | // Check for a root schema. |
|||
132 | $value = reset($arr); |
||||
133 | 173 | $key = key($arr); |
|||
134 | 171 | if (is_int($key)) { |
|||
135 | 63 | $key = $value; |
|||
136 | $value = null; |
||||
137 | } |
||||
138 | list ($name, $param) = $this->parseShortParam($key, $value); |
||||
139 | if (empty($name)) { |
||||
140 | 111 | return $this->parseNode($param, $value); |
|||
141 | } |
||||
142 | } |
||||
143 | 111 | ||||
144 | 111 | // If we are here then this is n object schema. |
|||
145 | 111 | list($properties, $required) = $this->parseProperties($arr); |
|||
146 | |||||
147 | $result = [ |
||||
148 | 111 | 'type' => 'object', |
|||
149 | 'properties' => $properties, |
||||
150 | 'required' => $required |
||||
151 | ]; |
||||
152 | |||||
153 | return array_filter($result); |
||||
154 | } |
||||
155 | |||||
156 | /** |
||||
157 | * Parse a schema node. |
||||
158 | * |
||||
159 | 172 | * @param array|Schema $node The node to parse. |
|||
160 | 172 | * @param mixed $value Additional information from the node. |
|||
161 | 66 | * @return array|\ArrayAccess Returns a JSON schema compatible node. |
|||
162 | * @throws ParseException Throws an exception if there was a problem parsing the schema node. |
||||
163 | */ |
||||
164 | private function parseNode($node, $value = null) { |
||||
165 | if (is_array($value)) { |
||||
166 | 66 | if (is_array($node['type'])) { |
|||
167 | 66 | trigger_error('Schemas with multiple types are deprecated.', E_USER_DEPRECATED); |
|||
168 | 11 | } |
|||
169 | |||||
170 | 4 | // The value describes a bit more about the schema. |
|||
171 | switch ($node['type']) { |
||||
172 | 7 | case 'array': |
|||
173 | if (isset($value['items'])) { |
||||
174 | 11 | // The value includes array schema information. |
|||
175 | 56 | $node = array_replace($node, $value); |
|||
0 ignored issues
–
show
It seems like
$node can also be of type Garden\Schema\Schema ; however, parameter $array of array_replace() does only seem to accept array , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
176 | } else { |
||||
177 | 12 | $node['items'] = $this->parseInternal($value); |
|||
178 | } |
||||
179 | break; |
||||
180 | 12 | case 'object': |
|||
181 | 12 | // The value is a schema of the object. |
|||
182 | 12 | if (isset($value['properties'])) { |
|||
183 | list($node['properties']) = $this->parseProperties($value['properties']); |
||||
184 | } else { |
||||
185 | 12 | list($node['properties'], $required) = $this->parseProperties($value); |
|||
186 | if (!empty($required)) { |
||||
187 | 44 | $node['required'] = $required; |
|||
188 | 66 | } |
|||
189 | } |
||||
190 | 132 | break; |
|||
191 | 102 | default: |
|||
192 | 6 | $node = array_replace($node, $value); |
|||
193 | 98 | break; |
|||
194 | 102 | } |
|||
195 | } elseif (is_string($value)) { |
||||
196 | 35 | if ($node['type'] === 'array' && $arrType = $this->getType($value)) { |
|||
197 | $node['items'] = ['type' => $arrType]; |
||||
198 | 31 | } elseif (!empty($value)) { |
|||
199 | $node['description'] = $value; |
||||
200 | } |
||||
201 | 31 | } elseif ($value === null) { |
|||
202 | 1 | // Parse child elements. |
|||
203 | if ($node['type'] === 'array' && isset($node['items'])) { |
||||
204 | // The value includes array schema information. |
||||
205 | $node['items'] = $this->parseInternal($node['items']); |
||||
0 ignored issues
–
show
It seems like
$node['items'] can also be of type null ; however, parameter $arr of Garden\Schema\Schema::parseInternal() does only seem to accept array , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
206 | 172 | } elseif ($node['type'] === 'object' && isset($node['properties'])) { |
|||
207 | 171 | list($node['properties']) = $this->parseProperties($node['properties']); |
|||
208 | 1 | } |
|||
209 | } |
||||
210 | 171 | ||||
211 | if (is_array($node)) { |
||||
212 | 171 | if (!empty($node['allowNull'])) { |
|||
213 | 4 | $node['nullable'] = true; |
|||
214 | } |
||||
215 | unset($node['allowNull']); |
||||
216 | |||||
217 | 172 | if ($node['type'] === null || $node['type'] === []) { |
|||
218 | unset($node['type']); |
||||
219 | } |
||||
220 | } |
||||
221 | |||||
222 | return $node; |
||||
223 | } |
||||
224 | |||||
225 | /** |
||||
226 | * Parse the schema for an object's properties. |
||||
227 | 112 | * |
|||
228 | 112 | * @param array $arr An object property schema. |
|||
229 | 112 | * @return array Returns a schema array suitable to be placed in the **properties** key of a schema. |
|||
230 | 112 | * @throws ParseException Throws an exception if a property name cannot be determined for an array item. |
|||
231 | */ |
||||
232 | 112 | private function parseProperties(array $arr): array { |
|||
233 | 82 | $properties = []; |
|||
234 | 82 | $requiredProperties = []; |
|||
235 | 82 | foreach ($arr as $key => $value) { |
|||
236 | // Fix a schema specified as just a value. |
||||
237 | if (is_int($key)) { |
||||
238 | if (is_string($value)) { |
||||
239 | $key = $value; |
||||
240 | $value = ''; |
||||
241 | } else { |
||||
242 | 112 | throw new ParseException("Schema at position $key is not a valid parameter.", 500); |
|||
243 | } |
||||
244 | 112 | } |
|||
245 | |||||
246 | 112 | // The parameter is defined in the key. |
|||
247 | 112 | list($name, $param, $required) = $this->parseShortParam($key, $value); |
|||
0 ignored issues
–
show
$value of type string is incompatible with the type array expected by parameter $value of Garden\Schema\Schema::parseShortParam() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
248 | 112 | ||||
249 | $node = $this->parseNode($param, $value); |
||||
250 | |||||
251 | 112 | $properties[$name] = $node; |
|||
252 | if ($required) { |
||||
253 | $requiredProperties[] = $name; |
||||
254 | } |
||||
255 | } |
||||
256 | return [$properties, $requiredProperties]; |
||||
257 | } |
||||
258 | |||||
259 | /** |
||||
260 | * Parse a short parameter string into a full array parameter. |
||||
261 | * |
||||
262 | 174 | * @param string $key The short parameter string to parse. |
|||
263 | * @param array $value An array of other information that might help resolve ambiguity. |
||||
264 | 174 | * @return array Returns an array in the form `[string name, array param, bool required]`. |
|||
265 | 70 | * @throws ParseException Throws an exception if the short param is not in the correct format. |
|||
266 | 70 | */ |
|||
267 | public function parseShortParam(string $key, $value = []): array { |
||||
268 | 126 | // Is the parameter optional? |
|||
269 | if (substr($key, -1) === '?') { |
||||
270 | $required = false; |
||||
271 | $key = substr($key, 0, -1); |
||||
272 | 174 | } else { |
|||
273 | 168 | $required = true; |
|||
274 | 168 | } |
|||
275 | |||||
276 | // Check for a type. |
||||
277 | 168 | if (false !== ($pos = strrpos($key, ':'))) { |
|||
278 | 2 | $name = substr($key, 0, $pos); |
|||
279 | 168 | $typeStr = substr($key, $pos + 1); |
|||
280 | |||||
281 | // Kludge for names with colons that are not specifying an array of a type. |
||||
282 | 16 | if (isset($value['type']) && 'array' !== $this->getType($typeStr)) { |
|||
283 | 16 | $name = $key; |
|||
284 | $typeStr = ''; |
||||
285 | 174 | } |
|||
286 | 174 | } else { |
|||
287 | $name = $key; |
||||
288 | 174 | $typeStr = ''; |
|||
289 | 166 | } |
|||
290 | 166 | $types = []; |
|||
291 | 166 | $param = []; |
|||
292 | 166 | ||||
293 | 1 | if (!empty($typeStr)) { |
|||
294 | 165 | $shortTypes = explode('|', $typeStr); |
|||
295 | 9 | foreach ($shortTypes as $alias) { |
|||
296 | 9 | $found = $this->getType($alias); |
|||
297 | 157 | if ($found === null) { |
|||
298 | 12 | throw new ParseException("Unknown type '$alias'.", 500); |
|||
299 | 12 | } elseif ($found === 'datetime') { |
|||
300 | 151 | $param['format'] = 'date-time'; |
|||
301 | 11 | $types[] = 'string'; |
|||
302 | } elseif ($found === 'timestamp') { |
||||
303 | 165 | $param['format'] = 'timestamp'; |
|||
304 | $types[] = 'integer'; |
||||
305 | } elseif ($found === 'null') { |
||||
306 | $nullable = true; |
||||
307 | } else { |
||||
308 | 173 | $types[] = $found; |
|||
309 | 6 | } |
|||
310 | 1 | } |
|||
311 | } |
||||
312 | 6 | ||||
313 | if ($value instanceof Schema) { |
||||
314 | 171 | if (count($types) === 1 && $types[0] === 'array') { |
|||
315 | 10 | $param += ['type' => $types[0], 'items' => $value]; |
|||
316 | } else { |
||||
317 | 10 | $param = $value; |
|||
318 | } |
||||
319 | } elseif (isset($value['type'])) { |
||||
320 | $param = $value + $param; |
||||
321 | 10 | ||||
322 | if (!empty($types) && $types !== (array)$param['type']) { |
||||
323 | $typesStr = implode('|', $types); |
||||
324 | 166 | $paramTypesStr = implode('|', (array)$param['type']); |
|||
325 | |||||
326 | throw new ParseException("Type mismatch between $typesStr and {$paramTypesStr} for field $name.", 500); |
||||
327 | 166 | } |
|||
328 | 4 | } else { |
|||
329 | if (empty($types) && !empty($parts[1])) { |
||||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||||
330 | 165 | throw new ParseException("Invalid type {$parts[1]} for field $name.", 500); |
|||
331 | } |
||||
332 | if (empty($types)) { |
||||
333 | $param += ['type' => null]; |
||||
334 | 166 | } else { |
|||
335 | 41 | $param += ['type' => count($types) === 1 ? $types[0] : $types]; |
|||
336 | } |
||||
337 | |||||
338 | // Parsed required strings have a minimum length of 1. |
||||
339 | 173 | if (in_array('string', $types) && !empty($name) && $required && (!isset($value['default']) || $value['default'] !== '')) { |
|||
340 | 11 | $param['minLength'] = 1; |
|||
341 | } |
||||
342 | } |
||||
343 | 173 | ||||
344 | 1 | if (!empty($nullable)) { |
|||
345 | $param['nullable'] = true; |
||||
346 | } |
||||
347 | 172 | ||||
348 | if (is_array($param['type'])) { |
||||
349 | trigger_error('Schemas with multiple types is deprecated.', E_USER_DEPRECATED); |
||||
350 | } |
||||
351 | |||||
352 | return [$name, $param, $required]; |
||||
353 | } |
||||
354 | |||||
355 | /** |
||||
356 | 168 | * Look up a type based on its alias. |
|||
357 | 168 | * |
|||
358 | * @param string $alias The type alias or type name to lookup. |
||||
359 | * @return mixed |
||||
360 | 168 | */ |
|||
361 | 168 | private function getType($alias) { |
|||
362 | 168 | if (isset(self::$types[$alias])) { |
|||
363 | return $alias; |
||||
364 | } |
||||
365 | 12 | foreach (self::$types as $type => $aliases) { |
|||
366 | if (in_array($alias, $aliases, true)) { |
||||
367 | return $type; |
||||
368 | } |
||||
369 | } |
||||
370 | return null; |
||||
371 | } |
||||
372 | |||||
373 | /** |
||||
374 | 36 | * Unescape a JSON reference segment. |
|||
375 | 36 | * |
|||
376 | * @param string $str The segment to unescapeRef. |
||||
377 | * @return string Returns the unescaped string. |
||||
378 | */ |
||||
379 | public static function unescapeRef(string $str): string { |
||||
380 | return str_replace(['~1', '~0'], ['/', '~'], $str); |
||||
381 | } |
||||
382 | |||||
383 | /** |
||||
384 | 36 | * Explode a references into its individual parts. |
|||
385 | 36 | * |
|||
386 | * @param string $ref A JSON reference. |
||||
387 | * @return string[] The individual parts of the reference. |
||||
388 | */ |
||||
389 | public static function explodeRef(string $ref): array { |
||||
390 | return array_map([self::class, 'unescapeRef'], explode('/', $ref)); |
||||
391 | } |
||||
392 | |||||
393 | 1 | /** |
|||
394 | 1 | * Grab the schema's current description. |
|||
395 | * |
||||
396 | * @return string |
||||
397 | */ |
||||
398 | public function getDescription(): string { |
||||
399 | return $this->schema['description'] ?? ''; |
||||
400 | } |
||||
401 | |||||
402 | /** |
||||
403 | 1 | * Set the description for the schema. |
|||
404 | 1 | * |
|||
405 | 1 | * @param string $description The new description. |
|||
406 | * @return $this |
||||
407 | */ |
||||
408 | public function setDescription(string $description) { |
||||
409 | $this->schema['description'] = $description; |
||||
410 | return $this; |
||||
411 | } |
||||
412 | |||||
413 | 1 | /** |
|||
414 | 1 | * Get the schema's title. |
|||
415 | * |
||||
416 | * @return string Returns the title. |
||||
417 | */ |
||||
418 | public function getTitle(): string { |
||||
419 | return $this->schema['title'] ?? ''; |
||||
420 | } |
||||
421 | |||||
422 | 1 | /** |
|||
423 | 1 | * Set the schema's title. |
|||
424 | 1 | * |
|||
425 | * @param string $title The new title. |
||||
426 | */ |
||||
427 | public function setTitle(string $title) { |
||||
428 | $this->schema['title'] = $title; |
||||
429 | } |
||||
430 | |||||
431 | /** |
||||
432 | * Get a schema field. |
||||
433 | 10 | * |
|||
434 | 10 | * @param string|array $path The JSON schema path of the field with parts separated by dots. |
|||
435 | 10 | * @param mixed $default The value to return if the field isn't found. |
|||
436 | 1 | * @return mixed Returns the field value or `$default`. |
|||
437 | 1 | */ |
|||
438 | public function getField($path, $default = null) { |
||||
439 | 9 | if (is_string($path)) { |
|||
440 | if (strpos($path, '.') !== false && strpos($path, '/') === false) { |
||||
441 | trigger_error('Field selectors must be separated by "/" instead of "."', E_USER_DEPRECATED); |
||||
442 | $path = explode('.', $path); |
||||
443 | 10 | } else { |
|||
444 | 10 | $path = explode('/', $path); |
|||
445 | 10 | } |
|||
446 | 10 | } |
|||
447 | 1 | ||||
448 | 1 | $value = $this->schema; |
|||
449 | foreach ($path as $i => $subKey) { |
||||
450 | 10 | if (is_array($value) && isset($value[$subKey])) { |
|||
451 | $value = $value[$subKey]; |
||||
452 | } elseif ($value instanceof Schema) { |
||||
453 | 10 | return $value->getField(array_slice($path, $i), $default); |
|||
454 | } else { |
||||
455 | return $default; |
||||
456 | } |
||||
457 | } |
||||
458 | return $value; |
||||
459 | } |
||||
460 | |||||
461 | /** |
||||
462 | * Set a schema field. |
||||
463 | 7 | * |
|||
464 | 7 | * @param string|array $path The JSON schema path of the field with parts separated by slashes. |
|||
465 | 7 | * @param mixed $value The new value. |
|||
466 | 1 | * @return $this |
|||
467 | 1 | */ |
|||
468 | public function setField($path, $value) { |
||||
469 | 6 | if (is_string($path)) { |
|||
470 | if (strpos($path, '.') !== false && strpos($path, '/') === false) { |
||||
471 | trigger_error('Field selectors must be separated by "/" instead of "."', E_USER_DEPRECATED); |
||||
472 | $path = explode('.', $path); |
||||
473 | 7 | } else { |
|||
474 | 7 | $path = explode('/', $path); |
|||
475 | 7 | } |
|||
476 | 7 | } |
|||
477 | 7 | ||||
478 | $selection = &$this->schema; |
||||
479 | 1 | foreach ($path as $i => $subSelector) { |
|||
480 | 1 | if (is_array($selection)) { |
|||
481 | 1 | if (!isset($selection[$subSelector])) { |
|||
482 | $selection[$subSelector] = []; |
||||
483 | } |
||||
484 | } elseif ($selection instanceof Schema) { |
||||
485 | 7 | $selection->setField(array_slice($path, $i), $value); |
|||
486 | return $this; |
||||
487 | } else { |
||||
488 | 7 | $selection = [$subSelector => []]; |
|||
489 | 7 | } |
|||
490 | $selection = &$selection[$subSelector]; |
||||
491 | } |
||||
492 | |||||
493 | $selection = $value; |
||||
494 | return $this; |
||||
495 | } |
||||
496 | |||||
497 | 1 | /** |
|||
498 | 1 | * Return the validation flags. |
|||
499 | * |
||||
500 | * @return int Returns a bitwise combination of flags. |
||||
501 | */ |
||||
502 | public function getFlags(): int { |
||||
503 | return $this->flags; |
||||
504 | } |
||||
505 | |||||
506 | /** |
||||
507 | 8 | * Set the validation flags. |
|||
508 | 8 | * |
|||
509 | * @param int $flags One or more of the **Schema::FLAG_*** constants. |
||||
510 | 8 | * @return Schema Returns the current instance for fluent calls. |
|||
511 | */ |
||||
512 | public function setFlags(int $flags) { |
||||
513 | $this->flags = $flags; |
||||
514 | |||||
515 | return $this; |
||||
516 | } |
||||
517 | |||||
518 | /** |
||||
519 | * Set a flag. |
||||
520 | 1 | * |
|||
521 | 1 | * @param int $flag One or more of the **Schema::VALIDATE_*** constants. |
|||
522 | 1 | * @param bool $value Either true or false. |
|||
523 | * @return $this |
||||
524 | 1 | */ |
|||
525 | public function setFlag(int $flag, bool $value) { |
||||
526 | 1 | if ($value) { |
|||
527 | $this->flags = $this->flags | $flag; |
||||
528 | } else { |
||||
529 | $this->flags = $this->flags & ~$flag; |
||||
530 | } |
||||
531 | return $this; |
||||
532 | } |
||||
533 | |||||
534 | /** |
||||
535 | 4 | * Merge a schema with this one. |
|||
536 | 4 | * |
|||
537 | 4 | * @param Schema $schema A scheme instance. Its parameters will be merged into the current instance. |
|||
538 | * @return $this |
||||
539 | */ |
||||
540 | public function merge(Schema $schema) { |
||||
541 | $this->mergeInternal($this->schema, $schema->getSchemaArray(), true, true); |
||||
542 | return $this; |
||||
543 | } |
||||
544 | |||||
545 | /** |
||||
546 | * The internal implementation of schema merging. |
||||
547 | * |
||||
548 | * @param array $target The target of the merge. |
||||
549 | 7 | * @param array $source The source of the merge. |
|||
550 | * @param bool $overwrite Whether or not to replace values. |
||||
551 | 7 | * @param bool $addProperties Whether or not to add object properties to the target. |
|||
552 | 5 | * @return array |
|||
553 | */ |
||||
554 | 5 | private function mergeInternal(array &$target, array $source, $overwrite = true, $addProperties = true) { |
|||
555 | 4 | // We need to do a fix for required properties here. |
|||
556 | 4 | if (isset($target['properties']) && !empty($source['required'])) { |
|||
557 | $required = isset($target['required']) ? $target['required'] : []; |
||||
558 | 4 | ||||
559 | if (isset($source['required']) && $addProperties) { |
||||
560 | $newProperties = array_diff(array_keys($source['properties']), array_keys($target['properties'])); |
||||
561 | $newRequired = array_intersect($source['required'], $newProperties); |
||||
562 | |||||
563 | 7 | $required = array_merge($required, $newRequired); |
|||
564 | 7 | } |
|||
565 | 7 | } |
|||
566 | |||||
567 | 2 | ||||
568 | 2 | foreach ($source as $key => $val) { |
|||
569 | 2 | if (is_array($val) && array_key_exists($key, $target) && is_array($target[$key])) { |
|||
570 | if ($key === 'properties' && !$addProperties) { |
||||
571 | 2 | // We just want to merge the properties that exist in the destination. |
|||
572 | 2 | foreach ($val as $name => $prop) { |
|||
573 | 1 | if (isset($target[$key][$name])) { |
|||
574 | $targetProp = &$target[$key][$name]; |
||||
575 | 1 | ||||
576 | 2 | if (is_array($targetProp) && is_array($prop)) { |
|||
577 | $this->mergeInternal($targetProp, $prop, $overwrite, $addProperties); |
||||
578 | } elseif (is_array($targetProp) && $prop instanceof Schema) { |
||||
579 | $this->mergeInternal($targetProp, $prop->getSchemaArray(), $overwrite, $addProperties); |
||||
580 | 7 | } elseif ($overwrite) { |
|||
581 | 5 | $targetProp = $prop; |
|||
582 | } |
||||
583 | 3 | } |
|||
584 | 3 | } |
|||
585 | 3 | } elseif (isset($val[0]) || isset($target[$key][0])) { |
|||
586 | if ($overwrite) { |
||||
587 | 5 | // This is a numeric array, so just do a merge. |
|||
588 | $merged = array_merge($target[$key], $val); |
||||
589 | if (is_string($merged[0])) { |
||||
590 | 7 | $merged = array_keys(array_flip($merged)); |
|||
591 | } |
||||
592 | 7 | $target[$key] = $merged; |
|||
593 | } |
||||
594 | } else { |
||||
595 | 7 | $target[$key] = $this->mergeInternal($target[$key], $val, $overwrite, $addProperties); |
|||
596 | } |
||||
597 | } elseif (!$overwrite && array_key_exists($key, $target) && !is_array($val)) { |
||||
598 | // Do nothing, we aren't replacing. |
||||
599 | 7 | } else { |
|||
600 | 5 | $target[$key] = $val; |
|||
601 | 1 | } |
|||
602 | } |
||||
603 | 5 | ||||
604 | if (isset($required)) { |
||||
605 | if (empty($required)) { |
||||
606 | unset($target['required']); |
||||
607 | 7 | } else { |
|||
608 | $target['required'] = $required; |
||||
609 | } |
||||
610 | } |
||||
611 | |||||
612 | return $target; |
||||
613 | } |
||||
614 | |||||
615 | /** |
||||
616 | 17 | * Returns the internal schema array. |
|||
617 | 17 | * |
|||
618 | * @return array |
||||
619 | * @see Schema::jsonSerialize() |
||||
620 | */ |
||||
621 | public function getSchemaArray(): array { |
||||
622 | return $this->schema; |
||||
623 | } |
||||
624 | |||||
625 | /** |
||||
626 | * Add another schema to this one. |
||||
627 | * |
||||
628 | * Adding schemas together is analogous to array addition. When you add a schema it will only add missing information. |
||||
629 | 4 | * |
|||
630 | 4 | * @param Schema $schema The schema to add. |
|||
631 | 4 | * @param bool $addProperties Whether to add properties that don't exist in this schema. |
|||
632 | * @return $this |
||||
633 | */ |
||||
634 | public function add(Schema $schema, $addProperties = false) { |
||||
635 | $this->mergeInternal($this->schema, $schema->getSchemaArray(), false, $addProperties); |
||||
636 | return $this; |
||||
637 | } |
||||
638 | |||||
639 | /** |
||||
640 | * Add a custom filter to change data before validation. |
||||
641 | * |
||||
642 | * @param string $fieldname The name of the field to filter, if any. |
||||
643 | * |
||||
644 | 4 | * If you are adding a filter to a deeply nested field then separate the path with dots. |
|||
645 | 4 | * @param callable $callback The callback to filter the field. |
|||
646 | 4 | * @param bool $validate Whether or not the filter should also validate. If true default validation is skipped. |
|||
647 | 4 | * @return $this |
|||
648 | */ |
||||
649 | public function addFilter(string $fieldname, callable $callback, bool $validate = false) { |
||||
650 | $fieldname = $this->parseFieldSelector($fieldname); |
||||
651 | $this->filters[$fieldname][] = [$callback, $validate]; |
||||
652 | return $this; |
||||
653 | } |
||||
654 | |||||
655 | /** |
||||
656 | * Parse a nested field name selector. |
||||
657 | * |
||||
658 | * Field selectors should be separated by "/" characters, but may currently be separated by "." characters which |
||||
659 | 17 | * triggers a deprecated error. |
|||
660 | 17 | * |
|||
661 | 6 | * @param string $field The field selector. |
|||
662 | * @return string Returns the field selector in the correct format. |
||||
663 | */ |
||||
664 | 11 | private function parseFieldSelector(string $field): string { |
|||
665 | 1 | if (strlen($field) === 0) { |
|||
666 | 1 | return $field; |
|||
667 | } |
||||
668 | 1 | ||||
669 | 1 | if (strpos($field, '.') !== false) { |
|||
670 | if (strpos($field, '/') === false) { |
||||
671 | 1 | trigger_error('Field selectors must be separated by "/" instead of "."', E_USER_DEPRECATED); |
|||
672 | |||||
673 | 11 | $parts = explode('.', $field); |
|||
674 | 1 | $parts = @array_map([$this, 'parseFieldSelector'], $parts); // silence because error triggered already. |
|||
675 | 1 | ||||
676 | 10 | $field = implode('/', $parts); |
|||
677 | 3 | } |
|||
678 | 3 | } elseif ($field === '[]') { |
|||
679 | trigger_error('Field selectors with item selector "[]" must be converted to "items".', E_USER_DEPRECATED); |
||||
680 | $field = 'items'; |
||||
681 | 11 | } elseif (strpos($field, '/') === false && !in_array($field, ['items', 'additionalProperties'], true)) { |
|||
682 | 1 | trigger_error("Field selectors must specify full schema paths. ($field)", E_USER_DEPRECATED); |
|||
683 | 1 | $field = "/properties/$field"; |
|||
684 | } |
||||
685 | |||||
686 | 11 | if (strpos($field, '[]') !== false) { |
|||
687 | trigger_error('Field selectors with item selector "[]" must be converted to "/items".', E_USER_DEPRECATED); |
||||
688 | $field = str_replace('[]', '/items', $field); |
||||
689 | } |
||||
690 | |||||
691 | return ltrim($field, '/'); |
||||
692 | } |
||||
693 | |||||
694 | /** |
||||
695 | * Add a custom filter for a schema format. |
||||
696 | * |
||||
697 | * Schemas can use the `format` property to specify a specific format on a field. Adding a filter for a format |
||||
698 | * allows you to customize the behavior of that format. |
||||
699 | * |
||||
700 | 2 | * @param string $format The format to filter. |
|||
701 | 2 | * @param callable $callback The callback used to filter values. |
|||
702 | * @param bool $validate Whether or not the filter should also validate. If true default validation is skipped. |
||||
703 | * @return $this |
||||
704 | */ |
||||
705 | 2 | public function addFormatFilter(string $format, callable $callback, bool $validate = false) { |
|||
706 | 2 | if (empty($format)) { |
|||
707 | throw new \InvalidArgumentException('The filter format cannot be empty.', 500); |
||||
708 | 2 | } |
|||
709 | |||||
710 | $filter = "/format/$format"; |
||||
711 | $this->filters[$filter][] = [$callback, $validate]; |
||||
712 | |||||
713 | return $this; |
||||
714 | } |
||||
715 | |||||
716 | /** |
||||
717 | * Require one of a given set of fields in the schema. |
||||
718 | * |
||||
719 | 3 | * @param array $required The field names to require. |
|||
720 | 3 | * @param string $fieldname The name of the field to attach to. |
|||
721 | 3 | * @param int $count The count of required items. |
|||
722 | 3 | * @return Schema Returns `$this` for fluent calls. |
|||
723 | */ |
||||
724 | 3 | public function requireOneOf(array $required, string $fieldname = '', int $count = 1) { |
|||
725 | 1 | $result = $this->addValidator( |
|||
726 | $fieldname, |
||||
727 | function ($data, ValidationField $field) use ($required, $count) { |
||||
728 | 2 | // This validator does not apply to sparse validation. |
|||
729 | 2 | if ($field->isSparse()) { |
|||
730 | return true; |
||||
731 | 2 | } |
|||
732 | 2 | ||||
733 | $hasCount = 0; |
||||
734 | 2 | $flattened = []; |
|||
735 | |||||
736 | 1 | foreach ($required as $name) { |
|||
737 | 1 | $flattened = array_merge($flattened, (array)$name); |
|||
738 | 1 | ||||
739 | 1 | if (is_array($name)) { |
|||
740 | // This is an array of required names. They all must match. |
||||
741 | 1 | $hasCountInner = 0; |
|||
742 | foreach ($name as $nameInner) { |
||||
743 | if (array_key_exists($nameInner, $data)) { |
||||
744 | 1 | $hasCountInner++; |
|||
745 | 1 | } else { |
|||
746 | break; |
||||
747 | 2 | } |
|||
748 | 1 | } |
|||
749 | if ($hasCountInner >= count($name)) { |
||||
750 | $hasCount++; |
||||
751 | 2 | } |
|||
752 | 2 | } elseif (array_key_exists($name, $data)) { |
|||
753 | $hasCount++; |
||||
754 | } |
||||
755 | |||||
756 | 2 | if ($hasCount >= $count) { |
|||
757 | 1 | return true; |
|||
758 | } |
||||
759 | 1 | } |
|||
760 | |||||
761 | if ($count === 1) { |
||||
762 | 2 | $message = 'One of {properties} are required.'; |
|||
763 | 2 | } else { |
|||
764 | 2 | $message = '{count} of {properties} are required.'; |
|||
765 | 2 | } |
|||
766 | |||||
767 | 2 | $field->addError('oneOfRequired', [ |
|||
768 | 3 | 'messageCode' => $message, |
|||
769 | 'properties' => $required, |
||||
770 | 'count' => $count |
||||
771 | 3 | ]); |
|||
772 | return false; |
||||
773 | } |
||||
774 | ); |
||||
775 | |||||
776 | return $result; |
||||
777 | } |
||||
778 | |||||
779 | /** |
||||
780 | * Add a custom validator to to validate the schema. |
||||
781 | * |
||||
782 | * @param string $fieldname The name of the field to validate, if any. |
||||
783 | 5 | * |
|||
784 | 5 | * If you are adding a validator to a deeply nested field then separate the path with dots. |
|||
785 | 5 | * @param callable $callback The callback to validate with. |
|||
786 | 5 | * @return Schema Returns `$this` for fluent calls. |
|||
787 | */ |
||||
788 | public function addValidator(string $fieldname, callable $callback) { |
||||
789 | $fieldname = $this->parseFieldSelector($fieldname); |
||||
790 | $this->validators[$fieldname][] = $callback; |
||||
791 | return $this; |
||||
792 | } |
||||
793 | |||||
794 | /** |
||||
795 | * Validate data against the schema and return the result. |
||||
796 | * |
||||
797 | 45 | * @param mixed $data The data to validate. |
|||
798 | * @param array $options Validation options. See `Schema::validate()`. |
||||
799 | 45 | * @return bool Returns true if the data is valid. False otherwise. |
|||
800 | 31 | * @throws RefNotFoundException Throws an exception when there is an unknown `$ref` in the schema. |
|||
801 | 24 | */ |
|||
802 | 24 | public function isValid($data, $options = []) { |
|||
803 | try { |
||||
804 | $this->validate($data, $options); |
||||
805 | return true; |
||||
806 | } catch (ValidationException $ex) { |
||||
807 | return false; |
||||
808 | } |
||||
809 | } |
||||
810 | |||||
811 | /** |
||||
812 | * Validate data against the schema. |
||||
813 | * |
||||
814 | * @param mixed $data The data to validate. |
||||
815 | * @param array $options Validation options. |
||||
816 | * |
||||
817 | 240 | * - **sparse**: Whether or not this is a sparse validation. |
|||
818 | 240 | * @return mixed Returns a cleaned version of the data. |
|||
819 | 1 | * @throws ValidationException Throws an exception when the data does not validate against the schema. |
|||
820 | 1 | * @throws RefNotFoundException Throws an exception when a schema `$ref` is not found. |
|||
821 | */ |
||||
822 | 240 | public function validate($data, $options = []) { |
|||
823 | if (is_bool($options)) { |
||||
0 ignored issues
–
show
|
|||||
824 | trigger_error('The $sparse parameter is deprecated. Use [\'sparse\' => true] instead.', E_USER_DEPRECATED); |
||||
825 | 240 | $options = ['sparse' => true]; |
|||
826 | 236 | } |
|||
827 | $options += ['sparse' => false]; |
||||
828 | 236 | ||||
829 | |||||
830 | 232 | list($schema, $schemaPath) = $this->lookupSchema($this->schema, ''); |
|||
831 | $field = new ValidationField($this->createValidation(), $schema, '', $schemaPath, $options); |
||||
832 | 7 | ||||
833 | $clean = $this->validateField($data, $field); |
||||
834 | |||||
835 | 232 | if (Invalid::isInvalid($clean) && $field->isValid()) { |
|||
836 | 82 | // This really shouldn't happen, but we want to protect against seeing the invalid object. |
|||
837 | $field->addError('invalid', ['messageCode' => 'The value is invalid.']); |
||||
838 | } |
||||
839 | 166 | ||||
840 | if (!$field->getValidation()->isValid()) { |
||||
841 | throw new ValidationException($field->getValidation()); |
||||
842 | } |
||||
843 | |||||
844 | return $clean; |
||||
845 | } |
||||
846 | |||||
847 | /** |
||||
848 | * Lookup a schema based on a schema node. |
||||
849 | * |
||||
850 | * The node could be a schema array, `Schema` object, or a schema reference. |
||||
851 | * |
||||
852 | * @param mixed $schema The schema node to lookup with. |
||||
853 | * @param string $schemaPath The current path of the schema. |
||||
854 | 240 | * @return array Returns an array with two elements: |
|||
855 | 240 | * - Schema|array|\ArrayAccess The schema that was found. |
|||
856 | 6 | * - string The path of the schema. This is either the reference or the `$path` parameter for inline schemas. |
|||
857 | * @throws RefNotFoundException Throws an exception when a reference could not be found. |
||||
858 | 240 | */ |
|||
859 | 240 | private function lookupSchema($schema, string $schemaPath) { |
|||
860 | if ($schema instanceof Schema) { |
||||
861 | return [$schema, $schemaPath]; |
||||
862 | 240 | } else { |
|||
863 | 33 | $lookup = $this->getRefLookup(); |
|||
864 | $visited = []; |
||||
865 | 33 | ||||
866 | 1 | // Resolve any references first. |
|||
867 | while (!empty($schema['$ref'])) { |
||||
868 | 33 | $schemaPath = $schema['$ref']; |
|||
869 | |||||
870 | if (isset($visited[$schemaPath])) { |
||||
871 | 33 | throw new RefNotFoundException("Cyclical reference cannot be resolved. ($schemaPath)", 508); |
|||
872 | 1 | } |
|||
873 | 1 | $visited[$schemaPath] = true; |
|||
874 | |||||
875 | 32 | try { |
|||
876 | 3 | $schema = call_user_func($lookup, $schemaPath); |
|||
877 | } catch (\Exception $ex) { |
||||
878 | throw new RefNotFoundException($ex->getMessage(), $ex->getCode(), $ex); |
||||
879 | } |
||||
880 | 236 | if ($schema === null) { |
|||
881 | throw new RefNotFoundException("Schema reference could not be found. ($schemaPath)"); |
||||
882 | } |
||||
883 | } |
||||
884 | |||||
885 | return [$schema, $schemaPath]; |
||||
886 | } |
||||
887 | } |
||||
888 | |||||
889 | 240 | /** |
|||
890 | 240 | * Get the function used to resolve `$ref` lookups. |
|||
891 | * |
||||
892 | * @return callable Returns the current `$ref` lookup. |
||||
893 | */ |
||||
894 | public function getRefLookup(): callable { |
||||
895 | return $this->refLookup; |
||||
896 | } |
||||
897 | |||||
898 | /** |
||||
899 | * Set the function used to resolve `$ref` lookups. |
||||
900 | * |
||||
901 | * The function should have the following signature: |
||||
902 | * |
||||
903 | * ```php |
||||
904 | * function(string $ref): array|Schema|null { |
||||
905 | * ... |
||||
906 | * } |
||||
907 | * ``` |
||||
908 | 10 | * The function should take a string reference and return a schema array, `Schema` or **null**. |
|||
909 | 10 | * |
|||
910 | 10 | * @param callable $refLookup The new lookup function. |
|||
911 | * @return $this |
||||
912 | */ |
||||
913 | public function setRefLookup(callable $refLookup) { |
||||
914 | $this->refLookup = $refLookup; |
||||
915 | return $this; |
||||
916 | } |
||||
917 | |||||
918 | 236 | /** |
|||
919 | 236 | * Create a new validation instance. |
|||
920 | * |
||||
921 | * @return Validation Returns a validation object. |
||||
922 | */ |
||||
923 | protected function createValidation(): Validation { |
||||
924 | return call_user_func($this->getValidationFactory()); |
||||
925 | } |
||||
926 | |||||
927 | 236 | /** |
|||
928 | 236 | * Get factory used to create validation objects. |
|||
929 | * |
||||
930 | * @return callable Returns the current factory. |
||||
931 | */ |
||||
932 | public function getValidationFactory(): callable { |
||||
933 | return $this->validationFactory; |
||||
934 | } |
||||
935 | |||||
936 | /** |
||||
937 | 2 | * Set the factory used to create validation objects. |
|||
938 | 2 | * |
|||
939 | 2 | * @param callable $validationFactory The new factory. |
|||
940 | 2 | * @return $this |
|||
941 | */ |
||||
942 | public function setValidationFactory(callable $validationFactory) { |
||||
943 | $this->validationFactory = $validationFactory; |
||||
944 | $this->validationClass = null; |
||||
0 ignored issues
–
show
The property
Garden\Schema\Schema::$validationClass has been deprecated.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
945 | return $this; |
||||
946 | } |
||||
947 | |||||
948 | /** |
||||
949 | * Validate a field. |
||||
950 | * |
||||
951 | * @param mixed $value The value to validate. |
||||
952 | 236 | * @param ValidationField $field A validation object to add errors to. |
|||
953 | 236 | * @return mixed|Invalid Returns a clean version of the value with all extra fields stripped out or invalid if the value |
|||
954 | 236 | * is completely invalid. |
|||
955 | * @throws RefNotFoundException Throws an exception when a schema `$ref` is not found. |
||||
956 | 236 | */ |
|||
957 | 3 | protected function validateField($value, ValidationField $field) { |
|||
958 | 233 | $validated = false; |
|||
959 | $result = $value = $this->filterField($value, $field, $validated); |
||||
960 | 5 | ||||
961 | 2 | if ($validated) { |
|||
962 | return $result; |
||||
963 | 5 | } elseif ($field->getField() instanceof Schema) { |
|||
964 | try { |
||||
965 | 233 | $result = $field->getField()->validate($value, $field->getOptions()); |
|||
966 | 14 | } catch (ValidationException $ex) { |
|||
967 | // The validation failed, so merge the validations together. |
||||
968 | $field->getValidation()->merge($ex->getValidation(), $field->getName()); |
||||
969 | 233 | } |
|||
970 | 12 | } elseif (($value === null || ($value === '' && !$field->hasType('string'))) && ($field->val('nullable') || $field->hasType('null'))) { |
|||
0 ignored issues
–
show
|
|||||
971 | $result = null; |
||||
972 | } else { |
||||
973 | 232 | // Look for a discriminator. |
|||
974 | 225 | if (!empty($field->val('discriminator'))) { |
|||
975 | 4 | $field = $this->resolveDiscriminator($value, $field); |
|||
976 | } |
||||
977 | |||||
978 | 224 | if ($field !== null) { |
|||
979 | 224 | if($field->hasAllOf()) { |
|||
980 | 29 | $result = $this->validateAllOf($value, $field); |
|||
981 | } else { |
||||
982 | 203 | // Validate the field's type. |
|||
983 | $type = $field->getType(); |
||||
984 | if (is_array($type)) { |
||||
985 | 224 | $result = $this->validateMultipleTypes($value, $type, $field); |
|||
0 ignored issues
–
show
The function
Garden\Schema\Schema::validateMultipleTypes() has been deprecated: Multiple types are being removed next version.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This function has been deprecated. The supplier of the function has supplied an explanatory message. The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead. ![]() |
|||||
986 | 224 | } else { |
|||
987 | $result = $this->validateSingleType($value, $type, $field); |
||||
988 | } |
||||
989 | |||||
990 | 7 | if (Invalid::isValid($result)) { |
|||
991 | $result = $this->validateEnum($result, $field); |
||||
992 | } |
||||
993 | } |
||||
994 | } else { |
||||
995 | 231 | $result = Invalid::value(); |
|||
996 | 215 | } |
|||
997 | } |
||||
998 | |||||
999 | 231 | // Validate a custom field validator. |
|||
1000 | if (Invalid::isValid($result)) { |
||||
1001 | $this->callValidators($result, $field); |
||||
0 ignored issues
–
show
It seems like
$field can also be of type null ; however, parameter $field of Garden\Schema\Schema::callValidators() does only seem to accept Garden\Schema\ValidationField , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
1002 | } |
||||
1003 | |||||
1004 | return $result; |
||||
1005 | } |
||||
1006 | |||||
1007 | /** |
||||
1008 | * Filter a field's value using built in and custom filters. |
||||
1009 | * |
||||
1010 | 236 | * @param mixed $value The original value of the field. |
|||
1011 | * @param ValidationField $field The field information for the field. |
||||
1012 | 236 | * @param bool $validated Whether or not a filter validated the value. |
|||
1013 | 8 | * @return mixed Returns the filtered field or the original field value if there are no filters. |
|||
1014 | 8 | */ |
|||
1015 | 4 | private function filterField($value, ValidationField $field, bool &$validated = false) { |
|||
1016 | 4 | // Check for limited support for Open API style. |
|||
1017 | if (!empty($field->val('style')) && is_string($value)) { |
||||
1018 | $doFilter = true; |
||||
1019 | if ($field->hasType('boolean') && in_array($value, ['true', 'false', '0', '1'], true)) { |
||||
1020 | 8 | $doFilter = false; |
|||
1021 | 4 | } elseif (($field->hasType('integer') || $field->hasType('number')) && is_numeric($value)) { |
|||
1022 | 4 | $doFilter = false; |
|||
1023 | 2 | } |
|||
1024 | 2 | ||||
1025 | 2 | if ($doFilter) { |
|||
1026 | 1 | switch ($field->val('style')) { |
|||
1027 | 1 | case 'form': |
|||
1028 | 1 | $value = explode(',', $value); |
|||
1029 | 1 | break; |
|||
1030 | 1 | case 'spaceDelimited': |
|||
1031 | $value = explode(' ', $value); |
||||
1032 | break; |
||||
1033 | case 'pipeDelimited': |
||||
1034 | $value = explode('|', $value); |
||||
1035 | 236 | break; |
|||
1036 | } |
||||
1037 | 236 | } |
|||
1038 | } |
||||
1039 | |||||
1040 | $value = $this->callFilters($value, $field, $validated); |
||||
1041 | |||||
1042 | return $value; |
||||
1043 | } |
||||
1044 | |||||
1045 | /** |
||||
1046 | * Call all of the filters attached to a field. |
||||
1047 | * |
||||
1048 | 236 | * @param mixed $value The field value being filtered. |
|||
1049 | * @param ValidationField $field The validation object. |
||||
1050 | 236 | * @param bool $validated Whether or not a filter validated the field. |
|||
1051 | 236 | * @return mixed Returns the filtered value. If there are no filters for the field then the original value is returned. |
|||
1052 | 4 | */ |
|||
1053 | 4 | private function callFilters($value, ValidationField $field, bool &$validated = false) { |
|||
1054 | 4 | // Strip array references in the name except for the last one. |
|||
1055 | $key = $field->getSchemaPath(); |
||||
1056 | 4 | if (!empty($this->filters[$key])) { |
|||
1057 | 4 | foreach ($this->filters[$key] as list($filter, $validate)) { |
|||
1058 | $value = call_user_func($filter, $value, $field); |
||||
1059 | $validated |= $validate; |
||||
1060 | |||||
1061 | 235 | if (Invalid::isInvalid($value)) { |
|||
1062 | 235 | return $value; |
|||
1063 | 2 | } |
|||
1064 | 2 | } |
|||
1065 | 2 | } |
|||
1066 | $key = '/format/'.$field->val('format'); |
||||
1067 | 2 | if (!empty($this->filters[$key])) { |
|||
1068 | 2 | foreach ($this->filters[$key] as list($filter, $validate)) { |
|||
1069 | $value = call_user_func($filter, $value, $field); |
||||
1070 | $validated |= $validate; |
||||
1071 | |||||
1072 | if (Invalid::isInvalid($value)) { |
||||
1073 | 235 | return $value; |
|||
1074 | } |
||||
1075 | } |
||||
1076 | } |
||||
1077 | |||||
1078 | return $value; |
||||
1079 | } |
||||
1080 | |||||
1081 | /** |
||||
1082 | * Validate a field against multiple basic types. |
||||
1083 | * |
||||
1084 | * The first validation that passes will be returned. If no type can be validated against then validation will fail. |
||||
1085 | * |
||||
1086 | * @param mixed $value The value to validate. |
||||
1087 | * @param string[] $types The types to validate against. |
||||
1088 | 29 | * @param ValidationField $field Contains field and validation information. |
|||
1089 | 29 | * @return mixed Returns the valid value or `Invalid`. |
|||
1090 | * @throws RefNotFoundException Throws an exception when a schema `$ref` is not found. |
||||
1091 | * @deprecated Multiple types are being removed next version. |
||||
1092 | 29 | */ |
|||
1093 | 29 | private function validateMultipleTypes($value, array $types, ValidationField $field) { |
|||
1094 | 4 | trigger_error('Multiple schema types are deprecated.', E_USER_DEPRECATED); |
|||
1095 | 4 | ||||
1096 | // First check for an exact type match. |
||||
1097 | 4 | switch (gettype($value)) { |
|||
1098 | 26 | case 'boolean': |
|||
1099 | 7 | if (in_array('boolean', $types)) { |
|||
1100 | 5 | $singleType = 'boolean'; |
|||
1101 | 2 | } |
|||
1102 | 1 | break; |
|||
1103 | case 'integer': |
||||
1104 | 7 | if (in_array('integer', $types)) { |
|||
1105 | 21 | $singleType = 'integer'; |
|||
1106 | 4 | } elseif (in_array('number', $types)) { |
|||
1107 | 4 | $singleType = 'number'; |
|||
1108 | } |
||||
1109 | break; |
||||
1110 | case 'double': |
||||
1111 | 4 | if (in_array('number', $types)) { |
|||
1112 | 18 | $singleType = 'number'; |
|||
1113 | 9 | } elseif (in_array('integer', $types)) { |
|||
1114 | 1 | $singleType = 'integer'; |
|||
1115 | 8 | } |
|||
1116 | 4 | break; |
|||
1117 | case 'string': |
||||
1118 | 9 | if (in_array('datetime', $types) && preg_match(self::$DATE_REGEX, $value)) { |
|||
1119 | 10 | $singleType = 'datetime'; |
|||
1120 | 10 | } elseif (in_array('string', $types)) { |
|||
1121 | 1 | $singleType = 'string'; |
|||
1122 | 9 | } |
|||
1123 | break; |
||||
1124 | 9 | case 'array': |
|||
1125 | 9 | if (in_array('array', $types) && in_array('object', $types)) { |
|||
1126 | $singleType = isset($value[0]) || empty($value) ? 'array' : 'object'; |
||||
1127 | 10 | } elseif (in_array('object', $types)) { |
|||
1128 | 1 | $singleType = 'object'; |
|||
1129 | } elseif (in_array('array', $types)) { |
||||
1130 | $singleType = 'array'; |
||||
1131 | } |
||||
1132 | break; |
||||
1133 | case 'NULL': |
||||
1134 | 29 | if (in_array('null', $types)) { |
|||
1135 | 25 | $singleType = $this->validateSingleType($value, 'null', $field); |
|||
1136 | } |
||||
1137 | break; |
||||
1138 | } |
||||
1139 | 6 | if (!empty($singleType)) { |
|||
1140 | return $this->validateSingleType($value, $singleType, $field); |
||||
1141 | } |
||||
1142 | 6 | ||||
1143 | 6 | // Clone the validation field to collect errors. |
|||
1144 | 6 | $typeValidation = new ValidationField(new Validation(), $field->getField(), '', '', $field->getOptions()); |
|||
1145 | 6 | ||||
1146 | // Try and validate against each type. |
||||
1147 | foreach ($types as $type) { |
||||
1148 | $result = $this->validateSingleType($value, $type, $typeValidation); |
||||
1149 | if (Invalid::isValid($result)) { |
||||
1150 | return $result; |
||||
1151 | } |
||||
1152 | } |
||||
1153 | |||||
1154 | // Since we got here the value is invalid. |
||||
1155 | $field->merge($typeValidation->getValidation()); |
||||
1156 | return Invalid::value(); |
||||
1157 | } |
||||
1158 | |||||
1159 | /** |
||||
1160 | * Validate a field against a single type. |
||||
1161 | * |
||||
1162 | * @param mixed $value The value to validate. |
||||
1163 | * @param string $type The type to validate against. |
||||
1164 | 224 | * @param ValidationField $field Contains field and validation information. |
|||
1165 | * @return mixed Returns the valid value or `Invalid`. |
||||
1166 | 224 | * @throws \InvalidArgumentException Throws an exception when `$type` is not recognized. |
|||
1167 | 34 | * @throws RefNotFoundException Throws an exception when internal validation has a reference that isn't found. |
|||
1168 | 34 | */ |
|||
1169 | 204 | protected function validateSingleType($value, string $type, ValidationField $field) { |
|||
1170 | 66 | switch ($type) { |
|||
1171 | 66 | case 'boolean': |
|||
1172 | 184 | $result = $this->validateBoolean($value, $field); |
|||
1173 | 17 | break; |
|||
1174 | 17 | case 'integer': |
|||
1175 | 175 | $result = $this->validateInteger($value, $field); |
|||
1176 | 96 | break; |
|||
1177 | 96 | case 'number': |
|||
1178 | 151 | $result = $this->validateNumber($value, $field); |
|||
1179 | 1 | break; |
|||
1180 | 1 | case 'string': |
|||
1181 | 1 | $result = $this->validateString($value, $field); |
|||
1182 | 151 | break; |
|||
1183 | 2 | case 'timestamp': |
|||
1184 | 2 | trigger_error('The timestamp type is deprecated. Use an integer with a format of timestamp instead.', E_USER_DEPRECATED); |
|||
1185 | 2 | $result = $this->validateTimestamp($value, $field); |
|||
1186 | 150 | break; |
|||
1187 | 38 | case 'datetime': |
|||
1188 | 38 | trigger_error('The datetime type is deprecated. Use a string with a format of date-time instead.', E_USER_DEPRECATED); |
|||
1189 | 130 | $result = $this->validateDatetime($value, $field); |
|||
1190 | 128 | break; |
|||
1191 | 126 | case 'array': |
|||
1192 | 6 | $result = $this->validateArray($value, $field); |
|||
1193 | 1 | break; |
|||
1194 | 1 | case 'object': |
|||
1195 | 5 | $result = $this->validateObject($value, $field); |
|||
1196 | break; |
||||
1197 | 5 | case 'null': |
|||
1198 | 5 | $result = $this->validateNull($value, $field); |
|||
1199 | break; |
||||
1200 | case '': |
||||
1201 | // No type was specified so we are valid. |
||||
1202 | 224 | $result = $value; |
|||
1203 | break; |
||||
1204 | default: |
||||
1205 | throw new \InvalidArgumentException("Unrecognized type $type.", 500); |
||||
1206 | } |
||||
1207 | return $result; |
||||
1208 | } |
||||
1209 | |||||
1210 | /** |
||||
1211 | * Validate a boolean value. |
||||
1212 | 34 | * |
|||
1213 | 34 | * @param mixed $value The value to validate. |
|||
1214 | 34 | * @param ValidationField $field The validation results to add. |
|||
1215 | 4 | * @return bool|Invalid Returns the cleaned value or invalid if validation fails. |
|||
1216 | 4 | */ |
|||
1217 | protected function validateBoolean($value, ValidationField $field) { |
||||
1218 | $value = $value === null ? $value : filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); |
||||
1219 | 31 | if ($value === null) { |
|||
1220 | $field->addTypeError($value, 'boolean'); |
||||
1221 | return Invalid::value(); |
||||
1222 | } |
||||
1223 | |||||
1224 | return $value; |
||||
1225 | } |
||||
1226 | |||||
1227 | /** |
||||
1228 | * Validate and integer. |
||||
1229 | 66 | * |
|||
1230 | 66 | * @param mixed $value The value to validate. |
|||
1231 | 7 | * @param ValidationField $field The validation results to add. |
|||
1232 | * @return int|Invalid Returns the cleaned value or **null** if validation fails. |
||||
1233 | */ |
||||
1234 | 61 | protected function validateInteger($value, ValidationField $field) { |
|||
1235 | if ($field->val('format') === 'timestamp') { |
||||
1236 | 61 | return $this->validateTimestamp($value, $field); |
|||
1237 | 11 | } |
|||
1238 | 11 | ||||
1239 | $result = filter_var($value, FILTER_VALIDATE_INT); |
||||
1240 | |||||
1241 | 54 | if ($result === false) { |
|||
1242 | $field->addTypeError($value, 'integer'); |
||||
1243 | 54 | return Invalid::value(); |
|||
1244 | } |
||||
1245 | |||||
1246 | $result = $this->validateNumberProperties($result, $field); |
||||
1247 | |||||
1248 | return $result; |
||||
1249 | } |
||||
1250 | |||||
1251 | /** |
||||
1252 | * Validate a unix timestamp. |
||||
1253 | 8 | * |
|||
1254 | 8 | * @param mixed $value The value to validate. |
|||
1255 | 3 | * @param ValidationField $field The field being validated. |
|||
1256 | 5 | * @return int|Invalid Returns a valid timestamp or invalid if the value doesn't validate. |
|||
1257 | 1 | */ |
|||
1258 | protected function validateTimestamp($value, ValidationField $field) { |
||||
1259 | 4 | if (is_numeric($value) && $value > 0) { |
|||
1260 | 4 | $result = (int)$value; |
|||
1261 | } elseif (is_string($value) && $ts = strtotime($value)) { |
||||
1262 | 8 | $result = $ts; |
|||
1263 | } else { |
||||
1264 | $field->addTypeError($value, 'timestamp'); |
||||
1265 | $result = Invalid::value(); |
||||
1266 | } |
||||
1267 | return $result; |
||||
1268 | } |
||||
1269 | |||||
1270 | /** |
||||
1271 | * Validate specific numeric validation properties. |
||||
1272 | 64 | * |
|||
1273 | 64 | * @param int|float $value The value to test. |
|||
1274 | * @param ValidationField $field Field information. |
||||
1275 | 64 | * @return int|float|Invalid Returns the number of invalid. |
|||
1276 | 4 | */ |
|||
1277 | private function validateNumberProperties($value, ValidationField $field) { |
||||
1278 | 4 | $count = $field->getErrorCount(); |
|||
1279 | 2 | ||||
1280 | if ($multipleOf = $field->val('multipleOf')) { |
||||
1281 | $divided = $value / $multipleOf; |
||||
1282 | |||||
1283 | 64 | if ($divided != round($divided)) { |
|||
1284 | 4 | $field->addError('multipleOf', ['messageCode' => 'The value must be a multiple of {multipleOf}.', 'multipleOf' => $multipleOf]); |
|||
1285 | } |
||||
1286 | 4 | } |
|||
1287 | 2 | ||||
1288 | 1 | if ($maximum = $field->val('maximum')) { |
|||
1289 | $exclusive = $field->val('exclusiveMaximum'); |
||||
1290 | 1 | ||||
1291 | if ($value > $maximum || ($exclusive && $value == $maximum)) { |
||||
1292 | if ($exclusive) { |
||||
1293 | $field->addError('maximum', ['messageCode' => 'The value must be less than {maximum}.', 'maximum' => $maximum]); |
||||
1294 | } else { |
||||
1295 | 64 | $field->addError('maximum', ['messageCode' => 'The value must be less than or equal to {maximum}.', 'maximum' => $maximum]); |
|||
1296 | 4 | } |
|||
1297 | } |
||||
1298 | 4 | } |
|||
1299 | 2 | ||||
1300 | 1 | if ($minimum = $field->val('minimum')) { |
|||
1301 | $exclusive = $field->val('exclusiveMinimum'); |
||||
1302 | 1 | ||||
1303 | if ($value < $minimum || ($exclusive && $value == $minimum)) { |
||||
1304 | if ($exclusive) { |
||||
1305 | $field->addError('minimum', ['messageCode' => 'The value must be greater than {minimum}.', 'minimum' => $minimum]); |
||||
1306 | } else { |
||||
1307 | 64 | $field->addError('minimum', ['messageCode' => 'The value must be greater than or equal to {minimum}.', 'minimum' => $minimum]); |
|||
1308 | } |
||||
1309 | } |
||||
1310 | } |
||||
1311 | |||||
1312 | return $field->getErrorCount() === $count ? $value : Invalid::value(); |
||||
1313 | } |
||||
1314 | |||||
1315 | /** |
||||
1316 | * Validate a float. |
||||
1317 | 17 | * |
|||
1318 | 17 | * @param mixed $value The value to validate. |
|||
1319 | 17 | * @param ValidationField $field The validation results to add. |
|||
1320 | 4 | * @return float|Invalid Returns a number or **null** if validation fails. |
|||
1321 | 4 | */ |
|||
1322 | protected function validateNumber($value, ValidationField $field) { |
||||
1323 | $result = filter_var($value, FILTER_VALIDATE_FLOAT); |
||||
1324 | 13 | if ($result === false) { |
|||
1325 | $field->addTypeError($value, 'number'); |
||||
1326 | 13 | return Invalid::value(); |
|||
1327 | } |
||||
1328 | |||||
1329 | $result = $this->validateNumberProperties($result, $field); |
||||
1330 | |||||
1331 | return $result; |
||||
1332 | } |
||||
1333 | |||||
1334 | /** |
||||
1335 | * Validate a string. |
||||
1336 | 96 | * |
|||
1337 | 96 | * @param mixed $value The value to validate. |
|||
1338 | 12 | * @param ValidationField $field The validation results to add. |
|||
1339 | 12 | * @return string|Invalid Returns the valid string or **null** if validation fails. |
|||
1340 | */ |
||||
1341 | protected function validateString($value, ValidationField $field) { |
||||
1342 | 85 | if ($field->val('format') === 'date-time') { |
|||
1343 | 83 | $result = $this->validateDatetime($value, $field); |
|||
1344 | return $result; |
||||
0 ignored issues
–
show
|
|||||
1345 | 5 | } |
|||
1346 | 5 | ||||
1347 | if (is_string($value) || is_numeric($value)) { |
||||
1348 | $value = $result = (string)$value; |
||||
1349 | 83 | } else { |
|||
1350 | 4 | $field->addTypeError($value, 'string'); |
|||
1351 | 4 | return Invalid::value(); |
|||
1352 | } |
||||
1353 | 4 | ||||
1354 | 4 | $strFn = $this->hasFlag(self::VALIDATE_STRING_LENGTH_AS_UNICODE) ? "mb_strlen" : "strlen"; |
|||
1355 | $strLen = $strFn($value); |
||||
1356 | if (($minLength = $field->val('minLength', 0)) > 0 && $strLen < $minLength) { |
||||
1357 | $field->addError( |
||||
1358 | 83 | 'minLength', |
|||
1359 | 1 | [ |
|||
1360 | 1 | 'messageCode' => 'The value should be at least {minLength} {minLength,plural,character,characters} long.', |
|||
1361 | 'minLength' => $minLength, |
||||
1362 | 1 | ] |
|||
1363 | 1 | ); |
|||
1364 | 1 | } |
|||
1365 | |||||
1366 | if (($maxLength = $field->val('maxLength', 0)) > 0 && $strLen > $maxLength) { |
||||
1367 | $field->addError( |
||||
1368 | 83 | 'maxLength', |
|||
1369 | 4 | [ |
|||
1370 | 'messageCode' => 'The value is {overflow} {overflow,plural,character,characters} too long.', |
||||
1371 | 4 | 'maxLength' => $maxLength, |
|||
1372 | 2 | 'overflow' => $strLen - $maxLength, |
|||
1373 | 2 | ] |
|||
1374 | ); |
||||
1375 | 2 | } |
|||
1376 | 2 | if ($pattern = $field->val('pattern')) { |
|||
1377 | $regex = '`'.str_replace('`', preg_quote('`', '`'), $pattern).'`'; |
||||
1378 | |||||
1379 | if (!preg_match($regex, $value)) { |
||||
1380 | $field->addError( |
||||
1381 | 83 | 'pattern', |
|||
1382 | 11 | [ |
|||
1383 | 'messageCode' => $field->val('x-patternMessageCode', 'The value doesn\'t match the required pattern {pattern}.'), |
||||
1384 | 11 | 'pattern' => $regex, |
|||
1385 | ] |
||||
1386 | ); |
||||
1387 | } |
||||
1388 | } |
||||
1389 | if ($format = $field->val('format')) { |
||||
1390 | 11 | $type = $format; |
|||
1391 | 1 | switch ($format) { |
|||
1392 | 1 | case 'date': |
|||
1393 | 10 | $result = $this->validateDatetime($result, $field); |
|||
1394 | 1 | if ($result instanceof \DateTimeInterface) { |
|||
1395 | 1 | $result = $result->format("Y-m-d\T00:00:00P"); |
|||
1396 | 1 | } |
|||
1397 | 9 | break; |
|||
1398 | 1 | case 'email': |
|||
1399 | 1 | $result = filter_var($result, FILTER_VALIDATE_EMAIL); |
|||
1400 | 1 | break; |
|||
1401 | 8 | case 'ipv4': |
|||
1402 | 1 | $type = 'IPv4 address'; |
|||
1403 | 1 | $result = filter_var($result, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4); |
|||
1404 | 1 | break; |
|||
1405 | 7 | case 'ipv6': |
|||
1406 | 7 | $type = 'IPv6 address'; |
|||
1407 | 7 | $result = filter_var($result, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6); |
|||
1408 | 7 | break; |
|||
1409 | case 'ip': |
||||
1410 | $type = 'IP address'; |
||||
1411 | $result = filter_var($result, FILTER_VALIDATE_IP); |
||||
1412 | 11 | break; |
|||
1413 | 5 | case 'uri': |
|||
1414 | 5 | $type = 'URL'; |
|||
1415 | 5 | $result = filter_var($result, FILTER_VALIDATE_URL); |
|||
1416 | 5 | break; |
|||
1417 | 5 | default: |
|||
1418 | trigger_error("Unrecognized format '$format'.", E_USER_NOTICE); |
||||
1419 | } |
||||
1420 | if ($result === false) { |
||||
1421 | $field->addError('format', [ |
||||
1422 | 83 | 'format' => $format, |
|||
1423 | 75 | 'formatCode' => $type, |
|||
1424 | 'value' => $value, |
||||
1425 | 12 | 'messageCode' => '{value} is not a valid {formatCode}.' |
|||
1426 | ]); |
||||
1427 | } |
||||
1428 | } |
||||
1429 | |||||
1430 | if ($field->isValid()) { |
||||
1431 | return $result; |
||||
1432 | } else { |
||||
1433 | return Invalid::value(); |
||||
1434 | } |
||||
1435 | } |
||||
1436 | 14 | ||||
1437 | 14 | /** |
|||
1438 | * Validate a date time. |
||||
1439 | 11 | * |
|||
1440 | * @param mixed $value The value to validate. |
||||
1441 | 7 | * @param ValidationField $field The validation results to add. |
|||
1442 | 6 | * @return \DateTimeInterface|Invalid Returns the cleaned value or **null** if it isn't valid. |
|||
1443 | 6 | */ |
|||
1444 | protected function validateDatetime($value, ValidationField $field) { |
||||
1445 | 6 | if ($value instanceof \DateTimeInterface) { |
|||
1446 | // do nothing, we're good |
||||
1447 | 1 | } elseif (is_string($value) && $value !== '' && !is_numeric($value)) { |
|||
1448 | 7 | try { |
|||
1449 | $dt = new \DateTimeImmutable($value); |
||||
1450 | 4 | if ($dt) { |
|||
0 ignored issues
–
show
|
|||||
1451 | $value = $dt; |
||||
1452 | 1 | } else { |
|||
1453 | $value = null; |
||||
1454 | 1 | } |
|||
1455 | } catch (\Throwable $ex) { |
||||
1456 | $value = Invalid::value(); |
||||
1457 | 3 | } |
|||
1458 | } elseif (is_int($value) && $value > 0) { |
||||
1459 | try { |
||||
1460 | 14 | $value = new \DateTimeImmutable('@'.(string)round($value)); |
|||
1461 | 4 | } catch (\Throwable $ex) { |
|||
1462 | $value = Invalid::value(); |
||||
1463 | 14 | } |
|||
1464 | } else { |
||||
1465 | $value = Invalid::value(); |
||||
1466 | } |
||||
1467 | |||||
1468 | if (Invalid::isInvalid($value)) { |
||||
1469 | $field->addTypeError($value, 'date/time'); |
||||
1470 | } |
||||
1471 | return $value; |
||||
1472 | } |
||||
1473 | |||||
1474 | 4 | /** |
|||
1475 | 4 | * Recursively resolve allOf inheritance tree and return a merged resource specification |
|||
1476 | * |
||||
1477 | 4 | * @param ValidationField $field The validation results to add. |
|||
1478 | 4 | * @return array Returns an array of merged specs. |
|||
1479 | 1 | * @throws ParseException Throws an exception if an invalid allof member is provided |
|||
1480 | * @throws RefNotFoundException Throws an exception if the array has an items `$ref` that cannot be found. |
||||
1481 | */ |
||||
1482 | 4 | private function resolveAllOfTree(ValidationField $field) { |
|||
1483 | $result = []; |
||||
1484 | 4 | ||||
1485 | 4 | foreach($field->getAllOf() as $allof) { |
|||
1486 | 4 | if (!is_array($allof) || empty($allof)) { |
|||
1487 | 4 | throw new ParseException("Invalid allof member in {$field->getSchemaPath()}, array expected", 500); |
|||
1488 | 4 | } |
|||
1489 | 4 | ||||
1490 | list ($items, $schemaPath) = $this->lookupSchema($allof, $field->getSchemaPath()); |
||||
1491 | |||||
1492 | 4 | $allOfValidation = new ValidationField( |
|||
1493 | 3 | $field->getValidation(), |
|||
1494 | $items, |
||||
1495 | 4 | '', |
|||
1496 | $schemaPath, |
||||
1497 | $field->getOptions() |
||||
1498 | ); |
||||
1499 | 4 | ||||
1500 | if($allOfValidation->hasAllOf()) { |
||||
1501 | $result = array_replace_recursive($result, $this->resolveAllOfTree($allOfValidation)); |
||||
1502 | } else { |
||||
1503 | $result = array_replace_recursive($result, $items); |
||||
1504 | } |
||||
1505 | } |
||||
1506 | |||||
1507 | return $result; |
||||
1508 | } |
||||
1509 | |||||
1510 | 4 | /** |
|||
1511 | 4 | * Validate allof tree |
|||
1512 | 4 | * |
|||
1513 | 4 | * @param mixed $value The value to validate. |
|||
1514 | 3 | * @param ValidationField $field The validation results to add. |
|||
1515 | 3 | * @return array|Invalid Returns an array or invalid if validation fails. |
|||
1516 | 3 | * @throws RefNotFoundException Throws an exception if the array has an items `$ref` that cannot be found. |
|||
1517 | */ |
||||
1518 | private function validateAllOf($value, ValidationField $field) { |
||||
1519 | 3 | $allOfValidation = new ValidationField( |
|||
1520 | $field->getValidation(), |
||||
1521 | $this->resolveAllOfTree($field), |
||||
1522 | '', |
||||
1523 | $field->getSchemaPath(), |
||||
1524 | $field->getOptions() |
||||
1525 | ); |
||||
1526 | |||||
1527 | return $this->validateField($value, $allOfValidation); |
||||
1528 | } |
||||
1529 | |||||
1530 | 38 | /** |
|||
1531 | 38 | * Validate an array. |
|||
1532 | 6 | * |
|||
1533 | 6 | * @param mixed $value The value to validate. |
|||
1534 | * @param ValidationField $field The validation results to add. |
||||
1535 | 33 | * @return array|Invalid Returns an array or invalid if validation fails. |
|||
1536 | 1 | * @throws RefNotFoundException Throws an exception if the array has an items `$ref` that cannot be found. |
|||
1537 | 1 | */ |
|||
1538 | protected function validateArray($value, ValidationField $field) { |
||||
1539 | 1 | if ((!is_array($value) || (count($value) > 0 && !array_key_exists(0, $value))) && !$value instanceof \Traversable) { |
|||
0 ignored issues
–
show
|
|||||
1540 | 1 | $field->addTypeError($value, 'array'); |
|||
1541 | return Invalid::value(); |
||||
1542 | } else { |
||||
1543 | if ((null !== $minItems = $field->val('minItems')) && count($value) < $minItems) { |
||||
0 ignored issues
–
show
It seems like
$value can also be of type Traversable ; however, parameter $value of count() does only seem to accept Countable|array , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
1544 | 33 | $field->addError( |
|||
1545 | 1 | 'minItems', |
|||
1546 | 1 | [ |
|||
1547 | 'messageCode' => 'This must contain at least {minItems} {minItems,plural,item,items}.', |
||||
1548 | 1 | 'minItems' => $minItems, |
|||
1549 | 1 | ] |
|||
1550 | ); |
||||
1551 | } |
||||
1552 | if ((null !== $maxItems = $field->val('maxItems')) && count($value) > $maxItems) { |
||||
1553 | $field->addError( |
||||
1554 | 33 | 'maxItems', |
|||
1555 | 1 | [ |
|||
1556 | 1 | 'messageCode' => 'This must contain no more than {maxItems} {maxItems,plural,item,items}.', |
|||
1557 | 'maxItems' => $maxItems, |
||||
1558 | 1 | ] |
|||
1559 | ); |
||||
1560 | } |
||||
1561 | |||||
1562 | if ($field->val('uniqueItems') && count($value) > count(array_unique($value))) { |
||||
0 ignored issues
–
show
It seems like
$value can also be of type Traversable ; however, parameter $array of array_unique() does only seem to accept array , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
1563 | 33 | $field->addError( |
|||
1564 | 25 | 'uniqueItems', |
|||
1565 | [ |
||||
1566 | 'messageCode' => 'The array must contain unique items.', |
||||
1567 | 25 | ] |
|||
1568 | 25 | ); |
|||
1569 | 25 | } |
|||
1570 | 25 | ||||
1571 | 25 | if ($field->val('items') !== null) { |
|||
1572 | 25 | list ($items, $schemaPath) = $this->lookupSchema($field->val('items'), $field->getSchemaPath().'/items'); |
|||
1573 | |||||
1574 | // Validate each of the types. |
||||
1575 | 25 | $itemValidation = new ValidationField( |
|||
1576 | 25 | $field->getValidation(), |
|||
1577 | 25 | $items, |
|||
1578 | 25 | '', |
|||
1579 | 25 | $schemaPath, |
|||
1580 | 25 | $field->getOptions() |
|||
1581 | 25 | ); |
|||
1582 | |||||
1583 | 25 | $result = []; |
|||
1584 | $count = 0; |
||||
1585 | foreach ($value as $i => $item) { |
||||
1586 | 25 | $itemValidation->setName($field->getName()."/$i"); |
|||
1587 | $validItem = $this->validateField($item, $itemValidation); |
||||
1588 | if (Invalid::isValid($validItem)) { |
||||
1589 | 8 | $result[] = $validItem; |
|||
1590 | 8 | } |
|||
1591 | $count++; |
||||
1592 | } |
||||
1593 | |||||
1594 | return empty($result) && $count > 0 ? Invalid::value() : $result; |
||||
1595 | } else { |
||||
1596 | // Cast the items into a proper numeric array. |
||||
1597 | $result = is_array($value) ? array_values($value) : iterator_to_array($value); |
||||
1598 | return $result; |
||||
1599 | } |
||||
1600 | } |
||||
1601 | } |
||||
1602 | |||||
1603 | 128 | /** |
|||
1604 | 128 | * Validate an object. |
|||
1605 | 6 | * |
|||
1606 | 6 | * @param mixed $value The value to validate. |
|||
1607 | 128 | * @param ValidationField $field The validation results to add. |
|||
1608 | * @return object|Invalid Returns a clean object or **null** if validation fails. |
||||
1609 | 121 | * @throws RefNotFoundException Throws an exception when a schema `$ref` is not found. |
|||
1610 | 7 | */ |
|||
1611 | 3 | protected function validateObject($value, ValidationField $field) { |
|||
1612 | if (!$this->isArray($value) || isset($value[0])) { |
||||
1613 | $field->addTypeError($value, 'object'); |
||||
1614 | 126 | return Invalid::value(); |
|||
1615 | 1 | } elseif (is_array($field->val('properties')) || null !== $field->val('additionalProperties')) { |
|||
1616 | 1 | // Validate the data against the internal schema. |
|||
1617 | $value = $this->validateProperties($value, $field); |
||||
1618 | 1 | } elseif (!is_array($value)) { |
|||
1619 | 1 | $value = $this->toObjectArray($value); |
|||
1620 | } |
||||
1621 | |||||
1622 | if (($maxProperties = $field->val('maxProperties')) && count($value) > $maxProperties) { |
||||
1623 | $field->addError( |
||||
1624 | 126 | 'maxProperties', |
|||
1625 | 1 | [ |
|||
1626 | 1 | 'messageCode' => 'This must contain no more than {maxProperties} {maxProperties,plural,item,items}.', |
|||
1627 | 'maxItems' => $maxProperties, |
||||
1628 | 1 | ] |
|||
1629 | 1 | ); |
|||
1630 | } |
||||
1631 | |||||
1632 | if (($minProperties = $field->val('minProperties')) && count($value) < $minProperties) { |
||||
1633 | $field->addError( |
||||
1634 | 126 | 'minProperties', |
|||
1635 | [ |
||||
1636 | 'messageCode' => 'This must contain at least {minProperties} {minProperties,plural,item,items}.', |
||||
1637 | 'minItems' => $minProperties, |
||||
1638 | ] |
||||
1639 | ); |
||||
1640 | } |
||||
1641 | |||||
1642 | return $value; |
||||
0 ignored issues
–
show
|
|||||
1643 | 136 | } |
|||
1644 | 136 | ||||
1645 | /** |
||||
1646 | * Check whether or not a value is an array or accessible like an array. |
||||
1647 | * |
||||
1648 | * @param mixed $value The value to check. |
||||
1649 | * @return bool Returns **true** if the value can be used like an array or **false** otherwise. |
||||
1650 | */ |
||||
1651 | private function isArray($value) { |
||||
1652 | return is_array($value) || ($value instanceof \ArrayAccess && $value instanceof \Traversable); |
||||
1653 | } |
||||
1654 | |||||
1655 | /** |
||||
1656 | 121 | * Validate data against the schema and return the result. |
|||
1657 | 121 | * |
|||
1658 | 121 | * @param array|\Traversable|\ArrayAccess $data The data to validate. |
|||
1659 | 121 | * @param ValidationField $field This argument will be filled with the validation result. |
|||
1660 | 121 | * @return array|\ArrayObject|Invalid Returns a clean array with only the appropriate properties and the data coerced to proper types. |
|||
1661 | 121 | * or invalid if there are no valid properties. |
|||
1662 | * @throws RefNotFoundException Throws an exception of a property or additional property has a `$ref` that cannot be found. |
||||
1663 | 121 | */ |
|||
1664 | 117 | protected function validateProperties($data, ValidationField $field) { |
|||
1665 | 117 | $properties = $field->val('properties', []); |
|||
1666 | $additionalProperties = $field->val('additionalProperties'); |
||||
1667 | 4 | $required = array_flip($field->val('required', [])); |
|||
1668 | 4 | $isRequest = $field->isRequest(); |
|||
1669 | 4 | $isResponse = $field->isResponse(); |
|||
1670 | |||||
1671 | 4 | if (is_array($data)) { |
|||
1672 | 3 | $keys = array_keys($data); |
|||
1673 | 3 | $clean = []; |
|||
1674 | } else { |
||||
1675 | $keys = array_keys(iterator_to_array($data)); |
||||
0 ignored issues
–
show
It seems like
$data can also be of type ArrayAccess ; however, parameter $iterator of iterator_to_array() does only seem to accept Traversable , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
1676 | 121 | $class = get_class($data); |
|||
1677 | $clean = new $class; |
||||
1678 | 121 | ||||
1679 | if ($clean instanceof \ArrayObject && $data instanceof \ArrayObject) { |
||||
1680 | $clean->setFlags($data->getFlags()); |
||||
1681 | 121 | $clean->setIteratorClass($data->getIteratorClass()); |
|||
1682 | 119 | } |
|||
1683 | } |
||||
1684 | $keys = array_combine(array_map('strtolower', $keys), $keys); |
||||
1685 | 119 | ||||
1686 | 119 | $propertyField = new ValidationField($field->getValidation(), [], '', '', $field->getOptions()); |
|||
1687 | 119 | ||||
1688 | // Loop through the schema fields and validate each one. |
||||
1689 | 119 | foreach ($properties as $propertyName => $property) { |
|||
1690 | 119 | list($property, $schemaPath) = $this->lookupSchema($property, $field->getSchemaPath().'/properties/'.self::escapeRef($propertyName)); |
|||
1691 | |||||
1692 | $propertyField |
||||
1693 | 119 | ->setField($property) |
|||
1694 | 6 | ->setName(ltrim($field->getName().'/'.self::escapeRef($propertyName), '/')) |
|||
1695 | 6 | ->setSchemaPath($schemaPath); |
|||
1696 | |||||
1697 | $lName = strtolower($propertyName); |
||||
1698 | $isRequired = isset($required[$propertyName]); |
||||
1699 | 119 | ||||
1700 | 37 | // Check to strip this field if it is readOnly or writeOnly. |
|||
1701 | if (($isRequest && $propertyField->val('readOnly')) || ($isResponse && $propertyField->val('writeOnly'))) { |
||||
1702 | 36 | unset($keys[$lName]); |
|||
1703 | 6 | continue; |
|||
1704 | 30 | } |
|||
1705 | 6 | ||||
1706 | 6 | // Check for required fields. |
|||
1707 | 37 | if (!array_key_exists($lName, $keys)) { |
|||
1708 | if ($field->isSparse()) { |
||||
1709 | // Sparse validation can leave required fields out. |
||||
1710 | } elseif ($propertyField->hasVal('default')) { |
||||
1711 | 107 | $clean[$propertyName] = $propertyField->val('default'); |
|||
1712 | } elseif ($isRequired) { |
||||
1713 | 107 | $propertyField->addError( |
|||
1714 | 5 | 'required', |
|||
1715 | 2 | ['messageCode' => '{property} is required.', 'property' => $propertyName] |
|||
1716 | ); |
||||
1717 | } |
||||
1718 | } else { |
||||
1719 | 105 | $value = $data[$keys[$lName]]; |
|||
1720 | |||||
1721 | if (in_array($value, [null, ''], true) && !$isRequired && !($propertyField->val('nullable') || $propertyField->hasType('null'))) { |
||||
1722 | 117 | if ($propertyField->getType() !== 'string' || $value === null) { |
|||
1723 | continue; |
||||
1724 | } |
||||
1725 | } |
||||
1726 | 121 | ||||
1727 | 24 | $clean[$propertyName] = $this->validateField($value, $propertyField); |
|||
1728 | 10 | } |
|||
1729 | 10 | ||||
1730 | 10 | unset($keys[$lName]); |
|||
1731 | } |
||||
1732 | |||||
1733 | 10 | // Look for extraneous properties. |
|||
1734 | 10 | if (!empty($keys)) { |
|||
1735 | 10 | if ($additionalProperties) { |
|||
1736 | 10 | list($additionalProperties, $schemaPath) = $this->lookupSchema( |
|||
1737 | 10 | $additionalProperties, |
|||
1738 | 10 | $field->getSchemaPath().'/additionalProperties' |
|||
1739 | ); |
||||
1740 | |||||
1741 | 10 | $propertyField = new ValidationField( |
|||
1742 | $field->getValidation(), |
||||
1743 | 10 | $additionalProperties, |
|||
1744 | '', |
||||
1745 | 10 | $schemaPath, |
|||
1746 | 10 | $field->getOptions() |
|||
1747 | 10 | ); |
|||
1748 | |||||
1749 | foreach ($keys as $key) { |
||||
1750 | 14 | $propertyField |
|||
1751 | 2 | ->setName(ltrim($field->getName()."/$key", '/')); |
|||
1752 | 2 | ||||
1753 | 12 | $valid = $this->validateField($data[$key], $propertyField); |
|||
1754 | 3 | if (Invalid::isValid($valid)) { |
|||
1755 | 3 | $clean[$key] = $valid; |
|||
1756 | 3 | } |
|||
1757 | } |
||||
1758 | } elseif ($this->hasFlag(Schema::VALIDATE_EXTRA_PROPERTY_NOTICE)) { |
||||
0 ignored issues
–
show
As per coding style,
self should be used for accessing local static members.
This check looks for accesses to local static members using the fully qualified name instead
of <?php
class Certificate {
const TRIPLEDES_CBC = 'ASDFGHJKL';
private $key;
public function __construct()
{
$this->key = Certificate::TRIPLEDES_CBC;
}
}
While this is perfectly valid, the fully qualified name of ![]() |
|||||
1759 | $msg = sprintf("Unexpected properties: %s.", implode(', ', $keys)); |
||||
1760 | trigger_error($msg, E_USER_NOTICE); |
||||
1761 | 119 | } elseif ($this->hasFlag(Schema::VALIDATE_EXTRA_PROPERTY_EXCEPTION)) { |
|||
0 ignored issues
–
show
As per coding style,
self should be used for accessing local static members.
This check looks for accesses to local static members using the fully qualified name instead
of <?php
class Certificate {
const TRIPLEDES_CBC = 'ASDFGHJKL';
private $key;
public function __construct()
{
$this->key = Certificate::TRIPLEDES_CBC;
}
}
While this is perfectly valid, the fully qualified name of ![]() |
|||||
1762 | $field->addError('unexpectedProperties', [ |
||||
1763 | 'messageCode' => 'Unexpected {extra,plural,property,properties}: {extra}.', |
||||
1764 | 'extra' => array_values($keys), |
||||
1765 | ]); |
||||
1766 | } |
||||
1767 | } |
||||
1768 | |||||
1769 | return $clean; |
||||
1770 | 127 | } |
|||
1771 | 127 | ||||
1772 | /** |
||||
1773 | * Escape a JSON reference field. |
||||
1774 | * |
||||
1775 | * @param string $field The reference field to escape. |
||||
1776 | * @return string Returns an escaped reference. |
||||
1777 | */ |
||||
1778 | public static function escapeRef(string $field): string { |
||||
1779 | return str_replace(['~', '/'], ['~0', '~1'], $field); |
||||
1780 | 15 | } |
|||
1781 | 15 | ||||
1782 | /** |
||||
1783 | * Whether or not the schema has a flag (or combination of flags). |
||||
1784 | * |
||||
1785 | * @param int $flag One or more of the **Schema::VALIDATE_*** constants. |
||||
1786 | * @return bool Returns **true** if all of the flags are set or **false** otherwise. |
||||
1787 | */ |
||||
1788 | public function hasFlag(int $flag): bool { |
||||
1789 | return ($this->flags & $flag) === $flag; |
||||
1790 | 3 | } |
|||
1791 | 3 | ||||
1792 | 3 | /** |
|||
1793 | 2 | * Cast a value to an array. |
|||
1794 | 1 | * |
|||
1795 | 1 | * @param \Traversable $value The value to convert. |
|||
1796 | 1 | * @return array Returns an array. |
|||
1797 | 1 | */ |
|||
1798 | private function toObjectArray(\Traversable $value) { |
||||
1799 | 1 | $class = get_class($value); |
|||
1800 | if ($value instanceof \ArrayObject) { |
||||
1801 | return new $class($value->getArrayCopy(), $value->getFlags(), $value->getIteratorClass()); |
||||
0 ignored issues
–
show
|
|||||
1802 | } elseif ($value instanceof \ArrayAccess) { |
||||
1803 | $r = new $class; |
||||
1804 | foreach ($value as $k => $v) { |
||||
1805 | $r[$k] = $v; |
||||
1806 | } |
||||
1807 | return $r; |
||||
0 ignored issues
–
show
|
|||||
1808 | } |
||||
1809 | return iterator_to_array($value); |
||||
1810 | } |
||||
1811 | 1 | ||||
1812 | 1 | /** |
|||
1813 | * Validate a null value. |
||||
1814 | * |
||||
1815 | 1 | * @param mixed $value The value to validate. |
|||
1816 | 1 | * @param ValidationField $field The error collector for the field. |
|||
1817 | * @return null|Invalid Returns **null** or invalid. |
||||
1818 | */ |
||||
1819 | protected function validateNull($value, ValidationField $field) { |
||||
1820 | if ($value === null) { |
||||
1821 | return null; |
||||
1822 | } |
||||
1823 | $field->addError('type', ['messageCode' => 'The value should be null.', 'type' => 'null']); |
||||
1824 | return Invalid::value(); |
||||
1825 | } |
||||
1826 | 214 | ||||
1827 | 214 | /** |
|||
1828 | 214 | * Validate a value against an enum. |
|||
1829 | 213 | * |
|||
1830 | * @param mixed $value The value to test. |
||||
1831 | * @param ValidationField $field The validation object for adding errors. |
||||
1832 | 4 | * @return mixed|Invalid Returns the value if it is one of the enumerated values or invalid otherwise. |
|||
1833 | 1 | */ |
|||
1834 | 1 | protected function validateEnum($value, ValidationField $field) { |
|||
1835 | $enum = $field->val('enum'); |
||||
1836 | 1 | if (empty($enum)) { |
|||
1837 | 1 | return $value; |
|||
1838 | } |
||||
1839 | |||||
1840 | 1 | if (!in_array($value, $enum, true)) { |
|||
1841 | $field->addError( |
||||
1842 | 4 | 'enum', |
|||
1843 | [ |
||||
1844 | 'messageCode' => 'The value must be one of: {enum}.', |
||||
1845 | 'enum' => $enum, |
||||
1846 | ] |
||||
1847 | ); |
||||
1848 | return Invalid::value(); |
||||
1849 | } |
||||
1850 | return $value; |
||||
1851 | 215 | } |
|||
1852 | 215 | ||||
1853 | /** |
||||
1854 | * Call all of the validators attached to a field. |
||||
1855 | 215 | * |
|||
1856 | 215 | * @param mixed $value The field value being validated. |
|||
1857 | 5 | * @param ValidationField $field The validation object to add errors. |
|||
1858 | 5 | */ |
|||
1859 | private function callValidators($value, ValidationField $field) { |
||||
1860 | 5 | $valid = true; |
|||
1861 | 5 | ||||
1862 | // Strip array references in the name except for the last one. |
||||
1863 | $key = $field->getSchemaPath(); |
||||
1864 | if (!empty($this->validators[$key])) { |
||||
1865 | foreach ($this->validators[$key] as $validator) { |
||||
1866 | $r = call_user_func($validator, $value, $field); |
||||
1867 | 215 | ||||
1868 | 1 | if ($r === false || Invalid::isInvalid($r)) { |
|||
1869 | $valid = false; |
||||
1870 | 215 | } |
|||
1871 | } |
||||
1872 | } |
||||
1873 | |||||
1874 | // Add an error on the field if the validator hasn't done so. |
||||
1875 | if (!$valid && $field->isValid()) { |
||||
1876 | $field->addError('invalid', ['messageCode' => 'The value is invalid.']); |
||||
1877 | } |
||||
1878 | } |
||||
1879 | |||||
1880 | /** |
||||
1881 | 19 | * Specify data which should be serialized to JSON. |
|||
1882 | 19 | * |
|||
1883 | 19 | * This method specifically returns data compatible with the JSON schema format. |
|||
1884 | * |
||||
1885 | * @return mixed Returns data which can be serialized by **json_encode()**, which is a value of any type other than a resource. |
||||
1886 | * @link http://php.net/manual/en/jsonserializable.jsonserialize.php |
||||
1887 | * @link http://json-schema.org/ |
||||
1888 | */ |
||||
1889 | public function jsonSerialize() { |
||||
1890 | $seen = [$this]; |
||||
1891 | return $this->jsonSerializeInternal($seen); |
||||
1892 | } |
||||
1893 | |||||
1894 | /** |
||||
1895 | * Return the JSON data for serialization with massaging for Open API. |
||||
1896 | 19 | * |
|||
1897 | 19 | * - Swap data/time & timestamp types for Open API types. |
|||
1898 | 4 | * - Turn recursive schema pointers into references. |
|||
1899 | 3 | * |
|||
1900 | * @param Schema[] $seen Schemas that have been seen during traversal. |
||||
1901 | 2 | * @return array Returns an array of data that `json_encode()` will recognize. |
|||
1902 | 2 | */ |
|||
1903 | private function jsonSerializeInternal(array $seen): array { |
||||
1904 | $fix = function ($schema) use (&$fix, $seen) { |
||||
1905 | if ($schema instanceof Schema) { |
||||
1906 | 19 | if (in_array($schema, $seen, true)) { |
|||
1907 | 18 | return ['$ref' => '#/components/schemas/'.($schema->getID() ?: '$no-id')]; |
|||
1908 | } else { |
||||
1909 | 18 | $seen[] = $schema; |
|||
1910 | return $schema->jsonSerializeInternal($seen); |
||||
1911 | 18 | } |
|||
1912 | 4 | } |
|||
1913 | 4 | ||||
1914 | 17 | if (!empty($schema['type'])) { |
|||
1915 | 2 | $types = (array)$schema['type']; |
|||
1916 | 18 | ||||
1917 | foreach ($types as $i => &$type) { |
||||
1918 | // Swap datetime and timestamp to other types with formats. |
||||
1919 | 18 | if ($type === 'datetime') { |
|||
1920 | 18 | $type = 'string'; |
|||
1921 | $schema['format'] = 'date-time'; |
||||
1922 | } elseif ($schema['type'] === 'timestamp') { |
||||
1923 | 19 | $type = 'integer'; |
|||
1924 | 5 | $schema['format'] = 'timestamp'; |
|||
1925 | } |
||||
1926 | 19 | } |
|||
1927 | 14 | $types = array_unique($types); |
|||
1928 | 14 | $schema['type'] = count($types) === 1 ? reset($types) : $types; |
|||
1929 | 14 | } |
|||
1930 | |||||
1931 | 14 | if (!empty($schema['items'])) { |
|||
1932 | $schema['items'] = $fix($schema['items']); |
||||
1933 | } |
||||
1934 | 19 | if (!empty($schema['properties'])) { |
|||
1935 | 19 | $properties = []; |
|||
1936 | foreach ($schema['properties'] as $key => $property) { |
||||
1937 | 19 | $properties[$key] = $fix($property); |
|||
1938 | } |
||||
1939 | 19 | $schema['properties'] = $properties; |
|||
1940 | } |
||||
1941 | |||||
1942 | return $schema; |
||||
1943 | }; |
||||
1944 | |||||
1945 | $result = $fix($this->schema); |
||||
1946 | |||||
1947 | return $result; |
||||
1948 | 1 | } |
|||
1949 | 1 | ||||
1950 | 1 | /** |
|||
1951 | * Get the class that's used to contain validation information. |
||||
1952 | * |
||||
1953 | * @return Validation|string Returns the validation class. |
||||
1954 | * @deprecated |
||||
1955 | */ |
||||
1956 | public function getValidationClass() { |
||||
1957 | trigger_error('Schema::getValidationClass() is deprecated. Use Schema::getValidationFactory() instead.', E_USER_DEPRECATED); |
||||
1958 | return $this->validationClass; |
||||
0 ignored issues
–
show
The property
Garden\Schema\Schema::$validationClass has been deprecated.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
1959 | } |
||||
1960 | 1 | ||||
1961 | 1 | /** |
|||
1962 | * Set the class that's used to contain validation information. |
||||
1963 | 1 | * |
|||
1964 | * @param Validation|string $class Either the name of a class or a class that will be cloned. |
||||
1965 | * @return $this |
||||
1966 | * @deprecated |
||||
1967 | 1 | */ |
|||
1968 | 1 | public function setValidationClass($class) { |
|||
1969 | 1 | trigger_error('Schema::setValidationClass() is deprecated. Use Schema::setValidationFactory() instead.', E_USER_DEPRECATED); |
|||
1970 | |||||
1971 | 1 | if (!is_a($class, Validation::class, true)) { |
|||
1972 | throw new \InvalidArgumentException("$class must be a subclass of ".Validation::class, 500); |
||||
1973 | 1 | } |
|||
1974 | 1 | ||||
1975 | 1 | $this->setValidationFactory(function () use ($class) { |
|||
1976 | 1 | if ($class instanceof Validation) { |
|||
1977 | $result = clone $class; |
||||
1978 | } else { |
||||
1979 | $result = new $class; |
||||
1980 | } |
||||
1981 | return $result; |
||||
1982 | }); |
||||
1983 | $this->validationClass = $class; |
||||
0 ignored issues
–
show
The property
Garden\Schema\Schema::$validationClass has been deprecated.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
1984 | return $this; |
||||
1985 | } |
||||
1986 | 2 | ||||
1987 | 2 | /** |
|||
1988 | 2 | * Return a sparse version of this schema. |
|||
1989 | * |
||||
1990 | * A sparse schema has no required properties. |
||||
1991 | * |
||||
1992 | * @return Schema Returns a new sparse schema. |
||||
1993 | */ |
||||
1994 | public function withSparse() { |
||||
1995 | $sparseSchema = $this->withSparseInternal($this, new \SplObjectStorage()); |
||||
1996 | return $sparseSchema; |
||||
1997 | } |
||||
1998 | 2 | ||||
1999 | 2 | /** |
|||
2000 | 2 | * The internal implementation of `Schema::withSparse()`. |
|||
2001 | 1 | * |
|||
2002 | * @param array|Schema $schema The schema to make sparse. |
||||
2003 | 2 | * @param \SplObjectStorage $schemas Collected sparse schemas that have already been made. |
|||
2004 | 2 | * @return mixed |
|||
2005 | 2 | */ |
|||
2006 | private function withSparseInternal($schema, \SplObjectStorage $schemas) { |
||||
2007 | if ($schema instanceof Schema) { |
||||
2008 | if ($schemas->contains($schema)) { |
||||
2009 | 2 | return $schemas[$schema]; |
|||
2010 | } else { |
||||
2011 | $schemas[$schema] = $sparseSchema = new Schema(); |
||||
2012 | $sparseSchema->schema = $schema->withSparseInternal($schema->schema, $schemas); |
||||
2013 | 2 | if ($id = $sparseSchema->getID()) { |
|||
2014 | $sparseSchema->setID($id.'Sparse'); |
||||
2015 | 2 | } |
|||
2016 | 1 | ||||
2017 | return $sparseSchema; |
||||
2018 | 2 | } |
|||
2019 | 2 | } |
|||
2020 | 2 | ||||
2021 | unset($schema['required']); |
||||
2022 | |||||
2023 | if (isset($schema['items'])) { |
||||
2024 | 2 | $schema['items'] = $this->withSparseInternal($schema['items'], $schemas); |
|||
2025 | } |
||||
2026 | if (isset($schema['properties'])) { |
||||
2027 | foreach ($schema['properties'] as $name => &$property) { |
||||
2028 | $property = $this->withSparseInternal($property, $schemas); |
||||
2029 | } |
||||
2030 | } |
||||
2031 | |||||
2032 | 6 | return $schema; |
|||
2033 | 6 | } |
|||
2034 | |||||
2035 | /** |
||||
2036 | * Get the ID for the schema. |
||||
2037 | * |
||||
2038 | * @return string |
||||
2039 | */ |
||||
2040 | public function getID(): string { |
||||
0 ignored issues
–
show
This method is not in camel caps format.
This check looks for method names that are not written in camelCase. In camelCase names are written without any punctuation, the start of each new
word being marked by a capital letter. Thus the name
database connection seeker becomes ![]() |
|||||
2041 | return $this->schema['id'] ?? ''; |
||||
2042 | 1 | } |
|||
2043 | 1 | ||||
2044 | /** |
||||
2045 | 1 | * Set the ID for the schema. |
|||
2046 | * |
||||
2047 | * @param string $id The new ID. |
||||
2048 | * @return $this |
||||
2049 | */ |
||||
2050 | public function setID(string $id) { |
||||
0 ignored issues
–
show
This method is not in camel caps format.
This check looks for method names that are not written in camelCase. In camelCase names are written without any punctuation, the start of each new
word being marked by a capital letter. Thus the name
database connection seeker becomes ![]() |
|||||
2051 | $this->schema['id'] = $id; |
||||
2052 | |||||
2053 | return $this; |
||||
2054 | } |
||||
2055 | 7 | ||||
2056 | 7 | /** |
|||
2057 | * Whether a offset exists. |
||||
2058 | * |
||||
2059 | * @param mixed $offset An offset to check for. |
||||
2060 | * @return boolean true on success or false on failure. |
||||
2061 | * @link http://php.net/manual/en/arrayaccess.offsetexists.php |
||||
2062 | */ |
||||
2063 | public function offsetExists($offset) { |
||||
2064 | return isset($this->schema[$offset]); |
||||
2065 | } |
||||
2066 | 7 | ||||
2067 | 7 | /** |
|||
2068 | * Offset to retrieve. |
||||
2069 | * |
||||
2070 | * @param mixed $offset The offset to retrieve. |
||||
2071 | * @return mixed Can return all value types. |
||||
2072 | * @link http://php.net/manual/en/arrayaccess.offsetget.php |
||||
2073 | */ |
||||
2074 | public function offsetGet($offset) { |
||||
2075 | return isset($this->schema[$offset]) ? $this->schema[$offset] : null; |
||||
2076 | } |
||||
2077 | 1 | ||||
2078 | 1 | /** |
|||
2079 | 1 | * Offset to set. |
|||
2080 | * |
||||
2081 | * @param mixed $offset The offset to assign the value to. |
||||
2082 | * @param mixed $value The value to set. |
||||
2083 | * @link http://php.net/manual/en/arrayaccess.offsetset.php |
||||
2084 | */ |
||||
2085 | public function offsetSet($offset, $value) { |
||||
2086 | $this->schema[$offset] = $value; |
||||
2087 | 1 | } |
|||
2088 | 1 | ||||
2089 | 1 | /** |
|||
2090 | * Offset to unset. |
||||
2091 | * |
||||
2092 | * @param mixed $offset The offset to unset. |
||||
2093 | * @link http://php.net/manual/en/arrayaccess.offsetunset.php |
||||
2094 | */ |
||||
2095 | public function offsetUnset($offset) { |
||||
2096 | unset($this->schema[$offset]); |
||||
2097 | } |
||||
2098 | |||||
2099 | 12 | /** |
|||
2100 | 12 | * Resolve the schema attached to a discriminator. |
|||
2101 | 12 | * |
|||
2102 | 1 | * @param mixed $value The value to search for the discriminator. |
|||
2103 | * @param ValidationField $field The current node's schema information. |
||||
2104 | * @return ValidationField|null Returns the resolved schema or **null** if it can't be resolved. |
||||
2105 | 12 | * @throws ParseException Throws an exception if the discriminator isn't a string. |
|||
2106 | */ |
||||
2107 | private function resolveDiscriminator($value, ValidationField $field, array $visited = []) { |
||||
2108 | 12 | $propertyName = $field->val('discriminator')['propertyName'] ?? ''; |
|||
2109 | 1 | if (empty($propertyName) || !is_string($propertyName)) { |
|||
2110 | 1 | throw new ParseException("Invalid propertyName for discriminator at {$field->getSchemaPath()}", 500); |
|||
2111 | 11 | } |
|||
2112 | 1 | ||||
2113 | 1 | $propertyFieldName = ltrim($field->getName().'/'.self::escapeRef($propertyName), '/'); |
|||
2114 | 1 | ||||
2115 | 1 | // Do some basic validation checking to see if we can even look at the property. |
|||
2116 | if (!$this->isArray($value)) { |
||||
2117 | 1 | $field->addTypeError($value, 'object'); |
|||
2118 | return null; |
||||
2119 | } elseif (empty($value[$propertyName])) { |
||||
2120 | 10 | $field->getValidation()->addError( |
|||
2121 | 10 | $propertyFieldName, |
|||
2122 | 1 | 'required', |
|||
2123 | 1 | ['messageCode' => '{property} is required.', 'property' => $propertyName] |
|||
2124 | 1 | ); |
|||
2125 | return null; |
||||
2126 | 1 | } |
|||
2127 | 1 | ||||
2128 | 1 | $propertyValue = $value[$propertyName]; |
|||
2129 | if (!is_string($propertyValue)) { |
||||
2130 | $field->getValidation()->addError( |
||||
2131 | 1 | $propertyFieldName, |
|||
2132 | 'type', |
||||
2133 | [ |
||||
2134 | 9 | 'type' => 'string', |
|||
2135 | 9 | 'value' => is_scalar($value) ? $value : null, |
|||
2136 | 2 | 'messageCode' => is_scalar($value) ? "{value} is not a valid string." : "The value is not a valid string." |
|||
2137 | ] |
||||
2138 | 2 | ); |
|||
2139 | 2 | return null; |
|||
2140 | } |
||||
2141 | |||||
2142 | $mapping = $field->val('discriminator')['mapping'] ?? ''; |
||||
2143 | 7 | if (isset($mapping[$propertyValue])) { |
|||
2144 | $ref = $mapping[$propertyValue]; |
||||
2145 | |||||
2146 | if (strpos($ref, '#') === false) { |
||||
2147 | 9 | $ref = '#/components/schemas/'.self::escapeRef($ref); |
|||
2148 | 9 | } |
|||
2149 | 1 | } else { |
|||
2150 | 1 | // Don't let a property value provide its own ref as that may pose a security concern.. |
|||
2151 | 1 | $ref = '#/components/schemas/'.self::escapeRef($propertyValue); |
|||
2152 | } |
||||
2153 | 1 | ||||
2154 | 1 | // Validate the reference against the oneOf constraint. |
|||
2155 | 1 | $oneOf = $field->val('oneOf', []); |
|||
2156 | if (!empty($oneOf) && !in_array(['$ref' => $ref], $oneOf)) { |
||||
2157 | $field->getValidation()->addError( |
||||
2158 | 1 | $propertyFieldName, |
|||
2159 | 'oneOf', |
||||
2160 | [ |
||||
2161 | 'type' => 'string', |
||||
2162 | 'value' => is_scalar($propertyValue) ? $propertyValue : null, |
||||
0 ignored issues
–
show
|
|||||
2163 | 9 | 'messageCode' => is_scalar($propertyValue) ? "{value} is not a valid option." : "The value is not a valid option." |
|||
0 ignored issues
–
show
|
|||||
2164 | ] |
||||
2165 | 9 | ); |
|||
2166 | 8 | return null; |
|||
2167 | 2 | } |
|||
2168 | |||||
2169 | try { |
||||
2170 | 7 | // Lookup the schema. |
|||
2171 | 7 | $visited[$field->getSchemaPath()] = true; |
|||
2172 | 7 | ||||
2173 | 7 | list($schema, $schemaPath) = $this->lookupSchema(['$ref' => $ref], $field->getSchemaPath()); |
|||
2174 | 7 | if (isset($visited[$schemaPath])) { |
|||
2175 | 7 | throw new RefNotFoundException('Cyclical ref.', 508); |
|||
2176 | } |
||||
2177 | 7 | ||||
2178 | 4 | $result = new ValidationField( |
|||
2179 | $field->getValidation(), |
||||
2180 | 4 | $schema, |
|||
2181 | $field->getName(), |
||||
2182 | 4 | $schemaPath, |
|||
2183 | $field->getOptions() |
||||
2184 | 3 | ); |
|||
2185 | 3 | if (!empty($schema['discriminator'])) { |
|||
2186 | 3 | return $this->resolveDiscriminator($value, $result, $visited); |
|||
2187 | } else { |
||||
2188 | 3 | return $result; |
|||
2189 | 3 | } |
|||
2190 | 3 | } catch (RefNotFoundException $ex) { |
|||
2191 | // Since this is a ref provided by the value it is technically a validation error. |
||||
2192 | $field->getValidation()->addError( |
||||
2193 | 3 | $propertyFieldName, |
|||
2194 | 'propertyName', |
||||
2195 | [ |
||||
2196 | 'type' => 'string', |
||||
2197 | 'value' => is_scalar($propertyValue) ? $propertyValue : null, |
||||
0 ignored issues
–
show
|
|||||
2198 | 'messageCode' => is_scalar($propertyValue) ? "{value} is not a valid option." : "The value is not a valid option." |
||||
0 ignored issues
–
show
|
|||||
2199 | ] |
||||
2200 | ); |
||||
2201 | return null; |
||||
2202 | } |
||||
2203 | } |
||||
2204 | } |
||||
2205 |