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.

Esc