Skip to content

3. The Action Template

An action requires information sent by the browser: the parameters of the URL ([?name=martin&age=42]), form fields, the request body JSON, and sometimes a request header HTTP. The original course refers to all of this information as the action model. This chapter shows how an action [Laravel] retrieves this information, converts it to the correct type, and validates it. This process is called data binding.

[Laravel] provides access to the request information via the [Request] object (Example 05), in the form of character strings, along with a few conversion methods. To verify the data, it has a comprehensive validation system: the expected data is described using rules (“required,” “an integer between 0 and 150,” etc.), and [Laravel] checks that the data complies with them. We’ll first read the parameters manually (example 07), then use these rules to validate them (examples 08 through 12). In the next chapter, the same system will validate the HTML forms.

Note on terminology: In the next chapter, the word “model” will primarily refer to the data that an action passes to its view. An action’s model, as used in this chapter, is what it receives. [Laravel] also uses the word *model* for the classes that represent database tables (Chapter 9): this is a third meaning.

Examples 07 through 12 are run in the same way as those in the previous chapter: open a terminal in the example directory, [php artisan serve]; [Ctrl-C] stops the server. Their [bootstrap/app.php] files are all similar: they add the [ReadableJson] middleware from Example 03, resulting in a readable JSON file. We will only comment on them when they introduce something new.

3.1. Example 07: The parameters of URL

The parameters for URL are the part that follows [?]: [?name=martin&age=42]. This part is called the query string (*query string*). Each parameter is a pair of [name=value]; the pairs are separated by [&]. The controller [ParametersController]:

<?php
// app/Http/Controllers/ParametersController.php: the parameters of the URL (the "query string")

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class ParametersController
{
    // /parameters/action01?name=martin: query returns the value of the parameter, or
    // null if it is missing
    public function action01(Request $request): string
    {
        $name = $request->query('name');
        return "name = [$name], of type ".get_debug_type($name);
    }

    // /parameters/action02?age=42: integer CONVERTS the value (a string) into an
    // integer; a missing parameter, or one that is not an integer, gives 0
    public function action02(Request $request): string
    {
        $age = $request->integer('age');
        return "age = [$age], of type ".get_debug_type($age);
    }

    // /parameters/action03?age=42: a REQUIRED parameter; its absence, or a
    // value that is not an integer, stops the action with a 400 response (Bad Request)
    public function action03(Request $request): string
    {
        $text = $request->query('age');
        if ($text === null) {
            // abort raises an exception, which Laravel turns into an error response
            abort(400, 'the [age] parameter is required');
        }
        // the conversion is up to us: filter_var returns the integer, or false
        $age = filter_var($text, FILTER_VALIDATE_INT);
        if ($age === false) {
            abort(400, "[$text] is not an integer");
        }
        return "age = [$age], of type ".get_debug_type($age);
    }

    // /parameters/action04?page=2: a default value if the parameter is missing
    public function action04(Request $request): string
    {
        $page = $request->integer('page', 1);
        return "page = [$page]";
    }

    // /parameters/action05?q=laravel: the name of the URL parameter is not necessarily
    // that of the PHP variable
    public function action05(Request $request): string
    {
        $search = $request->query('q');
        return "search = [$search]";
    }

    // /parameters/action06?name=martin&age=42: all the parameters at once, as an array
    public function action06(Request $request): array
    {
        return $request->query();
    }
}
  • line 14: the [query] method in the request returns the value of a parameter from URL, which has already been decoded, or [null] if it is missing; it never causes an error;
  • Line 15: The action displays the value and its type: the [get_debug_type] function returns the name of a value’s type. A value retrieved from URL is always a string ([string]). The operator [.] concatenates the interpolated string and the type;
  • line 22: the method [integer] reads the parameter and converts it to an integer. If the conversion fails ([abc]), or if the parameter is missing, it returns 0: the action cannot distinguish these cases from an age of 0 that was actually entered, and the input error goes unnoticed. Similarly, there are [float], [boolean] ([1], [true], [on], [yes] are valid), [string], [date] (example 12), [enum];
  • lines 30–40: a required parameter and a manual conversion that distinguishes between the two possible errors. [abort(400, message)] terminates the action with a 400 response (*Bad Request*, the request is invalid), accompanied by the message (see example 03). The function [filter_var] of [PHP], with the filter [FILTER_VALIDATE_INT], returns the integer contained in the string, or [false] if the string is not an integer;
  • line 46: the second parameter of [integer] is a default value, used if the parameter is missing. This is typically the case for a page number in a list;
  • line 54: There is no requirement to name the variable [PHP] the same as the parameter of URL. Search engines often use a short parameter, such as [q] (*query*);
  • line 61: without a parameter, [query] returns all the parameters of URL in an associative array, which the action returns: [Laravel] transforms it into JSON (see example 03).

The routing table declares the six routes using a loop:

<?php
// routes/web.php: the routes of example 07

use App\Http\Controllers\ParametersController;
use Illuminate\Support\Facades\Route;

Route::prefix('parameters')->controller(ParametersController::class)->group(function () {
    foreach (['action01', 'action02', 'action03', 'action04', 'action05', 'action06'] as $action) {
        Route::get($action, $action);
    }
});
  • Line 8: The loop [foreach] iterates through an array: on each iteration, the variable [$action] takes on the value of the next element. Each route is named after its action: [Route::get('action01', 'action01')]... The routing table is just another [PHP] file: you can write any code you want in it.

Let’s test each action with [curl]. A parameter present, then absent:

curl -i "http://localhost:8000/parameters/action01?name=martin"
curl http://localhost:8000/parameters/action01
1
2
3
4
5
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8

name = [martin], of type string
name = [], of type null

The quotation marks around "URL" are necessary in a terminal: without them, the shell would interpret [&] (which runs a command in the background under [Linux]) and [?]. The value [null], when inserted into a string, becomes an empty string. A value containing spaces or special characters must be encoded in URL: a space becomes [%20], and [&] becomes [%26] (otherwise, it would separate two parameters). The browser handles this automatically; with [curl], you have to write it out. [PHP] decodes the value before the action reads it:

curl "http://localhost:8000/parameters/action01?name=jean%20dupont%20%26%20sons"
name = [jean dupont & sons], of type string

Conversion using [integer]—successful, failed, then without parameters:

