2. Actions, Routing, Responses
This chapter examines the path a request takes, from the browser to the action that processes it, and the response that action returns. Views will not be covered until Chapter 4: here, actions return text or JSON directly.
2.1. Example 01: The Structure of a Laravel Application
The first example is the smallest possible [Laravel] application. A project created by [composer create-project laravel/laravel] contains about fifty files and folders (see introduction); here, we’ll keep only those that the application cannot do without:
- [public/index.php]: the application’s entry point. All requests for HTTP, regardless of their URL, are handled by this file. The [public] folder is the only folder that the web server serves: it also contains static files (style sheets, images, scripts), and only those;
- [bootstrap/app.php]: the application’s build and configuration. The [bootstrap/cache] folder receives files that [Laravel] calculates itself to start up faster;
- [routes/web.php]: the routing table, which associates each URL with the action that processes it;
- [storage]: the files that the application writes during operation: the error log ([storage/logs]), sessions, the cache, and compiled views ([storage/framework]);
- [.env]: the application configuration, in the form of [NAME=value] pairs;
- [artisan]: the command-line tool for [Laravel];
- [composer.json]: the project description for [Composer].
2.1.1. The entry point: public/index.php
- Line 1: A [PHP] file begins with the [<?php] tag. Everything that follows is [PHP] code; a file containing only code does not close this tag;
- line 2: a comment, from [//] to the end of the line ([/* ... */] delimits a multi-line comment);
- line 4: the statement [use] imports a class name. The full name of a class [PHP] includes its namespace (*namespace*), the equivalent of packages in [Java]: [Illuminate\Http\Request] is the class [Request] in the namespace [Illuminate\Http]. After [use], the file can simply be named [Request]. [Illuminate] is the code name for the components of [Laravel];
- Line 7: [require] executes another file, [PHP]. [__DIR__] is the directory of the current file (here, [public]); the operator [.] concatenates two strings. The file [vendor/autoload.php], written by [Composer], installs the class loader: the first time a class is used, it finds and loads the file that defines it. Thanks to this, we never have to write a [require] file for classes. The [vendor] folder is the same as the [examples] folder, shared by all the examples (see introduction): hence the two [..] files;
- Line 10: A file executed by [require] can return a value using the instruction [return]: [bootstrap/app.php] returns the application—an object of class [Illuminate\Foundation\Application]—stored in the variable [$app]. In [PHP], variable names always begin with [$]. [require_once] would do nothing if the file had already been executed;
- line 11: [Request::capture()] calls the static method [capture] of the class [Request] (the operator [::] refers to a member of a class, while the operator [->] refers to a member of an object). It creates a [Request] object from the HTTP request received by [PHP]. [$app->handleRequest(...)] has the application process this request and sends the response to the browser.
This file is the same for all examples, and—with the exception of two [..] files—is the same as that of a standard [Laravel] project. It highlights a unique feature of [PHP], which explains a lot about [Laravel]: the application is rebuilt with every request. The web server executes [public/index.php] from start to finish for each request it receives: it loads the classes, builds the application, processes the request, sends the response, and then forgets everything. No objects survive from one request to the next. In the chapter on data scopes, we will see the consequences of this behavior, which is the opposite of that of [Java], [C#], or [Python], whose applications remain in memory between requests.
2.1.2. The application structure: bootstrap/app.php
- Line 7: The class loader for [Composer] knows the classes in the libraries located in the [vendor] folder. It must also know the application’s classes, which are conventionally stored in its [app] folder, under the [App] namespace. In a standard project, [Composer] obtains this information from the project’s [composer.json] file. Our examples share a single folder named [vendor], but each has its own folder named [app]: each example therefore declares it to the loader itself, via its [addPsr4] method (the [PSR-4] standard maps namespaces to folders: the [App\Http\Controllers\FirstController] class is in the [app/Http/Controllers/FirstController.php] file). A [require] returns the value of the executed file; [vendor/autoload.php] returns the loader (it installs it only once, even if executed twice). In a string enclosed in single quotes, [\\] refers to a single character, [\]. This example does not yet have a [app] file: the line is there for the following ones;
- line 9: the application’s construction. [Application::configure(...)] returns a builder object (*builder*), each method of which configures an aspect of the application and returns the builder itself, allowing calls to be chained; [create()] finally builds the application. The parameter [basePath: dirname(__DIR__)] is a named parameter: the value is associated with the parameter [basePath] by its name, not by its position. [dirname(__DIR__)] is the parent folder of [bootstrap]—that is, the folder in the example: it is from this folder that [Laravel] locates all the others ([routes], [storage]...);
- Line 11: The application routes are those in the [routes/web.php] file. The name [web] is significant: these routes receive middleware from the [web] group (sessions, encrypted cookies, anti-CSRF protection, etc.), which are suitable for an application that generates HTML pages. We’ll encounter them throughout the chapters;
- lines 13–14: the middleware and exception handling, with their default settings. We’ll expand on these in the following examples.
2.1.3. The routing table: routes/web.php
- line 4: importing the [Route] class. This is a facade for [Laravel]: a class whose static methods ([Route::get]) forward the call to an application object, in this case the router. Facades are one of the hallmarks of [Laravel]: they provide quick access to the application’s services without having to obtain the object that provides them;
- Lines 7–10: [Route::get] declares a route: requests for [GET /] will be handled by the function specified as the second parameter. This is routing. This function has no name: it is an anonymous function—a value like any other—that can be passed as a parameter. The function associated with a route is an action. There are also [Route::post], [Route::put], [Route::delete]...;
- line 9: the action returns a string. The value returned by an action is the response sent to the browser: [Laravel] places it in the body of the response HTTP. A string enclosed in single quotes (['...']) is written as-is; when enclosed in double quotes (["..."]), the variables it contains are replaced with their values (e.g., 02).
2.1.4. Configuration: .env
The file [.env] contains settings specific to a given installation of the application: the same application, installed on a developer’s machine and on a production server, will not have the same [.env]. Furthermore, this file must not be published with the source code (it contains secrets). Lines beginning with [#] are comments.
- Line 2: the application name. [Laravel] uses this, for example, to name the session cookie ([example01_session], below);
- line 4: the environment: [local] for development, [production] on a server;
- line 6: the application’s secret key: 32 random bytes, encoded in base 64. [Laravel] uses it to encrypt cookies and sign certain data (we’ll come back to this). It is created by the command [php artisan key:generate], which writes it to the file [.env]; each example has its own. Without it, the application refuses to start (“No application encryption key has been specified”);
- line 8: debug mode: an unhandled exception displays a detailed page (see Example 03). This mode is intended for development, and development only: the error page reveals the application’s source code and configuration;
- Lines 10–11: Sessions and the cache (see the chapter on scopes) are stored in files within the [storage] folder. By default, [Laravel] stores them in a database, which we do not yet have.
Anything that [Laravel] cannot find in [.env] retains its default value. These default values are defined by the framework’s configuration files ([vendor/laravel/framework/config/*.php]); a standard project has a copy of these in its [config] folder, which can be modified. Our examples do without them: a few lines of [.env] are sufficient.
2.1.5. The artisan tool and the composer.json file
The [artisan] file is a [PHP] script that builds the application, just like [public/index.php], but to execute a command rather than process a request:
- Line 1: In [Linux] and [macOS], this line allows the file to be executed directly ([./artisan]); for [Windows], you always enter [php artisan];
- Line 13: [ArgvInput] represents the command-line parameters ([serve], [route:list]...); [handleCommand] executes the command and returns its exit code.
[php artisan list] displays the list of available commands: about a hundred, of which we will use a few. Finally, the file [composer.json] describes the example:
This is a JSON document. [Laravel] reads the application’s class namespace ([App\], located in the [app/] folder) from this file, which it needs, for example, to distinguish the application’s code from the framework’s code in its error pages. The [composer.json] file for a standard project also contains the list of its libraries; for the examples, this list is in the [examples/composer.json] file (see introduction).
2.1.6. Execution
In a terminal window open to the [01-structure] folder, type [php artisan serve]. [Laravel] displays:
The command [serve] launches the development server integrated into [PHP] ([php -S]), which is configured to forward all requests to [public/index.php] (except for requests for files located in the [public] folder, which it sends as-is). It listens on port 8000 at the address [127.0.0.1], which is the address of the machine itself ([localhost]): it accepts only requests from this machine, which is suitable for development ([--host=0.0.0.0] would open it to other machines on the network; [--port=8001] would change the port). It is stopped by [Ctrl-C]. This server is intended for development: in production, it will be a web server such as [Apache] or [Nginx], paired with [PHP-FPM] (see case study). It is not necessary to restart it after a code change: since [PHP] re-reads the files with every request, the change takes effect on the next request. Only a change to the file [.env] requires a restart, which the command [serve] performs automatically.
Let’s request URL and [http://localhost:8000/] using a browser:

then with [curl]. The [-i] option also displays the HTTP headers from the response:
The first line of the response is the protocol version, the status code (200), and its label ([OK]). Next come the headers: the requested host, [Connection: close] (the development server closes the connection after each response), the version, [PHP], the content type, a cache directive, the date, and two cookies. An empty line separates the headers from the body of the response. In the rest of this document, we will no longer reproduce the headers for [Host], [Connection], [X-Powered-By], [Cache-Control], and [Date]—which do not vary—nor the cookies, except when they are the subject of the example.
The header [Content-Type: text/html; charset=utf-8] indicates that the response is HTML, whose characters are encoded in UTF-8. This is the default encoding ([Laravel]) for any string returned by an action: the browser therefore interprets the string as HTML (here, without a tag, it is displayed as-is). Example 03 will show how to specify a different encoding type.
The two cookies may come as a surprise: the application did not request either of them. They are set by two middleware components from the [web] group (see [bootstrap/app.php]): [example01_session] identifies the user’s session, and [XSRF-TOKEN] contains the anti-CSRF token. Their values are encrypted with the key [APP_KEY]. We’ll examine them in Examples 20 and 21.
For each request, the server console displays a log entry: the date, the requested path, and the processing time.
2.2. Example 02: Routing
Routing associates a URL and a method HTTP (GET, POST...) with an action. Writing all the actions in [routes/web.php] as anonymous functions would quickly become unreadable; instead, they are organized into classes—the controllers. Example 02 has two of them:
The files [artisan], [public/index.php], [composer.json], and [.env], as well as the folders [bootstrap/cache] and [storage], are the same as those in Example 01: we will not mention them again (only the example name and the secret key change). Each controller is a class in the [App\Http\Controllers] namespace, located in the [app/Http/Controllers] folder: this is the convention for [Laravel], and the standard [PSR-4] maps the two together (Example 01).
2.2.1. The HomeController controller
- line 4: the class namespace. Its full name is therefore [App\Http\Controllers\HomeController];
- line 6: the class. By convention, a class name is written with a capital letter at the beginning of each word ([HomeController]), while a method or variable name starts with a lowercase letter ([action01Post]). A controller named [Laravel] does not need to inherit from any class;
- line 10: the action [index], a public method of the class (the router can only call public methods). [: string] declares the type of the returned value: a string. Types are optional in [PHP], but when they are specified, [PHP] checks them at runtime;
- lines 12–23: a multi-line string. The syntax [<<<'HTML'] ... [HTML] (*nowdoc*) delimits text that is written as-is, between two lines containing the same word, in this case [HTML]; the indentation of the closing line is removed from all lines. The action returns HTML, sent with the type [text/html] (see Example 01).
The action [index] thus returns a page HTML that contains links to the actions of the other controller:

2.2.2. The controller FirstController
- lines 9–11: the same [index] method as in the other controller: there is no conflict, as each controller is a different class;
- lines 21–23: the action for the request [POST /first/action01]. The name of an action can be anything; it is the routing table that associates it with a URL and a method HTTP;
- lines 28–30: The action’s parameter [$id] receives the value of the route parameter [{id}] (see [routes/web.php]): for URL and [/first/products/17], it is 17. The [int] parameter type instructs [PHP] to convert the value (a string, extracted from URL) into an integer. This is data binding, which will be discussed in detail in the next chapter. In the string enclosed in quotes ["Product no. $id"], the variable [$id] is replaced by its value: this is interpolation;
- lines 34–36: two route parameters, passed to two parameters of the action. [Laravel] associates them by name: the order of the action’s parameters does not matter;
- lines 40–42: a route parameter that contains [/].
2.2.3. The routing table: routes/web.php
- Lines 4–5: Importing the two controllers;
- Line 9: The action is no longer an anonymous function, but an array with two elements: the controller class and the method name. [HomeController::class] is the full name of the class, as a string (['App\Http\Controllers\HomeController']). For each request [GET /], [Laravel] creates a [HomeController] object and calls its method [index];
- line 12: a route group: the routes declared in the function passed to [group] share the group’s settings. [prefix('first')] prepends [/first] to their paths; [controller(FirstController::class)] assigns them their controller, and each route then simply needs to name its method. The URL for an action is the concatenation of the group prefix and the path of its route;
- line 14: the route ['/'] refers to the prefix itself: [GET /first];
- Lines 16–18: The same path, HTTP, corresponds to two methods: [GET] and [POST], which are handled by two different actions. There is also [Route::match(['get', 'post'], ...)], for an action that handles multiple methods, and [Route::any], for all of them;
- Line 21: [{id}] is a route parameter: this segment of URL can take multiple values. By default, a parameter accepts any text without a slash; [whereNumber('id')] restricts the parameter to contain only digits: otherwise, the route does not match. There are also [whereAlpha] (letters), [whereUuid], [whereIn('id', [...])] (a list of values), and [where], which accepts a regular expression;
- line 23: two route parameters, both of which are constrained ([whereNumber] accepts an array of names). In [PHP], [[a, b]] is an array (*array*);
- line 26: the constraint [where('path', '.*')] is a regular expression: [.] denotes any character, and [*] denotes its repetition. The parameter therefore accepts any ending of URL, including [/].
2.2.4. The configuration: bootstrap/app.php
- Lines 11–15: The [withMiddleware] method this time receives a function, which [Laravel] calls by passing it the [$middleware] object that describes the application’s middleware. [: void] indicates that the function returns nothing. We use it to bypass the anti-CSRF protection of URL and [/first/...]: This protection, which we will examine in Example 21, rejects any POST request that does not originate from an application form—including those we will send using [curl]. The parameter named [except] specifies the list of paths to exclude, where [*] stands for any sequence of characters. Without this line, the request POST from [curl] would receive a 419 error response (*Page Expired*).
2.2.5. Execution
Here are the responses to a few requests:
To send a POST request, use the [-X POST] option of [curl]:
A HTTP method for which no action handles this path results in a 405 error ([Method Not Allowed]). The [allow] header lists the accepted methods; [Laravel] adds [HEAD] (the same request as GET, without the response body), which it handles itself for any route GET:
In debug mode, the response body is the detailed error page for [Laravel], which we’ll see in Example 03: It explains that the PUT method is not supported for this route (“The PUT method is not supported for route first/action01.” Supported methods: GET, HEAD, POST.”).
Finally, a URL that no route handles results in a 404 error. This is also the case for an identifier that is not a number: the constraint [whereNumber] prevents the route from matching [/first/products/abc].
The response is an error page for [Laravel], in English. In a browser:

Chapter 7 will show how to replace it with a page specific to the application. Note also that [Laravel] does not distinguish between [/first] and [/first/]: the trailing slash is ignored by the router, and both URL entries lead to the same action.
The command [php artisan route:list], run in the example directory, lists all the routes in the application:
Each line lists the HTTP methods, the path, and the action ([Controller@method]). The last two routes, named [storage.local] and [storage.local.upload], are created by [Laravel] itself: they serve the files that the application stores in its [storage/app] folder, provided they are accompanied by a signature (a signed URL, which only the application knows how to generate). We will not use them.
2.3. Example 03: Responses to an Action
What an action returns determines the HTTP response. Example 03 reviews the main cases:
The routing table maps each URL and [/responses/xxx] to the controller action [xxx]:
The controller [ResponsesController]:
Let’s comment on this code:
- Lines 16–19: The function [response] constructs a complete response, HTTP, which is a [Illuminate\Http\Response] object whose first parameter is the body. [Laravel] provides many functions of this type, available everywhere, which it calls “helpers”: [response], [redirect], [abort], [view], [collect]... The [header] method sets a response header—in this case, its type: [text/plain] (plain text). The browser will not interpret the tags. [header] returns the response itself: the calls follow one after another;
- lines 23–25: the same string, rendered on its own: [Laravel] sends it with the type [text/html], and the browser interprets it;
- Lines 29–31: [response()], called without any parameters, returns a response factory, whose method [json] converts its parameter to JSON and generates a response of type [application/json] (a [JsonResponse] object). The conversion is performed by the [json_encode] function of [PHP]: an object becomes a JSON object whose fields are the public properties of the object. The [Person] class is described below;
- lines 35–37: even simpler: an action that returns an array produces a JSON response, without calling [response()->json]. The array [PHP] is a general-purpose structure: [['lastName' => 'Martin', ...]] is an associative array, where each element has a key (here, ['lastName']); it becomes a JSON object. An ordinary object, on the other hand, cannot be rendered on its own: [Laravel] would attempt to convert it to a string, and the operation would fail (“Object of class App\Models\Person could not be converted to string”); it must go through [response()->json];
- lines 41–43: an array with implicit keys (0, 1, 2) is a list; it becomes an array JSON;
- lines 47–49: the function [collect] creates a [Laravel] collection—an object that wraps an array and provides about a hundred methods to transform it—which are then chained together: [sort] sorts the elements, [values] renumbers the keys starting from 0, and [map] applies a function to each element. [fn (int $n) => $n * 10] is an arrow function: an anonymous function reduced to an expression, whose value it returns. A collection returned by an action also becomes an array, JSON. We’ll revisit collections in the context of databases in Chapter 9;
- lines 53–55: the second parameter of [json] is the status code: 201 ([Created]) replaces 200. A third, optional parameter would provide headers: [['X-Example' => '03']];
- lines 59–61: the code 204 ([No Content]) means “nothing to display”: the response has no body;
- lines 65–67: an “expected” error: a 404 response ([Not Found]), with a body (JSON) chosen by the action;
- lines 71–73: the same error, generated by the function [abort]. [abort] returns nothing: it throws an exception ([NotFoundHttpException], for code 404), which terminates the action; [Laravel] converts it into an error response. The type [never] indicates that the method never terminates normally. This is the most common way to terminate an action due to an error, even deep within a function called by the action;
- lines 77–79: an “unexpected” error—an exception that no one handles—produces a 500 response ([Internal Server Error]). The statement [throw] throws an exception; [RuntimeException] is an exception class from the standard library of [PHP], imported by a [use] (line 10) like any other class. The action is not named [exception], as in the other versions of the course: for [Laravel], an action named ['exception'] would refer to the class [Exception] from [PHP] (class names are case-insensitive), and the route would be rejected.
The class [Person]:
- Line 8: [readonly] makes all properties of the class non-modifiable after they are initialized: a [Person] object is immutable;
- Lines 10–14: [__construct] is the class constructor, called by [new Person('Martin', 'Jean', 42)]. Its parameters, preceded by [public], are promoted: each declares a property of the same name, initialized by the parameter. This syntax avoids having to declare the properties and then assign them one by one in the body of the constructor (which is empty here). The properties are public: [json_encode] can access them.
That leaves the file [bootstrap/app.php]:
- line 15: a middleware component is added to the application: the [ReadableJson] class, shown below.
By default, [Laravel] produces a compact, single-line JSON, in which non-ASCII characters are written as escape sequences: [créée] becomes [cr\u00e9\u00e9e], which is valid JSON, but not very readable. For the examples in this course, we want a readable JSON. A middleware is a class whose method [handle]*** is called for every request, *wrapped around the action: [$next($request)] passes the request to the rest of the application (the other middlewares, then the action), and returns the response (line 17). The middleware can then modify it: if it is a JSON response (the [instanceof] operator checks an object’s class), it changes its encoding options: non-ASCII characters and slashes are written as-is, and the output is indented (line 21). The [|] operator combines the three options, which are constants of [PHP]. The chapter on the request lifecycle will examine middleware in detail; examples 05 through 12 reuse this one.
In a browser, the difference between [text/plain] and [text/html] is visible:


Here are the responses from the other actions:
The object was converted to object JSON, with one field per public property. The [/responses/array] action produces exactly the same response.
The response from [abort] is the standard 404 error page for [Laravel], as shown in Example 02. Our message does not appear there: the standard page displays only the code and its label. However, it does appear when the client requests a JSON response, via the [Accept: application/json] header (option [-H] of [curl]): [Laravel] then adapts its error responses, which become JSON documents:
In debug mode, the JSON document also contains the name of the exception, the file and line where it was thrown, and the call stack (the list of functions that were executing at the time of the error). Without debug mode, it contains only the message. Chapter 7 will show how to display the message on a custom error page.
That leaves the unhandled exception. In debug mode, the browser displays the error page for [Laravel]: the exception type and message, the file and line where it was thrown, and then the call stack—from most recent to oldest—with the relevant line of code for each:

The first entry in the stack is our action, [unexpectedError], on line 79 of the file [ResponsesController.php]. Further down, the page displays the request (its headers and parameters) and the application (routes and settings): this is why debug mode should never be enabled on a server accessible to others. The “Copy as Markdown” button copies the error description, ready to be pasted into a question on IA. The error is also written to the application log, the file [storage/logs/laravel.log] (we’re only showing the beginning here):
Without debug mode ([APP_DEBUG=false] in [.env]), the same request produces a brief error page, “500 | Server Error,” which reveals nothing about the code; the error is described only in the log. Chapter 7 will show how to display an application-specific error page.
2.4. Example 04: Redirects
A redirect is a response that instructs the browser to fetch another URL: a 3xx code and the [Location] header, which contains the new address. The browser immediately sends a second request to that address. The controller [RedirectsController]:
- lines 11–13: the function [redirect] generates a redirect response (a [RedirectResponse] object) to the specified address. Code 301 ([Moved Permanently]) means that the page has permanently changed its address: the browser (and search engines) can remember the new one;
- lines 23–25: without a second parameter, [redirect] generates a temporary redirect with a 302 status code ([Found]). The URL is constructed by concatenating text fragments (the [.] operator), which poses a problem, as we will see;
- lines 30–32: When called without a parameter, [redirect()] returns an object whose [route] method creates a redirect to a route, identified by its name (['page']), rather than by its path. The associative array provides the route parameters (here, [name]), correctly encoded for a URL. If the path of the route [page] is ever changed, the redirects to it will update automatically;
- lines 41–43: a redirect to another site. The [away] method takes the address as is.
It is assigned a route name in the routing table:
- line 7: a group of routes without a prefix, which share only their controller (example 02);
- line 13: the [name] method assigns a name to the route. The [route] function, available everywhere, generates the URL for a named route: [route('page', ['name' => 'a?b'])] yields [http://localhost:8000/page/a%3Fb].
The results, using [curl] (which, without options, does not follow redirects):
[Laravel] converted the address [/new] into an absolute address (including the protocol, host, and port), calculated from the received request. The response also includes a short body, HTML, for a client that does not follow redirects. The option [-L] from [curl] follows the redirect, just as a browser would:
Now let’s compare the two calculated redirects, with a destination that contains a question mark, encoded as [%3F] in URL (an uncoded [?] would start the parameters of the URL):
[Laravel] decoded the route parameter before processing it: [$destination] equals [a?b]. The first action inserts it back into the address as-is: [/page/a?b], which the browser interprets as the path [/page/a] followed by the parameter [b]. The second action follows the named route, whose parameter is encoded again: [/page/a%3Fb]. Following the redirects:
Only the second one leads to the correct page. Hence the rule: an address containing a variable value is constructed using [route], never by concatenating strings. In the browser, only the final page is visible; the address bar displays the new URL:

Two other redirect codes exist: 303 ([See Other]) and 307 ([Temporary Redirect]), which are obtained via [redirect($url, 303)]. After a POST request, codes 302 and 303 cause the browser to send a GET request to the new address, whereas 307 (and 308, the permanent version) cause it to repeat the POST request. Example 20 will use redirection following a POST.
2.5. Example 05: The Request and Response Objects
Until now, actions only received route parameters and returned a value that [Laravel] converted into a response. An action can also access the current request and construct the response itself. The [HttpController] controller:
- line 15: the action has a parameter of type [Request]. This parameter does not come from URL: [Laravel] detects its type and provides it with the [Request] object from the current request. This is dependency injection, which the following example illustrates in detail: an action declares, through the type of its parameters, the objects it needs, and [Laravel] provides them. The order of the parameters does not matter: an action can receive both the request and route parameters;
- lines 17–24: some methods of the request: its HTTP method, its path (without the leading slash), its full URL, its raw parameters (the part of the URL that follows the [?]), the [User-Agent] header (the browser; [$request->header('User-Agent')] is the shorter version), and the client’s IP address. The class [Illuminate\Http\Request] inherits from the class [Request], which in turn inherits from [Symfony], another framework, [PHP], on whose components [Laravel] is built. The parameters, one by one, will be discussed in the next chapter. The rendered type, [array], produces a response JSON (Example 03);
- lines 28–31: the response generated by [response] receives a header [X-Example] (by convention, application-specific headers often begin with [X-]);
- lines 35–38: the response built “by hand” by the operator [new]: the body, the status code, and the headers;
- lines 43–52: a response sent in chunks (*streaming*). [response()->stream] receives a function, which itself writes the response body, via [echo] (the instruction from [PHP] that writes to the response) ; [flush()] immediately sends what has been written to the browser. The response’s length is not known in advance: the server sends the status code and headers, then each chunk as soon as it is generated. This is useful for a very long response that is generated bit by bit. But once the first chunk has been sent, the status code can no longer change: here, the function throws an exception after the first chunk. This is the mistake to avoid: a response that has already begun cannot become an error response.
The routing table and [bootstrap/app.php] contain nothing new ([bootstrap/app.php] adds the [ReadableJson] middleware from Example 03). The responses:
The quotation marks around "URL" prevent the terminal from interpreting the character "[&]." Note that the parameters have been sorted alphabetically: [getQueryString] and [fullUrl] produce a normalized form of the parameter string, which allows for comparing two URL values (this is useful, for example, for a cache). In a browser, the header [User-Agent] is the browser’s header.
After one second, the response stops: [curl] received a 200 status code and a truncated body, with no way to know that an error occurred. Only the application log indicates this, through two errors: our exception, followed by the failure of [Laravel], which attempted to send an error page after the headers had already been sent:
2.6. Example 06: Shared Services and Dependency Injection
An action should not contain the application logic itself; it delegates it to services—objects that know how to perform a specific task (read products, calculate a shopping cart, etc.). Multiple actions often use the same service. [Laravel], like [Spring] or [ASP.NET Core], has a service container: an object that creates services and automatically provides them to those that need them. This is dependency injection, which we already encountered in Example 05 for the object [Request]. Example 06 organizes its code by domain (products, customers) rather than by type (controllers, models):
There is no requirement to place controllers in [app/Http/Controllers]: [Laravel] locates a class by its full name, using the [PSR-4] standard (the class [App\Products\ProductsController] is in the file [app/Products/ProductsController.php]).
2.6.1. The product
- line 9: an immutable class (see example 03). An immutable object can be safely shared among multiple parts of the code;
- lines 12–15: the properties, promoted by the constructor, along with their types. The price is a [BigDecimal], an exact decimal number, provided by the [brick/math] library (installed with [Laravel], which uses it itself). [PHP] does not have a decimal type: a floating-point number, of type [float], is represented in binary and cannot exactly represent 0.1 or 3.2 (in binary, these numbers have an infinite number of digits): a price total in [float] might result in 5.500000000000001.
2.6.2. The service contract: ProductsService.php
- Line 6: the service contract: what it can do, without specifying how. In [PHP], as in [Java] or [C#], a contract is defined by an interface: a list of methods without bodies, all of which must be implemented by a class that implements the interface;
- line 8: a documentation comment ([/** ... */]). The type [array] does not specify what the array contains; the annotation [@return list<Product>]—which [PHP] ignores—specifies this for the code editor: a list of products;
- line 11: [?Product]: a product, or [null] if it does not exist.
2.6.3. The implementation: InMemoryProductsService.php
- line 8: the class implements the contract and defines its two methods;
- line 11: a private property: only the class’s code can use it;
- Lines 13–19: The constructor stores the products in the property designated by [$this->products] ([$this] is the current object). [BigDecimal::of('1.5')] constructs a decimal from a string: constructed from [float] 1.5, it would inherit its inaccuracies;
- lines 22–25: the method returns the array of products. In [PHP], an array is a value that is copied when passed: the caller receives a copy, which it can modify without affecting the service’s data (the [Product] objects, on the other hand, are not copied, but they are immutable);
- lines 28–30: The function [array_find] ([PHP] 8.4) returns the first element of an array for which the given function returns [true], or [null] if there are none. [===] represents strict equality: same value and same type.
2.6.4. The product controller: ProductsController.php
- Lines 12–14: The controller needs the service; it declares it as a parameter of its constructor. When [Laravel] creates the controller (to process a request), it examines the types of the constructor’s parameters, requests an object of each type from the container, and passes them to the constructor: this is constructor injection. The parameter, prefixed with [private], is promoted to a private property (see Example 03): the actions use it via [$this->service]. The controller knows only the service contract (the interface), not its implementation: it is unaware that the products are in memory;
- lines 16–18: the action delegates the work to the service. The returned array of products becomes a JSON array (Example 03): each [Product] becomes a JSON object (its properties are public);
- lines 21–27: An unknown product returns a 404 response, with a message in JSON. A standalone [Product] object must pass through [response()->json] (example 03).
2.6.5. The client controller: ClientsController.php
- Line 13: This controller uses the same service as the product controller. This time, it receives it as a parameter of the action: [Laravel] also injects the action parameters, as in the request from Example 05. Injection via the constructor is suitable for services required by all actions; injection via the action is suitable for those required by a single action;
- Lines 17–18: [array_reduce] reduces an array to a single value: the function receives the accumulated value ([$sum]) and the current element, and returns the new accumulated value; the third parameter is the starting value, a zero [BigDecimal]. [plus] adds two decimals and returns a new decimal (a [BigDecimal] is immutable);
- line 19: [count] returns the number of elements in an array.
2.6.6. The service declaration: bootstrap/app.php
- lines 20–22: it is here, and only here, that the service implementation is chosen. The array associates a requested type (the interface) with the class that provides it. When a class requests a [ProductsService], the container creates a [InMemoryProductsService]; [withSingletons] indicates that it is created only once: anyone who subsequently requests a [ProductsService] will receive the same object. This is a singleton. To switch to an implementation that reads from a database, you would simply need to change this line: the controllers, which only know the contract, would remain unchanged. That is the whole point of dependency injection: every part of the application depends on contracts, and a single location decides on the implementations.
A concrete class (which is not an interface) does not even need to be declared: the container knows how to create it on its own, by injecting the parameters into its constructor. This is how it creates controllers. An interface, on the other hand, must be associated with an implementation; otherwise, the container fails (“Target [App\Products\ProductsService] is not instantiable”). This association can also be declared on the interface itself, using an attribute (an annotation, in [PHP]): [#[Bind(InMemoryProductsService::class)]] above [interface ProductsService], and [#[Singleton]] above the implementation class. We will keep the declaration in [bootstrap/app.php], which brings together all the application’s choices in a single location; the case study will group them into a service provider, the class that [Laravel] provides for this purpose.
Finally, a word about the lifetime of this singleton. As we saw in Example 01: in [PHP], the application is rebuilt with each request. The singleton is therefore unique to a single request: all classes that request it during the processing of a request receive the same object, but the next request will receive a new one. For a service that only reads static data, like this one, this makes no difference. For a service whose data changes (the task list in Example 20), this data must be stored somewhere other than in the object: in a file, a database, or the cache of [Laravel]. On the other hand, a [PHP] service cannot be used by two requests at the same time: each request has its own. The precautions required for services shared by multiple simultaneous requests in [Java], [C#], or [Python] (locks, in particular) are unnecessary here.
The routing table:
2.6.7. Execution
The prices have become strings (["1.5"]): JSON has only one number type, which most programs read as a floating-point number; a [BigDecimal] is therefore converted to a string, so as not to lose any precision (the [BigDecimal] class implements the [JsonSerializable] interface from [PHP], which allows it to choose its form JSON).