Revision: Thu, 25 Apr 2024 11:07:00 GMT

Filters — Getting started

The spiral/filters is a powerful component for filtering and optional validating input data. It allows you to define a set of rules for each input field, and then use those rules to ensure that the input data is in the correct format and meets any other requirements you have set. You can use filters to validate data from various sources like HTTP requests, gRPC requests, console commands, and others.

Here is a simple example of a filter:

php
app/src/Endpoint/Web/Filter/UserFilter.php
namespace App\Endpoint\Web\Filter;

use Spiral\Filters\Attribute\Input\Query;
use Spiral\Filters\Model\Filter;

final class UserFilter extends Filter
{
    #[Query(key: 'username')]
    public string $username;
}

This example shows a simple filter that can be requested in a controller action and will automatically map data from the HTTP request to the filter properties. Optionally, you can define a set of validation rules for each property to ensure that the input data is in the correct format.

One of the benefits of using filters is that at it helps to centralize your input validation logic in a single place. This can make it easier to maintain your code, as you don't need to duplicate validation logic in multiple places throughout your application.

Additionally, filters can be reused across different parts of your application, which can help to reduce code duplication and make it easier to manage your validation logic.

Filters Illustration of the process of filtering and validating input data in an HTTP layer

See more
Read more about how to use filters for console commands in the Cookbook — Console command input validation section.


Installation

Note
The component relies on Validation component, make sure to read it first if you want to use validation features.

The component does not require any configuration and can be activated using the bootloader Spiral\Bootloader\Security\FiltersBootloader:

php
app/src/Application/Kernel.php
public function defineBootloaders(): array
{
    return [
        // ...
        \Spiral\Bootloader\Security\FiltersBootloader::class,
        // ...
    ];
}

Read more about bootloaders in the Framework — Bootloaders section.

Input sources

The filter components operate using the Spiral\Filter\InputInterface as a primary data source:

php
interface InputInterface
{
    public function withPrefix(string $prefix, bool $add = true): InputInterface;

    public function getValue(string $source, string $name = null);
}

By default, this interface is bound to InputManager and allows to access any request's attribute using a source and origin pair with dot-notation support.

For example:

php
app/src/Endpoint/Web/HomeController.php
namespace App\Endpoint\Web;

use Spiral\Filters\InputInterface;

class HomeController
{
    public function index(InputInterface $input): void
    {
        dump($input->getValue('query', 'abc')); // ?abc=1

        // dot notation
        dump($input->getValue('query', 'a.b.c')); // ?a[b][c]=2

        // same as above
        dump($input->withPrefix('a')->getValue('query', 'b.c')); // ?a[b][c]=2
    }
}

Input binding is the primary way of delivering data from request into the filter object.

Create Filter

The implementation of the filter object might vary from package to package. The default implementation is provided via the abstract class Spiral\Filters\Model\Filter.

To create a custom filter to validate a simple query value with key username, use the scaffolding command:

php app.php create:filter UserFilter -p username:query

Note
Read more about scaffolding in the Basics — Scaffolding section.

After executing this command, the following output will confirm the successful creation:

Declaration of 'UserFilter' has been successfully written into 'app/src/Endpoint/Web/Filter/UserFilter.php'.
php
app/src/Endpoint/Web/Filter/UserFilter.php
namespace App\Endpoint\Web\Filter;

use Spiral\Filters\Attribute\Input\Query;
use Spiral\Filters\Model\Filter;

final class UserFilter extends Filter
{
    #[Query(key: 'username')]
    public string $username;
}

Warning
Be careful when using typed properties in your filter. Filter does not perform any type casting and will throw an exception if the input data does not match the property type.

You can request the Filter as a method injection (it will be automatically bound to the current HTTP request input):

php
app/src/Endpoint/Web/UserController.php
namespace App\Endpoint\Web;

class UserController
{
    public function show(Filter\UserFilter $filter): void
    {     
        dump($filter->username);
    }
}

Input casting

For more advanced scenarios, where data needs to be transformed into custom types, filter input casters come into play.

Understanding the Caster

Before diving into the application of casters, it's essential to comprehend the Spiral\Filters\Model\Mapper\CasterInterface.

php
Spiral\Filters\Model\Mapper\CasterInterface
use Spiral\Filters\Model\FilterInterface;

interface CasterInterface
{

    public function supports(\ReflectionNamedType $type): bool;

    public function setValue(FilterInterface $filter, \ReflectionProperty $property, mixed $value): void;
}

This interface has two pivotal methods:

  • supports: Takes in a $type and lets you decide whether this value is castable with this caster or not.
  • setValue: Transforms the incoming value to your desired type.

Available Casters

The component provides a set of casters out of the box:

UuidCaster

Transforms strings into Ramsey\Uuid\Uuid objects.

php
namespace App\Endpoint\Web\Filter;

use Ramsey\Uuid\UuidInterface;
use Spiral\Filters\Attribute\Input\Query;
use Spiral\Filters\Model\Filter;

final class UserFilter extends Filter
{
    #[Query]
    public UuidInterface $uuid;
}

