Revision: Tue, 23 Jun 2020 11:59:44 GMT

Filter Object

The filter object used to perform complex data validation and filtration using PSR-7 or any other input. You can create Filter manually or using scaffolder php app.php create:filter my:

namespace App\Filter;

use Spiral\Filters\Filter;

class MyFilter extends Filter
{
    protected const SCHEMA    = [  
    ];

    protected const VALIDATES = [
    ];

    protected const SETTERS   = [       
    ];
}

Use Spiral\Filter\FilterProviderInterface->createFilter or simply request filter dependency to create an instance:

use App\Filter\MyFilter;

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

Filter Schema

The core of any filter object is SCHEMA; this constant defines mapping between fields and values provided by input. Every key pair is defined as field => source:origin or field => source. The source is the subset of data from user input. In the HTTP scope, the sources can be cookie, data, query, input (data+query), header, file, server. The origin is the name of the external field (dot notation is supported).

You can use any input bag from InputManager as source.

For example we can tell our Filter to point field login to the QUERY param username:

namespace App\Filter;

use Spiral\Filters\Filter;

class MyFilter extends Filter
{
    protected const SCHEMA = [
        'login' => 'query:username'
    ];
}

You can combine multiple sources inside the Filter:

namespace App\Filter;

use Spiral\Filters\Filter;

class MyFilter extends Filter
{
    protected const SCHEMA = [
        'redirectTo'   => 'query:redirectURL',
        'memberCookie' => 'cookie:memberCookie',
        'username'     => 'data:username',
        'password'     => 'data:password',
        'rememberMe'   => 'data:rememberMe'
    ];
}

The most common source is data (points to PSR-7 - parsed body), you can use this data to fetch values from incoming JSON payloads.

Dot Notation

The data origin can be specified using dot notation pointing to some nested structure. For example:

namespace App\Filter;

use Spiral\Filters\Filter;

class MyFilter extends Filter
{
    protected const SCHEMA = [
        'firstName' => 'data:name.first'
    ];

    protected const VALIDATES = [
        'firstName' => [
            ['notEmpty']
        ]
    ];

    protected const SETTERS = [
        'firstName' => 'strval'
    ];
}

We can accept and validate the following data structure:

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

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

Other Sources

By design you can use any method of [InputManager](/docs/http-request-response) as source where origin is passed parameter. Following sources are available:

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

Read more about InputManager here.

For example to check if a user request made over https:

namespace App\Filter;

use Spiral\Filters\Filter;

class MyFilter extends Filter
{
    protected const SCHEMA = [
        'httpsRequest' => 'isSecure'
    ];

    protected const VALIDATES = [
        'httpsRequest' => [
            ['notEmpty', 'error' => 'Connection is not secure.']
        ]
    ];
}

Read more about the validation below.

Route Parameters

Every route writes matched parameters into ServerRequestInterface attribute matches, is it possible to access route values inside your filter using attribute:matches.{name} notation:

$router->setRoute(
    'sample',
    new Route('/action/<id>.html', new Controller(HomeController::class))
);

Filter definition:

namespace App\Filter;

use Spiral\Filters\Filter;

class MyFilter extends Filter
{
    protected const SCHEMA = [
        'routeID' => 'attribute:matches.id'
    ];
}

Setters

Use setters to typecast the incoming value before passing it to the validator. The Filter will assign null to the value in case of typecast error:

namespace App\Filter;

use Spiral\Filters\Filter;

class MyFilter extends Filter
{
    protected const SCHEMA = [
        'number' => 'query:number'
    ];

    protected const VALIDATES = [
        'number' => [
            ['notEmpty'],
            ['number::higher', 5]
        ]
    ];

    protected const SETTERS = [
        'number' => 'intval'
    ];
}

You can use any of the default PHP functions like intval, strval etc.

namespace App\Controller;

use App\Filter\MyFilter;

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

Validation

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

namespace App\Filter;

use Spiral\Filters\Filter;

class MyFilter extends Filter
{
    protected const SCHEMA = [
        'name' => 'data:name'
    ];

    protected const VALIDATES = [
        'name' => [
            ['notEmpty']
        ]
    ];
}

You can use all the checkers, conditions, and rules.

Custom Errors

You can specify the custom error message to any of the rules similar way as in the validation component.

namespace App\Filter;

use Spiral\Filters\Filter;

class MyFilter extends Filter
{
    protected const SCHEMA = [
        'name' => 'data:name'
    ];

    protected const VALIDATES = [
        'name' => [
            ['notEmpty', 'error' => 'Name must not be empty']
        ]
    ];
}

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

namespace App\Filter;

use Spiral\Filters\Filter;

class MyFilter extends Filter
{
    protected const SCHEMA = [
        'name' => 'data:name'
    ];

    protected const VALIDATES = [
        'name' => [
            ['notEmpty', 'error' => '[[Name must not be empty]]']
        ]
    ];
}

Usage

Once the Filter configured you can access its fields (filtered data), check if the data valid and return the set of errors in case of failure.

Use Domain Core Interceptors to validate your filters before they will arrive to the controller.

Get Fields

To get a filtered list of fields, use methods getField and getFields. For the Filter like that:

namespace App\Filter;

use Spiral\Filters\Filter;

class MyFilter extends Filter
{
    protected const SCHEMA = [
        'name'  => 'data:name',
        'email' => 'data:email'
    ];

    protected const VALIDATES = [
        'name' => [
            ['notEmpty']
        ]
    ];

    protected const SETTERS = [

    ];
}

Following fields are available:

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

    dump($filter->getField('name'));

    // same as above
    dump($filter->email);
}

Get Errors

To check if filter is valid use isValid, list of field errors is available via getErrors:

public function index(MyFilter $filter)
{
    if (!$filter->isValid()) {
        dump($filter->getErrors());
    }
}

The errors automatically mapped to the origin property name, for example:

namespace App\Filter;

use Spiral\Filters\Filter;

class MyFilter extends Filter
{
    protected const SCHEMA = [
        'name' => 'data:names.name.0'
    ];

    protected const VALIDATES = [
        'name' => [
            ['notEmpty']
        ]
    ];

    protected const SETTERS = [

    ];
}

Will produce the following error if the field name is invalid:

{
  "status": 400,
  "errors": {
    "names": {
      "name": "This value is required."
    } 
  } 
}

The error format is identical to one described in validation.

Inheritance

You can extend one filter from another, the schema, validation, and setters will be inherited:

namespace App\Filter;

class MyFilter extends BaseFilter
{
    protected const SCHEMA = [
        'name' => 'data:name'
    ];

    protected const VALIDATES = [
        'name' => [
            ['notEmpty']
        ]
    ];
}

Where BaseFilter is:

namespace App\Filter;

use Spiral\Filters\Filter;

class BaseFilter extends Filter
{
    protected const SCHEMA = [
        'token' => 'data:token'
    ];

    protected const VALIDATES = [
        'token' => [
            ['notEmpty']
        ]
    ];
}

Now filter MyFilter will require token value as well:

{
  "status": 400,
  "errors": {
    "token": "This value is required.",
    "name": "This value is required."
  }
}
Edit this page