In many cases you might want to validate user request before any further action, you can do that by manually creating Validator instance in your controllers or dedicate such functionality to RequestFilters.
It may be best to read about DataEntities and Validations first.
You can create a new RequestFilter
by using the command create:request user
(make sure spiral/scaffolder module is installed). Generated class will look like:
class UserRequest extends RequestFilter
{
const SCHEMA = [];
const SETTERS = [];
const VALIDATES = [];
}
In many cases you might want to pre-create request with a specific set of fields without manually entering them every time, scaffolder config provides you ability to define shortcuts for field definitions:
'request' => [
'namespace' => 'Requests',
'postfix' => 'Request',
'class' => Declarations\RequestDeclaration::class,
'mapping' => [
'int' => [
'source' => 'data',
'setter' => 'intval',
'validates' => [
'notEmpty',
'integer'
]
],
'float' => [
'source' => 'data',
'setter' => 'floatval',
'validates' => [
'notEmpty',
'float'
]
],
'string' => [
'source' => 'data',
'setter' => 'strval',
'validates' => [
'notEmpty',
'string'
]
],
'bool' => [
'source' => 'data',
'setter' => 'boolval',
'validates' => [
'notEmpty',
'boolean'
]
],
'email' => [
'source' => 'data',
'type' => 'string',
'setter' => 'strval',
'validates' => [
'notEmpty',
'string',
'email'
]
],
'file' => [
'source' => 'file',
'type' => '\Psr\Http\Message\UploadedFileInterface',
'validates' => [
'file::uploaded'
]
],
'image' => [
'source' => 'file',
'type' => '\Psr\Http\Message\UploadedFileInterface',
'validates' => [
"image::uploaded",
"image::valid"
]
],
/*{{request.mapping}}*/
]
],
Let's create new request: spiral create:request sample -f image:image -f name:string -f test:int
Our generated class is going to look like:
class SampleRequest extends RequestFilter
{
const SCHEMA = [
'image' => 'file:image',
'name' => 'data:name',
'test' => 'data:test'
];
const SETTERS = [
'name' => 'strval',
'test' => 'intval'
];
const VALIDATES = [
'image' => [
'image::uploaded',
'image::valid'
],
'name' => [
'notEmpty',
'string'
],
'test' => [
'notEmpty',
'integer'
]
];
}
Let's check out an example of request usage in controller method and then walk through it's schema definition:
public function createUser(SampleRequest $request)
{
if(!$request->isValid()) {
dump($request->getErrors());
}
//Doing something with request data
}
Requests will be automatically created and populated with data received from request active in current IoC scope, we can define field/input mapping using our SCHEMA:
const SCHEMA = [
'image' => 'file:image',
'name' => 'data:name',
'test' => 'data:test'
];
Schema definition will include the target field name, it's source and origin (client name) name can specified using dot notation. Let's switch request to read values from query string:
const SCHEMA = [
'name' => 'query:name',
'test' => 'query:test'
];
And demonstrate dot notation:
const SCHEMA = [
'name' => 'query:user.name', //user[status]
'test' => 'query:user.test' //user[test]
];
RequestFilter supports different sources you can use for definition. Any method in InputManager
can be used as source:
protected $schema = [
'name' => 'post:name', //identical to "data:name"
'field' => 'query:field',
'file' => 'file:images.preview', //Will be represented by UploadedFile Interface
'secure' => 'isSecure' //Alias for InputManager->isSecure()
];
You can use following sources for your request filters:
Read more about InputManager here.
You can always access request values using DataEntity methods:
public function doSomething(SomeRequest $request)
{
if (!$request->isValid()) {
//Working with errors
}
$data = $request->getFields();
//You can also access some fields using magic methods
dump($request->image);
}
You can also set fields using
setField
method, this can be beneficial when using requests in non http environment.
You can add some context to request, it can be used in request filters or in checkers later:
public function doSomething(SomeRequest $request)
{
$request->setContext(['some', 'context']);
}
/**
* Inside request:
* Perform data validation. Method might include custom validations and errors
*/
protected function validate()
{
//Configuring validator
if (!empty($this->getContext())) {
//do something
}
}
Every error generated by request or target entity will be mapped to it's origin name.
const SCHEMA = [
'name' => 'query:user.name'
];
Fox example, if the request within a given schema returns any errors related to "name" field getErrors
, method will return the following structure:
[
'user' => [
'name' => 'Error message.'
]
];
You can nest RequestFilters into each other to create more complex validations:
class AddressRequest extends RequestFilter
{
const SCHEMA = [
'country' => 'data:countryCode',
'city' => 'data',
'address' => 'data',
];
const VALIDATES = [
'country' => ['notEmpty'],
'city' => ['notEmpty'],
'address' => ['notEmpty'],
];
}
Now we can use this classes as sub-requests:
class DemoRequest extends RequestFilter
{
const SCHEMA = [
'name' => 'data',
'address' => AddressRequest::class
];
const VALIDATES = [
'name' => ['notEmpty'],
'uploads' => [
['notEmpty', 'message' => '[[Please upload at least one file]]']
]
];
const SETTERS = [
'name' => 'strval'
];
}
In a given request AddressRequest will be automatically populated based on values located in a sub-array "address" in incoming request:
{
"address":{
"city": "value",
...
}
}
And will be represented as property in a parent request:
dump($demoRequest->address->getField('city'));
All nested requests will be validated with parent.
todo: WRITE ABOUT IT
On another end, UploadRequest will be represented as array of requests:
foreach($demoRequest->uploads as $upload)
{
dump($upload->upload->getClientFilename());
}
You can find demonstration of how nested requests work here.