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

Filters — Filter object

The Filter class represents a set of request input fields that can be filtered and validated. It provides a set of basic features for creating filters, such as the ability to bind request input data to filter properties and to access filtered data.

Validators

There are three validator bridges available for use with Spiral filers. You can use any of these validator bridges in your application, depending on your needs and preferences.

Spiral Validator

This is the default validator bridge. It is a simple, lightweight validator that can handle basic validation tasks.

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

use Spiral\Filters\Model\FilterInterface;
use Spiral\Filters\Model\HasFilterDefinition;
use Spiral\Validator\FilterDefinition;
use Spiral\Filters\Attribute\Input\Post;
use Spiral\Filters\Attribute\Input\File;

class CreatePostFilter implements FilterInterface, HasFilterDefinition
{
    #[Post(key: 'title')]
    public string $title;
    
    #[Post(key: 'text')]
    public string $text;
    
    #[File]
    public UploadedFile $image;
    
    // ...

    public function filterDefinition(): FilterDefinitionInterface
    {
        return new FilterDefinition(
            validationRules: [
                'title' => [
                    ['notEmpty'],
                    ['string::length', 50]
                ],
                'text' => [['notEmpty']],
                'image' => [['image::valid'], ['file::size', 1024]]
                
                // ...
            ]
        );
    }
}

Symfony Validator

This validator bridge provides integration with the Symfony Validator component, which is a more powerful and feature-rich validation library.

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

use Psr\Http\Message\UploadedFileInterface;
use Spiral\Filters\Attribute\Input\Post;
use Spiral\Validation\Symfony\Attribute\Input\File;
use Spiral\Validation\Symfony\AttributesFilter;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\Validator\Constraints;

final class CreatePostFilter extends AttributesFilter
{
    #[Post]
    #[Constraints\NotBlank]
    #[Constraints\Length(min: 5)]
    public string $title;

    #[Post]
    #[Constraints\NotBlank]
    #[Constraints\Length(min: 5)]
    public string $slug;

    #[Post]
    #[Constraints\NotBlank]
    #[Constraints\Positive]
    public int $sort;
    
    #[File]
    #[Constraints\Image]
    public UploadedFile $image;
}

Laravel Validator

This validator bridge provides integration with the Laravel Validator, which is a validation component used in the Laravel framework.

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

use Spiral\Filters\Attribute\Input\Post;
use Spiral\Filters\Model\Filter;
use Spiral\Filters\Model\FilterDefinitionInterface;
use Spiral\Filters\Model\HasFilterDefinition;
use Spiral\Validation\Laravel\FilterDefinition;
use Spiral\Validation\Laravel\Attribute\Input\File;
use Symfony\Component\HttpFoundation\File\UploadedFile;

final class CreatePostFilter extends Filter implements HasFilterDefinition
{
    #[Post]
    public string $title;

    #[Post]
    public string $slug;

    #[Post]
    public int $sort;

    #[File]
    public UploadedFile $image;

    public function filterDefinition(): FilterDefinitionInterface
    {
        return new FilterDefinition([
            'title' => 'string|required|min:5',
            'slug' => 'string|required|min:5',
            'sort' => 'integer|required',
            'image' => 'required|image'
        ]);
    }
}

Usage

All the filter objects should implement Spiral\Filters\Model\FilterInterface. The interface is injectable, it means that when you request a filter class from the container, it will be automatically created and request data will be mapped to each filter property.

See more
Read more about injectors in the Advanced — Container injectors section.

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

use Spiral\Filters\Model\FilterInterface;
use Spiral\Filters\Attribute\Input\Post;

class MyFilter implements FilterInterface
{
    #[Post(key: 'text')]
    public string $text;
}

Now we can use the filter in our application. There are three ways to do it:

Simply request the filter as a dependency for example, in some controller method:

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

use App\Endpoint\Web\Filter\MyFilter;

class HomeController
{
    public function index(MyFilter $filter): void
    {
        dump($filter);
    }
}

Filter Schema

There are two ways to define the filter schema:

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

use Spiral\Filters\Attribute\Input\Post;

class MyFilter implements FilterInterface
{
    #[Post(key: 'text')]
    public string $text;
}

Request data will be mapped to the filter properties, and you will be able to access them directly.

