4. The View and Its Model
Up to this point, actions have generated their own responses: text, JSON, and a few HTML tags written as a string of characters. In a MVC application, this is the role of a view: a file that describes the page HTML, with placeholders for the data passed to it by the action—the view’s model.
[Laravel] comes with its own view engine, [Blade]. A [Blade] view is a file whose name ends with [.blade.php], stored in the application’s [resources/views] folder. It is a text file—in this case, HTML—in which three types of tags are interpreted on the server:
- [{{ expression }}]: the value of a [PHP] expression, inserted into the page;
- [@instruction]: a directive: condition ([@if]), loop ([@foreach]), template inheritance ([@extends], [@section]), inclusion ([@include])... A directive begins with [@], followed by its name;
- [{{-- commentaire --}}]: a comment, which does not appear on the generated page.
Everything else is copied as-is. Expressions are written in [PHP]: there is no second language to learn. [Blade] thus resembles [Razor], the view engine of [ASP.NET Core MVC], which also mixes HTML and code, and which also uses the character [@]; it differs from [Thymeleaf] ([Spring MVC]), whose views are valid HTML files enriched with [th:] attributes. A [Blade] view is not executed as-is: upon its first use, [Blade] compiles it into a standard [PHP] file, stored in [storage/framework/views], which it then reuses as long as the view remains unchanged. We’ll see what this file looks like.
4.1. Example 13: A First View
4.1.1. The view template: app/Models/PageModel.php
The view model is a simple immutable class (see Example 03): the three pieces of information that the view will display. There’s no requirement to define a view’s model as a class: you could pass three separate variables to the view. However, a class documents what the view expects and allows the editor to verify the action’s code. This is the approach the case study will follow, where each view has its own model class.
4.1.2. The controller: app/Http/Controllers/ViewsController.php
- Lines 17–20: The action builds the view model. The function
***[date]from*[PHP]formats the current time: [H] for the hours, [i] for the minutes, and [s]** for the seconds; - Line 21: The function [view] causes [Blade] to generate the page. The first parameter is the view name: the file path, relative to the [resources/views] folder, where the [/]s are replaced by dots, and without the [.blade.php] extension: [views.index] refers to the file [resources/views/views/index.blade.php]. The second parameter is an associative array, where each key becomes a variable in the view—in this case, a single one: [$model]. [view] returns an object [View], which [Laravel] transforms into a response (of type [text/html]) by generating the page;
- Line 29: another view, with a different variable ([$name]). [response()->view(...)] generates a response from a view, using the specified code—in this case, 404—to display a custom error page. The action type [View|Response] means: either a [View] or a [Response];
- lines 31–35: the same view as the [index] action, but with a different template. This is the benefit of separating actions from views: a single view can be used by multiple actions.
4.1.3. The view: resources/views/views/index.blade.php
- line 5: a comment [Blade];
- line 6: [{{ $model->title }}] inserts the value of the [title] property of the [$model] variable. Between the curly braces, you can write any [PHP] expression.
Everything inserted by [{{ }}] is escaped: the characters [<], [>], [&], ["], and ['] are replaced by their entities HTML. This is the role that the [e] function played in Example 09, but this time it is automatic: a view therefore cannot inadvertently insert the HTML provided by a user into the page.
4.1.4. The view: resources/views/views/unknown.blade.php
A view is not required to receive an object: the variable [$name] is a string.
4.1.5. Execution


The displayed time is in Coordinated Universal Time (UTC), with the default time zone set to [Laravel]; the case study will show how to change this. The raw output shows the processing done by [Blade]: the [{{ }}] tags have been replaced with their values, and the comment has been removed (a blank line remains in its place).
The “home” error page, followed by a name containing HTML ([<b>martin], encoded in URL):
The [<b>] tag was escaped, even though neither the action nor the view had anything to do with it. Here, finally, is the view compiled by [Blade], as found in the [storage/framework/views] folder (under a calculated name, such as [bfadc851307fb914e48325630a6c9191.php]):
Each [{{ expression }}] has become [<?php echo e(expression); ?>]: the conversion is performed by the [e] function from Example 09. A [Blade] view is therefore just a more elegant way of writing a [PHP] file that generates HTML. [PHP] was originally designed as a page language, where the code is inserted into the HTML file between [<?php] and [?>]; [Blade] retains its efficiency and corrects its flaws (notably the missing escape sequence).
4.2. Example 14: The Blade Language
This example brings together the main constructs of [Blade] in a single view.
4.2.1. The Model and the Action
The view’s model is described by three immutable classes, each in its own file ([PHP] does not require one class per file, but the class loader in [Composer] does look for the class [App\Models\Product] in the file [app/Models/Product.php]):
The model contains values of various types: strings, [null], Booleans, arrays of objects, empty arrays, and objects containing other objects. The prices here are of type [float], to simplify calculations in the view (Example 06 showed why an exact decimal is preferred).
The action constructs the model using named parameters (see Example 01): each value is preceded by the name of the parameter it is intended for, which makes the call readable despite the number of parameters.
4.2.2. The view: resources/views/basics/index.blade.php
- Lines 9–12: The directives [@php] and [@endphp] delimit a block of code [PHP], which is executed at this point in the page. Two view variables are created there: the total price, calculated by the [sum] method of a collection (example 03), which sums a property of each element, and the number of products. The [@php] block should be reserved for this type of preparation: the calculation normally belongs to the action;
- line 17: a [null] value is displayed as an empty string. The [??] operator in [PHP] (Example 08) replaces a [null] value with a default value;
- Lines 19–20: [{{ }}] escapes HTML (2a); [{!! !!}], on the other hand, inserts the value as-is, without escaping it (2b). [{!! !!}] is reserved for HTML generated by the application itself, never for user input;
- lines 22–26: a condition: [@if], [@elseif], [@else], [@endif], with a [PHP] condition in parentheses. Each block is closed by a [@end...] directive;
- lines 30–36: a loop [@foreach] ... [@endforeach], which is written as the loop [foreach] of [PHP]. Within it, the special variable [$loop] describes the state of the loop: [$loop->index] (the iteration number, starting at 0), [$loop->iteration] (starting at 1), [$loop->first], [$loop->last] (true on the first and last iterations), [$loop->count] (the number of elements). Variables outside the loop ([$model->name]) remain accessible;
- Line 32: the ternary operator for [PHP], [condition ? a : b] (Example 11);
- line 40: the directive [@forelse] ... [@empty] ... [@endforelse] is a loop in which the block [@empty] is executed if the list is empty. This is simpler than a [@if] that would test the length of the list;
- lines 43–44: calculated expressions ([$total / $count]) and the variables from the [@php] block. The function [number_format] from [PHP] formats a number: two decimal places, a period as the decimal separator, and a comma as the thousands separator. In Example 16, we’ll see a much better way to format numbers and amounts;
- Line 46: The [->] operator iterates through the objects ([$model->address->city]), and any [PHP] function can be called: [mb_strtoupper] capitalizes a string (the [mb_...] functions, for *multi-byte*, correctly handle accented characters encoded in UTF-8);
- line 48: the character [@] is interpreted by [Blade] only at the beginning of a word; in an email address, it is copied as-is. To write a [@] followed by a directive name, it is doubled: [@@if] produces [@if]. Similarly, [@{{ ... }}] produces [{{ ... }}], which is useful when a page contains curly braces intended for a framework [JavaScript].
A note on comments: [Blade] processes [@php] blocks before comments. A comment containing the word [@php] would therefore be interpreted as the start of a code block, with unexpected results: this is why the comment on line 8 does not include it.
4.2.3. Execution

The generated page:
The comments and directives have disappeared, but they leave behind their spaces and line breaks: [Blade] does not attempt to produce a neatly formatted HTML. This has no consequence: the browser ignores these whitespace characters. A number like [float] inserted into a string is written with a decimal point ([12.5]); [number_format] uses the separators passed to it.
4.3. Example 15: Templates, Fragments, Static Files
The pages of a website usually share the same structure: a header, a navigation bar, a footer, and the same style sheets. Rather than repeating this structure in every view, it is written once in a template (*layout*), which the pages extend. This example also uses [Bootstrap], the most widely used CSS framework, for the layout.
4.3.1. Static Files
A HTML page references other files: style sheets, [JavaScript] scripts, and images. These files, which the server sends as-is, are called static files. With [Laravel], they are stored in the [public] folder, which the web server serves as-is (example 01): The file [public/css/styles.css] is available at URL [/css/styles.css], without [Laravel] being involved.
A few additional CSS rules in addition to [Bootstrap]: a light gray background, the footer style, and the style for an error area that will be used in the following examples.
[Bootstrap] is used in examples 15 through 28. Rather than placing a copy in each user’s [public] folder, the examples share it: it is located in the [examples/common/bootstrap] folder. This folder is not served by the web server: a route, common to all these examples, serves its files. It is described by the file [examples/common/bootstrap.php]:
- line 7: a route whose action is an anonymous function (example 01). The route parameter [{file}] accepts any path ending, including slashes (constraint [.*], line 19, see example 02): the URL [/bootstrap/css/bootstrap.min.css] refers to the file [css/bootstrap.min.css] in the folder. The path is named [bootstrap];
- lines 8–14: Serving files designated by URL requires a precaution: a URL such as [/bootstrap/../../15-layout/.env] must not grant access to a file located outside the folder. The [realpath] function of [PHP] calculates the absolute path of a file, stripped of its [.] and [..] components, or returns [false] if the file does not exist. We verify that the resulting path does indeed start with the folder path ([str_starts_with]) and that it refers to a file ([is_file]); otherwise, [abort_unless] terminates the request with a 404 error. [abort_unless(condition, code)] is equivalent to [if (! condition) abort(code)]; similarly, [abort_if] also exists. [DIRECTORY_SEPARATOR] is the system path separator: [\] under [Windows], [/] elsewhere;
- lines 16–18: [response()->file] generates a response that sends the contents of a file. The content type is chosen based on the file extension (provided by [pathinfo]), so that a stylesheet is correctly declared as [text/css]: a browser will reject a stylesheet declared as something else.
The route is declared by the [bootstrap/app.php] file in each example:
- lines 11–14: the [then] parameter of [withRouting] is a function, called after the routes in [routes/web.php] have been declared, to declare additional routes. These are not part of the [web] group: they have neither a session nor cookies, which is suitable for static files. The arrowed function executes the file [common/bootstrap.php];
- lines 18–24: the [booted] method registers a function that is executed once the application starts;
- lines 21–23: the [View] facade registers a view composer: a function that [Laravel] executes before rendering each view whose name matches the given pattern, in this case [*] (all of them). It receives the view and can add variables to it ([with]). Here, [$path] (the path to the requested URL, provided by [request()], the current request) and [$year] (the current year) are thus available in all views, without the actions having to provide them. The template uses them for its footer. The method [View::share('name', value)] would similarly share a fixed value with all views.
4.3.2. The template: resources/views/layout.blade.php
- line 6: the [viewport] tag instructs mobile browsers to display the page at the screen’s width, which is what [Bootstrap] expects;
- Line 8: The directive [@yield] marks a named slot—which pages that extend the template can fill—with a section of the same name. The second parameter is the default content for the slot, used by a page that does not provide the section;
- Lines 10–11: [route('bootstrap', 'css/bootstrap.min.css')] generates the URL for the named route [bootstrap] (Example 04), using the value of its parameter [{file}]. The function [asset] creates URL from a file in the [public] folder. Writing the URL file manually ([/css/styles.css]) would also work, but [asset] works if the application is installed under a URL prefix;
- line 16: [@include] inserts the content of another view, in this case the navigation bar. The included view has access to the variables of the view that includes it;
- line 19: the area that each page fills with its own content;
- line 22: the footer uses the two variables from the view composer.
The class [container] of [Bootstrap] centers the content and limits its width; [py-3] adds vertical padding: [Bootstrap] provides hundreds of classes like this, which are combined in the [class] attribute.
4.3.3. The navigation bar: resources/views/fragments/navbar.blade.php
A fragment: a piece of a page that has no meaning on its own. The classes [navbar], [navbar-brand], and [nav-link] are those of the navigation bar in [Bootstrap].
4.3.4. A component: resources/views/components/card.blade.php
- Line 2: A component is a section of a page that has parameters; this is the function of [Blade]. An anonymous component is a simple view from the [resources/views/components] folder; the [@props] directive declares its parameters, which become variables of the view. It is used as a HTML tag whose name begins with [x-]: [<x-card name="..." role="..." />] inserts the page fragment generated with these values. Parameters can have a default value: [@props(['name', 'role' => ''])]. The map ([card]) is a component of [Bootstrap].
4.3.5. The pages
- line 2: the page extends the template: [Blade] generates the template page, replacing its [@yield] placeholders with the sections defined by the page. In a view that extends a template, text written outside the sections does not appear in the generated page;
- Lines 4–10: The section [content], which fills the placeholder of the same name. The page does not define the section [title]: the placeholder retains its default content, “Home”;
- line 9: the use of the [card] component. The parameters are written as HTML attributes; their values are strings.
- line 3: a short section, whose content is provided as the second parameter;
- lines 8–13: one card per team member, in a grid of [Bootstrap] ([row], [col-md-4]: three columns on a medium or large screen, one column on a narrow screen);
- line 11: a parameter whose name is preceded by [:] is assigned the value of an expression [PHP], not a string: [:name="$member->name"].
The controller does not introduce any new features:
Finally, the third page does not use the template:
4.3.6. Execution



The team page, as received by the browser (the blank lines left by comments and directives have been removed):
The browser received a complete page: the template, fragment, page, and components were assembled on the server. The URL values in the style sheets were calculated by [route] and [asset]: these are absolute URL values. The path to the home page is [/]; the paths to other pages are written without a leading slash ([team]): this is the convention for [request()->path()]. You can verify that the style sheets are being applied correctly (the [-I] option of [curl] displays only the response headers):
The first file was sent via our route; the second was sent directly by the development server, without going through [Laravel].
4.4. Example 16: Helpers
A view often needs to format values: an amount with its currency, a date written out in full, or text in uppercase. This code doesn’t belong in the action (which doesn’t handle presentation), and would be cumbersome to write in the view. Instead, it is written in [PHP], in functions that the views call: these are the view’s helpers. [Blade] offers several ways to make them available: calling a function or a static method, writing your own directives ([@money($price)]), and using components, which we’ve already encountered.
4.4.1. Functions: app/Helpers/Formats.php
- Lines 14–17: a static method ([static]): it is called on the class, without an object ([Formats::uppercase('keyboard')]). It accepts [null];
- lines 22–24: formatting an amount. This depends on the language: in French, [1 149,90 €] (space to separate thousands, decimal comma, symbol after the amount); in English, [€1,149.90]. The class [Number] of [Laravel] is based on the extension [intl] of [PHP], which supports the conventions of all countries (it uses data from the Unicode project *CLDR*): [Number::currency] formats an amount, with the currency designated by its international code ([EUR], [USD]). [intl] can only format numbers [float]: the decimal is converted ([toFloat()]), which is safe here since the number is no longer used for calculations. The language ([fr], [en]) is that of the application for the current query, returned by [App::getLocale()]; we’ll see who sets it;
- Lines 28–30: Formatting a date [Carbon] (Example 12). [locale(...)] returns a copy of the date set for a specific language; [isoFormat] formats it as follows: [dddd] spells out the day of the week, [D] the day of the month, [MMMM] spells out the month, and [YYYY] the year. [Carbon] comes with translations of these names in over two hundred languages. The [CarbonInterface] type accepts all [Carbon] dates.
4.4.2. The language of each request: app/Http/Middleware/SetLanguage.php
The formatting language depends on the request: the [?lang=fr] parameter of URL selects French; English is the default language. A middleware component (example 03) sets it before each action, via [App::setLocale]: the facade [App] refers to the application itself. The language set in this way applies only to the current request: the next request will revert to the language of [.env] ([APP_LOCALE], with English as the default). The chapter on internationalization will use this same language to translate the texts.
4.4.3. Configuration: bootstrap/app.php
- Line 19: The middleware is added to the end of the [web] group: it runs for all routes in [routes/web.php], after the middleware in the group (session, cookies, etc.). Example 03 used [append], which adds a global middleware that runs for all requests, including those from routes outside the [web] group;
- Lines 25–28: The [Blade] facade registers two “custom” directives: [@money(...)] and [@longDate(...)]. A directive is processed when the view is compiled (Example 13): its function receives the text written between the parentheses (for example, [$product->price, 'USD']) and returns the code [PHP], which will replace the directive in the compiled view. Here, a call is made to the corresponding method of
***[Formats]***, whose result is escaped ([e]***) and written ([echo]***). In a string enclosed in quotes, [\\] refers to a single [\]; - the directives are stored in a function [booted] (Example 15) once the application has started: the facade [Blade] is only usable at that point.
Because they generate compiled code, directives require a precaution: after modifying a directive, you must clear the compiled views (command [php artisan view:clear]) so that the views are recompiled with the new version.
4.4.4. The controller: app/Http/Controllers/PagesController.php
Actions pass raw values to the views: a date ([CarbonImmutable::today()], today’s date at midnight) and decimal numbers. Formatting is handled by the views. The [Product] class combines a name, a price (a [BigDecimal], see Example 06), and a stock level. The routes are named [home] and [products].
4.4.5. The views
- line 5: a component that creates a link in the navigation bar. The link for the current page is assigned the class [active] from [Bootstrap], which highlights it;
- Line 6: The directive [@class] generates the attribute [class] for a tag from an array: a class written on its own (['nav-link']) is always present; a class written as a key (['active' => condition]) is present only if the condition is true. The function [request()] returns the current query; its method [is] compares the path of URL to a pattern, where [*] matches any sequence of characters. The variable [$slot] contains the content between the component’s opening and closing tags ([<x-link ...>Products</x-link>]).
- line 5: the same thing, written directly in the view, without a component, using the ternary operator. [request()->is('/')] only recognizes the home page. The component avoids repeating the condition for each link;
- line 7: using the component;
- line 10: a parameter from [route] that is not a route parameter is added to URL as a parameter: [/products?lang=fr].
- Line 3: A badge from [Bootstrap], green ([text-bg-success]) or red ([text-bg-danger]) depending on inventory, using [@class]. The text on the badge is selected by the ternary operator; ["$stock in stock"] is an interpolated string.
- line 7: the call to the “home” directive;
- line 9: the function [app()] returns the application, and [getLocale()] sets its language for the current request.
- line 3: the [@use] directive imports a class into the view, just like the [use] statement from a [PHP] file;
- line 14: a call to a static method, which acts as a function;
- Lines 15–16: the [@money] directive, first with its default currency, then with a parameter;
- line 18: the use of the [badge] component.
The [layout.blade.php] template is a simplified version of the one in Example 15.
4.4.6. Execution


The same page, with the parameter [?lang=fr]. Only the formatting of the numbers changes; the text itself is hard-coded in the views. The chapter on internationalization will also translate the text.

An excerpt from HTML produces:
4.5. Example 17: A GET form
A HTML form sends its fields to the server in one of two ways, depending on its [method] attribute:
- [method="get"]: The fields are added to the URL as parameters ([/search?keyword=le&genre=novel]). This is the method used by a form that performs a query: a search or a filter. The URL containing the results can be bookmarked, shared, or reloaded safely;
- [method="post"]: The fields are included in the request body (see Example 11). This is the method used by a form that modifies data: a registration, an addition, or a deletion.
This first form is a book search.
4.5.1. The model: app/Models/Book.php and SearchModel.php
- Lines 14–17: The view template contains the user’s input—which the form will display—the list of suggested genres, and the results.
4.5.2. The controller: app/Http/Controllers/BooksController.php
- Line 14: a private class constant named [self::GENRES] ([self] refers to the current class). A constant cannot contain objects: therefore, the books are provided by a static method (lines 17–25). In a real-world application, they would come from a database;
- lines 32–33: the form fields are read as parameters of URL (example 07). The [string] method returns the parameter as a [Stringable] object of type [Laravel] (a string enriched with methods), which is empty if the parameter is missing; [value()] extracts the string [PHP] from it;
- Lines 35–38: filtering. The function [array_filter] retains the elements of an array for which the given function returns [true]: a book is retained if the keyword is empty or appears in its title or author (in lowercase on both sides), and if the genre is empty or matches its own. [array_filter] retains the keys of the selected elements (0, 2...): [array_values] renumbers them starting from 0;
- lines 39–41: the view template.
The routing table associates two URL entries with the same action: on the first visit ([/]), there are no parameters, and all books are displayed.
4.5.3. The view: resources/views/books/search.blade.php
- Line 6: the [<form>] tag. The [action] attribute is the URL to which the form is submitted; [method="get"] places the fields within the URL. The classes of [Bootstrap] arrange the fields on a single line;
- Line 8: a label. Its [for] attribute specifies the identifier ([id]) of the field it describes: clicking the label places the cursor in the field, and screen readers read the label along with the field;
- line 10: an input field. Its attribute [name] is the name of the parameter sent to the server: this is the link between the form and the action ([$request->string('keyword')]). The attribute [value] is the displayed value: the previous entry, redisplayed (and escaped by [{{ }}], which is essential in an attribute: an entry containing a quotation mark ["] would otherwise close the attribute);
- lines 14–20: a drop-down list. Each [<option>] has a value ([value]), sent to the server, and text, displayed. The first option, with an empty value, means “all genres”;
- line 18: the option selected in the previous request is assigned the attribute [selected]. This is a Boolean attribute: its presence alone is sufficient; it has no value. The directive [@selected(condition)] sets it if the condition is true, and sets nothing otherwise. Similarly, there are [@checked], [@disabled], [@readonly], and [@required];
- line 23: the submit button;
- line 26: the number of results.
4.5.4. Execution

Enter [le] and select the type [novel]:

The URL on the results page contains the form fields: [/search?keyword=le&genre=novel]. The response from the form:
“Madame Bovary” is not selected: “le” does not appear in either the title or the author’s name.
4.6. Example 18: Form fields
This example presents all the field types in a HTML form, which is sent in this instance by POST. It demonstrates how to redisplay each field type along with its current value, how the browser sends the values for each field, and how the server verifies that a received value is indeed one of the available options. [Laravel] does not have a library that defines forms using classes (as in [WTForms] and [Python]): the fields are written in HTML within the view, and the directives in [Blade] ([@checked], [@selected]...) help to render them.
4.6.1. The lists service: app/Services/ListsService.php
The form’s dropdown lists (titles, countries, hobbies, languages) are provided by a service, following the model in Example 06: in a real application, they would come from a database. Each list is an associative array: the key is the value sent to the server, and the value is the text displayed. The service is a concrete class: the container for [Laravel] knows how to create it without a declaration (Example 06).
4.6.2. The controller: app/Http/Controllers/FormController.php
- line 15: the service is injected by the constructor;
- lines 20–23: the initial form values, an associative array: one value per field (an array for a multi-value field). The view receives these values in the variable [$input], along with the list service;
- lines 31–39: The received fields are validated. Dropdown fields must contain one of the suggested values: a malicious user could send anything, not just what the page suggested. [Rule::in(...)] generates the rule [in] (example 08) from an array of values—in this case, the list keys ([array_keys]). Multiple fields are checked element by element ([hobbies.*], example 10). [date_format:Y-m-d]: A [<input type="date">] field exchanges its values with the server in the [yyyy-mm-dd] format, regardless of the format in which the browser displays it;
- lines 41–44: for the example, the list of what the browser actually sent. [(array) $value] converts a single value into a one-element array (an array remains an array); [implode] combines the elements of an array into a string, separated by the first parameter;
- lines 45–51: the form is redisplayed with the received values ([$input]), what the server received, the validation result ([passes()], the opposite of [fails()]), and any errors.
The routing table associates URL and [/form] with action [display] for method GET, and to action [process] for method POST.
4.6.3. The view: resources/views/form/form.blade.php
- line 5: a form named POST;
- line 7: the [@csrf] directive writes a hidden field named [_token], which contains the anti-CSRF token. A CSRF attack (*Cross-Site Request Forgery*) involves a malicious site tricking the victim’s browser into sending a form to our application. The browser includes our application’s cookies in the request—and thus the logged-in user’s session—making the request appear legitimate. The defense: every form in our application contains a random token linked to the user’s session, which the malicious site cannot know. The [PreventRequestForgery] middleware in the [web] group (Example 01) rejects any POST request (as well as PUT, PATCH, and DELETE) whose token is missing or invalid, even before the action is called: no action can omit it. This is the one that Examples 02 and 11 excluded for their [curl] requests;
- line 8: our own hidden field;
- lines 13–19: a group of radio buttons, one per title. All have the same attribute [name]: only one is selected at a time, and only one value is sent. Each button has its own identifier ([title-0], [title-1]...), generated using [$loop->index], to which its label refers. The [@checked] directive checks the button whose value matches the current title;
- line 25: the name input field; [?? ''] handles the case where a name is not in the table;
- Line 30: A password should never be sent back and forth within a page HTML: The field has no attribute [value]; it is always empty when displayed;
- Lines 41–47: Checkboxes, styled like radio buttons. They also share the same name; however, multiple checkboxes can be selected, and the browser then sends multiple values: the name [hobbies[]] instructs [PHP] to organize them into an array (Example 10). The function [in_array] checks whether a value is present in an array;
- lines 53–57: a drop-down list, whose current option is marked by [@selected];
- line 62: a multi-select list (attribute [multiple]), displayed over four lines ([size]), whose name also ends with [[]];
- line 73: a text field spanning two lines. Its value is entered between its tags, not in an attribute;
- Lines 80–90: The directive [@isset] checks for the existence of a variable: [$received] was passed to the view only by the action POST. [$errors->messages()] returns, for each invalid field, a list of its error messages (example 09).
4.6.4. Execution

The default values are visible: the “Mme” radio button, the country “France,” and the language “French.” HTML produces (excerpts):
The anti-CSRF token is a random 40-character string, stored in the user’s session (example 20). Let’s fill out the form and submit it:

The page shows what the server received: one parameter per field, with multiple values for the checkboxes and the dropdown list; the password was sent but is not displayed again in its field. The hidden field [token] was returned exactly as it was: this is how to send and receive information that the user cannot see (the case study uses this for a record’s version number). However, a hidden field is not a protected field: the user can modify it using their browser’s developer tools, and the server must validate it just like any other field. We also note that the anti-CSRF token ([_token]) is among the received fields.
Now let’s submit a form without a token, using [curl]:
The request is rejected by the [PreventRequestForgery] middleware with status code 419, specific to [Laravel] (*Page Expired*: this is the message seen by a user whose session expired between the time the form was displayed and the time it was submitted). To resolve this, [curl] must behave like a browser: first request the form, while retaining the session cookie (option [-c], which saves received cookies to a file), then resend the cookie (option [-b]) and the token read from the page:
(the token is the one from the page received by the first command). Two errors: the country [xx] is not among the options; no title was sent. The messages are Laravel's default ones: the following example customizes them.
4.7. Example 19: Form Validation
A registration form, where each field has its own validation rules. If an error occurs, the form is redisplayed with the user’s input and a message below each invalid field. This example uses the standard approach of [Laravel]: a form request (Example 08) validates the fields before the action; if an error occurs, [Laravel] redirects to the form page, preserving the user’s input and the errors for the next request.
4.7.1. The form: app/Http/Requests/RegistrationRequest.php
- Lines 12–23: the validation rules for each field (Example 08). The spaces surrounding the input have already been removed by the [TrimStrings] middleware (example 08): a name consisting of spaces becomes an empty string, then [null], and will be rejected by [required];
- Line 18: The [email] rule accepts variants, which determine which validation is applied. By default, it follows the email address standard, which accepts [martin@example] (a hostname without a domain). [email:filter] uses the [filter_var] function from [PHP] (example 07), which requires a fully qualified domain name ([example.com]);
- line 19: [integer] followed by [between:18,120];
- Line 22: A rule that applies to two fields: [same:password] compares the value of one field to that of another field. [Laravel] also provides the rule [confirmed], which, when applied to the field [password], would require a field named [password_confirmation];
- Lines 27–40: The method [messages] provides messages specific to this form. Each key designates a field and a rule ([name.required]); a single key such as [required] would apply to all fields. These messages take precedence over those in the language files (example 09).
Messages not provided by the class come from the file [lang/en/validation.php] (the file [.env] sets the language to [en]; see example 09):
This is the case for the message from rule [integer], which is displayed only if something other than a number is entered in the age field—something a browser does not do (we’ll see this with [curl]).
4.7.2. The controller: app/Http/Controllers/RegistrationController.php
- lines 14–17: the form display. The parameter [?html5=true] enables browser validation (see below);
- lines 22–25: the action only accepts valid forms: the form request [RegistrationRequest] was validated before it was called (example 08). It displays the welcome page with the validated data. (The following example will show that it is better to redirect after a successful POST.)
What about an invalid form? It never reaches the action. The form request triggers a [ValidationException] (example 08), which [Laravel] handles as follows for a request coming from a browser: it stores the errors and the entered values (except passwords) in the user’s session, then responds with a redirect to the previous page—the form page. The browser immediately requests this page again (via a GET), and the form view retrieves the errors and user input, which the session provides only once (we will see this “flash” data mechanism in the next example). The form is thus redisplayed via its GET action, rather than via the POST action: this is already the Post/Redirect/Get pattern from the following example, applied to errors.
4.7.3. A reusable field: resources/views/components/field.blade.php
Each field is displayed in the same way: a label, the input field, and, in the event of an error, the area outlined in red and the message below it. A component prevents you from having to write this five times:
- line 5: the component’s parameters: the field name, its label, and its type, [text] by default;
- line 8: the variable [$attributes] contains the attributes assigned to the component that are not parameters declared by [@props] (here, [required], [min], [max]...): when written in the tag, it copies them over. Its method [class] adds the attribute [class] to it, similar to the directive [@class] (Example 16): The [is-invalid] class of [Bootstrap] outlines the field in red if the field contains an error. The variable [$errors] is available in all views: it is a “bag” of messages (Example 09)—containing errors from the previous form stored in the session—or an empty bag; [has] checks for errors in a field;
- Line 10: The function [old] returns the value entered in the previous form, stored in the session, or [null]. A field of type [password] does not have the [value] attribute (example 18);
- Lines 13–15: The block [@error] ... [@enderror] is displayed only if the field contains an error; in that case, its first message is stored in the variable [$message]. The element [invalid-feedback] is displayed in red by [Bootstrap], below a zone labeled [is-invalid].
4.7.4. The views
- line 12: the [novalidate] attribute disables browser validation: the form is submitted as-is, and only the server validates it. This is what we want to observe first;
- lines 14–18: one component per field. The attributes HTML5 ([required], [maxlength], [min], [max], [minlength], [type="email"]) allow the browser to verify the input even before it is submitted (we’ll cover this later). [Laravel] does not derive these from the validation rules: they are written in the view and must remain consistent with the rules.
- lines 16–18: a general message at the bottom of the page if the previous form contained errors ([any()] checks whether the bag contains at least one message), in an area whose style is defined by [public/css/styles.css] (example 15). The [role="alert"] attribute flags this for screen readers.
[$registration] contains the validated data: the name with spaces removed, the age (a string; see Example 08).
4.7.5. Execution
Let’s submit a form full of errors: a name consisting of spaces, an email address without a full domain, an age of 12, a password that’s too short, and a different confirmation:

The input was preserved (except for the passwords), and each error is flagged below its corresponding field. URL is the form’s token, and [/register] indicates that the page was displayed by the action GET after the redirect. With [curl], by providing the anti-CSRF token as in Example 18, we see this redirection:
You can also send something a browser wouldn’t send, such as a non-numeric age. The following page ([curl -b cookies.txt http://localhost:8000/register]) shows the age field, with its input and the message from rule [integer], taken from the language file:
We also see the attributes HTML5 copied by [$attributes]: [required] (written as [required="required"], an equivalent form), [min], [max].
4.7.6. Browser validation
Let’s remove the [novalidate] attribute by following the “Enable” link ([?html5=true] setting). The browser then checks the attributes [required], [type="email"], [min], [maxlength]... before submitting the form, and displays its own messages in a tooltip below the first invalid field (“Please fill in this field,” “The value must be greater than or equal to 18,” etc.): the form is not submitted as long as there is an error.
This validation is convenient for the user: it’s immediate, with no back-and-forth with the server. But it does not replace server-side validation: a user can disable it, and a program like [curl] ignores it completely. The rule is absolute: the server always validates; browser-side validation is merely an added convenience.
A correct entry, at last. The name, entered with spaces on either side, was cleaned up by the [TrimStrings] middleware:

4.8. Example 20: The Post/Redirect/Get pattern
When a POST action returns a page, that page is the result of a POST. If the user refreshes the page (by pressing [F5]), the browser sends the POST request, following a warning that few people read: the command is sent twice, and the message is posted twice. The Post/Redirect/Get pattern (PRG) avoids this problem: a successful POST action does not return a page; it responds with a redirect (example 04) to a page GET, which the browser requests immediately. Reloading this page simply re-executes the GET action.
One problem remains: the GET page would like to display a message (“the task has been added”), but it is the POST action that knows this message, and it cannot pass a variable to a request that has not yet occurred. The solution: the flash message, stored in the session by the action POST, is retained until the next request, displayed, and then discarded. This is the mechanism that [Laravel] was already using, in Example 19, for form errors.
4.8.1. The service: app/Services/TasksService.php
The task list is shared by all users and must persist from one request to the next. In [Java], [C#], or [Python], it would be kept in memory within a single service. In [PHP], this is impossible: as we saw in examples 01 and 06, nothing survives the query. The list must therefore be stored outside of [PHP]: in a database, a file... or in the cache of [Laravel], which we are using here.
- Line 18: The [Cache] interface provides access to the application’s cache: a store of key→value pairs. The [.env] file in the examples stores this data in files within the [storage/framework/cache] folder ([CACHE_STORE=file], Example 01); in production, a dedicated server, such as [Redis], is preferred. [get] returns the value stored under a key, or the default value provided as the second parameter. Any value (in this case, an array of strings) can be stored: [Laravel] serializes it—that is, it converts it to text—using the [serialize] function from [PHP];
- Lines 27–32: The development server, like a production server, can process multiple requests at the same time, in multiple processes (the environment variable [PHP_CLI_SERVER_WORKERS] sets the number of processes for [php artisan serve]). Two simultaneous inserts could read the same list, each add its own task to it, and then write it back: the second write would overwrite the first task. A lock ensures that only one request at a time executes the code it protects: [Cache::lock('tasks-lock', 10)] creates a named lock—which is also stored in the cache—that expires after no more than 10 seconds (if the request holding it were to abruptly terminate); [block(5, function)] waits up to 5 seconds for the lock to become available, acquires it, executes the function, and then releases it. The keyword [use] in an anonymous function gives it access to a variable in the containing function (here, [$label]); an arrow function, on the other hand, automatically sees these variables;
- Line 31: [forever] stores a value indefinitely; [put(key, value, seconds)] would store it for a specified duration.
4.8.2. The controller: app/Http/Controllers/TasksController.php
- lines 26–31: The input is validated manually to keep the example simple. An invalid entry is displayed again, without redirection: there is nothing to protect against a duplicate submission, since nothing has been modified. [mb_strlen] counts the characters in a string UTF-8;
- line 36: the redirect to the page GET, named [tasks]. The [with] method of a redirect places a flash message in the session: data retained only for the next request;
- lines 40–43: the counterexample: a page in response to POST. The operator [?:] returns its left operand if it is “true” (here, a non-empty string), and the right operand otherwise.
The routing table:
4.8.3. The views
- Lines 13–15: The block [@session('message')] ... [@endsession] is displayed only if the session contains a [message] entry, in which case its value is stored in the variable [$value]. The class [alert alert-success] of [Bootstrap] displays the message in a green frame.
- Line 15: The [formaction] attribute of a button submits the form to an address other than the one specified by its [action] attribute: a single form can thus trigger two different actions.
4.8.4. Where are flash messages stored?
Between the POST request and the subsequent GET request, the message must be stored somewhere: in the user’s session. The [Laravel] session is stored on the server: the [.env] file in the examples stores it in a file within the [storage/framework/sessions] folder ([SESSION_DRIVER=file]), with one file per user. The browser, on the other hand, receives only a session ID in the [example20_session] cookie (the name is derived from [APP_NAME]), which it sends back with every request. Let’s examine this using [curl], while storing the cookies in a file (options [-c] and [-b], Example 18), and providing the anti-CSRF token:
The response to POST is a redirect (status code 302, header [Location]), accompanied by the session cookie. Its value is encrypted with the key [APP_KEY] (this is the role of the [EncryptCookies] middleware in the [web] group): the user cannot read or modify it. When decrypted, it contains only the session ID—40 random characters—which is also the name of the session file on the server: [storage/framework/sessions/HNuWctWJ0HcPvmsAhFN5v3iCiw1SFFDMXv46F6og]. Here is that file, following the request POST (displayed across multiple lines):
This is a serialized [PHP] array: [a:4] indicates an array of 4 elements, [s:6:"_token"] a 6-byte string... It contains the session’s anti-CSRF token, the URL from the previous page ([_previous], which is used by the redirect to the previous page in Example 19), our message, and the [_flash] list of flash data. At the end of each request, the flash data set during the request (list [new]) is moved to the list [old], and the data that was already in [old] is removed: thus, a piece of flash data survives for exactly one request. Let’s follow the redirection by sending the cookie back:
The message is displayed, and after this request, the session file no longer contains it:
A new request, GET, would no longer display the message. The task list, however, is stored in a cache file whose name is calculated based on the key ([storage/framework/cache/data/55/d8/55d8727a05eb80b0788af57a5317f3e0a1f4aa55]):
The first ten digits represent the value’s expiration date (in this case, never); the rest is the serialized list.
4.8.5. Execution
Adding a task: the browser followed the redirect; URL corresponds to the page GET, which can be safely reloaded.

An invalid entry, redisplayed with its error message:

The counterexample: the page is the response to POST, and its URL is [/tasks/without-prg]. If you click on [F5], the browser asks if you want to resubmit the form; if you agree, the task is added a second time.

4.9. Example 21: Links, Buttons, and Confirmation
A principle of the HTTP protocol: a GET request does not change anything. Browsers, search engines, and tools that preload pages freely follow links without asking the user for permission. A “delete” link ([<a href="/contacts/2/delete">]) would therefore be dangerous: a bot crawling the site would delete everything. In a web application:
- viewing is done via a link ([<a href>]), which sends a GET request;
- editing (save, duplicate, delete) is done via a button placed in a form, protected by an anti-CSRF token.
This example manages a contact list and displays a confirmation window before each deletion.
4.9.1. The service: app/Services/ContactsService.php
Contacts are stored in the cache, just like the tasks in Example 20. Each contact is an immutable [Contact] object (an ID, a name, a phone number), stored in an array with the most recently assigned ID. A few points about [PHP]:
- Lines 29–37: All modifications follow the same pattern: acquire the lock, read the data, modify it, write it back, and release the lock. This method writes to the array once; each modification passes a function (of type [callable], “callable”) to it, which receives the data and modifies it. The function must modify the array itself, not a copy: its parameter is declared as [array &$data], where [&] specifies a pass-by-reference (without [&], an array is copied; see Example 06);
- lines 51–63: A contact is immutable: a new one is created, which replaces the old one in the array. An assignment is an expression whose value is the assigned value: [return $data['contacts'][$i] = new Contact(...)] replaces the contact and returns it;
- line 68: [++] placed before a variable increments it by 1 and returns the new value;
- line 80: [array_splice] removes an element from the array and renumbers the subsequent elements;
- The methods return [null] for a nonexistent contact: it is up to the controller to decide on the response.
4.9.2. The controller: app/Http/Controllers/ContactsController.php
- lines 19–23: a utility function: a nonexistent contact terminates the action with a 404 response ([abort_if], see Example 15);
- lines 26–34: the two actions GET, which do not modify anything: the list and a contact’s record;
- lines 37–42: saving the record: an action POST, which follows the schema PRG. The same URL and [/contacts/2] are handled by two different actions, depending on the method: GET displays, POST saves;
- Lines 45–50: duplication—another action, POST, triggered by a second button on the same form;
- Lines 53–57: Deletion, handled by a query DELETE.
The routing table:
- Line 12: Deletion is declared for the HTTP method DELETE, which the HTTP protocol specifies for deleting a resource. However, a HTML form can only send GET and POST requests. [Laravel] circumvents this limitation: a POST form containing a hidden field [_method], with a value of [DELETE] (or [PUT], [PATCH]), is treated as a request for that method. This is method spoofing, which is implemented by the [@method] directive in views.
4.9.3. The views
- line 13: a link, presented as a button by the classes of [Bootstrap] ([btn]...): the appearance has nothing to do with the HTTP method. [route('contacts.details', $contact->id)] places the ID in the path: [/contacts/2] (a single parameter can be provided without an array);
- Lines 15–22: One form for each “Delete” button, with no fields other than the token and the method. The [d-inline] class displays it on the same line as the link. The [data-confirmation] attribute contains the confirmation message; the [data-...] attributes are unspecified and intended for the page’s [JavaScript];
- line 18: the anti-CSRF token (example 18), required for any request that is not a GET;
- line 20: the [@method] directive writes the hidden field [_method]: [<input type="hidden" name="_method" value="DELETE">].
- Lines 16–18: Two submit buttons for the same form: “Save” sends the form to the address specified by its [action] attribute, and “Duplicate” sends it to the address specified by its [formaction] attribute (Example 20). This form does not use [@method]: the field [_method] would be submitted by both buttons, and the duplication would also become a request for PUT or DELETE;
- Line 19: Return to the list: a link.
4.9.4. The confirmation window
The browser can display a confirmation box (function [JavaScript] [confirm]), but it is not very attractive, and its appearance depends on the browser. Here we use a modal window from [Bootstrap], displayed by a few lines of [JavaScript]. The window is a fragment included in the template and is therefore present—though hidden—on all pages:
- line 2: the [d-none] class from [Bootstrap] hides the element;
- lines 3–4: the gray background, followed by the window, are rendered by the classes of [Bootstrap];
- line 11: the message's position;
- lines 14–15: the two buttons, of type [button] (not [submit]: they do not submit any form), distinguished by their [data-action] attribute.
- line 19: the window, included on all pages;
- line 21: the script, placed at the end of the page: when it runs, the elements it searches for already exist. It is stored in the [public/js] folder.
The script [public/js/app.js]:
- line 3: the code is enclosed in an anonymous function, which is executed immediately; its variables do not interfere with those of the other scripts on the page;
- lines 4–6: the window, the message location, and the form awaiting confirmation;
- lines 9–14: for each form that has a [data-confirmation] attribute, the [submit] event—triggered when the form is submitted—is intercepted. [preventDefault()] suspends submission; the form is put on hold, and its message ([dataset.confirmation], the value of the [data-confirmation] attribute) is displayed in the window, which appears;
- lines 19–25: a single handler for clicks in the window: [event.target] is the element clicked; its attribute [data-action] specifies which button it is. “Yes” submits the pending form using the [submit()] method, which—unlike clicking a button—does not trigger the [submit] event (otherwise, the window would reappear); “No” hides the window.
Without [JavaScript], the form is simply submitted without confirmation: the page remains usable. This is progressive enhancement: [JavaScript] adds convenience, but the application does not depend on it.
4.9.5. Execution
The contact list:

Clicking “Delete” displays the confirmation window:

After confirmation, the (simulated) query DELETE, the redirection, and the pop-up message:

A contact's profile, with its two send buttons:

A request without an anti-CSRF token is rejected, and a nonexistent contact returns a 404 error:
The error pages are still the English ones from [Laravel]. The chapter on the request lifecycle will show how to replace them with custom pages.