API validation
Validate every request body and query string before you use it. A failed validation answers 422 with a message per field, which API clients can show next to their form fields.
Validating a request
Use the ValidatesRequests trait in a controller:
use FloCMS\Api\Controller;
use FloCMS\Api\Validation\ValidatesRequests;
use FloCMS\Core\Http\Request;
use FloCMS\Core\Http\Response;
final class ContactController extends Controller
{
use ValidatesRequests;
public function store(Request $request): Response
{
$message = $this->validate($request, [
'name' => 'required|string|max:100',
'email' => 'required|email|max:190',
'phone' => 'nullable|string|max:30',
'message' => 'required|string|min:10|max:5000',
]);
// $message holds only these four fields
return $this->success(['received' => true], 202);
}
}
This is the skeleton's ContactController. Sending {"name": "A"} gives:
{"success": false, "message": "Validation failed.",
"errors": {"email": ["email is required."], "message": ["message is required."]},
"request_id": "5f9b9e9f..."}
validate() reads the query string, form fields and JSON body. It returns only the fields in your rules, and converts integer, numeric and bool fields to PHP types, so "12" comes back as 12.
Validating the query string
validateQuery() checks only the query string, for list filters:
$filters = $this->validateQuery($request, [
'min_price' => 'sometimes|integer|min:0',
'city' => 'sometimes|string|max:100',
]);
Without it, ?min_price=abc would silently become 0 somewhere in your code. With it, the client gets a 422.
Rules
| Rule | Passes when |
|---|---|
required |
The field is present and not empty (null, '' and [] fail) |
string |
A string |
integer |
An integer, or a string like "5" |
numeric |
A number, or a numeric string like "12.5" |
bool |
true, false, 1, 0, "1", "0", "true" or "false" |
array |
An array |
email |
A valid email address |
url |
A valid http or https URL |
date, date:Y-m-d |
A date. Without a format: Y-m-d, Y-m-d H:i:s or ISO 8601. |
min:n, max:n |
At least / at most n: the value for integer and numeric fields, otherwise characters for strings and items for arrays |
between:min,max |
Both of the above |
in:a,b,c |
One of the listed values |
regex:pattern |
Matches the pattern |
exists:table,column |
A row with this value exists |
unique:table,column |
No row has this value yet |
nullable |
null or '' is accepted, and returned as null |
sometimes |
The rules only run when the field is present |
A field that isn't required and wasn't sent is skipped and left out of the result. When a pattern contains |, use the array form: 'code' => ['required', 'regex:/^[A-Z]{2}\d+$/'].
unique on update
When a record is updated, its own value isn't a duplicate. Pass its ID to ignore it:
'email' => 'required|email|unique:users,email,' . $id, // ignores the row with id = $id
'slug' => 'required|string|unique:posts,slug,' . $id . ',post_id', // when the key column isn't "id"
exists and unique run prepared queries on the site's database. Table and column names must be plain names.
Custom messages
The third argument replaces messages, per field and rule, or per rule with :field for the field name:
$data = $this->validate($request, $rules, [
'email.required' => 'Please enter your email address.',
'max' => ':field is too long.',
]);
Custom rules
Override validator() in your controller to add rules. A rule returns an error message, or null when the value is fine:
use FloCMS\Api\Validation\Validator;
protected function validator(): Validator
{
return (new Validator())->extend('even', static fn (mixed $value): ?string
=> (int) $value % 2 === 0 ? null : 'Must be an even number.');
}
'quantity' => 'required|integer|even',
The callback also receives the rule's parameter, the field name and all the data: fn ($value, $parameter, $field, $data).
Request classes
For long rule lists, keep the rules out of the controller:
php flo make:request StorePost
creates api/Requests/StorePostRequest.php with rules(), messages() and a static validate():
use App\Api\Requests\StorePostRequest;
public function store(Request $request): Response
{
$data = StorePostRequest::validate($request);
// ...
}
Outside controllers
The validator works on any array:
use FloCMS\Api\Validation\Validator;
$data = (new Validator())->validate($input, ['email' => 'required|email']);
It throws FloCMS\Api\Exceptions\ValidationException, which the API turns into a 422 response. Its errors are in $e->errors. Core's Validator::validate() throws a different ValidationException, which the API also answers with a 422. See requests and validation for the core validator.