Skip to content

Commit 2213173

Browse files
committed
Accept already transformed values as input values for properties which are transformed by filters
1 parent 4ea8e87 commit 2213173

File tree

7 files changed

+180
-30
lines changed

7 files changed

+180
-30
lines changed

docs/source/nonStandardExtensions/filter.rst

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,20 @@ If a list is used filters may include additional option parameters. In this case
4242
}
4343
}
4444
45+
Transforming filter
46+
-------------------
47+
4548
.. warning::
4649

47-
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.
50+
Read this section carefully if you want to use filters which transform the type of the property
51+
52+
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.
53+
54+
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.
4855

49-
The return type of the last applied filter will be used to define the type of the property inside the generated model (in the example given above the method **getCreated** will return a DateTime object).
56+
The return type of the last applied 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.
57+
58+
For working models you must define the return type of your filter function as the implementation uses Reflection methods to determine whether a filter transforms a value or keeps the type (like the **trim** filter).
5059

5160
Builtin filter
5261
--------------
@@ -121,24 +130,29 @@ Generated interface:
121130

122131
.. code-block:: php
123132
124-
public function setProductionDate(?string $productionDate): self;
133+
// $productionDate accepts string|DateTime|null
134+
// if a string is provided the string will be transformed into a DateTime
135+
public function setProductionDate($productionDate): self;
125136
public function getProductionDate(): ?DateTime;
126137
127138
Let's have a look how the generated model behaves:
128139

