Skip to content

Commit 3180dfc

Browse files
committed
Check accepted types of filter
Map accepted types to PHP types Test scalar transformation filter
1 parent 2a2788e commit 3180dfc

File tree

6 files changed

+124
-11
lines changed

6 files changed

+124
-11
lines changed

docs/source/nonStandardExtensions/filter.rst

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ Filters can be either supplied as a string or as a list of filters (multiple fil
2323
}
2424
}
2525
26+
If the implementation of a filter throws an exception this exception will be caught by the generated model. The model will either throw the exception directly or insert it into the error collection based on your GeneratorConfiguration (compare `collecting errors <../gettingStarted.html#collect-errors-vs-early-return>`__). This behaviour also allows you to hook into the validation process and execute extended validations on the provided property.
27+
2628
If multiple filters are applied to a single property they will be executed in the order of their definition inside the JSON Schema.
2729

2830
If a list is used filters may include additional option parameters. In this case a single filter must be provided as an object with the key **filter** defining the filter:
@@ -208,6 +210,8 @@ The callable filter method must be a static method. Internally it will be called
208210
209211
public function getAcceptedTypes(): array
210212
{
213+
// return an array of types which can be handled by the filter.
214+
// valid types are: [integer, number, boolean, string, array]
211215
return ['string'];
212216
}
213217
@@ -287,8 +291,7 @@ The option will be available if your JSON-Schema uses the object-notation for th
287291
Custom transforming filter
288292
^^^^^^^^^^^^^^^^^^^^^^^^^^
289293

290-
If you want to provide a custom filter which transforms a value (eg. redirect data into a manually written model) you must implement the **PHPModelGenerator\\PropertyProcessor\\Filter\\TransformingFilterInterface**. This interface adds the **getSerializer** method to your filter. The method is similar to the **getFilter** method. It must return a callable which is available during the render process as well as during code execution. The returned callable must return null or a string and undo a transformation (eg. the serializer method of the builtin **dateTime** filter transforms a DateTime object back into a formatted string). The serializer method will be called with the current value of the property as the first argument and with the (optionally provided) additional options of the filter as the second argument. Your custom transforming filter might look like:
291-
294+
If you want to provide a custom filter which transforms a value (eg. redirect data into a manually written model, transforming between data types [eg. accepting values as an integer but handle them internally as binary strings]) you must implement the **PHPModelGenerator\\PropertyProcessor\\Filter\\TransformingFilterInterface**. This interface adds the **getSerializer** method to your filter. The method is similar to the **getFilter** method. It must return a callable which is available during the render process as well as during code execution. The returned callable must return null or a string and undo a transformation (eg. the serializer method of the builtin **dateTime** filter transforms a DateTime object back into a formatted string). The serializer method will be called with the current value of the property as the first argument and with the (optionally provided) additional options of the filter as the second argument. Your custom transforming filter might look like:
292295

293296
.. code-block:: php
294297

src/Model/GeneratorConfiguration.php

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

87+
if (array_diff($filter->getAcceptedTypes(), ['integer', 'number', 'boolean', 'string', 'array'])) {
88+
throw new InvalidFilterException(
89+
'Filter accepts invalid types. Allowed types are [integer, number, boolean, string, array]'
90+
);
91+
}
92+
8793
$this->filter[$filter->getToken()] = $filter;
8894

8995
return $this;