1
2
3
curl "http://localhost:8000/parameters/action02?age=42"
curl "http://localhost:8000/parameters/action02?age=abc"
curl http://localhost:8000/parameters/action02
1
2
3
age = [42], of type int
age = [0], of type int
age = [0], of type int

The required parameter for action 03 is missing, then invalid. We request a response JSON (header [Accept], see example 03), which contains the message from [abort]:

curl -i -H "Accept: application/json" http://localhost:8000/parameters/action03
curl -H "Accept: application/json" "http://localhost:8000/parameters/action03?age=abc"
HTTP/1.1 400 Bad Request
Content-Type: application/json

{
    "message": "the [age] parameter is required",
    "exception": "Symfony\\Component\\HttpKernel\\Exception\\HttpException",
    ...
}
{
    "message": "[abc] is not an integer",
    "exception": "Symfony\\Component\\HttpKernel\\Exception\\HttpException",
    ...
}

In a browser, in debug mode, a 400 response displays the error page for [Laravel], with the message:

Image

The default value and the renamed parameter:

1
2
3
curl http://localhost:8000/parameters/action04
curl "http://localhost:8000/parameters/action04?page=3"
curl "http://localhost:8000/parameters/action05?q=laravel"
1
2
3
page = [1]
page = [3]
search = [laravel]

Finally, all the parameters at once. The parameter [name] appears twice:

curl "http://localhost:8000/parameters/action06?name=martin&age=42&name=durand"
1
2
3
4
{
    "name": "durand",
    "age": "42"
}

The parameter [name] retains only its last value: this is a rule for [PHP], which we’ll see again in Example 10. The age remained a string (["42"], enclosed in quotation marks within JSON): no conversion was requested. In a browser (here, [Chromium]), the JSON is displayed exactly as the server formatted it; the “Pretty-print” checkbox would indent it if it were not already indented. The address bar also shows the encoding of accented characters in URL ([é] becomes [%C3%A9], with its two bytes in [UTF-8]):

Image

This first example illustrates the limitations of the manual approach: for each parameter, you must write the reading, conversion, and handling of missing values and errors. With ten parameters, the action’s code would be overwhelmed by these checks. It is better to describe what is expected and let the framework verify it. That is the purpose of the following example.

3.2. Example 08: An action model described by a class

[Laravel] validates request data using validation rules. A rule has a name ([required], [integer], [email]...), and sometimes parameters ([min:2], [between:0,150]); the rules for a field are written as a string, separated by [|], or as an array. [Laravel] provides about a hundred of them. The natural place to describe an action’s model is a form request (*form request*): a class that inherits from [FormRequest], and whose [rules] method returns the rules for each parameter.

3.2.1. The model: app/Http/Requests/PersonRequest.php

<?php
// app/Http/Requests/PersonRequest.php: the model of the action, a class that
// describes the expected parameters and their constraints

namespace App\Http\Requests;

use App\Models\Person;
use Illuminate\Foundation\Http\FormRequest;

class PersonRequest extends FormRequest
{
    // the validation RULES, parameter by parameter
    public function rules(): array
    {
        return [
            // required, a string of 2 to 20 characters
            'name' => 'required|string|min:2|max:20',
            // required, an integer between 0 and 150
            'age' => 'required|integer|between:0,150',
            // one of these three values only
            'title' => 'required|in:Mr,Mme,Melle',
            // optional (nullable); if it is present, a valid email address
            'email' => 'nullable|email',
        ];
    }

    // the validated parameters, in the form of a typed object
    public function person(): Person
    {
        $values = $this->validated();
        return new Person($values['name'], (int) $values['age'], $values['title'],
            $values['email'] ?? null);
    }
}
  • Line 10: The class ([extends]) inherits from [FormRequest], which itself inherits from [Request]: A [PersonRequest] object is a query, with all the methods from Example 07, plus validation rules;
  • lines 13–24: the rules, in an associative array: for each parameter, the string containing its rules;
  • line 17: [required]: the parameter is required (if missing or empty, it is rejected); [string]: a string; [min:2] and [max:20]: for a string, its length must be between 2 and 20 characters;
  • line 19: [integer]: the value must be an integer (["42"] is valid, ["abc"] is not); [between:0,150]: For a number, its value must be between 0 and 150. The rules [min], [max], and [between] thus adapt to the value’s type: a length for a string, a value for a number, and a number of elements for an array (e.g., 10);
  • line 21: [in:Mr,Mme,Melle]: one of the three values listed, and only those;
  • line 23: [nullable]: the parameter is optional (it is accepted if absent or empty); [email]: if present, a valid email address;
  • lines 28–33: a method added to the class: it returns the validated data in the form of a [Person] object (see below). [$this->validated()] returns only the data that has been validated: a parameter without a rule is not included. The validation performed by [Laravel] checks the data but does not convert it: the validated age is still the string ["42"]. It is the conversion operator [(int)] that converts it to an integer. The operator [??] returns its left operand if it exists and is not [null]; otherwise, it returns the right operand: [$values['email'] ?? null] evaluates to [null] if the address is not in the array.

This template describes the expected data; it contains no validation instructions. This is known as a declarative style. The class [Person] is an immutable class, modeled after the one in Example 03:

<?php
// app/Models/Person.php: a person

namespace App\Models;

readonly class Person
{
    public function __construct(
        public string $name,
        public int $age,
        public string $title,
        public ?string $email,
    ) {
    }
}
  • line 12: [?string]: a string, or [null].

3.2.2. The action: app/Http/Controllers/PersonsController.php

<?php
// app/Http/Controllers/PersonsController.php: an action whose model is a FormRequest class

namespace App\Http\Controllers;

use App\Http\Requests\PersonRequest;

class PersonsController
{
    // /persons/action01?name=martin&age=42&title=Mr
    // The parameter of type PersonRequest is VALIDATED by Laravel BEFORE the call of
    // the action: an invalid model raises a ValidationException, turned into a
    // 400 response by bootstrap/app.php, and the action is never called
    public function action01(PersonRequest $request): array
    {
        $person = $request->person();
        return [
            'received' => $request->validated(),
            'person' => $person,
            'ageType' => get_debug_type($person->age),
        ];
    }
}
  • Line 14: The action requires a parameter of type [PersonRequest], whereas Example 07 required a [Request]. [Laravel] then creates a [PersonRequest] object from the current request and validates it before calling the action: it checks each rule for each parameter. If all rules are met, the action is called. Otherwise, [Laravel] throws a [ValidationException] exception, which contains a list of all errors (not just the first one), and the action is never called: it therefore does not have to deal with invalid data;
  • Lines 16–20: The action returns the validated data as-is, then as a [Person] object, along with the age type, to illustrate the difference.