php
public function index(MyFilter $filter): void
{
    dump($filter->text);
}

Sources

By design, you can use any method of InputManager as a source where origin is passed parameter. The following sources are available:

Source Description
uri The current page Uri in a form of Psr\Http\Message\UriInterface
path The current page path
method Http method (GET, POST, ...)
isSecure If https is used
isAjax If X-Requested-With is set as xmlhttprequest
isJsonExpected When the client expects application/json
remoteAddress User ip address

Note
You can use both ways in your filter object. In both cases the filter provider will build a mapping schema for properties with attributes and then merge the schema with the schema from the filter definition.

Attributes

To use request filter attributes, you can use one of the available attributes to specify where the data for the property should be sourced from. For example, you could use the Spiral\Filters\Attribute\Input\Query attribute to map a query string parameter to a class property, like this:

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

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

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

In this example, the Query attribute will map the value of the query string parameter with the key username to the login property.

When using an attribute, you can either specify a key argument or omit it. If you omit the key argument, the attribute will use the name of the class property as the key.

For example:

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

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

class MyFilter extends Filter
{
    #[Query]
    public string $login;
}

In this case, the Query attribute will map the value of the query string parameter with the key login to the login property.

You can use multiple attributes in a single filter class to map different parts of the request data to different class properties.

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

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

class MyFilter extends Filter
{
    #[Query(key: 'redirectURL')]
    public string $redirectTo;

    #[Cookie]
    public string $memberCookie;

    #[Post(key: 'user')]
    public string $username;

    #[Post]
    public string $password;

    #[Post(key: 'remember')]
    public string $rememberMe;
}

By using multiple attributes in this way, you can easily extract and map different pieces of request data to the appropriate class properties.

Available attributes

Here is a list of the available request filter attributes:

Attribute Description
Spiral\Filters\Attribute\Input\Post a value from the POST data.
Spiral\Filters\Attribute\Input\Query a value from the query string
Spiral\Filters\Attribute\Input\Input a value from either the POST data or the query string.
Spiral\Filters\Attribute\Input\Data a value from any of the request data (POST, query string, etc.).
Spiral\Filters\Attribute\Input\File an uploaded file. It will return Psr\Http\Message\UploadedFileInterface object or null.
Spiral\Filters\Attribute\Input\Cookie a value from a cookie.
Spiral\Filters\Attribute\Input\Header a value from the request headers.
Spiral\Filters\Attribute\Input\IsAjax a boolean value indicating whether the request was made with the X-Requested-With header set to xmlhttprequest.
Spiral\Filters\Attribute\Input\IsJsonExpected a boolean value indicating whether the client expects a application/json response.
Spiral\Filters\Attribute\Input\IsSecure a boolean value indicating whether the request was made over HTTPS.
Spiral\Filters\Attribute\Input\Method the HTTP method of the request (e.g. GET, POST, etc.).
Spiral\Filters\Attribute\Input\Path the current request path.
Spiral\Filters\Attribute\Input\RemoteAddress the IP address of the client.
Spiral\Filters\Attribute\Input\Route a value from the current route attributes.
Spiral\Filters\Attribute\Input\Server a value from the request server data.
Spiral\Filters\Attribute\Input\Uri the current page URI in the form of a Psr\Http\Message\UriInterface object.
Spiral\Filters\Attribute\Input\BearerToken the value of the Authorization header to a class property.

Route Parameters

Every route writes the matching parameters into the ServerRequestInterface attribute matches, is it possible to access route values inside your filter.

php
$router->setRoute(
    'sample',
    new Route('/action/<id>.html', new Controller(HomeController::class))
);
php
app/src/Endpoint/Web/Filter/MyFilter.php
namespace App\Endpoint\Web\Filter;

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

class MyFilter extends Filter
{
    #[Route(key: 'id')]
    public string $routeId;
}

Creating a custom attribute

You can create your own custom request filter attributes by extending the Spiral\Filters\Attribute\Input\AbstractInput class and implementing the getValue and getSchema methods. This allows you to define your own data retrieval logic and specify the type of value that should be returned by the attribute.

php
namespace App\Validation\Attribute;