src/Model/Validator/FilterValidator.php

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public function __construct(
3939
) {
4040
if (!empty($filter->getAcceptedTypes()) &&
4141
$property->getType() &&
42-
!in_array($property->getType(), $filter->getAcceptedTypes())
42+
!in_array($property->getType(), $this->mapDataTypes($filter->getAcceptedTypes()))
4343
) {
4444
throw new SchemaException(
4545
sprintf(
@@ -63,7 +63,7 @@ public function __construct(
6363
// check if the given value has a type matched by the filter
6464
'typeCheck' => !empty($filter->getAcceptedTypes())
6565
? '($value !== null && (!is_' .
66-
implode('($value) && !is_', $filter->getAcceptedTypes()) .
66+
implode('($value) && !is_', $this->mapDataTypes($filter->getAcceptedTypes())) .
6767
'($value)))'
6868
: '',
6969
'filterClass' => $filter->getFilter()[0],
@@ -103,12 +103,34 @@ public function addTransformedCheck(
103103

104104
if ($typeAfterFilter &&
105105
$typeAfterFilter->getName() &&
106-
!in_array($typeAfterFilter->getName(), $filter->getAcceptedTypes())
106+
!in_array($typeAfterFilter->getName(), $this->mapDataTypes($filter->getAcceptedTypes()))
107107
) {
108108
$this->templateValues['skipTransformedValuesCheck'] =
109109
(new ReflectionTypeCheckValidator($typeAfterFilter, $property, $schema))->getCheck();
110110
}
111111

112112
return $this;
113113
}
114+
115+
/**
116+
* Map a list of accepted data types to their corresponding PHP types
117+
*
118+
* @param array $acceptedTypes
119+
*
120+
* @return array
121+
*/
122+
private function mapDataTypes(array $acceptedTypes): array
123+
{
124+
return array_map(function (string $jsonSchemaType): string {
125+
switch ($jsonSchemaType) {
126+
case 'integer': return 'int';
127+
case 'number': return 'float';
128+
case 'string': return 'string';
129+
case 'boolean': return 'bool';
130+
case 'array': return 'array';
131+
132+
default: throw new SchemaException("Invalid accepted type $jsonSchemaType");
133+
}
134+
}, $acceptedTypes);
135+
}
114136
}

src/Templates/Serializer/TransformingFilterSerializer.phptpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
22
* serialize the property {{ property }}
33
*/
4-
protected function serialize{{ viewHelper.ucfirst(property) }}(): ?string
4+
protected function serialize{{ viewHelper.ucfirst(property) }}()
55
{
66
return call_user_func_array(
77
[\{{ serializerClass }}::class, "{{ serializerMethod }}"],

tests/Basic/FilterTest.php

Lines changed: 78 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,18 @@ public function invalidCustomFilterDataProvider(): array
5959
];
6060
}
6161

62+
public function testFilterWithNotAllowedAcceptedTypeThrowsAnException(): void
63+
{
64+
$this->expectException(InvalidFilterException::class);
65+
$this->expectExceptionMessage(
66+
'Filter accepts invalid types. Allowed types are [integer, number, boolean, string, array]'
67+
);
68+
69+
(new GeneratorConfiguration())->addFilter(
70+
$this->getCustomFilter([self::class, 'uppercaseFilter'], 'customFilter', [DateTime::class])
71+
);
72+
}
73+
6274
public function testNonExistingFilterThrowsAnException(): void
6375
{
6476
$this->expectException(SchemaException::class);
@@ -323,21 +335,46 @@ public function testAddFilterWithInvalidSerializerThrowsAnException(array $custo
323335
}
324336

325337
protected function getCustomTransformingFilter(
326-
array $customSerializer
338+
array $customSerializer,
339+
array $customFilter = [],
340+
string $token = 'customTransformingFilter',
341+
array $acceptedTypes = ['string']
327342
): TransformingFilterInterface {
328-
return new class ($customSerializer) extends TrimFilter implements TransformingFilterInterface {
343+
return new class ($customSerializer, $customFilter, $token, $acceptedTypes)
344+
extends TrimFilter
345+
implements TransformingFilterInterface
346+
{
329347
private $customSerializer;
348+
private $customFilter;
349+
private $token;
350+
private $acceptedTypes;
330351

331-
public function __construct(array $customSerializer)
332-
{
352+
public function __construct(
353+
array $customSerializer,
354+
array $customFilter,
355+
string $token,
356+
array $acceptedTypes
357+
) {
333358
$this->customSerializer = $customSerializer;
359+
$this->customFilter = $customFilter;
360+
$this->token = $token;
361+
$this->acceptedTypes = $acceptedTypes;
362+
}
363+
364+
public function getAcceptedTypes(): array
365+
{
366+
return $this->acceptedTypes;
334367
}
335368

336369
public function getToken(): string
337370
{
338-
return 'customTransformingFilter';
371+
return $this->token;
339372
}
340373

374+
public function getFilter(): array
375+
{
376+
return empty($this->customFilter) ? parent::getFilter() : $this->customFilter;
377+
}
341378
public function getSerializer(): array
342379
{
343380
return $this->customSerializer;
@@ -499,4 +536,40 @@ public static function exceptionFilter(string $value): void
499536
{
500537
throw new Exception("Exception filter called with $value");
501538
}
539+
540+
public function testTransformingToScalarType()
541+
{
542+
$className = $this->generateClassFromFile(
543+
'TransformingScalarFilter.json',
544+
(new GeneratorConfiguration())
545+
->setSerialization(true)
546+
->addFilter(
547+
$this->getCustomTransformingFilter(
548+
[self::class, 'serializeBinaryToInt'],
549+
[self::class, 'filterIntToBinary'],
550+
'binary',
551+
['integer']
552+
)
553+
)
554+
);
555+
556+
$object = new $className(['value' => 9]);
557+
558+
$this->assertSame('1001', $object->getValue());
559+
$this->assertSame('1010', $object->setValue('1010')->getValue());
560+
$this->assertSame('1011', $object->setValue(11)->getValue());
561+
562+
$this->assertSame(['value' => 11], $object->toArray());
563+
$this->assertSame('{"value":11}', $object->toJSON());
564+
}
565+
566+
public static function filterIntToBinary(int $value): string
567+
{
568+
return decbin($value);
569+
}
570+
571+
public static function serializeBinaryToInt(string $binary): int
572+
{
573+
return bindec($binary);
574+
}
502575
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"type": "object",
3+
"properties": {
4+
"value": {
5+
"type": "integer",
6+
"filter": "binary"
7+
}
8+
}
9+
}

0 commit comments

Comments
 (0)