Who handles the [ValidationException] exception? By default, [Laravel] itself, and its response depends on the client. For a request expecting JSON (header [Accept: application/json]), the response is a 422 (*Unprocessable Content*), which describes the errors in JSON. For a regular request originating from a HTML form, the response is a redirect to the previous page, with the errors preserved for the form that will be redisplayed (we’ll cover this in the next chapter). Here, we want an error response in JSON, in all cases, with the 400 code. That is the role of [bootstrap/app.php].

3.2.3. Error handling: bootstrap/app.php

<?php
// bootstrap/app.php: the application of example 08

use App\Http\Middleware\ReadableJson;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Validation\ValidationException;

(require __DIR__.'/../../vendor/autoload.php')->addPsr4('App\\', __DIR__.'/../app/');

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(web: __DIR__.'/../routes/web.php')
    ->withMiddleware(function (Middleware $middleware): void {
        $middleware->append(ReadableJson::class);
    })
    ->withExceptions(function (Exceptions $exceptions): void {
        // an invalid model: the ValidationException exception becomes, for
        // the WHOLE application, a 400 response that details the errors
        $exceptions->render(function (ValidationException $e) {
            $errors = [];
            // errors(): field -> list of its error messages
            foreach ($e->errors() as $field => $messages) {
                foreach ($messages as $message) {
                    $errors[] = ['field' => $field, 'message' => $message];
                }
            }
            return response()->json(['status' => 400, 'errors' => $errors], 400);
        });
    })
    ->create();
  • Lines 17–30: The [withExceptions] method is now passed a function, which receives the [$exceptions] object describing the application’s exception handling;
  • lines 20–29: The [render] method registers an exception handler: a function that [Laravel] calls when an action throws an exception of the same type as its parameter (here, [ValidationException] or a derived class), and whose return value is the response sent. [Laravel] identifies the type of the exception being handled based on the function’s parameter type. Declared in [bootstrap/app.php], the handler applies to all actions in the application: every action that validates a model benefits from it, without having to write a single line of error-handling code;
  • Lines 23–27: [$e->errors()] returns errors in an associative array: for each field with an error, the list of its error messages. The [foreach ($array as $key => $value)] loop iterates through both the keys and the values. [$errors[] = ...] adds an element to the end of the [$errors] array;
  • line 28: a response JSON, with status code 400.

3.2.4. Execution

A valid template:

curl -i "http://localhost:8000/persons/action01?name=martin&age=42&title=Mr"
HTTP/1.1 200 OK
Content-Type: application/json

{
    "received": {
        "name": "martin",
        "age": "42",
        "title": "Mr"
    },
    "person": {
        "name": "martin",
        "age": 42,
        "title": "Mr",
        "email": null
    },
    "ageType": "int"
}

The validated data contains only the received parameters; the age is in string format; the [Person] object contains the age as an integer (without quotes in JSON), and the missing address is [null]. A name surrounded by spaces ([%20]) and a present address:

curl "http://localhost:8000/persons/action01?name=%20%20martin%20&age=42&title=Mme&email=martin@example.com"
{
    "received": {
        "name": "martin",
        "age": "42",
        "title": "Mme",
        "email": "martin@example.com"
    },
    "person": {
        "name": "martin",
        "age": 42,
        "title": "Mme",
        "email": "martin@example.com"
    },
    "ageType": "int"
}

The spaces surrounding the name have disappeared without us asking for it: two middleware components, executed by [Laravel] for all requests, transform the received parameters before an action reads them. [TrimStrings] removes the spaces surrounding each string; [ConvertEmptyStringsToNull] replaces each empty string with [null]. There are now no parameters: the three required fields are missing.

curl -i http://localhost:8000/persons/action01
HTTP/1.1 400 Bad Request
Content-Type: application/json

{
    "status": 400,
    "errors": [
        {
            "field": "name",
            "message": "The name field is required."
        },
        {
            "field": "age",
            "message": "The age field is required."
        },
        {
            "field": "title",
            "message": "The title field is required."
        }
    ]
}

Four invalid fields at once:

curl "http://localhost:8000/persons/action01?name=m&age=abc&title=Mlle&email=xx"
{
    "status": 400,
    "errors": [
        {
            "field": "name",
            "message": "The name field must be at least 2 characters."
        },
        {
            "field": "age",
            "message": "The age field must be an integer."
        },
        {
            "field": "title",
            "message": "The selected title is invalid."
        },
        {
            "field": "email",
            "message": "The email field must be a valid email address."
        }
    ]
}

All errors are reported at once, allowing the user to correct everything in one go. For the age field [abc], only rule [integer] is flagged: rule [between] does not apply to a value that is not a number. The messages are in English, the default language of [Laravel]. The following example replaces them with custom messages.

3.3. Example 09: Validation Performed by the Action

In Example 08, an invalid model terminates the action, and the application handler responds. This is convenient for a API, whose clients are programs that expect a standardized error response. However, a web page often prefers to redisplay the form with the error messages: in this case, the action must decide how to handle an invalid model. The classes [PersonRequest] and [Person] are the ones from Example 08.

09-action-validation/
├── app/
│   ├── Http/
│   │   ├── Controllers/
│   │   │   └── PersonsController.php
│   │   └── Requests/
│   │       └── PersonRequest.php
│   └── Models/
│       └── Person.php
├── lang/
│   └── en/
│       └── validation.php
└── ...

3.3.1. Custom messages: lang/en/validation.php

The error messages for [Laravel] are not hard-coded: each message is identified by a key, and a language file associates text with each key. The English files are included with the framework; the application can provide its own files in the [lang] folder, with one subfolder per language, to replace these messages or to add other languages. The application’s language is set by the [.env] file:

APP_LOCALE=en

The validation message file is named [validation.php]:

