Skip to content

Commit d39c56a

Browse files
shmaxbighappyface
authored andcommitted
Better coercion (#336)
* Better coercion * add phpdocs
1 parent 1da8622 commit d39c56a

File tree

9 files changed

+255
-173
lines changed

9 files changed

+255
-173
lines changed

README.md

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,7 @@ $request = (object)[
6363
'refundAmount'=>"17"
6464
];
6565

66-
$factory = new Factory( null, null, Constraint::CHECK_MODE_TYPE_CAST | Constraint::CHECK_MODE_COERCE );
67-
68-
$validator = new Validator($factory);
69-
$validator->check($request, (object) [
66+
$validator->coerce($request, (object) [
7067
"type"=>"object",
7168
"properties"=>(object)[
7269
"processRefund"=>(object)[
@@ -82,8 +79,6 @@ is_bool($request->processRefund); // true
8279
is_int($request->refundAmount); // true
8380
```
8481

85-
Note that the `CHECK_MODE_COERCE` flag will only take effect when an object is passed into the `check()` method.
86-
8782
### With inline references
8883

8984
```php
@@ -144,4 +139,4 @@ $jsonValidator->check($jsonToValidateObject, $jsonSchemaObject);
144139
composer test
145140
composer testOnly TestClass
146141
composer testOnly TestClass::testMethod
147-
```
142+
```

src/JsonSchema/Constraints/CollectionConstraint.php

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,24 @@
1919
*/
2020
class CollectionConstraint extends Constraint
2121
{
22+
2223
/**
2324
* {@inheritDoc}
2425
*/
2526
public function check($value, $schema = null, JsonPointer $path = null, $i = null)
27+
{
28+
$this->_check($value, $schema, $path, $i);
29+
}
30+
31+
/**
32+
* {@inheritDoc}
33+
*/
34+
public function coerce(&$value, $schema = null, JsonPointer $path = null, $i = null)
35+
{
36+
$this->_check($value, $schema, $path, $i, true);
37+
}
38+
39+
protected function _check(&$value, $schema = null, JsonPointer $path = null, $i = null, $coerce = false)
2640
{
2741
// Verify minItems
2842
if (isset($schema->minItems) && count($value) < $schema->minItems) {
@@ -47,7 +61,7 @@ public function check($value, $schema = null, JsonPointer $path = null, $i = nul
4761

4862
// Verify items
4963
if (isset($schema->items)) {
50-
$this->validateItems($value, $schema, $path, $i);
64+
$this->validateItems($value, $schema, $path, $i, $coerce);
5165
}
5266
}
5367

@@ -58,8 +72,9 @@ public function check($value, $schema = null, JsonPointer $path = null, $i = nul
5872
* @param \stdClass $schema
5973
* @param JsonPointer|null $path
6074
* @param string $i
75+
* @param boolean $coerce
6176
*/
62-
protected function validateItems($value, $schema = null, JsonPointer $path = null, $i = null)
77+
protected function validateItems(&$value, $schema = null, JsonPointer $path = null, $i = null, $coerce = false)
6378
{
6479
if (is_object($schema->items)) {
6580
// just one type definition for the whole array
@@ -74,27 +89,27 @@ protected function validateItems($value, $schema = null, JsonPointer $path = nul
7489
) {
7590
// performance optimization
7691
$type = $schema->items->type;
77-
$validator = $this->factory->createInstanceFor($type === 'integer' ? 'number' : $type);
92+
$typeValidator = $this->factory->createInstanceFor('type');
93+
$validator = $this->factory->createInstanceFor($type === 'integer' ? 'number' : $type);
7894

7995
foreach ($value as $k => $v) {
80-
$k_path = $this->incrementPath($path, $k);
96+
$k_path = $this->incrementPath($path, $k);
97+
if($coerce) {
98+
$typeValidator->coerce($v, $schema->items, $k_path, $i);
99+
} else {
100+
$typeValidator->check($v, $schema->items, $k_path, $i);
101+
}
81102

82-
if (($type === 'string' && !is_string($v))
83-
|| ($type === 'number' && !(is_numeric($v) && !is_string($v)))
84-
|| ($type === 'integer' && !is_int($v))
85-
){
86-
$this->addError($k_path, ucwords(gettype($v)) . " value found, but $type is required", 'type');
87-
} else {
88-
$validator->check($v, $schema, $k_path, $i);
89-
}
103+
$validator->check($v, $schema->items, $k_path, $i);
90104
}
105+
$this->addErrors($typeValidator->getErrors());
91106
$this->addErrors($validator->getErrors());
92107
} else {
93108
foreach ($value as $k => $v) {
94109
$initErrors = $this->getErrors();
95110

96111
// First check if its defined in "items"
97-
$this->checkUndefined($v, $schema->items, $path, $k);
112+
$this->checkUndefined($v, $schema->items, $path, $k, $coerce);
98113

99114
// Recheck with "additionalItems" if the first test fails
100115
if (count($initErrors) < count($this->getErrors()) && (isset($schema->additionalItems) && $schema->additionalItems !== false)) {
@@ -114,27 +129,27 @@ protected function validateItems($value, $schema = null, JsonPointer $path = nul
114129
// Defined item type definitions
115130
foreach ($value as $k => $v) {
116131
if (array_key_exists($k, $schema->items)) {
117-
$this->checkUndefined($v, $schema->items[$k], $path, $k);
132+
$this->checkUndefined($v, $schema->items[$k], $path, $k, $coerce);
118133
} else {
119134
// Additional items
120135
if (property_exists($schema, 'additionalItems')) {
121136
if ($schema->additionalItems !== false) {
122-
$this->checkUndefined($v, $schema->additionalItems, $path, $k);
137+
$this->checkUndefined($v, $schema->additionalItems, $path, $k, $coerce);
123138
} else {
124139
$this->addError(
125140
$path, 'The item ' . $i . '[' . $k . '] is not defined and the definition does not allow additional items', 'additionalItems', array('additionalItems' => $schema->additionalItems,));
126141
}
127142
} else {
128143
// Should be valid against an empty schema
129-
$this->checkUndefined($v, new \stdClass(), $path, $k);
144+
$this->checkUndefined($v, new \stdClass(), $path, $k, $coerce);
130145
}
131146
}
132147
}
133148

134149
// Treat when we have more schema definitions than values, not for empty arrays
135150
if (count($value) > 0) {
136151
for ($k = count($value); $k < count($schema->items); $k++) {
137-
$this->checkUndefined($this->factory->createInstanceFor('undefined'), $schema->items[$k], $path, $k);
152+
$this->checkUndefined($this->factory->createInstanceFor('undefined'), $schema->items[$k], $path, $k, $coerce);
138153
}
139154
}
140155
}

src/JsonSchema/Constraints/Constraint.php

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ abstract class Constraint implements ConstraintInterface
2727

2828
const CHECK_MODE_NORMAL = 0x00000001;
2929
const CHECK_MODE_TYPE_CAST = 0x00000002;
30-
const CHECK_MODE_COERCE = 0x00000004;
3130

3231
/**
3332
* @var Factory
@@ -124,11 +123,16 @@ protected function incrementPath(JsonPointer $path = null, $i)
124123
* @param mixed $schema
125124
* @param JsonPointer|null $path
126125
* @param mixed $i
126+
* @param boolean $coerce
127127
*/
128-
protected function checkArray($value, $schema = null, JsonPointer $path = null, $i = null)
128+
protected function checkArray(&$value, $schema = null, JsonPointer $path = null, $i = null, $coerce = false)
129129
{
130130
$validator = $this->factory->createInstanceFor('collection');
131-
$validator->check($value, $schema, $path, $i);
131+
if($coerce) {
132+
$validator->coerce($value, $schema, $path, $i);
133+
} else {
134+
$validator->check($value, $schema, $path, $i);
135+
}
132136

133137
$this->addErrors($validator->getErrors());
134138
}
@@ -141,11 +145,16 @@ protected function checkArray($value, $schema = null, JsonPointer $path = null,
141145
* @param JsonPointer|null $path
142146
* @param mixed $i
143147
* @param mixed $patternProperties
148+
* @param boolean $coerce
144149
*/
145-
protected function checkObject($value, $schema = null, JsonPointer $path = null, $i = null, $patternProperties = null)
150+
protected function checkObject(&$value, $schema = null, JsonPointer $path = null, $i = null, $patternProperties = null, $coerce = false)
146151
{
147152
$validator = $this->factory->createInstanceFor('object');
148-
$validator->check($value, $schema, $path, $i, $patternProperties);
153+
if($coerce){
154+
$validator->coerce($value, $schema, $path, $i, $patternProperties);
155+
} else {
156+
$validator->check($value, $schema, $path, $i, $patternProperties);
157+
}
149158

150159
$this->addErrors($validator->getErrors());
151160
}
@@ -157,11 +166,16 @@ protected function checkObject($value, $schema = null, JsonPointer $path = null,
157166
* @param mixed $schema
158167
* @param JsonPointer|null $path
159168
* @param mixed $i
169+
* @param boolean $coerce
160170
*/
161-
protected function checkType($value, $schema = null, JsonPointer $path = null, $i = null)
171+
protected function checkType(&$value, $schema = null, JsonPointer $path = null, $i = null, $coerce = false)
162172
{
163173
$validator = $this->factory->createInstanceFor('type');
164-
$validator->check($value, $schema, $path, $i);
174+
if($coerce) {
175+
$validator->coerce($value, $schema, $path, $i);
176+
} else {
177+
$validator->check($value, $schema, $path, $i);
178+
}
165179

166180
$this->addErrors($validator->getErrors());
167181
}
@@ -173,12 +187,17 @@ protected function checkType($value, $schema = null, JsonPointer $path = null, $
173187
* @param mixed $schema
174188
* @param JsonPointer|null $path
175189
* @param mixed $i
190+
* @param boolean $coerce
176191
*/
177-
protected function checkUndefined($value, $schema = null, JsonPointer $path = null, $i = null)
192+
protected function checkUndefined(&$value, $schema = null, JsonPointer $path = null, $i = null, $coerce = false)
178193
{
179194
$validator = $this->factory->createInstanceFor('undefined');
180195

181-
$validator->check($value, $this->factory->getSchemaStorage()->resolveRefSchema($schema), $path, $i);
196+
if($coerce){
197+
$validator->coerce($value, $this->factory->getSchemaStorage()->resolveRefSchema($schema), $path, $i);
198+
} else {
199+
$validator->check($value, $this->factory->getSchemaStorage()->resolveRefSchema($schema), $path, $i);
200+
}
182201

183202
$this->addErrors($validator->getErrors());
184203
}

src/JsonSchema/Constraints/ObjectConstraint.php

Lines changed: 23 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,23 @@
1919
*/
2020
class ObjectConstraint extends Constraint
2121
{
22-
/**
23-
* {@inheritDoc}
24-
*/
25-
public function check($element, $definition = null, JsonPointer $path = null, $additionalProp = null, $patternProperties = null)
22+
/**
23+
* {@inheritDoc}
24+
*/
25+
public function check($element, $definition = null, JsonPointer $path = null, $additionalProp = null, $patternProperties = null)
26+
{
27+
$this->_check($element, $definition, $path, $additionalProp, $patternProperties);
28+
}
29+
30+
/**
31+
* {@inheritDoc}
32+
*/
33+
public function coerce(&$element, $definition = null, JsonPointer $path = null, $additionalProp = null, $patternProperties = null)
34+
{
35+
$this->_check($element, $definition, $path, $additionalProp, $patternProperties, true);
36+
}
37+
38+
protected function _check(&$element, $definition = null, JsonPointer $path = null, $additionalProp = null, $patternProperties = null, $coerce = false)
2639
{
2740
if ($element instanceof UndefinedConstraint) {
2841
return;
@@ -35,7 +48,7 @@ public function check($element, $definition = null, JsonPointer $path = null, $a
3548

3649
if ($definition) {
3750
// validate the definition properties
38-
$this->validateDefinition($element, $definition, $path);
51+
$this->validateDefinition($element, $definition, $path, $coerce);
3952
}
4053

4154
// additional the element properties
@@ -119,105 +132,21 @@ public function validateElement($element, $matches, $objectDefinition = null, Js
119132
* @param \stdClass $element Element to validate
120133
* @param \stdClass $objectDefinition ObjectConstraint definition
121134
* @param JsonPointer|null $path Path?
135+
* @param boolean $coerce Whether to coerce strings to expected types or not
122136
*/
123-
public function validateDefinition($element, $objectDefinition = null, JsonPointer $path = null)
137+
public function validateDefinition(&$element, $objectDefinition = null, JsonPointer $path = null, $coerce = false)
124138
{
125139
$undefinedConstraint = $this->factory->createInstanceFor('undefined');
126140

127141
foreach ($objectDefinition as $i => $value) {
128-
$property = $this->getProperty($element, $i, $undefinedConstraint);
142+
$property = &$this->getProperty($element, $i, $undefinedConstraint);
129143
$definition = $this->getProperty($objectDefinition, $i);
130144

131-
if($this->factory->getCheckMode() & Constraint::CHECK_MODE_TYPE_CAST){
132-
if(!($property instanceof Constraint)) {
133-
$property = $this->coerce($property, $definition);
134-
135-
if($this->factory->getCheckMode() & Constraint::CHECK_MODE_COERCE) {
136-
if (is_object($element)) {
137-
$element->{$i} = $property;
138-
} else {
139-
$element[$i] = $property;
140-
}
141-
}
142-
}
143-
}
144-
145145
if (is_object($definition)) {
146146
// Undefined constraint will check for is_object() and quit if is not - so why pass it?
147-
$this->checkUndefined($property, $definition, $path, $i);
148-
}
149-
}
150-
}
151-
152-
/**
153-
* Converts a value to boolean. For example, "true" becomes true.
154-
* @param $value The value to convert to boolean
155-
* @return bool|mixed
156-
*/
157-
protected function toBoolean($value)
158-
{
159-
if($value === "true"){
160-
return true;
161-
}
162-
163-
if($value === "false"){
164-
return false;
165-
}
166-
167-
return $value;
168-
}
169-
170-
/**
171-
* Converts a numeric string to a number. For example, "4" becomes 4.
172-
*
173-
* @param mixed $value The value to convert to a number.
174-
* @return int|float|mixed
175-
*/
176-
protected function toNumber($value)
177-
{
178-
if(is_numeric($value)) {
179-
return $value + 0; // cast to number
180-
}
181-
182-
return $value;
183-
}
184-
185-
protected function toInteger($value)
186-
{
187-
if(is_numeric($value) && (int)$value == $value) {
188-
return (int)$value; // cast to number
189-
}
190-
191-
return $value;
192-
}
193-
194-
/**
195-
* Given a value and a definition, attempts to coerce the value into the
196-
* type specified by the definition's 'type' property.
197-
*
198-
* @param mixed $value Value to coerce.
199-
* @param \stdClass $definition A definition with information about the expected type.
200-
* @return bool|int|string
201-
*/
202-
protected function coerce($value, $definition)
203-
{
204-
$types = isset($definition->type)?$definition->type:null;
205-
if($types){
206-
foreach((array)$types as $type) {
207-
switch ($type) {
208-
case "boolean":
209-
$value = $this->toBoolean($value);
210-
break;
211-
case "integer":
212-
$value = $this->toInteger($value);
213-
break;
214-
case "number":
215-
$value = $this->toNumber($value);
216-
break;
217-
}
147+
$this->checkUndefined($property, $definition, $path, $i, $coerce);
218148
}
219149
}
220-
return $value;
221150
}
222151

223152
/**
@@ -229,7 +158,7 @@ protected function coerce($value, $definition)
229158
*
230159
* @return mixed
231160
*/
232-
protected function getProperty($element, $property, $fallback = null)
161+
protected function &getProperty(&$element, $property, $fallback = null)
233162
{
234163
if (is_array($element) && (isset($element[$property]) || array_key_exists($property, $element)) /*$this->checkMode == self::CHECK_MODE_TYPE_CAST*/) {
235164
return $element[$property];

0 commit comments

Comments
 (0)