use Spiral\Attributes\NamedArgumentConstructor;
use Spiral\Filters\Attribute\Input\AbstractInput;
use Spiral\Filters\InputInterface;
use Symfony\Bridge\PsrHttpMessage\Factory\UploadedFile;

#[\Attribute(\Attribute::TARGET_PROPERTY), NamedArgumentConstructor]
final class Base64DecodedQuery extends AbstractInput
{
    /**
     * @param non-empty-string|null $key
     */
    public function __construct(
        public readonly ?string $key = null,
    ) {
    }

    public function getValue(InputInterface $input, \ReflectionProperty $property): ?string
    {
        $value = $input->getValue('query', $this->getKey($property));
        if ($value === null) {
            return null;
        }
        
        return \base64_decode($value);
    }

    public function getSchema(\ReflectionProperty $property): string
    {
        return 'query:' . $this->getKey($property);
    }
}

After that, we can use our attribute in the Filter:

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

use App\Validation\Attribute\Base64DecodedQuery;
use Spiral\Filters\Model\Filter;

class MyFilter extends Filter
{
    #[Base64DecodedQuery]
    public ?string $hash = null;
}

Dot Notation

The data origin can be specified using the dot notation pointing to some nested structure.

Via attributes:

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

use Spiral\Filters\Attribute\Input\Post;
use Spiral\Filters\Model\Filter;
use Spiral\Filters\Model\FilterDefinitionInterface;
use Spiral\Filters\Model\HasFilterDefinition;
use Spiral\Validator\FilterDefinition;

class MyFilter extends Filter implements HasFilterDefinition
{
    #[Post(key: 'names.first')]
    public string $firstName;
    
    public function filterDefinition(): FilterDefinitionInterface
    {
        return new FilterDefinition([
            'firstName' => ['string', 'required']
        ]);
    }
}

We can accept and validate the following data structure:

json
{
  "names": {
    "first": "Antony"
  }
}

Note
Error messages will be correctly mounted into the original location. You can also use composite filters for more complex use-cases.

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

use App\Endpoint\Web\Filter\MyFilter;

class HomeController
{
    public function index(MyFilter $filter): void
    {
        dump($filter->number); // always int
    }
}

Data sanitization

The Spiral\Filters\Attribute\Setter attribute allows you to apply a filter function to the incoming value before it is set on the class property. This can be useful if you want to perform some kind of transformation or manipulation on the value before it is stored in the class.

To use this attribute, simply specify it alongside other filter attributes within the class definition.

For example:

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

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

class UserProfileFilter extends Filter
{
    #[Post]
    #[Setter(filter: 'trim')]
    public string $login;

    #[Post]
    #[Setter(filter: 'intval')]
    public int $age;

    #[Post]
    #[Setter(filter: [self::class, 'sanitizeContent'])]
    public string $bio = '';
    
    protected static function sanitizeContent(string $content): string
    {
        return \strip_tags($content);
    }
}

Note
Default PHP functions, such as intval and strval, can be used with this attribute. This makes it easy to apply common data manipulation functions to incoming values.

You can specify multiple Setter attributes for a single class property, allowing you to apply a series of filter functions to an incoming value before it is set.

Here is an example of how to use multiple setter attributes:

php
#[Post]
#[Setter(filter: 'strval')]
#[Setter('ltrim', '-')]
#[Setter('rtrim', ' ')]
#[Setter('htmlspecialchars')]
public string $name;

Note
the order in which the filter functions are applied matters. In the example above, strval is applied first, followed by ltrim, rtrim, and finally htmlspecialchars.

Validation

The validation rules can be defined using the same approach as in Validator component.

Note
FilterDefinition class should implement Spiral\Filters\Model\ShouldBeValidated if a filter object should be validated.

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

use Spiral\Filters\Attribute\Input\Post;
use Spiral\Filters\Model\Filter;
use Spiral\Filters\Model\FilterDefinitionInterface;
use Spiral\Filters\Model\HasFilterDefinition;
use Spiral\Validator\FilterDefinition;

class MyFilter extends Filter implements HasFilterDefinition
{
    #[Post]
    public string $name;
    
    public function filterDefinition(): FilterDefinitionInterface
    {
        return new FilterDefinition([
            'name' => ['string', 'required']
        ]);
    }
}

Handle Validation errors

