Filter
Filter can be used to preprocess values. Filters are applied after the required and type validation.
If the property type and the filter’s accepted types have no overlap at all, a SchemaException is thrown at generation time. When there is only a partial overlap, the filter is silently skipped at runtime for values whose type is not in the accepted set — the value passes through unchanged.
Filters can be either supplied as a string or as a list of filters (multiple filters can be applied to a single property):
{
"$id": "person",
"type": "object",
"properties": {
"firstname": {
"type": "string",
"filter": "trim"
},
"lastname": {
"type": "string",
"filter": [
"trim"
]
}
}
}
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 as a InvalidFilterValueException or insert it into the error collection based on your GeneratorConfiguration (compare collecting errors). This behaviour also allows you to hook into the validation process and execute extended validations on the provided property.
If multiple filters are applied to a single property they will be executed in the order of their definition inside the JSON Schema.
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 (possible as a single filter or as a list):
{
"type": "object",
"properties": {
"created": {
"type": "string",
"filter": {
"filter": "dateTime",
"denyEmptyValue": true
}
},
"updated": {
"type": "string",
"filter": [
{
"filter": "dateTime",
"denyEmptyValue": true
}
]
}
}
}
Array filter
Filters may be applied to arrays. In this case the filter operates on the whole array.
{
"type": "object",
"properties": {
"names": {
"type": "array",
"filter": "notEmpty",
"items": {
"type": "string",
"filter": "trim"
}
}
}
}
The array filter is executed before the items are processed. Consequently strings which aren’t empty at the beginning but are empty after the trim filter is applied to each element won’t be filtered out in the example given above.
It is not possible to use transforming filters on arrays.
Transforming filter
Warning
Read this section carefully and understand it if you want to use filters which transform the type of the property without breaking your bones
You may keep it simple and skip this for your first tries and only experiment with non-transforming filters like the trim filter
Filters may change the type of the property. For example the builtin filter dateTime creates a DateTime object. When a transforming filter is present, the generator automatically classifies all validators on the property into two groups based on which type-space they target:
Input-space validators (e.g.
pattern,minLengthfor a string property) run before the filter, against the raw input value.Output-space validators (e.g.
minimum,maximumfor an integer returned by a string-to-int filter) run after the filter, against the transformed value.
This classification is derived from the Draft type registry and applies to both schema validators and composition branches (see Composition with transforming filters below). For multi-type properties (e.g. ['string', 'integer']) with a transforming filter, validators that target only the string type run pre-transform and validators that target only the integer type run post-transform. Additionally enum validations will not be executed if an already transformed value is provided.
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. The filter 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.
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).
If you use a filter on a property which accepts multiple types (e.g. ['string', 'null'] or ['string', 'integer']), the filter only needs to overlap with at least one of those types. Values whose type is not accepted by the filter are silently skipped at runtime. Only if the filter’s accepted types have no overlap at all with the property’s types is a SchemaException raised at generation time.
Composition with transforming filters
Composition keywords (allOf, anyOf, oneOf, if/then/else, not) may be combined with a transforming filter on the same property. Each branch is automatically classified at generation time as targeting the input type-space or the output type-space of the filter:
Input-space branches are evaluated before the filter, against the raw input value.
Output-space branches are evaluated after the filter, against the transformed value.
A branch that spans both type-spaces raises a SchemaException at generation time because it cannot be placed correctly in either phase of the pipeline. The following additional constraints are also enforced:
A filter keyword inside any composition branch always raises a
SchemaException, regardless of whether the property itself carries a filter. The composition engine resets the value to the original input after each branch evaluation, which would silently discard any transformation applied inside the branch.anyOf/oneOf: all branches must share a single type-space; cross-space branches raise aSchemaException.not: the inner schema must target a single type-space.if/then/else: all three sub-schemas must share the same type-space.
Example — input-space allOf (validates the raw string before the dateTime filter runs):
{
"type": "object",
"properties": {
"scheduledAt": {
"type": "string",
"filter": "dateTime",
"allOf": [
{
"type": "string",
"pattern": "^\\d{4}-\\d{2}-\\d{2}$"
}
]
}
}
}
The pattern constraint fires against the raw string before the dateTime filter transforms it to a DateTime object. Passing "hello" raises an AllOfException because the pattern fails. Passing "2024-01-01" passes the pattern and is then converted to a DateTime. Passing an already-constructed DateTime object bypasses the pre-transform pipeline entirely and is accepted as-is.
Example — output-space allOf (validates the integer after a string-to-int filter):
{
"type": "object",
"properties": {
"quantity": {
"type": ["string", "integer"],
"filter": "stringToInt",
"allOf": [
{
"minimum": 0,
"maximum": 100
}
]
}
}
}
The property accepts both raw strings (transformed by the filter) and already-converted integers (which bypass the filter). The minimum and maximum constraints are output-space and fire against the final integer after the filter has run. Passing "50" is transformed to 50 and passes both constraints. Passing "200" is transformed to 200 and fails maximum. Passing the already-transformed integer 50 directly skips the filter and is still validated by the output-space allOf.
Hint
The type keyword inside a composition branch always validates against the raw input value (before the filter runs). For a stringToInt filter, a branch like {"type": "integer", "minimum": 0} mixes an input-space constraint (type) with an output-space constraint (minimum), which raises a SchemaException at generation time. Declare the type at the property level instead.
Exceptions from filter
If the filter throws an exception during execution the exception will be caught and converted into a PHPModelGenerator\Exception\Filter\InvalidFilterValueException which provides the following methods to get further error details:
// returns the token of the filter which wasn't able to be processed
public function getFilterToken(): string
// Returns the exception which was thrown inside the filter
public function getFilterException(): Throwable
// get the name of the property which failed
public function getPropertyName(): string
// get the value provided to the property
public function getProvidedValue()
Builtin filter
trim
The trim filter accepts string and null values. Applied to a property that also allows other types (e.g. string|integer), the filter is silently skipped for values of non-matching types and the value passes through unchanged. Only applying trim to a property whose type has no overlap with string or null (e.g. a pure integer property) raises an error at generation time.
{
"$id": "person",
"type": "object",
"properties": {
"name": {
"type": "string",
"filter": "trim",
"minLength": 2
}
}
}
Let’s have a look how the generated model behaves:
// valid, the name will be NULL as the name is not required
$person = new Person([]);
// Throws an exception as the name provides an invalid value after being trimmed.
// MinLengthException: 'Value for name must not be shorter than 2'
$person = new Person(['name' => ' A ']);
// A valid example
$person = new Person(['name' => ' Albert ']);
$person->getName(); // returns 'Albert'
// the raw model data input is not affected by the filter
$person->meta()->rawInput(); // returns ['name' => ' Albert ']
// If setters are generated the setters also execute the filter and perform validations.
// MinLengthException: 'Value for name must not be shorter than 2'
$person->setName(' D ');
If trim is applied to a property whose type has zero overlap with string or null (e.g. a pure boolean property), a SchemaException is raised at generation time:
Filter trim is not compatible with property type __TYPE__ for property __PROPERTY_NAME__
notEmpty
The notEmpty filter is only valid for array and null properties.
{
"$id": "family",
"type": "object",
"properties": {
"members": {
"type": "array",
"filter": "notEmpty"
}
}
}
Let’s have a look how the generated model behaves:
// A valid example
$family = new Family(['members' => [null, null]]]);
$family->getMembers(); // returns an empty array
// the raw model data input is not affected by the filter
$family->meta()->rawInput(); // returns ['members' => [null, null]]
$family->setMembers(['Hannes', null]);
$family->getMembers(); // returns ['Hannes']
dateTime
The dateTime filter is only valid for string, number, float and nullable properties. Number and float values will be handled as timestamps. With the type of your property, you can limit the possible inputs, e.g. to accept only strings:
{
"$id": "car",
"type": "object",
"properties": {
"productionDate": {
"type": "string",
"filter": "dateTime"
}
}
}
Warning
The dateTime filter modifies the type of your property
Generated interface:
// $productionDate accepts string|DateTime
// if a string is provided the string will be transformed into a DateTime
public function setProductionDate(string | DateTime $productionDate): static;
public function getProductionDate(): ?DateTime;
Let’s have a look how the generated model behaves:
// valid, the productionDate will be NULL as the productionDate is not required
$car = new Car([]);
// Throws an InvalidFilterValueException as the provided value is not valid for the DateTime constructor
$car = new Car(['productionDate' => 'Hello']);
// A valid example
$car = new Car(['productionDate' => '2020-10-10']);
$car->getProductionDate(); // returns a DateTime object
// the raw model data input is not affected by the filter
$car->meta()->rawInput(); // returns ['productionDate' => '2020-10-10']
// Another valid example with an already transformed value
$car = new Car(['productionDate' => $myDateTimeObject]);
Additional options
Option |
Default value |
Description |
|---|---|---|
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) |
convertEmptyValueToNull |
false |
If an empty string is provided and this option is set to true the property will contain null after the filter has been applied |
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) |
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) |
outputFormat |
DATE_ISO8601 |
The output format if serialization is enabled and toArray or toJSON is called on a transformed property. If a createFromFormat is defined but no outputFormat the createFromFormat value will override the default value |
Hint
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.
Hint
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
Custom filter
You can implement custom filter and use them in your schema files. You must add your custom filter to the generator configuration to make them available.
$generator = new Generator(
(new GeneratorConfiguration())
->addFilter(new UppercaseFilter())
);
Your filter must implement the interface PHPModelGenerator\Filter\FilterInterface. Make sure the given callable array returned by getFilter is accessible as well during the generation process as during code execution using the generated model. The callable filter method must be a static method. Internally it will be called via call_user_func_array.
The accepted value types are derived automatically from the type hint of the first parameter of the filter callable via reflection. Use a union type (string|int), a nullable type (?string), or mixed to express which types the filter handles. Every filter callable must declare a type hint on its first parameter — omitting it raises an InvalidFilterException at generation time. Using mixed signals that the filter accepts all types (no runtime type guard is generated). A custom filter may look like:
namespace MyApp\Model\Generator\Filter;
use PHPModelGenerator\Filter\FilterInterface;
class UppercaseFilter implements FilterInterface
{
// The ?string type hint tells the generator that this filter handles
// string and null values. Integer, float, etc. are silently skipped.
public static function uppercase(?string $value): ?string
{
return $value !== null ? strtoupper($value) : null;
}
public function getToken(): string
{
return 'uppercase';
}
public function getFilter(): array
{
return [self::class, 'uppercase'];
}
}
Hint
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.
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:
{
"$id": "person",
"type": "object",
"properties": {
"name": {
"type": "string",
"filter": [
"uppercase",
"trim"
]
}
}
}
$person = new Person(['name' => ' Albert ']);
$person->getName(); // returns 'ALBERT'
Accessing additional filter options
Filters may handle additional configuration options like the builtin dateTime-filter. The options will be passed as an array as the second argument of your filter function. Let’s assume you want to add additional options to your uppercase-filter you’d add the options parameter to your static filter implementation:
public static function uppercase(?string $value, array $options): ?string
{
// do something with a custom option
if ($options['onlyVocals'] ?? false) {
// uppercase only the vocals of the provided value
}
// ... default implementation
}
The option will be available if your JSON-Schema uses the object-notation for the filter:
{
"$id": "person",
"type": "object",
"properties": {
"name": {
"type": "string",
"filter": [
{
"filter": "uppercase",
"onlyVocals": true
},
"trim"
]
}
}
}
Validating filter options
If your filter accepts additional filter options you may want to validate the provided options during the model generation process to avoid runtime errors. To achieve a validation of the options your custom filter must implement the PHPModelGenerator\Filter\ValidateOptionsInterface. This interface adds the following method to your implementation:
public function validateOptions(array $options): void;
The options provided in the schema which utilizes the filter are passed to the method during the model generation. If invalid options are detected just throw an exception which will result in a SchemaException.
Custom transforming filter
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\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:
The custom serializer method will be called if the model utilizing the custom filter is generated with serialization methods and toArray or toJSON is called.
namespace MyApp\Model\Generator\Filter;
use MyApp\Model\ManuallyWrittenModels\Customer;
use PHPModelGenerator\Filter\TransformingFilterInterface;
class CustomerFilter implements TransformingFilterInterface
{
// Let's assume you have written a Customer model manually eg. due to advanced validations
// and you want to use the Customer model as a part of your generated model.
// The ?array type hint tells the generator that this filter accepts array and null values.
public static function instantiateCustomer(?array $data, array $additionalOptions): ?Customer
{
return $data !== null ? new Customer($data, $additionalOptions) : null;
}
// $customer will contain the current value of the property the filter is applied to
// $additionalOptions will contain all additional options from the JSON Schema
public static function serializeCustomer(?Customer $customer, array $additionalOptions): ?string
{
return $customer !== null ? $customer->serialize($additionalOptions) : null;
}
public function getToken(): string
{
return 'customer';
}
public function getFilter(): array
{
return [self::class, 'instantiateCustomer'];
}
public function getSerializer(): array
{
return [self::class, 'serializeCustomer'];
}
}