129140
.. code-block:: php
130141
131-
// valid, the name will be NULL as the productionDate is not required
132-
$person = new Car([]);
142+
// valid, the productionDate will be NULL as the productionDate is not required
143+
$car = new Car([]);
133144
134145
// Throws an exception as the provided value is not valid for the DateTime constructor
135-
$person = new Car(['productionDate' => 'Hello']);
146+
$car = new Car(['productionDate' => 'Hello']);
136147
137148
// A valid example
138-
$person = new Car(['productionDate' => '2020-10-10']);
139-
$person->productionDate(); // returns a DateTime object
149+
$car = new Car(['productionDate' => '2020-10-10']);
150+
$car->productionDate(); // returns a DateTime object
140151
// the raw model data input is not affected by the filter
141-
$person->getRawModelDataInput(); // returns ['productionDate' => '2020-10-10']
152+
$car->getRawModelDataInput(); // returns ['productionDate' => '2020-10-10']
153+
154+
// Another valid example with an already transformed value
155+
$car = new Car(['productionDate' => $myDateTimeObject]);
142156
143157
Additional options
144158
~~~~~~~~~~~~~~~~~~
@@ -148,6 +162,7 @@ Option Default value Description
148162
================ ============= ===========
149163
convertNullToNow false If null is provided a DateTime object with the current time will be created (works only if the property isn't required as null would be denied otherwise before the filter is executed)
150164
denyEmptyValue false An empty string value will be denied (by default an empty string value will result in a DateTime object with the current time)
165+
createFromFormat null Provide a pattern which is used to parse the provided value (DateTime object will be created via DateTime::createFromFormat if a format is provided)
151166
================ ============= ===========
152167

153168
Custom filter

src/Model/Property/Property.php

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,13 @@ public function getAttribute(): string
8282
*/
8383
public function getType(bool $outputType = false): string
8484
{
85+
// If the output type differs from an input type also accept the output type
86+
// (in this case the transforming filter is skipped)
87+
// TODO: PHP 8 use union types to accept multiple input types
88+
if (!$outputType && $this->outputType !== null && $this->outputType !== $this->type) {
89+
return '';
90+
}
91+
8592
return $outputType && $this->outputType !== null ? $this->outputType : $this->type;
8693
}
8794

@@ -101,12 +108,22 @@ public function setType(string $type, ?string $outputType = null): PropertyInter
101108
*/
102109
public function getTypeHint(bool $outputType = false): string
103110
{
104-
$input = $outputType && $this->outputType !== null ? $this->outputType : $this->type;
111+
$input = [$outputType && $this->outputType !== null ? $this->outputType : $this->type];
105112

106-
foreach ($this->typeHintDecorators as $decorator) {
107-
$input = $decorator->decorate($input);
113+
// If the output type differs from an input type also accept the output type
114+
if (!$outputType && $this->outputType !== null && $this->outputType !== $this->type) {
115+
$input = [$this->type, $this->outputType];
108116
}
109117

118+
$input = join('|', array_map(function (string $input) {
119+
foreach ($this->typeHintDecorators as $decorator) {
120+
$input = $decorator->decorate($input);
121+
}
122+
123+
return $input;
124+
}, $input));
125+
126+
110127
return $input ?? 'mixed';
111128
}
112129

src/Model/Validator/FilterValidator.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@
77
use PHPModelGenerator\Exception\SchemaException;
88
use PHPModelGenerator\Model\GeneratorConfiguration;
99
use PHPModelGenerator\Model\Property\PropertyInterface;
10+
use PHPModelGenerator\Model\Schema;
1011
use PHPModelGenerator\PropertyProcessor\Filter\FilterInterface;
1112
use PHPModelGenerator\Utils\RenderHelper;
13+
use ReflectionException;
14+
use ReflectionMethod;
1215

1316
class FilterValidator extends PropertyTemplateValidator
1417
{
@@ -18,14 +21,17 @@ class FilterValidator extends PropertyTemplateValidator
1821
* @param GeneratorConfiguration $generatorConfiguration
1922
* @param FilterInterface $filter
2023
* @param PropertyInterface $property
24+
* @param Schema $schema
2125
* @param array $filterOptions
2226
*
27+
* @throws ReflectionException
2328
* @throws SchemaException
2429
*/
2530
public function __construct(
2631
GeneratorConfiguration $generatorConfiguration,
2732
FilterInterface $filter,
2833
PropertyInterface $property,
34+
Schema $schema,
2935
array $filterOptions = []
3036
) {
3137
if (!empty($filter->getAcceptedTypes()) &&
@@ -42,6 +48,18 @@ public function __construct(
4248
);
4349
}
4450

51+
// check if the return type of the provided filter transforms the value. If the value is transformed by the
52+
// filter make sure the filter is only executed if a non-transformed value is provided.
53+
// This is required as a setter (eg. for a string property which is modified by the DateTime filter into a
54+
// DateTime object) also accepts a transformed value (in this case a DateTime object).
55+
$typeAfterFilter = (new ReflectionMethod($filter->getFilter()[0], $filter->getFilter()[1]))->getReturnType();
56+
if ($typeAfterFilter &&
57+
$typeAfterFilter->getName() &&
58+
!in_array($typeAfterFilter->getName(), $filter->getAcceptedTypes())
59+
) {
60+
$transformedCheck = (new ReflectionTypeCheckValidator($typeAfterFilter, $property, $schema))->getCheck();
61+
}
62+
4563
parent::__construct(
4664
sprintf(
4765
'Filter %s is not compatible with property type " . gettype($value) . " for property %s',
@@ -50,6 +68,7 @@ public function __construct(
5068
),
5169
DIRECTORY_SEPARATOR . 'Validator' . DIRECTORY_SEPARATOR . 'Filter.phptpl',
5270
[
71+
'skipTransformedValuesCheck' => $transformedCheck ?? '',
5372
// check if the given value has a type matched by the filter
5473
'typeCheck' => !empty($filter->getAcceptedTypes())
5574
? '($value !== null && (!is_' .
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace PHPModelGenerator\Model\Validator;
6+
7+
use PHPModelGenerator\Model\Property\PropertyInterface;
8+
use PHPModelGenerator\Model\Schema;
9+
use ReflectionType;
10+
11+
/**
12+
* Class ReflectionTypeCheckValidator
13+
*
14+
* @package PHPModelGenerator\Model\Validator
15+
*/
16+
class ReflectionTypeCheckValidator extends PropertyValidator
17+
{
18+
/**
19+
* ReflectionTypeCheckValidator constructor.
20+
*
21+
* @param ReflectionType $reflectionType
22+
* @param PropertyInterface $property
23+
* @param Schema $schema
24+
*/
25+
public function __construct(ReflectionType $reflectionType, PropertyInterface $property, Schema $schema)
26+
{
27+
if ($reflectionType->isBuiltin()) {
28+
$skipTransformedValuesCheck = "!is_{$reflectionType->getName()}(\$value)";
29+
} else {
30+
$skipTransformedValuesCheck = "!(\$value instanceof {$reflectionType->getName()})";
31+
// make sure the returned class is imported so the instanceof check can be performed
32+
$schema->addUsedClass($reflectionType->getName());
33+
}
34+
35+
parent::__construct(
36+
$skipTransformedValuesCheck,
37+
sprintf(
38+
'Invalid type for %s. Requires %s, got " . gettype($value) . "',
39+
$property->getName(),
40+
$reflectionType->getName()
41+
)
42+
);
43+
}
44+
}

src/PropertyProcessor/Filter/FilterProcessor.php

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,14 @@
88
use PHPModelGenerator\Model\GeneratorConfiguration;
99
use PHPModelGenerator\Model\Property\PropertyInterface;
1010
use PHPModelGenerator\Model\Schema;
11+
use PHPModelGenerator\Model\Validator;
1112
use PHPModelGenerator\Model\Validator\FilterValidator;
13+
use PHPModelGenerator\Model\Validator\PropertyValidator;
14+
use PHPModelGenerator\Model\Validator\ReflectionTypeCheckValidator;
15+
use PHPModelGenerator\Model\Validator\TypeCheckValidator;
1216
use ReflectionException;
1317
use ReflectionMethod;
18+
use ReflectionType;
1419

1520
/**
1621
* Class FilterProcessor
@@ -52,24 +57,70 @@ public function process(
5257
}
5358

5459
$property->addValidator(
55-
new FilterValidator($generatorConfiguration, $filter, $property, $filterOptions),
60+
new FilterValidator($generatorConfiguration, $filter, $property, $schema, $filterOptions),
5661
3
5762
);
5863
}
5964

60-
// check if the filter has changed the type of the property
65+
// check if the last applied filter has changed the type of the property
6166
if ($filter) {
6267
$typeAfterFilter = (new ReflectionMethod($filter->getFilter()[0], $filter->getFilter()[1]))
6368
->getReturnType();
6469

65-
if ($typeAfterFilter->getName()) {
70+
if ($typeAfterFilter &&
71+
$typeAfterFilter->getName() &&
72+
$property->getType() !== $typeAfterFilter->getName()
73+
) {
74+
$this->extendTypeCheckValidatorToAllowTransformedValue($property, $schema, $typeAfterFilter);
75+
6676
$property->setType($property->getType(), $typeAfterFilter->getName());
77+
}
78+
}
79+
}
80+
81+
/**
82+
* Extend a type check of the given property so the type check also allows the type of $typeAfterFilter. This is
83+
* used to allow also already transformed values as valid input values
84+
*
85+
* @param PropertyInterface $property
86+
* @param Schema $schema
87+
* @param ReflectionType $typeAfterFilter
88+
*/
89+
private function extendTypeCheckValidatorToAllowTransformedValue(
90+
PropertyInterface $property,
91+
Schema $schema,
92+
ReflectionType $typeAfterFilter
93+
): void {
94+
$typeCheckValidator = null;
6795

68-
// check if a class needs to be imported to use the new return value
69-
if (!$typeAfterFilter->isBuiltin()) {
70-
$schema->addUsedClass($typeAfterFilter->getName());
71-
}
96+
$property->filterValidators(function (Validator $validator) use (&$typeCheckValidator): bool {
97+
if (is_a($validator->getValidator(), TypeCheckValidator::class)) {
98+
$typeCheckValidator = $validator->getValidator();
99+
return false;
72100
}
101+
102+
return true;
103+
});
104+
105+
if ($typeCheckValidator instanceof TypeCheckValidator) {
106+
// add a combined validator which checks for the transformed value or the original type of the property as a
107+
// replacement for the removed TypeCheckValidator
108+
$property->addValidator(
109+
new PropertyValidator(
110+
sprintf(
111+
'%s && %s',
112+
(new ReflectionTypeCheckValidator($typeAfterFilter, $property, $schema))->getCheck(),
113+
$typeCheckValidator->getCheck()
114+
),
115+
sprintf(
116+
'Invalid type for %s. Requires [%s, %s], got " . gettype($value) . "',
117+
$property->getName(),
118+
$typeAfterFilter->getName(),
119+
$property->getType()
120+
)
121+
),
122+
2
123+
);
73124
}
74125
}
75126
}

src/PropertyProcessor/Property/AbstractValueProcessor.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ public function process(string $propertyName, array $propertyData): PropertyInte
5555
$this->schemaProcessor->getGeneratorConfiguration()->isImmutable()
5656
);
5757

58+
$this->generateValidators($property, $propertyData);
59+
5860
if (isset($propertyData['filter'])) {
5961
(new FilterProcessor())->process(
6062
$property,
@@ -64,8 +66,6 @@ public function process(string $propertyName, array $propertyData): PropertyInte
6466
);
6567
}
6668

67-
$this->generateValidators($property, $propertyData);
68-
6969
return $property;
7070
}
7171
}

src/Templates/Validator/Filter.phptpl

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1-
{{ typeCheck }} || (function (&$value) {
2-
// make sure exceptions from the filter are caught and added to the error handling
3-
try {
4-
$value = call_user_func_array([\{{ filterClass }}::class, "{{ filterMethod }}"], [$value, {{ filterOptions }}]);
5-
} catch (\Exception $e) {
6-
{{ viewHelper.validationError(transferExceptionMessage) }}
7-
}
1+
{% if skipTransformedValuesCheck %}{{ skipTransformedValuesCheck }} && {% endif %}
2+
(
3+
{% if typeCheck %}{{ typeCheck }} || {% endif %}
4+
(function (&$value) {
5+
// make sure exceptions from the filter are caught and added to the error handling
6+
try {
7+
$value = call_user_func_array([\{{ filterClass }}::class, "{{ filterMethod }}"], [$value, {{ filterOptions }}]);
8+
} catch (\Exception $e) {
9+
{{ viewHelper.validationError(transferExceptionMessage) }}
10+
}
811

9-
return false;
10-
})($value)
12+
return false;
13+
})($value)
14+
)

0 commit comments

Comments
 (0)