When some of the filter rule has an error, Spiral\Filters\Exception\ValidationException exception will be thrown.

Spiral will automatically catch this exception via the Spiral\Filter\ValidationHandlerMiddleware middleware and return a response with the error message via Spiral\Filters\ErrorsRendererInterface.

You just need to register middleware:

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

use Spiral\Bootloader\Http\RoutesBootloader as BaseRoutesBootloader;
use Spiral\Filter\ValidationHandlerMiddleware;

final class RoutesBootloader extends BaseRoutesBootloader
{
    protected function globalMiddleware(): array
    {
        return [
            // ...
            ValidationHandlerMiddleware::class,
        ];
    }
}

By default, the middleware uses Spiral\Filter\JsonErrorsRenderer for rendering filter errors. You can change the renderer by binding your own implementation of Spiral\Filters\ErrorsRendererInterface to the container.

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

use Psr\Http\Message\ResponseInterface;
use Spiral\Filters\ErrorsRendererInterface;
use Spiral\Http\ResponseWrapper;

final class CustomJsonErrorsRenderer implements ErrorsRendererInterface
{
    public function __construct(
        private readonly ResponseWrapper $wrapper
    ) {
    }

    public function render(array $errors, mixed $context = null): ResponseInterface
    {
        return $this->wrapper->json([
            'errors' => $errors,
            'context' => (string) $context
        ])
            ->withStatus(422, 'The given data was invalid.');
    }
}
php
app/src/Application/Bootloader/RoutesBootloader.php
namespace App\Application\Bootloader;

use Spiral\Bootloader\Http\RoutesBootloader as BaseRoutesBootloader;
use Spiral\Filters\ErrorsRendererInterface;
use App\Filter\CustomJsonErrorsRenderer;

final class RoutesBootloader extends BaseRoutesBootloader
{
    // Custom renderer to the container binding
    protected const SINGLETONS = [
        ErrorsRendererInterface::class => CustomJsonErrorsRenderer::class,
    ];

    // ...
}

Custom Errors

You can specify a custom error message to any of the rules in the same way as in the validator component.

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

use Spiral\Filters\Attribute\Input\Post;
use Spiral\Filters\Model\Filter;
use Spiral\Filters\Model\FilterDefinitionInterface;
use Spiral\Filters\Model\HasFilterDefinition;
use Spiral\Validator\FilterDefinition;

class MyFilter extends Filter implements HasFilterDefinition
{
    #[Post]
    public string $name;
    
    public function filterDefinition(): FilterDefinitionInterface
    {
        return new FilterDefinition([
            'name' => [
                ['required', 'error' => 'Name must not be empty']
            ]
        ]);
    }
}

If you plan to localize the error message later, wrap the text in [[]] to automatically index and replace the translation:

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

use Spiral\Filters\Attribute\Input\Post;
use Spiral\Filters\Model\Filter;
use Spiral\Filters\Model\FilterDefinitionInterface;
use Spiral\Filters\Model\HasFilterDefinition;
use Spiral\Validator\FilterDefinition;

class MyFilter extends Filter implements HasFilterDefinition
{
    #[Post]
    public string $name;
    
    public function filterDefinition(): FilterDefinitionInterface
    {
        return new FilterDefinition([
            'name' => [
                ['required', 'error' => '[[Name must not be empty]]']
            ]
        ]);
    }
}

Usage

Once the Filter is configured you can access its fields (filtered data).

Get Fields

To get a filtered data, use filter properties or the method getData (if it extends Spiral\Filters\Model\Filter):

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

use Spiral\Filters\Attribute\Input\Post;
use Spiral\Filters\Model\Filter;
use Spiral\Filters\Model\FilterDefinitionInterface;
use Spiral\Filters\Model\HasFilterDefinition;
use Spiral\Validator\FilterDefinition;

class MyFilter extends Filter implements HasFilterDefinition
{
    #[Post]
    public string $name;
    
    #[Post]
    public string $email;
    
    public function filterDefinition(): FilterDefinitionInterface
    {
        return new FilterDefinition([
            'name' => ['required']
        ]);
    }
}

The following fields are available:

php
public function index(MyFilter $filter): void
{
    dump($filter->getData()); // {name: ..., email: ...}

    // or
    dump($filter->email);
    dump($filter->name);
}