<?php
// lang/en/validation.php: the messages of the validation errors, in English.
// Each message is designated by the name of its RULE (required, min...). The markers
// :attribute, :min, :max... are replaced by the name of the field and by the
// parameters of the rule
return [
    'required' => 'the field is required',
    'string' => 'a string is expected',
    'integer' => 'an integer is expected',
    'email' => 'invalid email address',
    'in' => 'unknown value',
    // some rules have one message per type of value: for a string, min
    // applies to its length; for a number, to its value; for an array, to
    // its number of items
    'min' => [
        'string' => ':min character(s) at least',
        'numeric' => 'the value must be greater than or equal to :min',
        'array' => ':min item(s) at least',
    ],
    'max' => [
        'string' => ':max character(s) at most',
        'numeric' => 'the value must be less than or equal to :max',
    ],
    'between' => [
        'numeric' => 'the value must be between :min and :max',
    ],
    'array' => 'a list is expected',
];
  • Line 6: A language file is a [PHP] file that returns an associative array: for each key, its corresponding text;
  • lines 7–11: The key for a validation message is the name of the rule that failed. This technique (translating based on the error name rather than its English text) is the one used in the case study: the texts there are identified by keys, which are translated into the user’s language;
  • lines 15–19: Rules that adapt to the value type (e.g., 08) have a message for each type: [string], [numeric], [array]. The identifiers [:min] and [:max] are replaced by the rule’s parameters ([min:2] means “at least 2 characters”). The tag [:attribute], which we do not use, would be replaced by the field name.

A key missing from this file retains the framework's English message: [Laravel] merges the application's file with its own. For another language, a missing key falls back to the fallback language, [APP_FALLBACK_LOCALE], which is English by default. Complete translations of the messages in [Laravel], in many languages, are published by the community (the [laravel-lang] package).

3.3.2. The action: app/Http/Controllers/PersonsController.php

<?php
// app/Http/Controllers/PersonsController.php: it is the action that decides what to do with an invalid model

namespace App\Http\Controllers;

use App\Http\Requests\PersonRequest;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;

