Spiral allows for the creation of composite filters, which are filters that are composed of other filters.
Let's imagine that we have a AddressFilter
that represents a single address that can be used as a part of a profile
with its own validation rules:
Note
In our examples we will use Spiral Validator for validation, but you can use any other validation library.
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 AddressFilter extends Filter implements HasFilterDefinition
{
#[Post]
public string $city;
#[Post]
public string $address;
public function filterDefinition(): FilterDefinitionInterface
{
return new FilterDefinition(validationRules: [
'city' => ['required', 'string'],
'address' => ['required', 'string'],
]);
}
}
Spiral provides two types of nested filters:
Spiral allows you to create compound filters by nesting other filters inside them. This is done by declaring a property
in the parent filter class and decorating it with the Spiral\Filters\attribute\NestedFilter
attribute. The attribute
takes a class parameter, which is set to the child filter class. This allows the parent filter to accept input data in
a nested format, where the property decorated with this attribute contains the data for the child filter. This makes it
easy to validate and filter the data in multiple levels and reuse the child filter in different parent filters.
namespace App\Endpoint\Web\Filter;
use Spiral\Filters\Attribute\Input\Post;
use Spiral\Filters\Attribute\NestedFilter;
use Spiral\Filters\Model\Filter;
use Spiral\Filters\Model\FilterDefinitionInterface;
use Spiral\Filters\Model\HasFilterDefinition;
use Spiral\Validator\FilterDefinition;
class ProfileFilter extends Filter implements HasFilterDefinition
{
#[Post]
public string $name;
#[NestedFilter(class: AddressFilter::class)]
public AddressFilter $address;
public function filterDefinition(): FilterDefinitionInterface
{
return new FilterDefinition(validationRules: [
'name' => ['required', 'string'],
]);
}
}
This Filter will accept the data in the format:
{
"name": "Antony",
"address": {
"city": "San Francisco",
"address": "Address"
}
}
Once the request input data is passed through the filter, you can access the filtered data using the properties of the filter class.
public function index(ProfileFilter $profile): void
{
dump($profile->address->city); // San Francisco
}
When using nested filters, both the parent filter and the child filter(s) will be validated together. If there are any validation errors in the child filter, they will be mounted in a sub-array of the parent filter's errors. This allows you to easily identify which errors belong to which filter, and makes it easier to display the errors to the user.
{
"name": "This field is required.",
"address": {
"city": "This field is required."
}
}
The NestedFilter
attribute allows you to specify a custom prefix for the data that is passed to the child filter. By
default, the prefix is the same as the key assigned to the nested filter property, but in some cases, you may need to
use a different prefix.
class ProfileFilter extends Filter implements HasFilterDefinition
{
#[NestedFilter(class: AddressFilter::class, prefix: 'addr')]
public AddressFilter $address;
// ...
}
The json data format that will work with this filter is:
{
"name": "Antony",
"addr": {
"city": "San Francisco",
"address": "Address"
}
}
Note
You can skip the use of theaddress
key internally, errors will be mounted accordingly.
You can use nested child filters as part of a larger composite Filter. Use the prefix .
(root) to do that:
class MultipleAddressesFilter extends Filter implements HasFilterDefinition
{
#[NestedFilter(class: AddressFilter::class, prefix: '.')]
public AddressFilter $address;
// ...
}
The AddressFilter
will receive data from the top-level, meaning you can send a request like that:
{
"name": "Antony",
"city": "San Francisco",
"address": "Address"
}
You can use the Spiral\Filters\attribute\NestedArray
attribute to populate an array of filters at the same time. In
order to use this attribute, you need to declare an array property in your filter class and decorate it with the
NestedArray
attribute, specifying the class of the filter for each element in the array as the class parameter.
The input
parameter in the attribute is used to specify the input source that contains the array of filters.
Note
List of available input sources can be found in the Filters — Filter object section.
use Spiral\Filters\Attribute\Input\Post;
use Spiral\Filters\Attribute\NestedArray;
use Spiral\Filters\Model\Filter;
use Spiral\Filters\Model\FilterDefinitionInterface;
use Spiral\Filters\Model\HasFilterDefinition;
use Spiral\Validator\FilterDefinition;
final class MultipleAddressesFilter extends Filter implements HasFilterDefinition
{
#[Post]
public string $name;
#[NestedArray(class: AddressFilter::class, input: new Post]
public array $addresses;
public function filterDefinition(): FilterDefinitionInterface
{
return new FilterDefinition(validationRules: [
'name' => ['required', 'string'],
]);
}
}
This Filter will accept the data in the format:
{
"key": "value",
"addresses": [
{
"city": "San Francisco",
"address": "Address"
},
{
"city": "Minsk",
"address": "Address #2"
}
]
}
Once you have applied the filter, you can access the individual filters in the array using array notation.
public function index(MultipleAddressesFilter $filter)
{
dump($filter->addresses[0]->city); // San Francisco
dump($filter->addresses[1]->city); // Minsk
}
Note
If there are any validation errors in the nested filters, they will be mounted according to the structure of the nested filters.
Yes, you can pass the custom prefix as a parameter to the constructor of the input source class, when defining the
NestedArray
attribute. This way, the filter will look for input data using the custom prefix instead of the default
key name.
class MultipleAddressesFilter extends Filter
{
#[NestedArray(class: AddressFilter::class, input: new Post('addr'))]
public array $addresses;
// ...
}
This Filter supports the following data format:
{
"key": "value",
"addr": [
{
"city": "San Francisco",
"address": "Address"
},
{
"city": "Minsk",
"address": "Address #2"
}
]
}