Skip to content

Commit 7ea0042

Browse files
authored
Merge pull request #4 from wol-soft/implicitNullableConfiguration
Implicit/Explicit null values for optional properties
2 parents 66206c8 + 28169a3 commit 7ea0042

35 files changed

+715
-110
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ The recommended way to install php-json-schema-model-generator is through [Compo
3838
$ composer require --dev wol-soft/php-json-schema-model-generator
3939
$ composer require wol-soft/php-json-schema-model-generator-production
4040
```
41-
To avoid adding all dependencies of the php-json-schema-model-generator to your production dependencies it's recommended to add the library as a dev-dependency and include the [wol-soft/php-json-schema-model-generator-production](https://github.com/wol-soft/php-json-schema-model-generator-production) library. The production 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.
41+
42+
To avoid adding all dependencies of the php-json-schema-model-generator to your production dependencies it's recommended to add the library as a dev-dependency and include the [wol-soft/php-json-schema-model-generator-production](https://github.com/wol-soft/php-json-schema-model-generator-production) library. The production library provides all classes to run the generated code. Generating the classes should either be a step done in the development environment or as a build step of your application (for example you could generate the models in a [composer post-autoload-dump script](https://getcomposer.org/doc/articles/scripts.md#command-events), which is the recommended workflow).
4243

4344
## Basic usage ##
4445

@@ -83,6 +84,7 @@ Method | Configuration | Default
8384
--- | --- | ---
8485
``` 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
8586
``` 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
87+
``` setImplicitNull(bool $allowImplicitNull) ``` <br><br>Example:<br> ``` setImplicitNull(true) ``` | By setting the implicit null option to true all of your object properties which aren't required will implicitly accept null. | false
8688
``` setCollectErrors(bool $collectErrors) ``` <br><br>Example:<br> ``` setCollectErrors(false) ``` | By default the complete input is validated and in case of failing validations all error messages will be thrown in a single exception. If set to false the first failing validation will throw an exception. | true
8789
``` setPrettyPrint(bool $prettyPrint) ``` <br><br>Example:<br> ``` setPrettyPrint(true) ``` | If set to false, the generated model classes won't follow coding guidelines (but the generation is faster). If enabled the package [Symplify/EasyCodingStandard](https://github.com/Symplify/EasyCodingStandard) will be used to clean up the generated code. By default pretty printing is disabled. | false
8890
``` setSerialization(bool $serialization) ``` <br><br>Example:<br> ``` setSerialization(true) ``` | If set to true the serialization methods `toArray` and `toJSON` will be added to the public interface of the generated classes. | false

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
],
1313
"require": {
1414
"symplify/easy-coding-standard": "^7.2.3",
15-
"wol-soft/php-json-schema-model-generator-production": "^0.10.0",
15+
"wol-soft/php-json-schema-model-generator-production": "^0.11.0",
1616
"wol-soft/php-micro-template": "^1.3.1",
1717

1818
"php": ">=7.2",

docs/source/gettingStarted.rst

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ The recommended way to install php-json-model-generator is through `Composer <ht
1111
composer require --dev wol-soft/php-json-schema-model-generator
1212
composer require wol-soft/php-json-schema-model-generator-production
1313
14-
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.
14+
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 or as a build step of your application (for example you could generate the models in a `composer post-autoload-dump script<https://getcomposer.org/doc/articles/scripts.md#command-events>`__, which is the recommended workflow).
1515

1616
Generating classes
1717
------------------
@@ -154,6 +154,22 @@ If set to true the generated model classes will be delivered without setter meth
154154
(new GeneratorConfiguration())
155155
->setImmutable(false);
156156
157+
Implicit null
158+
^^^^^^^^^^^^^
159+
160+
By default the properties are strictly checked against their defined types. Consequently if you want a property to accept null you have to extend the type of your property explicitly (eg. ['string', 'null']).
161+
162+
By setting the implicit null option to true all of your object properties which aren't required will implicitly accept null. All properties which are required and don't explicitly allow null in the type definition will still reject null.
163+
164+
.. code-block:: php
165+
166+
setImplicitNull(bool $allowImplicitNull);
167+
168+
.. code-block:: php
169+
170+
(new GeneratorConfiguration())
171+
->setImplicitNull(true);
172+
157173
Collect errors vs. early return
158174
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
159175

@@ -209,7 +225,7 @@ Serialization methods
209225
210226
setSerialization(bool $serialization);
211227
212-
If set to true the serialization methods `toArray` and `toJSON` will be added to the public interface of the generated classes. By default no serialization methods are added.
228+
If set to true the serialization methods `toArray`, `toJSON` and `jsonSerialize` will be added to the public interface of the generated classes. By default no serialization methods are added.
213229

214230
.. code-block:: php
215231
@@ -222,9 +238,12 @@ Generated interface:
222238
223239
public function toArray([int $depth = 512]): array;
224240
public function toJSON([int $options = 0 [, int $depth = 512]]): string;
241+
public function jsonSerialize(): array;
225242
226243
The generated class will implement the interface **PHPModelGenerator\\Interfaces\\SerializationInterface** implemented in the php-json-schema-model-generator-production repository. This interface can be used to write additional generic modules to handle the generated models. The $depth parameter defines the maximum amount of nested objects which are serialized. The $options parameter for the toJSON method provides access to the underlying option bitmask of `json_encode <https://www.php.net/manual/de/function.json-encode.php>`_.
227244

245+
Additionally the class will implement the PHP builtin interface **\JsonSerializable** which allows the direct usage of the generated classes in a custom json_encode.
246+
228247
Output generation process
229248
^^^^^^^^^^^^^^^^^^^^^^^^^
230249

docs/source/nonStandardExtensions/filter.rst

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ Transforming filter
8080

8181
You may keep it simple and skip this for your first tries and only experiment with non-transforming filters like the trim filter
8282

83-
Filters may change the type of the property. For example the builtin filter **dateTime** creates a DateTime object. Consequently further validations like pattern checks for the string property won't be performed.
83+
Filters may change the type of the property. For example the builtin filter **dateTime** creates a DateTime object. Consequently further type-related validations like pattern checks for the string property won't be performed. Additionally enum validations will not be executed if an already transformed value is provided.
8484

8585
As the required check is executed before the filter a filter may transform a required value into a null value. Be aware when writing custom filters which transform values to not break your validation rules by adding filters to a property.
8686

@@ -90,13 +90,15 @@ If you write a custom transforming filter you must define the return type of you
9090

9191
The return type of the transforming filter will be used to define the type of the property inside the generated model (in the example one section above given above the method **getCreated** will return a DateTime object). Additionally the generated model also accepts the transformed type as input type. So **setCreated** will accept a string and a DateTime object. If an already transformed value is provided the filter which transforms the value will **not** be executed. Also all filters which are defined before the transformation will **not** be executed (eg. a trim filter before a dateTime filter will not be executed if a DateTime object is provided).
9292

93+
If you use a filter on a property which accepts multiple types (eg. explicit null ['string', 'null'] or ['string', 'integer']) the filter must accept each of the types defined on the property.
94+
9395
Builtin filter
9496
--------------
9597

9698
trim
9799
^^^^
98100

99-
The trim filter is only valid for string properties.
101+
The trim filter is only valid for string and null properties.
100102

101103
.. code-block:: json
102104
@@ -140,7 +142,7 @@ If the filter trim is used for a property which doesn't require a string value a
140142
notEmpty
141143
^^^^^^^^
142144

143-
The dateTime filter is only valid for array properties.
145+
The dateTime filter is only valid for array and null properties.
144146

145147
.. code-block:: json
146148
@@ -174,7 +176,7 @@ Let's have a look how the generated model behaves:
174176
dateTime
175177
^^^^^^^^
176178

177-
The dateTime filter is only valid for string properties.
179+
The dateTime filter is only valid for string and null properties.
178180

179181
.. code-block:: json
180182
@@ -274,9 +276,9 @@ The callable filter method must be a static method. Internally it will be called
274276
public function getAcceptedTypes(): array
275277
{
276278
// return an array of types which can be handled by the filter.
277-
// valid types are: [integer, number, boolean, string, array] or available classes (FQCN required, eg.
278-
// DateTime::class)
279-
return ['string'];
279+
// valid types are: [integer, number, boolean, string, array, null]
280+
// or available classes (FQCN required, eg. DateTime::class)
281+
return ['string', 'null'];
280282
}
281283
282284
public function getToken(): string
@@ -290,6 +292,10 @@ The callable filter method must be a static method. Internally it will be called
290292
}
291293
}
292294
295+
.. hint::
296+
297+
If your filter accepts null values add 'null' to your *getAcceptedTypes* to make sure your filter is compatible with explicit null type.
298+
293299
.. hint::
294300

