Skip to content

Commit cad7ade

Browse files
committed
Fix filter chain with transforming filter
1 parent f3cf462 commit cad7ade

File tree

9 files changed

+206
-89
lines changed

9 files changed

+206
-89
lines changed

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.9.0",
15+
"wol-soft/php-json-schema-model-generator-production": "^0.10.0",
1616
"wol-soft/php-micro-template": "^1.3.1",
1717

1818
"php": ">=7.2",

docs/source/nonStandardExtensions/filter.rst

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ Filters may change the type of the property. For example the builtin filter **da
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

87-
Only one transforming filter per property is allowed. may be positioned anywhere in the filter chain of a single property. If multiple filters are applied and a transforming filter is among them you have to make sure the property types are compatible.
87+
Only one transforming filter per property is allowed. may be positioned anywhere in the filter chain of a single property. If multiple filters are applied and a transforming filter is among them you have to make sure the property types are compatible. If you use a custom filter after the dateTime filter for example the custom filter has to accept a DateTime value. Filters used before a transforming filter must accept the base type of the property the filter is applied to defined in the schema.
8888

8989
If you write a custom transforming filter you must define the return type of your filter function as the implementation uses Reflection methods to determine to which type a value is transformed by a filter.
9090

@@ -238,6 +238,10 @@ outputFormat DATE_ISO8601 The output format if serialization is enab
238238

239239
If the dateTime filter is used without the createFromFormat option the string will be passed into the DateTime constructor. Consequently also strings like '+1 day' will be converted to the corresponding DateTime objects.
240240

241+
.. hint::
242+
243+
Beside defining custom formats the formatting options *createFromFormat* and *outputFormat* also accept PHPs builtin constants. To accept values formatted with DATE_ATOM simply set the option *createFromFormat* to **ATOM**. The following constants are available: ATOM, COOKIE, ISO8601, RFC822, RFC850, RFC1036, RFC1123, RFC2822, RFC3339, RFC3339_EXTENDED, RFC7231, RSS, W3C
244+
241245
Custom filter
242246
-------------
243247

@@ -270,7 +274,8 @@ The callable filter method must be a static method. Internally it will be called
270274
public function getAcceptedTypes(): array
271275
{
272276
// return an array of types which can be handled by the filter.
273-
// valid types are: [integer, number, boolean, string, array]
277+
// valid types are: [integer, number, boolean, string, array] or available classes (FQCN required, eg.
278+
// DateTime::class)
274279
return ['string'];
275280
}
276281
@@ -285,12 +290,12 @@ The callable filter method must be a static method. Internally it will be called
285290
}
286291
}
287292
288-
If the custom filter is added to the generator configuration you can now use the filter in your schema and the generator will resolve the function:
289-
290293
.. hint::
291294

292295
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.
293296

297+
If the custom filter is added to the generator configuration you can now use the filter in your schema and the generator will resolve the function:
298+
294299
.. code-block:: json
295300
296301
{

src/Model/GeneratorConfiguration.php

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,12 @@ public function addFilter(FilterInterface $filter): self
8585
}
8686
}
8787