class PersonsController
{
    public function action01(Request $request): string
    {
        // a VALIDATOR: the data to validate, and the rules (those of the
        // PersonRequest class of example 08)
        $validator = Validator::make($request->query(), (new PersonRequest())->rules());
        if ($validator->fails()) {
            // the errors, field by field: errors()->messages() gives their list
            $items = '';
            foreach ($validator->errors()->messages() as $field => $messages) {
                $items .= "<li>$field: $messages[0]</li>";
            }
            return "<p>The form contains errors:</p><ul>$items</ul>";
        }
        // the validated data
        $person = $validator->validated();
        // e(): the entered name is inserted into HTML; its characters < > & " '
        // are replaced by &lt; &gt;... (otherwise, an input could inject HTML)
        return "<p>Welcome {$person['title']} ".e($person['name'])
            ." ({$person['age']} years old)</p>";
    }
}
  • Line 16: The [Validator] facade creates a validator—an object that applies rules to data. The data consists of the parameters of URL; the rules are those of the [PersonRequest] class from Example 08. A form request is a class like any other: you can create an instance of it, via [new], to read its rules;
  • line 17: [fails()] performs validation and returns [true] if at least one rule fails. The action itself determines what happens next: no exception is thrown;
  • Lines 19–23: Errors, field by field. [errors()] returns a [MessageBag] object (a “bag” of messages), whose [messages()] method returns the associative array field → list of messages. For each field, the first message, [$messages[0]], is stored in a [<li>] tag. The [.=] operator appends one string to the end of another;
  • line 26: the validated data, in an associative array;
  • lines 29–30: if successful, a welcome message. Within a string enclosed in quotation marks, [{$person['title']}] inserts an array element (curly braces delimit the expression). The name is passed through the [e] function of [Laravel] (for *escape*), which replaces characters that have special meaning in HTML ([<], [>], [&], ["], [']) with their entities ([&lt;], [&gt;]...): they will be displayed as-is instead of being interpreted. Without this precaution, a user could enter a “name” containing HTML, or even [JavaScript], which would execute in the browser of anyone viewing the page. This is the most common security vulnerability on the web: script injection (*XSS*, *cross-site scripting*). The title and age do not require this protection: their values have been validated by the rules (one of three allowed values, an integer). In the next chapter, the [Blade] view engine will perform this escaping automatically for any value inserted into a page.

A valid entry, followed by an entry containing four errors:

Image

Image

The raw response to the second query, and the response for a name containing HTML:

curl "http://localhost:8000/persons/action01?name=m&age=abc&title=Mx&email=xx"
curl "http://localhost:8000/persons/action01?name=%3Cb%3Emartin%3C/b%3E&age=42&title=Mr"
<p>The form contains errors:</p><ul><li>name: 2 character(s) at least</li><li>age: an integer is expected</li><li>title: unknown value</li><li>email: invalid email address</li></ul>
<p>Welcome Mr &lt;b&gt;martin&lt;/b&gt; (42 years old)</p>

The name [<b>martin</b>] (encoded as [%3Cb%3E...] in URL) is displayed as-is, without being bolded: its tags have been neutralized. Note that the response this time is a 200 status code: this is the action that chose to return a normal page containing the errors.

3.4. Example 10: Multiple Parameters

The same parameter may appear multiple times in a URL: [?color=red&color=green]. This is what a browser sends for a group of checkboxes with the same name, or a multi-select list (see the next chapter). [PHP] handles this case in its own way. The [ArraysController] controller:

<?php
// app/Http/Controllers/ArraysController.php: a parameter that appears several times in the URL

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;

class ArraysController
{
    // /arrays/action01?color=red&color=green: for PHP, the second parameter
    // REPLACES the first one: only the LAST value is kept
    public function action01(Request $request): array
    {
        return ['color' => $request->query('color'), 'raw' => $request->server('QUERY_STRING')];
    }

    // /arrays/action02?color[]=red&color[]=green: the brackets ask
    // PHP to store ALL the values in an array
    public function action02(Request $request): array
    {
        // array: the parameter in the form of an array (empty if it is missing)
        $colors = $request->array('color');
        return ['color' => $colors, 'count' => count($colors)];
    }

    // /arrays/action03?color[]=red: an array, validated item by item
    public function action03(Request $request): array
    {
        $colors = $request->array('color');
        $validator = Validator::make(['color' => $colors], [
            // a list of at least one item
            'color' => 'array|min:1',
            // each item (*): "red", "green" or "blue"
            'color.*' => 'in:red,green,blue',
        ]);
        $errors = [];
        foreach ($validator->errors()->messages() as $field => $messages) {
            $errors[] = "$field: $messages[0]";
        }
        return ['colors' => $colors, 'valid' => ! $errors, 'errors' => $errors];
    }
}
  • line 15: for [PHP], the parameters of URL form an associative array, where each key has only one value: a repeated parameter replaces the previous one, and only the last value is retained. The action also returns, for comparison, the raw parameter string as sent by the browser: [$request->server(...)] reads the variables that the web server transmits to [PHP], including [QUERY_STRING];
  • lines 20–24: To retrieve all values, the parameter name must end with square brackets: [?color[]=red&color[]=green]. [PHP] then stores the values in an array, and the value of the parameter [color]. The [array] method of the query returns the parameter as an array: empty if it is absent, with a single element if it has only one value without square brackets;
  • Lines 31–36: The array is validated against rules. [array|min:1]: an array with at least one element (the rule [min] applies to the number of elements in an array; see Example 08). The key [color.*] identifies each element of the array: each must be one of the three colors;
  • line 41: the operator [!] (not) evaluates to [true] for an empty array, which evaluates to “false” in a condition.

The file [lang/en/validation.php] is the one from Example 09. The results:

1
2
3
curl "http://localhost:8000/arrays/action01?color=red&color=green"
curl -g "http://localhost:8000/arrays/action02?color[]=red&color[]=green"
curl http://localhost:8000/arrays/action02
{
    "color": "green",
    "raw": "color=red&color=green"
}
{
    "color": [
        "red",
        "green"
    ],
    "count": 2
}
{
    "color": [],
    "count": 0
}

The [-g] option in [curl] prevents it from interpreting the square brackets in URL on its own. A browser, on the other hand, encodes them ([color%5B%5D=red]), which [PHP] also understands. As for HTML, all you need to do is name the checkboxes in a group [color[]]: that’s what Example 18 will do. This rule applies specifically to [PHP]: frameworks for other languages ([Flask], [Spring MVC], [ASP.NET Core]) retain all values of a repeated parameter, without square brackets.

Validation: successful, then failed due to an unknown color, then due to an empty list:

1
2
3
curl -g "http://localhost:8000/arrays/action03?color[]=red&color[]=blue"
curl -g "http://localhost:8000/arrays/action03?color[]=red&color[]=yellow"
curl http://localhost:8000/arrays/action03
{
    "colors": [
        "red",
        "blue"
    ],
    "valid": true,
    "errors": []
}
{
    "colors": [
        "red",
        "yellow"
    ],
    "valid": false,
    "errors": [
        "color.1: unknown value"
    ]
}
{
    "colors": [],
    "valid": false,
    "errors": [
        "color: 1 item(s) at least"
    ]
}

The error regarding the unknown color refers to [color.1]: the field and index of the problematic element in the array (the second one, with indices starting at 0).

3.5. Example 11: Parameters of a POST query

The previous examples used GET queries, whose parameters are contained in the URL. A POST query carries its data in its body, in two main forms:

  • fields from a HTML form: the body has the same syntax as a query string ([name=martin&age=42]), and its type (header [Content-Type]) is [application/x-www-form-urlencoded];
  • a JSON document, sent by a program ([JavaScript] on a page, client of a API...): the type is [application/json].

The [PostController] controller:

<?php
// app/Http/Controllers/PostController.php: the parameters of a POST request

namespace App\Http\Controllers;

use App\Http\Requests\PersonRequest;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use JsonException;

class PostController
{
    // the fields of an HTML form (application/x-www-form-urlencoded body):
    // post() returns the parameters of the request BODY, in the form of an array
    public function action01(Request $request): array
    {
        return [
            'receivedType' => $request->header('Content-Type'),
            'fields' => $request->post(),
        ];
    }

    // a JSON body: json() deserializes it; isJson() tests the announced type
    public function action02(Request $request): array
    {
        if (! $request->isJson()) {
            abort(415, 'the body of the request must be JSON');
        }
        // json_decode, with JSON_THROW_ON_ERROR: a malformed JSON raises an exception
        try {
            $body = json_decode($request->getContent(), true, flags: JSON_THROW_ON_ERROR);
        } catch (JsonException $e) {
            abort(400, 'Malformed JSON: '.$e->getMessage());
        }
        return ['receivedType' => $request->header('Content-Type'), 'body' => $body,
            'name' => $request->input('name')];
    }

    // a single field of the form
    public function action03(Request $request): string
    {
        $name = $request->input('name');
        return "name = [$name]";
    }

    // the fields of the form, validated by the rules of the PersonRequest class
    public function action04(Request $request): array
    {
        $validator = Validator::make($request->post(), (new PersonRequest())->rules());
        $errors = [];
        foreach ($validator->errors()->messages() as $field => $messages) {
            $errors[] = "$field: $messages[0]";
        }
        sort($errors);
        return ['person' => $errors ? null : $validator->validated(),
            'valid' => ! $errors, 'errors' => $errors];
    }
}
  • line 18: the [Content-Type] header of the request;
  • line 19: the method [post] returns the parameters from the request body in an associative array; [post('name')] would return a single parameter. Form fields follow the rules in Example 10: a repeated field must have a name ending in [[]];
  • lines 26–27: [isJson()] checks the type declared by the [Content-Type] header. A body that is not declared as JSON is rejected with a 415 error (*Unsupported Media Type*);
  • lines 30–34: [getContent()] returns the raw body of the request, a string; the function [json_decode] from [PHP] deserializes it: the text JSON becomes a value [PHP] (the second parameter, [true], requires associative arrays for JSON objects). With the [JSON_THROW_ON_ERROR] option (a named parameter), malformed text raises a [JsonException] exception, which the [try] / [catch] intercepts to return a 400 error. The [json()] method of the request would do the same thing, but for a malformed JSON, it would simply return an empty result;
  • Line 36: The [input] method searches for a parameter everywhere: in the request body—whether it’s a form or JSON—and then in the URL. This is the method most commonly used, because it does not depend on the format of the request;
  • line 42: a form field, read by [input];
  • lines 49–56: the rules of the [PersonRequest] class from Example 08, applied this time to the form fields. The code is the same as for the parameters of URL: only the data source changes. [sort] sorts the messages to ensure a consistent response. The ternary operator [condition ? a : b] returns [a] if the condition is true, and [b] otherwise.

The routing table declares routes POST, and [bootstrap/app.php] disables CSRF protection for these URL, as shown in Example 02:

<?php
// routes/web.php: the routes of example 11

use App\Http\Controllers\PostController;
use Illuminate\Support\Facades\Route;

Route::prefix('post')->controller(PostController::class)->group(function () {
    Route::post('action01', 'action01');
    Route::post('action02', 'action02');
    Route::post('action03', 'action03');
    Route::post('action04', 'action04');
});
<?php
// bootstrap/app.php: the application of example 11

use App\Http\Middleware\ReadableJson;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Middleware;

(require __DIR__.'/../../vendor/autoload.php')->addPsr4('App\\', __DIR__.'/../app/');

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(web: __DIR__.'/../routes/web.php')
    ->withMiddleware(function (Middleware $middleware): void {
        $middleware->append(ReadableJson::class);
        // the POST requests sent by curl would not pass the anti-CSRF
        // protection (example 21)
        $middleware->preventRequestForgery(except: ['post/*']);
    })
    ->withExceptions()
    ->create();

With [curl], the [-d] (*data*) option sends a body; the request then becomes a POST request, of type [application/x-www-form-urlencoded], unless the option [-H] (*header*) specifies a different type. The option [-X POST] explicitly sets the method (it is optional here). The fields of a form, including a repeated field:

curl -i -g -X POST -d "name=martin&age=42&hobby[]=tennis&hobby[]=piano" http://localhost:8000/post/action01
HTTP/1.1 200 OK
Content-Type: application/json

{
    "receivedType": "application/x-www-form-urlencoded",
    "fields": {
        "name": "martin",
        "age": "42",
        "hobby": [
            "tennis",
            "piano"
        ]
    }
}

A JSON body, which contains an integer and an array:

curl -i -H "Content-Type: application/json" -d "{\"name\": \"martin\", \"age\": 42, \"children\": [\"léa\", \"tom\"]}" http://localhost:8000/post/action02
HTTP/1.1 200 OK
Content-Type: application/json

{
    "receivedType": "application/json",
    "body": {
        "name": "martin",
        "age": 42,
        "children": [
            "léa",
            "tom"
        ]
    },
    "name": "martin"
}

The quotation marks in JSON are preceded by [\] to prevent the command string from ending; In the case of [Linux] and [macOS], you can also enclose JSON in apostrophes: [-d '{"name": "martin"}']. Unlike form fields, the JSON carries data types: age is already an integer. A body that is not JSON, followed by a malformed JSON (requesting a JSON response to view the message):

curl -i -H "Accept: application/json" -d "name=martin" http://localhost:8000/post/action02
curl -i -H "Accept: application/json" -H "Content-Type: application/json" -d "{\"name\": " http://localhost:8000/post/action02
HTTP/1.1 415 Unsupported Media Type
Content-Type: application/json

{
    "message": "the body of the request must be JSON",
    ...
}
HTTP/1.1 400 Bad Request
Content-Type: application/json

{
    "message": "Malformed JSON: Syntax error",
    ...
}

A single field, then the same URL requested as GET:

curl -d "name=jean%20dupont" http://localhost:8000/post/action03
curl -i "http://localhost:8000/post/action03?name=martin"
1
2
3
4
5
name = [jean dupont]
HTTP/1.1 405 Method Not Allowed
allow: POST
Content-Type: text/html; charset=utf-8
...

URL exists, but not for the GET method: [Laravel] returns a 405 (*Method Not Allowed*), and its header [allow] lists the accepted methods. Finally, the rules applied to the form fields, which were first satisfied and then not:

curl -d "name=martin&age=42&title=Mr" http://localhost:8000/post/action04
curl -d "name=m&age=200" http://localhost:8000/post/action04
{
    "person": {
        "name": "martin",
        "age": "42",
        "title": "Mr"
    },
    "valid": true,
    "errors": []
}
{
    "person": null,
    "valid": false,
    "errors": [
        "age: the value must be between 0 and 150",
        "name: 2 character(s) at least",
        "title: the field is required"
    ]
}

3.6. Example 12: Custom Binding and Validation

Standard types and rules are not always sufficient. This example shows three ways to extend binding and validation:

  • a type bound to a route parameter: a date in French format, [23-09-2026], in the path of URL;
  • middleware that provides the action with a parameter that does not come from the URL, but from a HTTP header;
  • a custom validation rule (no spaces), and the conversion of a date [dd/mm/yyyy].
12-custom-binding/
├── app/
│   ├── Http/
│   │   ├── Controllers/
│   │   │   └── ExamplesController.php
│   │   └── Requests/
│   │       └── SearchRequest.php
│   └── Binding/
│       ├── DetectLanguage.php
│       ├── DayFr.php
│       └── NoSpace.php
├── lang/
│   └── en/
│       └── validation.php
└── ...

3.6.1. A type linked to a route parameter: app/Binding/DayFr.php

We saw in Example 02 that a route parameter is passed to the action as a string, or as an integer if the action’s parameter is of type [int]. [Laravel] can also convert a route parameter into an object if the action requires a parameter whose class implements the [UrlRoutable] interface. This is the route binding (*route model binding*), which [Laravel] primarily uses for database entities (an identifier in URL becomes the corresponding object; see Chapter 9). We use it here for a date.

<?php
// app/Binding/DayFr.php: a French date "23-09-2026" in the path of the URL

namespace App\Binding;

use Carbon\CarbonImmutable;
use Illuminate\Contracts\Routing\UrlRoutable;

// a type that knows how to BIND itself to a route parameter (resolveRouteBinding) and
// become a piece of URL again (getRouteKey): the UrlRoutable interface
class DayFr implements UrlRoutable
{
    public function __construct(public ?CarbonImmutable $date = null)
    {
    }

    // text of the path -> DayFr object; null: the value is not suitable (404 error)
    public function resolveRouteBinding($value, $field = null): ?static
    {
        // the ! of "!d-m-Y" resets the time to zero (otherwise, it would be the current time)
        $date = CarbonImmutable::createFromFormat('!d-m-Y', $value);
        // createFromFormat accepts "31-02-2026" (and makes it March 3rd): we check
        // that the date, formatted again, gives back the received text
        if ($date === null || $date->format('d-m-Y') !== $value) {
            return null;
        }
        return new static($date);
    }

    // DayFr object -> text of the path (used by the route function)
    public function getRouteKey(): string
    {
        return $this->date->format('d-m-Y');
    }

    public function getRouteKeyName(): string
    {
        return 'day';
    }

    public function resolveChildRouteBinding($childType, $value, $field): ?static
    {
        return null;
    }
}
  • line 11: the class implements the [UrlRoutable] interface, whose four methods are as follows;
  • line 13: the date, a [CarbonImmutable] object. [Carbon] is the date library for [Laravel]; It extends the [DateTime] class from [PHP] with hundreds of methods. The [Immutable] version creates a new object with each modification ([$date->addDay()] returns a new date), rather than modifying the existing object. The constructor has a default value, [null]: [Laravel] must be able to create an empty [DayFr] object, whose [resolveRouteBinding] method it will then call;
  • Lines 18–27: [resolveRouteBinding] converts the path text into an object: it returns the linked object, or [null] if the value is invalid. [CarbonImmutable::createFromFormat] parses a string according to a format: [d] for the day, [m] for the month, and [Y] for the four-digit year; the initial [!] sets the hour to zero. [createFromFormat] is tolerant: it accepts [31-02-2026] and interprets it as March 3. The validation on line 24 rejects this case: the date, after reformatting, must exactly match the received text. [new static(...)] creates an object of the current class. When the method returns [null], [Laravel] responds with a 404 error: URL does not refer to anything;
  • lines 31–34: the inverse operation, used by the [route] function to construct a URL from a [DayFr] object. [format] formats a date;
  • lines 36–41: The last two methods of the interface, which are used for database entities, have no role here.

3.6.2. A parameter provided by middleware: app/Binding/DetectLanguage.php

An action [Laravel] receives as parameters the route parameters and the objects that the container can provide (the request, the services). To provide it with additional information, calculated from the request, we can have it calculated by a middleware (example 03), which includes it in the request before the action:

<?php
// app/Binding/DetectLanguage.php: a middleware that COMPUTES a piece of information for the action

namespace App\Binding;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class DetectLanguage
{
    public function handle(Request $request, Closure $next): Response
    {
        // the header "Accept-Language: fr-FR,fr;q=0.9,en;q=0.8" -> "fr": the browser's
        // preferred language among those offered (the first one by default)
        $language = $request->getPreferredLanguage(['fr', 'en']);
        // the ATTRIBUTES of the request: information that the application stores there
        // for the functions that will handle the request after it
        $request->attributes->set('language', $language);
        return $next($request);
    }
}
  • Line 16: The request header [Accept-Language] specifies the user’s languages, in order of preference: [fr-FR,fr;q=0.9,en;q=0.8] (the coefficient [q] measures the preference, on a scale from 0 to 1). [getPreferredLanguage] selects, from among the languages offered by the application, the one the user prefers; if no header is provided, it is the first one on the list;
  • line 19: the request has attributes—an array of information that the application stores there for the functions that will process the request afterward. The language is stored there under the name [language];
  • Line 20: The request continues on to the action.

The action will receive this information via a [PHP] attribute placed before its parameter (see the controller).

3.6.3. A "custom" rule and a conversion: NoSpace.php and SearchRequest.php

<?php
// app/Binding/NoSpace.php: a "home-made" validation rule

namespace App\Binding;

use Closure;
use Illuminate\Contracts\Validation\ValidationRule;

class NoSpace implements ValidationRule
{
    // the value is not suitable: we call $fail with the error message
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        if (str_contains((string) $value, ' ')) {
            $fail('the field must not contain a space');
        }
    }
}
  • Line 9: A "custom" validation rule is a class that implements the [ValidationRule] interface;
  • Lines 12–17: Its method [validate] takes the field name, its value, and a function, [$fail], which it calls with the error message if the value is invalid. [str_contains] checks for the presence of text in a string; [(string)] converts the value to a string. The type [mixed] refers to any type.

The rule is used by the search form query:

<?php
// app/Http/Requests/SearchRequest.php: the model of the search

namespace App\Http\Requests;

use App\Binding\NoSpace;
use Carbon\CarbonImmutable;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Exceptions\HttpResponseException;

class SearchRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            // a "home-made" rule: an object, in a list of rules
            'keyword' => ['nullable', new NoSpace()],
            // date_format: a date in the dd/mm/yyyy format
            'since' => 'nullable|date_format:d/m/Y',
        ];
    }

    // the date, CONVERTED into a date object (null if it is missing)
    public function since(): ?CarbonImmutable
    {
        return $this->date('since', 'd/m/Y')?->toImmutable();
    }

    // the response to a failed validation, for THIS class only: the message
    // of the first error, as plain text, with the 400 code
    protected function failedValidation(Validator $validator): never
    {
        throw new HttpResponseException(response($validator->errors()->first(), 400,
            ['Content-Type' => 'text/plain; charset=utf-8']));
    }
}
  • line 18: A field’s rules can be specified in an array rather than as a string; this is essential for a “custom” rule, which is an object;
  • line 20: the rule [date_format:d/m/Y] verifies that the value is a date in the specified format and that this date exists;
  • Lines 25–28: Date conversion: The [date] method of a query reads a parameter and converts it to a date ([Carbon]) according to the specified format; it returns [null] if the parameter is missing. The operator [?->] calls a method if the object is not [null], and returns [null] otherwise; [toImmutable] converts the date to [CarbonImmutable];
  • lines 32–36: the method [failedValidation], inherited from [FormRequest], is called when validation fails; the method in [FormRequest] throws the exception [ValidationException] (example 08). We redefine it, for this class only: it throws a [HttpResponseException], an exception that carries a predefined response, which [Laravel] sends as-is. The response contains the first error message ([first()]), in plain text, with status code 400. This is a way to handle validation errors for an action differently from those in the rest of the application.