295301
If a filter with the token of your custom filter already exists the existing filter will be overwritten when adding the filter to the generator configuration. By overwriting filters you may change the behaviour of builtin filters by replacing them with your custom implementation.
@@ -387,12 +393,12 @@ The custom serializer method will be called if the model utilizing the custom fi
387393
388394
public function getAcceptedTypes(): array
389395
{
390-
return ['object'];
396+
return ['string', 'null'];
391397
}
392398
393399
public function getToken(): string
394400
{
395-
return 'uppercase';
401+
return 'customer';
396402
}
397403
398404
public function getFilter(): array

docs/source/types/null.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,5 @@ Generated interface (as null is no explicit type no typehints are generated):
2525
Possible exceptions:
2626

2727
* Invalid type for property. Requires null, got __TYPE__
28+
29+
The main use case for the **null** type is a property with `multiple types <complexTypes/multiType.html>`__ accepting for example a string and null values when using explicit null types.

src/Model/GeneratorConfiguration.php

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ class GeneratorConfiguration
2727
/** @var bool */
2828
protected $immutable = false;
2929
/** @var bool */
30+
protected $allowImplicitNull = false;
31+
/** @var bool */
3032
protected $prettyPrint = false;
3133
/** @var bool */
3234
protected $outputEnabled = true;
@@ -86,7 +88,7 @@ public function addFilter(FilterInterface $filter): self
8688
}
8789

