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): .. code-block:: json { "$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 <../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. 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): .. code-block:: json { "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. .. code-block:: json { "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``, ``minLength`` for a string property) run *before* the filter, against the raw input value. - **Output-space validators** (e.g. ``minimum``, ``maximum`` for 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 a ``SchemaException``. - ``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): .. code-block:: json { "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): .. code-block:: json { "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: .. code-block:: php // 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. .. code-block:: json { "$id": "person", "type": "object", "properties": { "name": { "type": "string", "filter": "trim", "minLength": 2 } } } Let's have a look how the generated model behaves: .. code-block:: php // 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. .. code-block:: json { "$id": "family", "type": "object", "properties": { "members": { "type": "array", "filter": "notEmpty" } } } Let's have a look how the generated model behaves: .. code-block:: php // 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: .. code-block:: json { "$id": "car", "type": "object", "properties": { "productionDate": { "type": "string", "filter": "dateTime" } } } .. warning:: The dateTime filter modifies the type of your property Generated interface: .. code-block:: php // $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: .. code-block:: php // 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. .. code-block:: php $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: .. code-block:: php 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: .. code-block:: json { "$id": "person", "type": "object", "properties": { "name": { "type": "string", "filter": [ "uppercase", "trim" ] } } } .. code-block:: php $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: .. code-block:: php 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: .. code-block:: json { "$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: .. code-block:: php 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 <../gettingStarted.html#serialization-methods>`__ and *toArray* or *toJSON* is called. .. code-block:: php 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']; } }