Skip to content

Commit 884f05a

Browse files
committed
Better error messages for invalid types and composed properties.
Error messages for invalid properties now include the required type (a list of possible types for MultiTypeProcessor) and the actually given type. Error messages for composed values now include how many of the composition elements must be matched and how many are matched. Additionally appends a list of all composition elements to the exception message which shows the elements which were matched and those who failed (only available if the error collection mode is enabled). Failing elements additionally include the exact reason for failing (the collected exception messages from the nested validation)
1 parent 1a5cc5d commit 884f05a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+425
-279
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1+
/tests/manual/result/
12
/vendor/
23
.idea

README.md

Lines changed: 77 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,16 @@
66
[![MIT License](https://img.shields.io/packagist/l/wol-soft/php-micro-template.svg)](https://github.com/wol-soft/php-json-schema-model-generator/blob/master/LICENSE)
77

88
# php-json-schema-model-generator
9-
Creates (immutable) PHP model classes from JSON-Schema files.
9+
Generates PHP model classes from JSON-Schema files including validation and providing a fluent auto completion for the generated classes.
1010

1111
## Table of Contents ##
1212

1313
* [Motivation](#Motivation)
1414
* [Requirements](#Requirements)
1515
* [Installation](#Installation)
16-
* [Examples](#Examples)
16+
* [Basic usage](#Basic-usage)
1717
* [Configuring using the GeneratorConfiguration](#Configuring-using-the-GeneratorConfiguration)
18+
* [Examples](#Examples)
1819
* [How the heck does this work?](#How-the-heck-does-this-work)
1920
* [Tests](#Tests)
2021

@@ -35,7 +36,7 @@ $ composer require wol-soft/php-json-model-generator-exception
3536
```
3637
To avoid adding all dependencies of the php-json-model-generator to your production dependencies it's recommended to add the library as a dev-dependency and include the php-json-model-generator-exception library. The exception library provides all classes to run the generated code. Generating the classes should either be a step done in the development environment (if you decide to commit the models) or as a build step of your application.
3738

38-
## Examples ##
39+
## Basic usage ##
3940

4041
The base object for generating models is the *Generator*. After you have created a Generator you can use the object to generate your model classes without any further configuration:
4142

@@ -59,11 +60,12 @@ $generator
5960
```
6061

6162
The generator will check the given source directory recursive and convert all found *.json files to models. All JSON-Schema files inside the source directory must provide a schema of an object.
63+
6264
## Configuring using the GeneratorConfiguration ##
6365

6466
The *GeneratorConfiguration* object offers the following methods to configure the generator in a fluid interface:
6567

66-
Method | Configuration | Example
68+
Method | Configuration | Default
6769
--- | --- | ---
6870
``` setNamespacePrefix(string $prefix) ``` <br><br>Example:<br> ``` setNamespacePrefix('\MyApp\Model') ``` | Configures a namespace prefix for all generated classes. The namespaces will be extended with the directory structure of the source directory. | Empty string so no namespace prefix will be used
6971
``` setImmutable(bool $immutable) ``` <br><br>Example:<br> ``` setImmutable(false) ``` | If set to true the generated model classes will be delivered without setter methods for the object properties. | true
@@ -73,6 +75,74 @@ Method | Configuration | Example
7375
``` setErrorRegistryClass(string $exceptionClass) ``` <br><br>Example:<br> ``` setErrorRegistryClass(CustomException::class) ``` | Define a custom exception implementing the ErrorRegistryExceptionInterface to decouple the generated code from the library (if you want to declare the library as a dev-dependency). The exception will be thrown if a validation fails error collection is **enabled** | ErrorRegistryException::class
7476
``` setExceptionClass(bool $prettyPrint) ``` <br><br>Example:<br> ``` setExceptionClass(CustomException::class) ``` | Define a custom exception to decouple the generated code from the library (if you want to declare the library as a dev-dependency). The exception will be thrown if a validation fails error collection is **disabled** | ValidationException::class
7577

78+
## Examples ##
79+
80+
The directory `./tests/manual` contains some easy examples which show the usage. After installing the dependencies of the library via `composer update` you can execute `php ./tests/manual/test.php` to generate the examples and play around with some JSON-Schema files to explore the library.
81+
82+
Let's have a look into an easy example. We create a simple model for a person with a name and an optional age. Our resulting JSON-Schema:
83+
```json
84+
{
85+
"id": "Person",
86+
"type": "object",
87+
"properties": {
88+
"name": {
89+
"type": "string"
90+
},
91+
"age": {
92+
"type": "integer"
93+
}
94+
},
95+
"required": [
96+
"name"
97+
]
98+
}
99+
```
100+
101+
After generating a class with this JSON-Schema our class with the name `Person` will provide the following interface:
102+
```php
103+
// the constructor takes an array with data which is validated and applied to the model
104+
public function __construct(array $modelData);
105+
106+
// the method getRawModelDataInput always delivers the raw input which was provided on instantiation
107+
public function getRawModelDataInput(): array;
108+
109+
// getters to fetch the validated properties. Age is nullable as it's not required
110+
public function getName(): string;
111+
public function getAge(): ?int;
112+
113+
// setters to change the values of the model after instantiation (only generated if immutability is disabled)
114+
public function setName(string $name): Person;
115+
public function setAge(int ?$age): Person;
116+
```
117+
118+
Now let's have a look at the behaviour of the generated model:
119+
```php
120+
// Throws an exception as the required name isn't provided.
121+
// Exception: 'Missing required value for name'
122+
$person = new Person();
123+
124+
// Throws an exception as the name provides an invalid value.
125+
// Exception: 'Invalid type for name. Requires string, got int'
126+
$person = new Person(['name' => 12]);
127+
128+
// A valid example as the age isn't required
129+
$person = new Person(['name' => 'Albert']);
130+
$person->getName(); // returns 'Albert'
131+
$person->getAge(); // returns NULL
132+
```
133+
134+
More complex exception messages eg. from a [allOf](https://json-schema.org/understanding-json-schema/reference/combining.html#allof) composition may look like:
135+
```
136+
Invalid value for Animal declined by composition constraint.
137+
Requires to match one composition element but matched 0 elements.
138+
- Composition element #1: Failed
139+
* Value for age must not be smaller than 0
140+
- Composition element #2: Valid
141+
- Composition element #3: Failed
142+
* Value for legs must not be smaller than 2
143+
* Value for legs must be a multiple of 2
144+
```
145+
76146
## How the heck does this work? ##
77147

78148
The class generation process basically splits up into three to four steps:
@@ -84,8 +154,10 @@ The class generation process basically splits up into three to four steps:
84154

85155
## Tests ##
86156

157+
The library is tested via [PHPUnit](https://phpunit.de/).
158+
87159
After installing the dependencies of the library via `composer update` you can execute the tests with `./vendor/bin/phpunit` (Linux) or `vendor\bin\phpunit.bat` (Windows). The test names are optimized for the usage of the `--testdox` output. Most tests are atomic integration tests which will set up a JSON-Schema file and generate a class from the schema and test the behaviour of the generated class afterwards.
88160

89161
During the execution the tests will create a directory PHPModelGeneratorTest in tmp where JSON-Schema files and PHP classes will be written to.
90162

91-
If a test which creates a PHP class from a JSON-Schema fails the JSON-Schema and the generated class(es) will be dumped to `./Failed-classes`
163+
If a test which creates a PHP class from a JSON-Schema fails the JSON-Schema and the generated class(es) will be dumped to the directory `./Failed-classes`

src/Model/Validator/AbstractPropertyValidator.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,14 @@ public function getExceptionMessage(): string
2323
{
2424
return $this->exceptionMessage;
2525
}
26+
27+
/**
28+
* By default a validator doesn't require a set up
29+
*
30+
* @return string
31+
*/
32+
public function getValidatorSetUp(): string
33+
{
34+
return '';
35+
}
2636
}

src/Model/Validator/ComposedPropertyValidator.php

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44

55
namespace PHPModelGenerator\Model\Validator;
66

7+
use PHPMicroTemplate\Exception\FileSystemException;
8+
use PHPMicroTemplate\Exception\SyntaxErrorException;
9+
use PHPMicroTemplate\Exception\UndefinedSymbolException;
710
use PHPModelGenerator\Model\Property\CompositionPropertyDecorator;
811
use PHPModelGenerator\Model\Property\PropertyInterface;
912

@@ -21,6 +24,10 @@ class ComposedPropertyValidator extends AbstractComposedPropertyValidator
2124
* @param CompositionPropertyDecorator[] $composedProperties
2225
* @param string $composedProcessor
2326
* @param array $validatorVariables
27+
*
28+
* @throws FileSystemException
29+
* @throws SyntaxErrorException
30+
* @throws UndefinedSymbolException
2431
*/
2532
public function __construct(
2633
PropertyInterface $property,
@@ -29,12 +36,31 @@ public function __construct(
2936
array $validatorVariables
3037
) {
3138
parent::__construct(
32-
"Invalid value for {$property->getName()} declined by composition constraint",
39+
$this->getRenderer()->renderTemplate(
40+
DIRECTORY_SEPARATOR . 'Exception' . DIRECTORY_SEPARATOR . 'ComposedValueException.phptpl',
41+
[
42+
'propertyName' => $property->getName(),
43+
'composedErrorMessage' => $validatorVariables['composedErrorMessage'],
44+
]
45+
),
3346
DIRECTORY_SEPARATOR . 'Validator' . DIRECTORY_SEPARATOR . 'ComposedItem.phptpl',
3447
$validatorVariables
3548
);
3649

3750
$this->composedProcessor = $composedProcessor;
3851
$this->composedProperties = $composedProperties;
3952
}
53+
54+
/**
55+
* Initialize all variables which are required to execute a composed property validator
56+
*
57+
* @return string
58+
*/
59+
public function getValidatorSetUp(): string
60+
{
61+
return '
62+
$succeededCompositionElements = 0;
63+
$compositionErrorCollection = [];
64+
';
65+
}
4066
}

src/Model/Validator/PropertyTemplateValidator.php

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ class PropertyTemplateValidator extends AbstractPropertyValidator
2020
/** @var array */
2121
protected $templateValues;
2222
/** @var Render */
23-
static protected $renderer;
23+
static private $renderer;
2424

2525
/**
2626
* PropertyValidator constructor.
@@ -37,12 +37,6 @@ public function __construct(
3737
$this->exceptionMessage = $exceptionMessage;
3838
$this->template = $template;
3939
$this->templateValues = $templateValues;
40-
41-
if (!static::$renderer) {
42-
static::$renderer = new Render(
43-
join(DIRECTORY_SEPARATOR, [__DIR__, '..', '..', 'Templates']) . DIRECTORY_SEPARATOR
44-
);
45-
}
4640
}
4741

4842
/**
@@ -55,9 +49,23 @@ public function __construct(
5549
public function getCheck(): string
5650
{
5751
try {
58-
return static::$renderer->renderTemplate($this->template, $this->templateValues);
52+
return $this->getRenderer()->renderTemplate($this->template, $this->templateValues);
5953
} catch (PHPMicroTemplateException $exception) {
6054
throw new RenderException("Can't render property validation template {$this->template}", 0, $exception);
6155
}
6256
}
57+
58+
/**
59+
* @return Render
60+
*/
61+
protected function getRenderer(): Render
62+
{
63+
if (!self::$renderer) {
64+
self::$renderer = new Render(
65+
join(DIRECTORY_SEPARATOR, [__DIR__, '..', '..', 'Templates']) . DIRECTORY_SEPARATOR
66+
);
67+
}
68+
69+
return self::$renderer;
70+
}
6371
}

src/Model/Validator/PropertyValidatorInterface.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,11 @@ public function getCheck(): string;
2424
* @return string
2525
*/
2626
public function getExceptionMessage(): string;
27+
28+
/**
29+
* Get the source code which is required to set up the validator (eg. initialize variables)
30+
*
31+
* @return string
32+
*/
33+
public function getValidatorSetUp(): string;
2734
}

src/Model/Validator/TypeCheckValidator.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ class TypeCheckValidator extends PropertyValidator
2121
*/
2222
public function __construct(string $type, PropertyInterface $property)
2323
{
24-
parent::__construct(
25-
'!is_' . strtolower($type) . '($value)' . ($property->isRequired() ? '' : ' && $value !== null'),
26-
"invalid type for {$property->getName()}"
27-
);
24+
parent::__construct(
25+
'!is_' . strtolower($type) . '($value)' . ($property->isRequired() ? '' : ' && $value !== null'),
26+
sprintf('Invalid type for %s. Requires %s, got " . gettype($value) . "', $property->getName(), $type)
27+
);
2828
}
2929
}

src/PropertyProcessor/ComposedValue/AbstractComposedValueProcessor.php

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,10 @@ protected function generateValidators(PropertyInterface $property, array $proper
7777
'viewHelper' => new RenderHelper($this->schemaProcessor->getGeneratorConfiguration()),
7878
'availableAmount' => $availableAmount,
7979
'composedValueValidation' => $this->getComposedValueValidation($availableAmount),
80-
// if the property is a composed property the resulting value of a validation must be proposed to be the
81-
// final value after the validations (eg. object instantiations may be performed). Otherwise (eg. a
82-
// NotProcessor) the value must be proposed before the validation
80+
'composedErrorMessage' => $this->getComposedValueValidationErrorLabel($availableAmount),
81+
// if the property is a composed property the resulting value of a validation must be proposed
82+
// to be the final value after the validations (eg. object instantiations may be performed).
83+
// Otherwise (eg. a NotProcessor) the value must be proposed before the validation
8384
'postPropose' => $this instanceof ComposedPropertiesInterface,
8485
'mergedProperty' =>
8586
$createMergedProperty
@@ -152,4 +153,11 @@ private function createMergedProperty(
152153
* @return string
153154
*/
154155
abstract protected function getComposedValueValidation(int $composedElements): string;
156+
157+
/**
158+
* @param int $composedElements The amount of elements which are composed together
159+
*
160+
* @return string
161+
*/
162+
abstract protected function getComposedValueValidationErrorLabel(int $composedElements): string;
155163
}

src/PropertyProcessor/ComposedValue/AllOfProcessor.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,12 @@ protected function getComposedValueValidation(int $composedElements): string
1818
{
1919
return "\$succeededCompositionElements === $composedElements";
2020
}
21+
22+
/**
23+
* @inheritdoc
24+
*/
25+
protected function getComposedValueValidationErrorLabel(int $composedElements): string
26+
{
27+
return "Requires to match $composedElements composition elements but matched %s elements.";
28+
}
2129
}

src/PropertyProcessor/ComposedValue/AnyOfProcessor.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,12 @@ protected function getComposedValueValidation(int $composedElements): string
1818
{
1919
return "\$succeededCompositionElements > 0";
2020
}
21+
22+
/**
23+
* @inheritdoc
24+
*/
25+
protected function getComposedValueValidationErrorLabel(int $composedElements): string
26+
{
27+
return "Requires to match at least one composition element.";
28+
}
2129
}

0 commit comments

Comments
 (0)