|
23 | 23 | */
|
24 | 24 | class UndefinedConstraint extends Constraint
|
25 | 25 | {
|
| 26 | + /** |
| 27 | + * @var array List of properties to which a default value has been applied |
| 28 | + */ |
| 29 | + protected $appliedDefaults = array(); |
| 30 | + |
26 | 31 | /**
|
27 | 32 | * {@inheritdoc}
|
28 | 33 | */
|
29 |
| - public function check(&$value, $schema = null, JsonPointer $path = null, $i = null) |
| 34 | + public function check(&$value, $schema = null, JsonPointer $path = null, $i = null, $fromDefault = false) |
30 | 35 | {
|
31 | 36 | if (is_null($schema) || !is_object($schema)) {
|
32 | 37 | return;
|
33 | 38 | }
|
34 | 39 |
|
35 | 40 | $path = $this->incrementPath($path ?: new JsonPointer(''), $i);
|
| 41 | + if ($fromDefault) { |
| 42 | + $path->setFromDefault(); |
| 43 | + } |
36 | 44 |
|
37 | 45 | // check special properties
|
38 | 46 | $this->validateCommonProperties($value, $schema, $path, $i);
|
@@ -68,7 +76,8 @@ public function validateTypes(&$value, $schema = null, JsonPointer $path, $i = n
|
68 | 76 | isset($schema->properties) ? $this->factory->getSchemaStorage()->resolveRefSchema($schema->properties) : $schema,
|
69 | 77 | $path,
|
70 | 78 | isset($schema->additionalProperties) ? $schema->additionalProperties : null,
|
71 |
| - isset($schema->patternProperties) ? $schema->patternProperties : null |
| 79 | + isset($schema->patternProperties) ? $schema->patternProperties : null, |
| 80 | + $this->appliedDefaults |
72 | 81 | );
|
73 | 82 | }
|
74 | 83 |
|
@@ -113,46 +122,8 @@ protected function validateCommonProperties(&$value, $schema = null, JsonPointer
|
113 | 122 | }
|
114 | 123 |
|
115 | 124 | // Apply default values from schema
|
116 |
| - if ($this->factory->getConfig(self::CHECK_MODE_APPLY_DEFAULTS)) { |
117 |
| - if ($this->getTypeCheck()->isObject($value) && isset($schema->properties)) { |
118 |
| - // $value is an object, so apply default properties if defined |
119 |
| - foreach ($schema->properties as $currentProperty => $propertyDefinition) { |
120 |
| - if (!$this->getTypeCheck()->propertyExists($value, $currentProperty) && isset($propertyDefinition->default)) { |
121 |
| - if (is_object($propertyDefinition->default)) { |
122 |
| - $this->getTypeCheck()->propertySet($value, $currentProperty, clone $propertyDefinition->default); |
123 |
| - } else { |
124 |
| - $this->getTypeCheck()->propertySet($value, $currentProperty, $propertyDefinition->default); |
125 |
| - } |
126 |
| - } |
127 |
| - } |
128 |
| - } elseif ($this->getTypeCheck()->isArray($value)) { |
129 |
| - if (isset($schema->properties)) { |
130 |
| - // $value is an array, but default properties are defined, so treat as assoc |
131 |
| - foreach ($schema->properties as $currentProperty => $propertyDefinition) { |
132 |
| - if (!isset($value[$currentProperty]) && isset($propertyDefinition->default)) { |
133 |
| - if (is_object($propertyDefinition->default)) { |
134 |
| - $value[$currentProperty] = clone $propertyDefinition->default; |
135 |
| - } else { |
136 |
| - $value[$currentProperty] = $propertyDefinition->default; |
137 |
| - } |
138 |
| - } |
139 |
| - } |
140 |
| - } elseif (isset($schema->items)) { |
141 |
| - // $value is an array, and default items are defined - treat as plain array |
142 |
| - foreach ($schema->items as $currentProperty => $itemDefinition) { |
143 |
| - if (!isset($value[$currentProperty]) && isset($itemDefinition->default)) { |
144 |
| - if (is_object($itemDefinition->default)) { |
145 |
| - $value[$currentProperty] = clone $itemDefinition->default; |
146 |
| - } else { |
147 |
| - $value[$currentProperty] = $itemDefinition->default; |
148 |
| - } |
149 |
| - } |
150 |
| - } |
151 |
| - } |
152 |
| - } elseif (($value instanceof self || $value === null) && isset($schema->default)) { |
153 |
| - // $value is a leaf, not a container - apply the default directly |
154 |
| - $value = is_object($schema->default) ? clone $schema->default : $schema->default; |
155 |
| - } |
| 125 | + if (!$path->fromDefault()) { |
| 126 | + $this->applyDefaultValues($value, $schema, $path); |
156 | 127 | }
|
157 | 128 |
|
158 | 129 | // Verify required values
|
@@ -216,6 +187,96 @@ protected function validateCommonProperties(&$value, $schema = null, JsonPointer
|
216 | 187 | }
|
217 | 188 | }
|
218 | 189 |
|
| 190 | + /** |
| 191 | + * Check whether a default should be applied for this value |
| 192 | + * |
| 193 | + * @param mixed $schema |
| 194 | + * @param mixed $parentSchema |
| 195 | + * @param bool $requiredOnly |
| 196 | + * |
| 197 | + * @return bool |
| 198 | + */ |
| 199 | + private function shouldApplyDefaultValue($requiredOnly, $schema, $name = null, $parentSchema = null) |
| 200 | + { |
| 201 | + // required-only mode is off |
| 202 | + if (!$requiredOnly) { |
| 203 | + return true; |
| 204 | + } |
| 205 | + // draft-04 required is set |
| 206 | + if ( |
| 207 | + $name !== null |
| 208 | + && isset($parentSchema->required) |
| 209 | + && is_array($parentSchema->required) |
| 210 | + && in_array($name, $parentSchema->required) |
| 211 | + ) { |
| 212 | + return true; |
| 213 | + } |
| 214 | + // draft-03 required is set |
| 215 | + if (isset($schema->required) && !is_array($schema->required) && $schema->required) { |
| 216 | + return true; |
| 217 | + } |
| 218 | + // default case |
| 219 | + return false; |
| 220 | + } |
| 221 | + |
| 222 | + /** |
| 223 | + * Apply default values |
| 224 | + * |
| 225 | + * @param mixed $value |
| 226 | + * @param mixed $schema |
| 227 | + * @param JsonPointer $path |
| 228 | + */ |
| 229 | + protected function applyDefaultValues(&$value, $schema, $path) |
| 230 | + { |
| 231 | + // only apply defaults if feature is enabled |
| 232 | + if (!$this->factory->getConfig(self::CHECK_MODE_APPLY_DEFAULTS)) { |
| 233 | + return; |
| 234 | + } |
| 235 | + |
| 236 | + // apply defaults if appropriate |
| 237 | + $requiredOnly = $this->factory->getConfig(self::CHECK_MODE_ONLY_REQUIRED_DEFAULTS); |
| 238 | + if (isset($schema->properties) && LooseTypeCheck::isObject($value)) { |
| 239 | + // $value is an object or assoc array, and properties are defined - treat as an object |
| 240 | + foreach ($schema->properties as $currentProperty => $propertyDefinition) { |
| 241 | + if ( |
| 242 | + !LooseTypeCheck::propertyExists($value, $currentProperty) |
| 243 | + && property_exists($propertyDefinition, 'default') |
| 244 | + && $this->shouldApplyDefaultValue($requiredOnly, $propertyDefinition, $currentProperty, $schema) |
| 245 | + ) { |
| 246 | + // assign default value |
| 247 | + if (is_object($propertyDefinition->default)) { |
| 248 | + LooseTypeCheck::propertySet($value, $currentProperty, clone $propertyDefinition->default); |
| 249 | + } else { |
| 250 | + LooseTypeCheck::propertySet($value, $currentProperty, $propertyDefinition->default); |
| 251 | + } |
| 252 | + $this->appliedDefaults[] = $currentProperty; |
| 253 | + } |
| 254 | + } |
| 255 | + } elseif (isset($schema->items) && LooseTypeCheck::isArray($value)) { |
| 256 | + // $value is an array, and items are defined - treat as plain array |
| 257 | + foreach ($schema->items as $currentItem => $itemDefinition) { |
| 258 | + if ( |
| 259 | + !array_key_exists($currentItem, $value) |
| 260 | + && property_exists($itemDefinition, 'default') |
| 261 | + && $this->shouldApplyDefaultValue($requiredOnly, $itemDefinition)) { |
| 262 | + if (is_object($itemDefinition->default)) { |
| 263 | + $value[$currentItem] = clone $itemDefinition->default; |
| 264 | + } else { |
| 265 | + $value[$currentItem] = $itemDefinition->default; |
| 266 | + } |
| 267 | + } |
| 268 | + $path->setFromDefault(); |
| 269 | + } |
| 270 | + } elseif ( |
| 271 | + $value instanceof self |
| 272 | + && property_exists($schema, 'default') |
| 273 | + && $this->shouldApplyDefaultValue($requiredOnly, $schema)) { |
| 274 | + // $value is a leaf, not a container - apply the default directly |
| 275 | + $value = is_object($schema->default) ? clone $schema->default : $schema->default; |
| 276 | + $path->setFromDefault(); |
| 277 | + } |
| 278 | + } |
| 279 | + |
219 | 280 | /**
|
220 | 281 | * Validate allOf, anyOf, and oneOf properties
|
221 | 282 | *
|
|
0 commit comments