8890
foreach ($filter->getAcceptedTypes() as $acceptedType) {
89-
if (!in_array($acceptedType, ['integer', 'number', 'boolean', 'string', 'array']) &&
91+
if (!in_array($acceptedType, ['integer', 'number', 'boolean', 'string', 'array', 'null']) &&
9092
!class_exists($acceptedType)
9193
) {
9294
throw new InvalidFilterException('Filter accepts invalid types');
@@ -285,4 +287,24 @@ public function setExceptionClass(string $exceptionClass): self
285287

286288
return $this;
287289
}
290+
291+
/**
292+
* @return bool
293+
*/
294+
public function isImplicitNullAllowed(): bool
295+
{
296+
return $this->allowImplicitNull;
297+
}
298+
299+
/**
300+
* @param bool $allowImplicitNull
301+
*
302+
* @return GeneratorConfiguration
303+
*/
304+
public function setImplicitNull(bool $allowImplicitNull): self
305+
{
306+
$this->allowImplicitNull = $allowImplicitNull;
307+
308+
return $this;
309+
}
288310
}

src/Model/Validator/EnumValidator.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace PHPModelGenerator\Model\Validator;
6+
7+
use PHPModelGenerator\Model\Property\PropertyInterface;
8+
9+
/**
10+
* Class EnumValidator
11+
*
12+
* @package PHPModelGenerator\Model\Validator
13+
*/
14+
class EnumValidator extends PropertyValidator
15+
{
16+
/**
17+
* EnumValidator constructor.
18+
*
19+
* @param PropertyInterface $property
20+
* @param array $allowedValues
21+
*/
22+
public function __construct(PropertyInterface $property, array $allowedValues)
23+
{
24+
25+
parent::__construct(
26+
'!in_array($value, ' .
27+
preg_replace('(\d+\s=>)', '', var_export($allowedValues, true)) .
28+
', true)',
29+
"Invalid value for {$property->getName()} declined by enum constraint"
30+
);
31+
}
32+
}

src/Model/Validator/FilterValidator.php

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,7 @@ public function __construct(
5656
'typeCheck' => !empty($filter->getAcceptedTypes())
5757
? '($value !== null && (' .
5858
implode(' && ', array_map(function (string $type) use ($property): string {
59-
return (new ReflectionTypeCheckValidator(
60-
in_array($type, ['int', 'float', 'string', 'bool', 'array', 'object']),
61-
$type,
62-
$property
63-
))->getCheck();
59+
return ReflectionTypeCheckValidator::fromType($type, $property)->getCheck();
6460
}, $this->mapDataTypes($filter->getAcceptedTypes()))) .
6561
'))'
6662
: '',
@@ -179,9 +175,7 @@ private function mapDataTypes(array $acceptedTypes): array
179175
switch ($jsonSchemaType) {
180176
case 'integer': return 'int';
181177
case 'number': return 'float';
182-
case 'string': return 'string';
183178
case 'boolean': return 'bool';
184-
case 'array': return 'array';
185179

186180
default: return $jsonSchemaType;
187181
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace PHPModelGenerator\Model\Validator;
6+
7+
use PHPModelGenerator\Model\Property\PropertyInterface;
8+
9+
/**
10+
* Class MultiTypeCheckValidator
11+
*
12+
* @package PHPModelGenerator\Model\Validator
13+
*/
14+
class MultiTypeCheckValidator extends PropertyValidator implements TypeCheckInterface
15+
{
16+
/** @var string[] */
17+
protected $types;
18+
19+
/**
20+
* MultiTypeCheckValidator constructor.
21+
*
22+
* @param string[] $types
23+
* @param PropertyInterface $property
24+
* @param bool $allowImplicitNull
25+
*/
26+
public function __construct(array $types, PropertyInterface $property, bool $allowImplicitNull)
27+
{
28+
$this->types = $types;
29+
30+
// if null is explicitly allowed we don't need an implicit null pass through
31+
if (in_array('null', $types)) {
32+
$allowImplicitNull = false;
33+
}
34+
35+
parent::__construct(
36+
join(
37+
' && ',
38+
array_map(
39+
function (string $allowedType) use ($property) : string {
40+
return ReflectionTypeCheckValidator::fromType($allowedType, $property)->getCheck();
41+
},
42+
$types
43+
)
44+
) . ($allowImplicitNull ? ' && $value !== null' : ''),
45+
sprintf(
46+
'Invalid type for %s. Requires [%s], got " . gettype($value) . "',
47+
$property->getName(),
48+
implode(', ', $types)
49+
)
50+
51+
);
52+
}
53+
54+
/**
55+
* @inheritDoc
56+
*/
57+
public function getTypes(): array
58+
{
59+
return $this->types;
60+
}
61+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace PHPModelGenerator\Model\Validator;
6+
7+
use PHPModelGenerator\Model\Property\PropertyInterface;
8+
use ReflectionType;
9+
10+
/**
11+
* Class PassThroughTypeCheckValidator
12+
*
13+
* @package PHPModelGenerator\Model\Validator
14+
*/
15+
class PassThroughTypeCheckValidator extends PropertyValidator implements TypeCheckInterface
16+
{
17+
/** @var string[] */
18+
protected $types;
19+
20+
/**
21+
* PassThroughTypeCheckValidator constructor.
22+
*
23+
* @param ReflectionType $passThroughType
24+
* @param PropertyInterface $property
25+
* @param TypeCheckValidator $typeCheckValidator
26+
*/
27+
public function __construct(
28+
ReflectionType $passThroughType,
29+
PropertyInterface $property,
30+
TypeCheckValidator $typeCheckValidator
31+
) {
32+
$this->types = array_merge($typeCheckValidator->getTypes(), [$passThroughType->getName()]);
33+
34+
parent::__construct(
35+
sprintf(
36+
'%s && %s',
37+
ReflectionTypeCheckValidator::fromReflectionType($passThroughType, $property)->getCheck(),
38+
$typeCheckValidator->getCheck()
39+
),
40+
sprintf(
41+
'Invalid type for %s. Requires [%s, %s], got " . gettype($value) . "',
42+
$property->getName(),
43+
$passThroughType->getName(),
44+
$property->getType()
45+
)
46+
);
47+
}
48+
49+
/**
50+
* @inheritDoc
51+
*/
52+
public function getTypes(): array
53+
{
54+
return $this->types;
55+
}
56+
}

0 commit comments

Comments
 (0)