Skip to content

Skip filters after a failing transforming filter #9

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/source/nonStandardExtensions/filter.rst
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ Filters may change the type of the property. For example the builtin filter **da

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.

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.
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. If the transformation of a property fails (the transforming filter throws an exception), subsequent filters won't be executed as their execution would add another error due to incompatible types which is irrelevant for the currently provided value.

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.

Expand Down
22 changes: 21 additions & 1 deletion src/Model/Validator/FilterValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
*/
class FilterValidator extends PropertyTemplateValidator
{
/** @var FilterInterface $filter */
protected $filter;

/**
* FilterValidator constructor.
*
Expand All @@ -31,6 +34,7 @@ class FilterValidator extends PropertyTemplateValidator
* @param TransformingFilterInterface|null $transformingFilter
*
* @throws SchemaException
* @throws ReflectionException
*/
public function __construct(
GeneratorConfiguration $generatorConfiguration,
Expand All @@ -39,6 +43,8 @@ public function __construct(
array $filterOptions = [],
?TransformingFilterInterface $transformingFilter = null
) {
$this->filter = $filter;

$transformingFilter === null
? $this->validateFilterCompatibilityWithBaseType($filter, $property)
: $this->validateFilterCompatibilityWithTransformedType($filter, $transformingFilter, $property);
Expand All @@ -51,7 +57,8 @@ public function __construct(
),
DIRECTORY_SEPARATOR . 'Validator' . DIRECTORY_SEPARATOR . 'Filter.phptpl',
[
'skipTransformedValuesCheck' => false,
'skipTransformedValuesCheck' => $transformingFilter !== null ? '!$transformationFailed' : '',
'isTransformingFilter' => $filter instanceof TransformingFilterInterface,
// check if the given value has a type matched by the filter
'typeCheck' => !empty($filter->getAcceptedTypes())
? '($value !== null && (' .
Expand All @@ -73,6 +80,19 @@ public function __construct(
);
}

/**
* Track if a transformation failed. If a transformation fails don't execute subsequent filter as they'd fail with
* an invalid type
*
* @return string
*/
public function getValidatorSetUp(): string
{
return $this->filter instanceof TransformingFilterInterface
? '$transformationFailed = false;'
: '';
}

/**
* Make sure the filter is only executed if a non-transformed value is provided.
* This is required as a setter (eg. for a string property which is modified by the DateTime filter into a DateTime
Expand Down
6 changes: 5 additions & 1 deletion src/Templates/Validator/Filter.phptpl
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
{% if skipTransformedValuesCheck %}{{ skipTransformedValuesCheck }} && {% endif %}
(
{% if typeCheck %}{{ typeCheck }} || {% endif %}
(function (&$value) {
(function (&$value) use (&$transformationFailed): bool {
// make sure exceptions from the filter are caught and added to the error handling
try {
$value = call_user_func_array([\{{ filterClass }}::class, "{{ filterMethod }}"], [$value, {{ filterOptions }}]);
} catch (\Exception $e) {
{% if isTransformingFilter %}
$transformationFailed = true;
{% endif %}

{{ viewHelper.validationError(transferExceptionMessage) }}
}

Expand Down
23 changes: 23 additions & 0 deletions tests/Basic/FilterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -679,6 +679,29 @@ public function testFilterChainWithTransformingFilterOnMultiTypeProperty(bool $i
$this->assertSame(['filteredProperty' => '2020-12-12T00:00:00+0000'], $object->toArray());
}

public function testFilterAfterTransformingFilterIsSkippedIfTransformingFilterFails(): void
{
$this->expectException(ErrorRegistryException::class);
$this->expectExceptionMessage(
'Invalid value for property filteredProperty denied by filter dateTime: Invalid Date Time value "Hello"'
);

$className = $this->generateClassFromFile(
'FilterChainMultiType.json',
(new GeneratorConfiguration())
->addFilter(
$this->getCustomFilter(
[self::class, 'exceptionFilter'],
'stripTime',
[DateTime::class]
)
),
false
);

new $className(['filteredProperty' => 'Hello']);
}

public function testFilterWhichAppliesToMultiTypePropertyPartiallyThrowsAnException(): void
{
$this->expectException(SchemaException::class);
Expand Down