88-
if (array_diff($filter->getAcceptedTypes(), ['integer', 'number', 'boolean', 'string', 'array'])) {
89-
throw new InvalidFilterException(
90-
'Filter accepts invalid types. Allowed types are [integer, number, boolean, string, array]'
91-
);
88+
foreach ($filter->getAcceptedTypes() as $acceptedType) {
89+
if (!in_array($acceptedType, ['integer', 'number', 'boolean', 'string', 'array']) &&
90+
!class_exists($acceptedType)
91+
) {
92+
throw new InvalidFilterException('Filter accepts invalid types');
93+
}
9294
}
9395

9496
$this->filter[$filter->getToken()] = $filter;

src/Model/Validator/FilterValidator.php

Lines changed: 80 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -28,28 +28,20 @@ class FilterValidator extends PropertyTemplateValidator
2828
* @param FilterInterface $filter
2929
* @param PropertyInterface $property
3030
* @param array $filterOptions
31+
* @param TransformingFilterInterface|null $transformingFilter
3132
*
3233
* @throws SchemaException
3334
*/
3435
public function __construct(
3536
GeneratorConfiguration $generatorConfiguration,
3637
FilterInterface $filter,
3738
PropertyInterface $property,
38-
array $filterOptions = []
39+
array $filterOptions = [],
40+
?TransformingFilterInterface $transformingFilter = null
3941
) {
40-
if (!empty($filter->getAcceptedTypes()) &&
41-
$property->getType() &&
42-
!in_array($property->getType(), $this->mapDataTypes($filter->getAcceptedTypes()))
43-
) {
44-
throw new SchemaException(
45-
sprintf(
46-
'Filter %s is not compatible with property type %s for property %s',
47-
$filter->getToken(),
48-
$property->getType(),
49-
$property->getName()
50-
)
51-
);
52-
}
42+
$transformingFilter === null
43+
? $this->validateFilterCompatibilityWithBaseType($filter, $property)
44+
: $this->validateFilterCompatibilityWithTransformedType($filter, $transformingFilter, $property);
5345

5446
parent::__construct(
5547
sprintf(
@@ -62,9 +54,15 @@ public function __construct(
6254
'skipTransformedValuesCheck' => false,
6355
// check if the given value has a type matched by the filter
6456
'typeCheck' => !empty($filter->getAcceptedTypes())
65-
? '($value !== null && (!is_' .
66-
implode('($value) && !is_', $this->mapDataTypes($filter->getAcceptedTypes())) .
67-
'($value)))'
57+
? '($value !== null && (' .
58+
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();
64+
}, $this->mapDataTypes($filter->getAcceptedTypes()))) .
65+
'))'
6866
: '',
6967
'filterClass' => $filter->getFilter()[0],
7068
'filterMethod' => $filter->getFilter()[1],
@@ -88,30 +86,86 @@ public function __construct(
8886
*
8987
* @param TransformingFilterInterface $filter
9088
* @param PropertyInterface $property
91-
* @param Schema $schema
9289
*
9390
* @return self
9491
*
9592
* @throws ReflectionException
9693
*/
97-
public function addTransformedCheck(
98-
TransformingFilterInterface $filter,
99-
PropertyInterface $property,
100-
Schema $schema
101-
): self {
94+
public function addTransformedCheck(TransformingFilterInterface $filter, PropertyInterface $property): self {
10295
$typeAfterFilter = (new ReflectionMethod($filter->getFilter()[0], $filter->getFilter()[1]))->getReturnType();
10396

10497
if ($typeAfterFilter &&
10598
$typeAfterFilter->getName() &&
10699
!in_array($typeAfterFilter->getName(), $this->mapDataTypes($filter->getAcceptedTypes()))
107100
) {
108-
$this->templateValues['skipTransformedValuesCheck'] =
109-
(new ReflectionTypeCheckValidator($typeAfterFilter, $property, $schema))->getCheck();
101+
$this->templateValues['skipTransformedValuesCheck'] = ReflectionTypeCheckValidator::fromReflectionType(
102+
$typeAfterFilter,
103+
$property
104+
)->getCheck();
110105
}
111106

112107
return $this;
113108
}
114109

110+
/**
111+
* Check if the given filter is compatible with the base type of the property defined in the schema
112+
*
113+
* @param FilterInterface $filter
114+
* @param PropertyInterface $property
115+
*
116+
* @throws SchemaException
117+
*/
118+
private function validateFilterCompatibilityWithBaseType(FilterInterface $filter, PropertyInterface $property)
119+
{
120+
if (!empty($filter->getAcceptedTypes()) &&
121+
$property->getType() &&
122+
!in_array($property->getType(), $this->mapDataTypes($filter->getAcceptedTypes()))
123+
) {
124+
throw new SchemaException(
125+
sprintf(
126+
'Filter %s is not compatible with property type %s for property %s',
127+
$filter->getToken(),
128+
$property->getType(),
129+
$property->getName()
130+
)
131+
);
132+
}
133+
}
134+
135+
/**
136+
* Check if the given filter is compatible with the result of the given transformation filter
137+
*
138+
* @param FilterInterface $filter
139+
* @param TransformingFilterInterface $transformingFilter
140+
* @param PropertyInterface $property
141+
*
142+
* @throws ReflectionException
143+
* @throws SchemaException
144+
*/
145+
private function validateFilterCompatibilityWithTransformedType(
146+
FilterInterface $filter,
147+
TransformingFilterInterface $transformingFilter,
148+
PropertyInterface $property
149+
): void {
150+
$transformedType = (new ReflectionMethod(
151+
$transformingFilter->getFilter()[0],
152+
$transformingFilter->getFilter()[1]
153+
))->getReturnType();
154+
155+
if (!empty($filter->getAcceptedTypes()) &&
156+
!in_array($transformedType->getName(), $this->mapDataTypes($filter->getAcceptedTypes()))
157+
) {
158+
throw new SchemaException(
159+
sprintf(
160+
'Filter %s is not compatible with transformed property type %s for property %s',
161+
$filter->getToken(),
162+
$transformedType->getName(),
163+
$property->getName()
164+
)
165+
);
166+
}
167+
}
168+
115169
/**
116170
* Map a list of accepted data types to their corresponding PHP types
117171
*
@@ -129,9 +183,7 @@ private function mapDataTypes(array $acceptedTypes): array
129183
case 'boolean': return 'bool';
130184
case 'array': return 'array';
131185

132-
// @codeCoverageIgnoreStart this must not occur as invalid types are filtered out before
133-
default: throw new SchemaException("Invalid accepted type $jsonSchemaType");
134-
// @codeCoverageIgnoreEnd
186+
default: return $jsonSchemaType;
135187
}
136188
}, $acceptedTypes);
137189
}

src/Model/Validator/ReflectionTypeCheckValidator.php

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
namespace PHPModelGenerator\Model\Validator;
66

77
use PHPModelGenerator\Model\Property\PropertyInterface;
8-
use PHPModelGenerator\Model\Schema;
98
use ReflectionType;
109

1110
/**
@@ -15,29 +14,47 @@
1514
*/
1615
class ReflectionTypeCheckValidator extends PropertyValidator
1716
{
17+
/**
18+
* @param ReflectionType $reflectionType
19+
* @param PropertyInterface $property
20+
*
21+
* @return static
22+
*/
23+
public static function fromReflectionType(
24+
ReflectionType $reflectionType,
25+
PropertyInterface $property
26+
): self {
27+
return new self(
28+
$reflectionType->isBuiltin(),
29+
$reflectionType->getName(),
30+
$property
31+
);
32+
}
33+
1834
/**
1935
* ReflectionTypeCheckValidator constructor.
2036
*
21-
* @param ReflectionType $reflectionType
37+
* @param bool $isBuiltin
38+
* @param string $name
2239
* @param PropertyInterface $property
23-
* @param Schema $schema
2440
*/
25-
public function __construct(ReflectionType $reflectionType, PropertyInterface $property, Schema $schema)
41+
public function __construct(bool $isBuiltin, string $name, PropertyInterface $property)
2642
{
27-
if ($reflectionType->isBuiltin()) {
28-
$typeCheck = "!is_{$reflectionType->getName()}(\$value)";
43+
if ($isBuiltin) {
44+
$typeCheck = "!is_{$name}(\$value)";
2945
} else {
30-
$typeCheck = "!(\$value instanceof {$reflectionType->getName()})";
31-
// make sure the returned class is imported so the instanceof check can be performed
32-
$schema->addUsedClass($reflectionType->getName());
46+
$parts = explode('\\', $name);
47+
$className = end($parts);
48+
49+
$typeCheck = "!(\$value instanceof $className)";
3350
}
3451

3552
parent::__construct(
3653
$typeCheck,
3754
sprintf(
3855
'Invalid type for %s. Requires %s, got " . gettype($value) . "',
3956
$property->getName(),
40-
$reflectionType->getName()
57+
$name
4158
)
4259
);
4360
}

src/PropertyProcessor/Filter/FilterProcessor.php

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use PHPModelGenerator\Model\Validator\PropertyValidator;
1515
use PHPModelGenerator\Model\Validator\ReflectionTypeCheckValidator;
1616
use PHPModelGenerator\Model\Validator\TypeCheckValidator;
17+
use PHPModelGenerator\Utils\RenderHelper;
1718
use ReflectionException;
1819
use ReflectionMethod;
1920
use ReflectionType;
@@ -44,7 +45,7 @@ public function process(
4445
$filterList = [$filterList];
4546
}
4647

47-
$hasTransformingFilter = false;
48+
$transformingFilter = null;
4849
// apply a different priority to each filter to make sure the order is kept
4950
$filterPriority = 10;
5051

@@ -60,7 +61,7 @@ public function process(
6061
}
6162

6263
$property->addValidator(
63-
new FilterValidator($generatorConfiguration, $filter, $property, $filterOptions),
64+
new FilterValidator($generatorConfiguration, $filter, $property, $filterOptions, $transformingFilter),
6465
$filterPriority++
6566
);
6667

@@ -70,13 +71,14 @@ public function process(
7071
"Applying a transforming filter to the array property {$property->getName()} is not supported"
7172
);
7273
}
73-
if ($hasTransformingFilter) {
74+
if ($transformingFilter) {
7475
throw new SchemaException(
7576
"Applying multiple transforming filters for property {$property->getName()} is not supported"
7677
);
7778
}
7879

79-
$hasTransformingFilter = true;
80+
// keep track of the transforming filter to modify type checks for following filters
81+
$transformingFilter = $filter;
8082

8183
$typeAfterFilter = (new ReflectionMethod($filter->getFilter()[0], $filter->getFilter()[1]))
8284
->getReturnType();
@@ -85,14 +87,19 @@ public function process(
8587
$typeAfterFilter->getName() &&
8688
$property->getType() !== $typeAfterFilter->getName()
8789
) {
88-
$this->addTransformedValuePassThrough($property, $schema, $filter);
90+
$this->addTransformedValuePassThrough($property, $filter);
8991
$this->extendTypeCheckValidatorToAllowTransformedValue($property, $schema, $typeAfterFilter);
9092

91-
$property->setType($property->getType(), $typeAfterFilter->getName());
92-
93-
$schema->addCustomSerializer(
94-
new TransformingFilterSerializer($property->getAttribute(), $filter, $filterOptions)
93+
$property->setType(
94+
$property->getType(),
95+
(new RenderHelper($generatorConfiguration))->getSimpleClassName($typeAfterFilter->getName())
9596
);
97+
98+
$schema
99+
->addUsedClass($typeAfterFilter->getName())
100+
->addCustomSerializer(
101+
new TransformingFilterSerializer($property->getAttribute(), $filter, $filterOptions)
102+
);
96103
}
97104
}
98105
}
@@ -105,21 +112,19 @@ public function process(
105112
* if a DateTime object is provided for the property
106113
*
107114
* @param PropertyInterface $property
108-
* @param Schema $schema
109115
* @param TransformingFilterInterface $filter
110116
*
111117
* @throws ReflectionException
112118
*/
113119
private function addTransformedValuePassThrough(
114120
PropertyInterface $property,
115-
Schema $schema,
116121
TransformingFilterInterface $filter
117122
): void {
118123
foreach ($property->getValidators() as $validator) {
119124
$validator = $validator->getValidator();
120125

121126
if ($validator instanceof FilterValidator) {
122-
$validator->addTransformedCheck($filter, $property, $schema);
127+
$validator->addTransformedCheck($filter, $property);
123128
}
124129
}
125130
}
@@ -155,7 +160,7 @@ private function extendTypeCheckValidatorToAllowTransformedValue(
155160
new PropertyValidator(
156161
sprintf(
157162
'%s && %s',
158-
(new ReflectionTypeCheckValidator($typeAfterFilter, $property, $schema))->getCheck(),
163+
ReflectionTypeCheckValidator::fromReflectionType($typeAfterFilter, $property)->getCheck(),
159164
$typeCheckValidator->getCheck()
160165
),
161166
sprintf(

0 commit comments

Comments
 (0)