EnumCaster

Enables casting of strings into enums, promoting type safety.

php
namespace App\Endpoint\Web\Filter;

use Spiral\Filters\Attribute\Input\Query;
use Spiral\Filters\Model\Filter;

final class UserFilter extends Filter
{
    #[Query]
    public RoleEnum $role;
}

Custom Casters

Here is an example of a simple caster:

Consider a scenario where you're handling a UUID string and intend to convert this into a UUID object.

php
app/src/Filter/Caster/UuidCaster.php
use Spiral\Filters\Model\FilterInterface;
use Spiral\Filters\Model\Mapper\CasterInterface;
use Ramsey\Uuid\UuidInterface;
use Ramsey\Uuid\Uuid;

final class UuidCaster implements CasterInterface
{
    public function supports(\ReflectionNamedType $type): bool
    {
        return $type->getName() === UuidInterface::class;
    }

    public function setValue(FilterInterface $filter, \ReflectionProperty $property, mixed $value): void
    {
        $property->setValue($filter, Uuid::fromString($value));
    }
}

Note
Uuid caster is already provided by the component.

To make your custom caster operational, you need to register it within the application's bootloader.

php
app/src/Application/Bootloader/AppBootloader.php
use Spiral\Boot\Bootloader\Bootloader;
use Spiral\Filters\Model\Mapper\CasterRegistryInterface;

class AppBootloader extends Bootloader
{
    public function boot(CasterRegistryInterface $casterRegistry)
    {
        $casterRegistry->register(new UuidCaster());
    }
}

After registering the caster, whenever you define filters with properties that match the caster's supported types, Spiral will automatically employ the registered caster to transform the data.

Handling Casting Errors

When dealing with request data filters, it's crucial to ensure that these parameters match the expected data types. For instance, a parameter expected as a string should not be processed if it comes in a different format, like an array or an integer. The Spiral Framework offers an effective solution to handle such type mismatches gracefully.

You can use the Spiral\Filters\Attribute\CastingErrorMessage attribute for any property that requires type validation:

php
app/src/Endpoint/Web/Filter/UserFilter.php
namespace App\Endpoint\Web\Filter;

use Spiral\Filters\Attribute\Input\Query;
use Spiral\Filters\Model\Filter;
use Spiral\Filters\Attribute\CastingErrorMessage;

final class UserFilter extends Filter
{
    #[Query(key: 'username')]
    #[CastingErrorMessage('Invalid type')]
    public string $username;
}

In this scenario, the username is expected to be a string. However, there might be instances where the input data is of the wrong type, such as an array or an integer. In such cases, the filter will catch the exception and return the validation error message.

Validation

By default, filters do not perform validation. However, if you want to validate a filter, you can implement the HasFilterDefinition interface and define a set of validation rules for the filter properties using the FilterDefinition class with Spiral\Filters\Model\ShouldBeValidated interface implementation:

php
app/src/Filter/MyFilterDefinition.php
namespace App\Filter;

use Spiral\Filters\Model\FilterDefinitionInterface;
use Spiral\Filters\Model\ShouldBeValidated;

final class MyFilterDefinition implements FilterDefinitionInterface, ShouldBeValidated
{
    public function __construct(
        private readonly array $validationRules = [],
        private readonly array $mappingSchema = []
    ) {
    }

    public function validationRules(): array
    {
        return $this->validationRules;
    }

    public function mappingSchema(): array
    {
        return $this->mappingSchema;
    }
}

Here is an example of registering a filter definition and binding with a validator that will be used to validate filters with the MyFilterDefinition definition:

php
app/src/Application/Bootloader/ValidatorBootloader.php
namespace App\Application\Bootloader;

use App\Validation;
use Spiral\Boot\Bootloader\Bootloader;
use Spiral\Validation\Bootloader\ValidationBootloader;
use Spiral\Validation\ValidationInterface;
use Spiral\Validation\ValidationProvider;

final class ValidatorBootloader extends Bootloader
{
    public function boot(ValidationProvider $provider): void
    {
        $provider->register(
            \App\Filter\MyFilterDefinition::class,
            static fn(Validation $validation): ValidationInterface => new MyValidation()
        );
    }
}

Note
Red more about Validation component here .

And now you can use the filter definition in your filter:

php
app/src/Endpoint/Web/Filter/UserFilter.php
namespace App\Endpoint\Web\Filter;

use Spiral\Filters\Attribute\Input\Query;
use Spiral\Filters\Model\Filter;
use Spiral\Filters\Model\FilterDefinitionInterface;
use Spiral\Filters\Model\HasFilterDefinition;
use App\Filter\MyFilterDefinition;

final class UserFilter extends Filter implements HasFilterDefinition
{
    #[Query]
    public string $username;

    public function filterDefinition(): FilterDefinitionInterface
    {
        return new MyFilterDefinition([
            'username' => ['string', 'required']
        ]);
    }
}

Note
Try URL with ?username=john. The UserFilter will automatically pre-validate your request before delivering it to the controller.