Skip to content

Commit bcb6367

Browse files
author
Enno Woortmann
committed
Add array contains validation
Better error message for invalid additional properties Validate in setter
1 parent 3d28f1d commit bcb6367

18 files changed

+275
-24
lines changed

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,8 @@ Let's have a look into an easy example. We create a simple model for a person wi
8989
"type": "string"
9090
},
9191
"age": {
92-
"type": "integer"
92+
"type": "integer",
93+
"minimum": 0
9394
}
9495
},
9596
"required": [
@@ -125,10 +126,18 @@ $person = new Person();
125126
// Exception: 'Invalid type for name. Requires string, got int'
126127
$person = new Person(['name' => 12]);
127128

129+
// Throws an exception as the age contains an invalid value due to the minimum definition.
130+
// Exception: 'Value for age must not be smaller than 0'
131+
$person = new Person(['name' => 'Albert', 'age' => -1]);
132+
128133
// A valid example as the age isn't required
129134
$person = new Person(['name' => 'Albert']);
130135
$person->getName(); // returns 'Albert'
131136
$person->getAge(); // returns NULL
137+
138+
// If setters are generated the setters also perform validations.
139+
// Exception: 'Value for age must not be smaller than 0'
140+
$person->setAge(-10);
132141
```
133142

134143
More complex exception messages eg. from a [allOf](https://json-schema.org/understanding-json-schema/reference/combining.html#allof) composition may look like:

src/PropertyProcessor/Property/ArrayProcessor.php

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
*
2020
* @package PHPModelGenerator\PropertyProcessor\Property
2121
*
22-
* TODO: contains and tuple validation
22+
* TODO: tuple validation
2323
*/
2424
class ArrayProcessor extends AbstractTypedValueProcessor
2525
{
@@ -28,6 +28,7 @@ class ArrayProcessor extends AbstractTypedValueProcessor
2828
private const JSON_FIELD_MIN_ITEMS = 'minItems';
2929
private const JSON_FIELD_MAX_ITEMS = 'maxItems';
3030
private const JSON_FIELD_ITEMS = 'items';
31+
private const JSON_FIELD_CONTAINS = 'contains';
3132

3233
/**
3334
* @param PropertyInterface $property
@@ -42,6 +43,7 @@ protected function generateValidators(PropertyInterface $property, array $proper
4243
$this->addLengthValidation($property, $propertyData);
4344
$this->addUniqueItemsValidation($property, $propertyData);
4445
$this->addItemsValidation($property, $propertyData);
46+
$this->addContainsValidation($property, $propertyData);
4547
}
4648

4749
/**
@@ -94,7 +96,7 @@ private function addUniqueItemsValidation(PropertyInterface $property, array $pr
9496
}
9597

9698
/**
97-
* Add the validator to check if the items inside an array are unique
99+
* Add the validator to check for constraints required for each item
98100
*
99101
* @param PropertyInterface $property
100102
* @param array $propertyData
@@ -107,26 +109,73 @@ private function addItemsValidation(PropertyInterface $property, array $property
107109
return;
108110
}
109111

112+
$this->addItemValidator(
113+
$property,
114+
$propertyData[self::JSON_FIELD_ITEMS],
115+
'ArrayItem.phptpl',
116+
"Invalid item in array {$property->getName()}"
117+
);
118+
}
119+
120+
/**
121+
* Add the validator to check for constraints required for at least one item
122+
*
123+
* @param PropertyInterface $property
124+
* @param array $propertyData
125+
*
126+
* @throws SchemaException
127+
*/
128+
private function addContainsValidation(PropertyInterface $property, array $propertyData): void
129+
{
130+
if (!isset($propertyData[self::JSON_FIELD_CONTAINS])) {
131+
return;
132+
}
133+
134+
$this->addItemValidator(
135+
$property,
136+
$propertyData[self::JSON_FIELD_CONTAINS],
137+
'ArrayContains.phptpl',
138+
"No item in array {$property->getName()} matches contains constraint"
139+
);
140+
}
141+
142+
/**
143+
* Add an item based validator
144+
*
145+
* @param PropertyInterface $property
146+
* @param array $propertyData
147+
* @param string $template
148+
* @param string $message
149+
*
150+
* @throws SchemaException
151+
*/
152+
private function addItemValidator(
153+
PropertyInterface $property,
154+
array $propertyData,
155+
string $template,
156+
string $message
157+
) {
110158
// an item of the array behaves like a nested property to add item-level validation
111159
$nestedProperty = (new PropertyFactory(new PropertyProcessorFactory()))
112160
->create(
113161
new PropertyCollectionProcessor(),
114162
$this->schemaProcessor,
115163
$this->schema,
116-
'array item',
117-
$propertyData[self::JSON_FIELD_ITEMS]
164+
"item of array {$property->getName()}",
165+
$propertyData
118166
);
119167

120168
$property
121169
->addNestedProperty($nestedProperty)
122170
->addTypeHintDecorator(new ArrayTypeHintDecorator($nestedProperty))
123171
->addValidator(
124172
new PropertyTemplateValidator(
125-
'Invalid array item',
126-
DIRECTORY_SEPARATOR . 'Validator' . DIRECTORY_SEPARATOR . 'ArrayItem.phptpl',
173+
$message,
174+
DIRECTORY_SEPARATOR . 'Validator' . DIRECTORY_SEPARATOR . $template,
127175
[
128176
'property' => $nestedProperty,
129177
'viewHelper' => new RenderHelper($this->schemaProcessor->getGeneratorConfiguration()),
178+
'generatorConfiguration' => $this->schemaProcessor->getGeneratorConfiguration(),
130179
]
131180
)
132181
);

src/PropertyProcessor/Property/BaseProcessor.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,10 @@ protected function addAdditionalPropertiesValidator(array $propertyData): void
6464
$this->schema->addBaseValidator(
6565
new PropertyValidator(
6666
sprintf(
67-
'array_diff(array_keys($modelData), %s)',
67+
'$additionalProperties = array_diff(array_keys($modelData), %s)',
6868
preg_replace('(\d+\s=>)', '', var_export(array_keys($propertyData['properties'] ?? []), true))
6969
),
70-
'Provided JSON contains not allowed additional properties'
70+
'Provided JSON contains not allowed additional properties [" . join(", ", $additionalProperties) . "]'
7171
)
7272
);
7373
}

src/Templates/Model.phptpl

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,27 @@ class {{ class }}
116116
public function set{{ viewHelper.ucfirst(property.getAttribute()) }}(
117117
{% if property.getType() %}{% if not property.isRequired() %}?{% endif %}{{ property.getType() }} {% endif %}${{ property.getAttribute() }}
118118
): self {
119-
$this->{{ property.getAttribute() }} = ${{ property.getAttribute() }};
119+
$value = $modelData['{{ property.getName() }}'] = ${{ property.getAttribute() }};
120+
121+
{% if generatorConfiguration.collectErrors() %}
122+
$this->errorRegistry = new {{ viewHelper.getSimpleClassName(generatorConfiguration.getErrorRegistryClass()) }}();
123+
{% endif%}
124+
125+
{% foreach property.getOrderedValidators() as validator %}
126+
{{ validator.getValidatorSetUp() }}
127+
if ({{ validator.getCheck() }}) {
128+
{{ viewHelper.validationError(validator.getExceptionMessage()) }}
129+
}
130+
{% endforeach %}
131+
132+
{% if generatorConfiguration.collectErrors() %}
133+
if ($this->errorRegistry->getErrors()) {
134+
throw $this->errorRegistry;
135+
}
136+
{% endif%}
137+
138+
$this->{{ property.getAttribute() }} = $value;
139+
120140
return $this;
121141
}
122142
{% endif %}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
is_array($value) && (function (&$items) {
2+
if (empty($items)) {
3+
return true;
4+
}
5+
6+
{% if generatorConfiguration.collectErrors() %}
7+
$originalErrorRegistry = $this->errorRegistry;
8+
{% endif%}
9+
10+
foreach ($items as &$value) {
11+
try {
12+
{% if generatorConfiguration.collectErrors() %}
13+
$this->errorRegistry = new {{ viewHelper.getSimpleClassName(generatorConfiguration.getErrorRegistryClass()) }}();
14+
{% endif %}
15+
16+
{{ viewHelper.resolvePropertyDecorator(property) }}
17+
18+
{% foreach property.getOrderedValidators() as validator %}
19+
{{ validator.getValidatorSetUp() }}
20+
if ({{ validator.getCheck() }}) {
21+
{{ viewHelper.validationError(validator.getExceptionMessage()) }}
22+
}
23+
{% endforeach %}
24+
25+
{% if generatorConfiguration.collectErrors() %}
26+
if ($this->errorRegistry->getErrors()) {
27+
continue;
28+
}
29+
30+
$this->errorRegistry = $originalErrorRegistry;
31+
{% endif%}
32+
33+
// one matched item is enough to pass the contains check
34+
return false;
35+
} catch (\Exception $e) {
36+
continue;
37+
}
38+
}
39+
40+
{% if generatorConfiguration.collectErrors() %}
41+
$this->errorRegistry = $originalErrorRegistry;
42+
{% endif%}
43+
44+
return true;
45+
})($value)

src/Templates/Validator/ArrayItem.phptpl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@ is_array($value) && (function (&$items) {
1313
}
1414
{% endforeach %}
1515
}
16+
1617
return false;
1718
})($value)

tests/AbstractPHPModelGeneratorTest.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,12 @@ protected function combineDataProvider(array $dataProvider1, array $dataProvider
232232
return $result;
233233
}
234234

235+
/**
236+
* Expect a validation error based on the given configuration
237+
*
238+
* @param GeneratorConfiguration $configuration
239+
* @param array|string $messages
240+
*/
235241
protected function expectValidationError(GeneratorConfiguration $configuration, $messages): void
236242
{
237243
if (!is_array($messages)) {
@@ -255,10 +261,10 @@ protected function expectValidationErrorRegExp(GeneratorConfiguration $configura
255261
if ($configuration->collectErrors()) {
256262
$exception = $this->getErrorRegistryException($messages);
257263
$this->expectException(get_class($exception));
258-
$this->expectExceptionMessageRegExp($exception->getMessage());
264+
$this->expectExceptionMessageMatches($exception->getMessage());
259265
} else {
260266
$this->expectException(ValidationException::class);
261-
$this->expectExceptionMessageRegExp($messages[0]);
267+
$this->expectExceptionMessageMatches($messages[0]);
262268
}
263269
}
264270

tests/Basic/AdditionalPropertiesTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ public function definedPropertiesDataProvider():array
8080
public function testAdditionalPropertiesThrowAnExceptionWhenSetToFalse(array $propertyValue): void
8181
{
8282
$this->expectException(ValidationException::class);
83-
$this->expectExceptionMessage('Provided JSON contains not allowed additional properties');
83+
$this->expectExceptionMessage('Provided JSON contains not allowed additional properties [additional]');
8484

8585
$className = $this->generateClassFromFileTemplate('AdditionalProperties.json', ['false']);
8686

tests/Basic/BasicSchemaGenerationTest.php

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,55 @@ public function testSetterChangeTheInternalState(): void
4646
$object = new $className(['property' => 'Hello']);
4747

4848
$this->assertSame('Hello', $object->getProperty());
49-
$this->assertSame($object, $object->setProperty('ChangedPropertyValue'));
50-
$this->assertSame('ChangedPropertyValue', $object->getProperty());
49+
$this->assertSame($object, $object->setProperty('NewValue'));
50+
$this->assertSame('NewValue', $object->getProperty());
51+
}
52+
53+
/**
54+
* @dataProvider invalidStringPropertyValueProvider
55+
*
56+
* @param GeneratorConfiguration $configuration
57+
* @param string $propertyValue
58+
* @param string $exceptionMessage
59+
*/
60+
public function testInvalidSetterThrowsAnException(
61+
GeneratorConfiguration $configuration,
62+
string $propertyValue,
63+
array $exceptionMessage
64+
) {
65+
$this->expectValidationError($configuration, $exceptionMessage);
66+
67+
$className = $this->generateClassFromFile('BasicSchema.json', $configuration);
68+
69+
$object = new $className([]);
70+
$object->setProperty($propertyValue);
71+
}
72+
73+
public function invalidStringPropertyValueProvider(): array
74+
{
75+
return $this->combineDataProvider(
76+
$this->validationMethodDataProvider(), [
77+
'Too long string' => [
78+
'HelloMyOldFriend',
79+
[
80+
'property must not be longer than 8'
81+
]
82+
],
83+
'Invalid pattern' => [
84+
'123456789',
85+
[
86+
'property doesn\'t match pattern ^[a-zA-Z]*$'
87+
]
88+
],
89+
'Too long and invalid pattern' => [
90+
'HelloMyOld1234567',
91+
[
92+
'property doesn\'t match pattern ^[a-zA-Z]*$',
93+
'property must not be longer than 8',
94+
]
95+
]
96+
]
97+
);
5198
}
5299

53100
public function testPropertyNamesAreNormalized(): void
@@ -116,7 +163,7 @@ public function testFolderIsGeneratedRecursively(): void
116163
public function testInvalidJsonSchemaFileThrowsAnException(): void
117164
{
118165
$this->expectException(SchemaException::class);
119-
$this->expectExceptionMessageRegExp('/^Invalid JSON-Schema file (.*)\.json$/');
166+
$this->expectExceptionMessageMatches('/^Invalid JSON-Schema file (.*)\.json$/');
120167

121168
$this->generateClassFromFile('InvalidJSONSchema.json');
122169
}

tests/Basic/ErrorCollectionTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ public function invalidValuesForSinglePropertyDataProvider(): array
8686
public function testInvalidValuesForMultipleValuesInCompositionThrowsAnException($value, string $message): void
8787
{
8888
$this->expectException(ErrorRegistryException::class);
89-
$this->expectExceptionMessageRegExp("/$message/");
89+
$this->expectExceptionMessageMatches("/$message/");
9090

9191
$className = $this->generateClassFromFile('MultipleChecksInComposition.json', new GeneratorConfiguration());
9292

0 commit comments

Comments
 (0)