The message for rule [date_format] is customized by the file [lang/en/validation.php]:

1
2
3
4
5
6
<?php
// lang/en/validation.php: the messages of the validation errors, in English
return [
    // :input: the received value
    'date_format' => '[:input] is not a valid dd/mm/yyyy date',
];
  • Line 5: The tag [:input] is replaced by the received value.

3.6.4. The controller: app/Http/Controllers/ExamplesController.php

<?php
// app/Http/Controllers/ExamplesController.php: custom binding and validation

namespace App\Http\Controllers;

use App\Http\Requests\SearchRequest;
use App\Binding\DayFr;
use Illuminate\Container\Attributes\RequestAttribute;

class ExamplesController
{
    // /examples/schedule/23-09-2026: the {day} route parameter is BOUND to a DayFr
    // object, because the action asks for a parameter of this type
    public function schedule(DayFr $day): string
    {
        // route() uses the getRouteKey method of DayFr: object -> "23-09-2026"
        return "schedule for the day [{$day->date->toDateString()}], of type "
            .get_debug_type($day->date).', URL: '.route('schedule', $day, false);
    }

    // /examples/hello: a parameter that does not come from the URL, but from a header,
    // computed by the DetectLanguage middleware and stored in the attributes of the request
    public function hello(#[RequestAttribute('language')] string $language): string
    {
        return $language === 'en' ? 'Hello!' : 'Bonjour !';
    }

    // /examples/search?keyword=laravel&since=01/09/2026
    public function search(SearchRequest $request): string
    {
        $keyword = $request->validated('keyword');
        $since = $request->since()?->toDateString();
        return "search for [$keyword] since [$since]";
    }
}
  • Lines 14–18: The parameter [$day] is of type [DayFr]: [Laravel] binds the route parameter [{day}] to an object [DayFr] using its method [resolveRouteBinding]. [toDateString()] writes the date in international format ([2026-09-23]). The function [route] constructs the URL for this same action from the object, using its method [getRouteKey]; its third parameter, [false], requests a relative URL (without the protocol or the machine);
  • Lines 23–25: The parameter [$language] is preceded by an attribute [PHP], written as [#[...]]: information attached to the code, which [PHP] ignores but which the libraries read (the equivalent of the annotations in [Java]). The [RequestAttribute('language')] attribute instructs the container to provide, for this parameter, the [language] attribute of the request, calculated by the [DetectLanguage] middleware;
  • Lines 29–33: The action receives the form request, which has already been validated (Example 08). [validated('keyword')] returns a single validated field; [since()] returns the converted date.

The routing table:

<?php
// routes/web.php: the routes of example 12

use App\Http\Controllers\ExamplesController;
use App\Binding\DetectLanguage;
use Illuminate\Support\Facades\Route;

Route::prefix('examples')->controller(ExamplesController::class)->group(function () {
    // where: only a piece of path of the form dd-mm-yyyy is offered to DayFr
    Route::get('schedule/{day}', 'schedule')->name('schedule')
        ->where('day', '[0-9]{2}-[0-9]{2}-[0-9]{4}');
    // middleware: DetectLanguage runs before the action
    Route::get('hello', 'hello')->middleware(DetectLanguage::class);
    Route::get('search', 'search');
});
  • lines 10–11: the calendar route is named (for function [route]), and its parameter is constrained by a regular expression: [[0-9]] denotes a single digit, [{2}] denotes the repetition of the preceding pattern: two digits, a hyphen, two digits, a hyphen, four digits. A path segment with a different format does not match the route (404), without even consulting [DayFr];
  • line 13: the method [middleware] associates a middleware with a route: it runs only for that route, before the action. We’ll see this process throughout the course: middlewares that require a logged-in user, in the chapter on authentication, are associated with routes in the same way.

3.6.5. Execution

The type associated with the route parameter:

curl "http://localhost:8000/examples/schedule/23-09-2026"
curl -i "http://localhost:8000/examples/schedule/31-02-2026"
1
2
3
4
schedule for the day [2026-09-23], of type Carbon\CarbonImmutable, URL: /examples/schedule/23-09-2026
HTTP/1.1 404 Not Found
Content-Type: text/html; charset=utf-8
...

The date is displayed in international format, but the URL constructed by [route] has reverted to the French format. February 31 does not exist: 404. A date in international format, [2026-09-23], does not match the pattern described by the regular expression: 404 as well.

The middleware. The [-H] option of [curl] sets the header [Accept-Language]:

1
2
3
curl -H "Accept-Language: en-US,en;q=0.9,fr;q=0.8" http://localhost:8000/examples/hello
curl -H "Accept-Language: fr-FR,fr;q=0.9" http://localhost:8000/examples/hello
curl http://localhost:8000/examples/hello
1
2
3
Hello!
Bonjour !
Bonjour !

Without the [Accept-Language] header ([curl] does not send one), it is the first language in the list. The chapter on internationalization will revisit this issue, with the user selecting the language.

The “house” rule and date conversion:

1
2
3
4
5
curl "http://localhost:8000/examples/search?keyword=laravel&since=01/09/2026"
curl "http://localhost:8000/examples/search"
curl -i "http://localhost:8000/examples/search?keyword=laravel%20mvc"
curl "http://localhost:8000/examples/search?since=2026-09-01"
curl "http://localhost:8000/examples/search?since=31/02/2026"
1
2
3
4
5
6
7
8
search for [laravel] since [2026-09-01]
search for [] since []
HTTP/1.1 400 Bad Request
Content-Type: text/plain; charset=utf-8

the field must not contain a space
[2026-09-01] is not a valid dd/mm/yyyy date
[31/02/2026] is not a valid dd/mm/yyyy date

3.6.6. Conclusion

Let’s summarize the methods available to an action for obtaining its template:

Route parameters
the action’s parameters, which share the same name; converted by their type ([int $id]), constrained by [whereNumber], [where]...; bound to an object by [UrlRoutable]
Parameters of URL
[$request->query(...)], [integer], [array], [date]...; a repeated parameter is written as [name[]]
Fields in a POST form
[$request->post(...)], or [$request->input(...)], which searches everywhere
Body JSON
[$request->input(...)], [$request->json()], or [json_decode($request->getContent())]
Headers
[$request->header(...)], and methods that parse them, such as [getPreferredLanguage]
Validation
of rules: a [FormRequest] (validation before the action) or [Validator::make] (validation by the action); "custom" rules ([ValidationRule])
Messages
the files [lang/xx/validation.php] and the language file [APP_LOCALE]
Common errors
[$exceptions->render(...)] in [bootstrap/app.php] (entire application), [failedValidation] (a form request)
Calculated parameter
A middleware component that stores information in the request attributes, [#[RequestAttribute]]

The HTML forms, on the other hand, require more than just validation: they must be displayed, re-displayed with the user’s input and any errors, and protected against forged requests. This is the subject of the next chapter, once views have been introduced.