The Filter object is used to perform complex data validation and filtration using PSR-7 or any other input.
You can use filters in two ways:
If you only need to populate the data from the request you don't need any validators for it. But if you need to validate data, at first, you need to choose a validator for it.
There are three validators for Spiral Framework that you can use:
<?php
declare(strict_types=1);
namespace App\Filters;
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]]
// ...
]
);
}
}
<?php
declare(strict_types=1);
namespace App\Filters;
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;
}
<?php
declare(strict_types=1);
namespace App\Filters;
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'
]);
}
}
We will use Spiral Validator package in the article examples.
All the filter objects should implement Spiral\Filters\Model\FilterInterface
. The interface will add the ability to inject
filters with populated data from Spiral\Filters\InputInterface
from the container.
namespace App\Filter;
use Spiral\Filters\Model\FilterInterface;
use Spiral\Filters\Attribute\Input\Post;
class MyFilter implements FilterInterface
{
#[Post(key: 'text')]
public string $text;
}
dump($container->get(MyFilter::class));
You can also use Spiral\Filters\Model\FilterProviderInterface
->createFilter
to create an instance:
$provider = $container->get(\Spiral\Filters\Model\FilterProviderInterface::class);
$provider->createFilter(MyFilter::class, $container->get(\Spiral\Filters\InputInterface::class));
or simply request the filter as a dependency for example, in some controller
use App\Filter\MyFilter;
class HomeController
{
public function index(MyFilter $filter): void
{
dump($filter);
}
}
There are two ways to define the filter schema.
class MyFilter implements FilterInterface
{
#[Post(key: 'text')]
public string $text;
}
$filter = $container->get(MyFilter::class);
dump($filter->text); // '...'
dump($filter->getData()); // ['text' => '...']
Spiral\Filters\Model\HasFilterDefinition
and
extend the Spiral\Filters\Model\Filter
class to have access to the mapped data from the request.namespace App\Filter;
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
{
public function filterDefinition(): FilterDefinitionInterface
{
return new FilterDefinition(mappingSchema:
[
'text' => 'data:text'
]
);
}
}
$filter = $container->get(MyFilter::class);
dump($filter->getData()); // ['text' => '...']
Note
You can use both ways in your filter object. In this case the filter provider will build a mapping schema for properties with attributes and then merge the schema with the schema from the filter definition.
You can add properties with the needed type and add an attribute that points to the data source.
For example, we can tell our Filter to map the field login
to the QUERY param username
:
namespace App\Filter;
use Spiral\Filters\Attribute\Input\Query;
use Spiral\Filters\Model\Filter;
class MyFilter extends Filter
{
#[Query(key: 'username')]
public string $login;
}
You can combine multiple sources inside the Filter object:
namespace App\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]
public string $username;
#[Post]
public string $password;
#[Post]
public string $rememberMe;
}
You can create your own attribute, which will contain a custom data retrieval logic. For example, we need to receive uploaded files as a Symfony\Bridge\PsrHttpMessage\Factory\UploadedFile object. First of all, we need to create custom input bag for this:
namespace App\Http\Request;
use Spiral\Http\Request\InputBag;
use Symfony\Bridge\PsrHttpMessage\Factory\UploadedFile;
final class FilesBag extends InputBag
{
public function __construct(array $data, string $prefix = '')
{
foreach ($data as $name => $file) {
$data[$name] = new UploadedFile($file, fn(): string => $this->getTemporaryPath());
}
parent::__construct($data, $prefix);
}
protected function getTemporaryPath(): string
{
return \tempnam(\sys_get_temp_dir(), \uniqid('symfony', true));
}
}
After that, add the created FilesBag
by the method addInputBag
in the HttpBootloader
.
namespace App\Bootloader;
use App\Http\Request\FilesBag;
use Spiral\Boot\Bootloader\Bootloader;
use Spiral\Bootloader\Http\HttpBootloader;
class AppBootloader extends Bootloader
{
public function init(HttpBootloader $http): void
{
$http->addInputBag('symfonyFiles', [
'class' => FilesBag::class,
'source' => 'getUploadedFiles',
'alias' => 'symfony-file'
]);
}
}
Let's create a filter attribute that will get the uploaded file. The attribute class must be extended from
the Spiral\Filters\Attribute\Input\AbstractInput
class.
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 File extends AbstractInput
{
/**
* @param non-empty-string|null $key
*/
public function __construct(
public readonly ?string $key = null,
) {
}
public function getValue(InputInterface $input, \ReflectionProperty $property): ?UploadedFile
{
return $input->getValue('symfony-file', $this->getKey($property));
}
public function getSchema(\ReflectionProperty $property): string
{
return 'symfony-file:' . $this->getKey($property);
}
}
After that, we can use our attribute in the Filter:
namespace App\Filter;
use App\Validation\Attribute\File;
use Spiral\Filters\Model\Filter;
use Symfony\Component\HttpFoundation\File\UploadedFile;
class MyFilter extends Filter
{
#[File]
public UploadedFile $image;
}
For example, we can tell our Filter to point the field login
to the QUERY param username
:
namespace App\Filter;
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
{
public function filterDefinition(): FilterDefinitionInterface
{
return new FilterDefinition(mappingSchema:
[
'login' => 'query:username'
]
);
}
}
You can combine multiple sources inside the Filter:
namespace App\Filter;
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
{
public function filterDefinition(): FilterDefinitionInterface
{
return new FilterDefinition(mappingSchema:
[
'redirectTo' => 'query:redirectURL',
'memberCookie' => 'cookie:memberCookie',
'username' => 'data:username',
'password' => 'data:password',
'rememberMe' => 'data:rememberMe'
]
);
}
}
Note
The most common source isdata
(points to PSR-7 - the parsed body), you can use this data to fetch values from the incoming JSON payloads.
The data origin can be specified using the dot notation pointing to some nested structure.
Via attributes:
namespace App\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']
]);
}
}
Via array mapping:
namespace App\Filter;
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
{
public function filterDefinition(): FilterDefinitionInterface
{
return new FilterDefinition(
[
'firstName' => ['string', 'required']
],
[
'firstName' => 'data:names.first'
]
);
}
}
We can accept and validate the following data structure:
{
"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.
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
Read more about the InputManager here.
For example, to check if a user request is made over https.
Via attributes:
namespace App\Filter;
use Spiral\Filters\Attribute\Input\IsSecure;
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
{
#[IsSecure]
public bool $httpsRequest;
public function filterDefinition(): FilterDefinitionInterface
{
return new FilterDefinition([
'httpsRequest' => [
['required', 'error' => 'Connection is not secure.']
]
]);
}
}
Via array mapping:
namespace App\Filter;
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
{
public function filterDefinition(): FilterDefinitionInterface
{
return new FilterDefinition(
[
'httpsRequest' => [
['required', 'error' => 'Connection is not secure.']
]
],
[
'httpsRequest' => 'isSecure:httpsRequest'
]
);
}
}
Every route writes the matching parameters into the ServerRequestInterface attribute matches
, is it possible to access route
values inside your filter.
$router->setRoute(
'sample',
new Route('/action/<id>.html', new Controller(HomeController::class))
);
Via attributes:
namespace App\Filter;
use Spiral\Filters\Attribute\Input\Route;
use Spiral\Filters\Model\Filter;
class MyFilter extends Filter
{
#[Route(key: 'id')]
public string $routeId;
}
Via array mapping using attribute:matches.{name}
notation:
namespace App\Filter;
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
{
public function filterDefinition(): FilterDefinitionInterface
{
return new FilterDefinition(mappingSchema:
[
'routeId' => 'attribute:matches.id'
]
);
}
}
Use setters to typecast the incoming value before passing it to the validator. The Filter will assign null to the value in case of a typecast error:
namespace App\Filter;
use Spiral\Filters\Attribute\Input\Query;
use Spiral\Filters\Attribute\Setter;
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
{
#[Query(key: 'id')]
#[Setter(filter: 'intval')]
public int $number;
public function filterDefinition(): FilterDefinitionInterface
{
return new FilterDefinition([
'number' => ['required', ['number::higher', 5]]
]);
}
}
Note
You can use any default PHP functions likeintval
,strval
etc.
namespace App\Controller;
use App\Filter\MyFilter;
class HomeController
{
public function index(MyFilter $filter): void
{
dump($filter->number); // always int
}
}
Note
FilterDefinition class should implementSpiral\Filters\Model\ShouldBeValidated
if a filter object should be validated.
The validation rules can be defined using the same approach as in Validator component.
namespace App\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']
]);
}
}
You can use all the checkers, conditions, and rules.
By default, the Spiral Framework doesn't handle filter validation errors. When some of the filter rules has an error,
Spiral\Filters\Exception\ValidationException
exception will be thrown.
There are two ways to handle a validation exception:
Both the ways are similar. You can see an example of a validation exception handler below:
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface as Response;
use Spiral\Filters\Exception\ValidationException;
class JsonValidationMiddleware implements MiddlewareInterface
{
public function __construct(
private readonly ResponseFactoryInterface $responseFactory
)
{}
public function process(Request $request, RequestHandlerInterface $handler): Response
{
try {
return $handler->handle($request);
} catch (ValidationException $e) {
return $this->responseFactory->createResponse($e->getCode(), $e->getMessage())
->getBody()
->write(\json_encode([
'errors' => $e->errors,
]));
}
}
}
And then you need to register middleware for specific route group.
namespace App\Bootloader;
use Spiral\Bootloader\Http\RoutesBootloader as BaseRoutesBootloader;
use Spiral\Cookies\Middleware\CookiesMiddleware;
use Spiral\Csrf\Middleware\CsrfMiddleware;
use Spiral\Session\Middleware\SessionMiddleware;
final class RoutesBootloader extends BaseRoutesBootloader
{
// ...
protected function middlewareGroups(): array
{
return [
'web' => [
CookiesMiddleware::class,
SessionMiddleware::class,
CsrfMiddleware::class,
],
'api' => [
JsonValidationMiddleware::class // <===== Our new middleware
],
];
}
}
You can specify a custom error message to any of the rules in the same way as in the validator component.
namespace App\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:
namespace App\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]]']
]
]);
}
}
Once the Filter is configured you can access its fields (filtered data).
The Spiral\Filters\Model\Interceptor\ValidateFilterInterceptor
will automatically validate the data when the filter
is requested and throw a Spiral\Filters\Exception\ValidationException
if the data is not valid.
To get a filtered data, use filter properties or the method getData
(if it extends Spiral\Filters\Model\Filter
):
namespace App\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:
public function index(MyFilter $filter): void
{
dump($filter->getData()); // {name: ..., email: ...}
// or
dump($filter->email);
dump($filter->name);
}