Laravel Behind the Scenes: Lifecycle - Response
I'm Josip and I've been working with Laravel for quite some time and I want to help others out figure out how this amazing framework works. During that process, I'll also show you some cool tricks that are happening behind the scenes. In this series of blog posts, I will try to explain how Laravel works under the hood, from getting a request in index.php to rendering the response back to the client. Since this is pretty advanced and thorough, I have split the guide into couple parts. This is the final part of the Lifecycle series and we will take a look at how Laravel responds back to the user, whether that is the View response, JSON response or anything else. Let's start with part 4!
This guide covers the source code of version 5.6 of the Laravel framework.
Review
In part 1, we reviewed what happens as soon as index.php
file loads up, even before core framework services load. In part 2, we reviewed how the framework loads the configuration, sets up error handling, registers all service providers and resolves the facades and in part 3 we reviewed how Laravel points your request to the specific route and loads up the controller to handle that route. Now, let's move onto the how your controller response is automagically converted to proper response format and displayed to your browser.
Prepare the Response
In part 3, we have seen that toResponse
method converts any data you pass to it into a Response object. We also noticed that $response
variable is direct output of the controller.
public static function toResponse($request, $response)
{
if ($response instanceof Responsable) {
$response = $response->toResponse($request);
}
if ($response instanceof PsrResponseInterface) {
$response = (new HttpFoundationFactory)->createResponse($response);
} elseif ($response instanceof Model && $response->wasRecentlyCreated) {
$response = new JsonResponse($response, 201);
} elseif (! $response instanceof SymfonyResponse && ($response instanceof Arrayable || $response instanceof Jsonable || $response instanceof ArrayObject || $response instanceof JsonSerializable || is_array($response))) {
$response = new JsonResponse($response);
} elseif (! $response instanceof SymfonyResponse) {
$response = new Response($response);
}
if ($response->getStatusCode() === Response::HTTP_NOT_MODIFIED) {
$response->setNotModified();
}
return $response->prepare($request);
}
We realized that this is the place where Laravel magically converts arrays, strings and Eloquent models to JSON (because they implement the serializable interface and contain toJson
method). If we now take a look at the Response class from the Illuminate\Http
namespace, we see that this class inherit the identically titled class from the Symfony's HttpFoundation component. We also see that this class doesn't contain a lot of code. That's because most of the setup is done behind the scenes by Symfony. Before we take a look at the preparation method, creating a new instance of the class calls the constructor, right? So we should probably take a look at what it does.
New instance
Right off the bat we see that we're creating new header bag from the empty $headers
array, because no initial headers have been sent during the instantiation process (new Response($response)
).
public function __construct($content = '', int $status = 200, array $headers = array())
{
$this->headers = new ResponseHeaderBag($headers);
$this->setContent($content);
$this->setStatusCode($status);
$this->setProtocolVersion('1.0');
}
If you examine the contents of the Response object after headers have been stored, you'll see that it's empty -- only date and cache-control headers have been set. Then, we set some initial properties like HTTP version, response content and status code and finally proceed with the preparation. If we dd
this class after initial properties have been set, we see the following: (keep in mind that this is fresh Laravel installation that loads welcome view):
We should now note couple of things: original
property is a View object, statusText
property sets the status text based on the HTTP code (404 - Not Found, 200 - OK, etc.) and content
contains literal output that is going to be rendered to the client (HTML in this case). I'm kinda curious about this View object... Fun fact is that setContent
method is actually called from the Laravel's Response class. Removing the comments and we see this method is pretty simple and clean.
public function setContent($content)
{
$this->original = $content;
if ($this->shouldBeJson($content)) {
$this->header('Content-Type', 'application/json');
$content = $this->morphToJson($content);
} elseif ($content instanceof Renderable) {
$content = $content->render();
}
parent::setContent($content);
return $this;
}
First things first, we save the initial content (object, view, etc.) into the original
property in case we need it later. Then, we modify the $content
variable however we need to. Have an object that should be returned as JSON (string, model, collection, JSON response, etc.)? Just set Content-Type
header to application/json
and encode the output to JSON. We returned a view? Or any object that implements Renderable
interface? Call the render
method on that object and call the setContent
method from the parent on the modified content. In our case, render
method compiles the view with Blade and creates raw HTML. In this moment, we can print out the $content
and it will literally show the final HTML, but we're not done here.
Prepare the headers
Yep, now that we have created a new instance of the Response class in our Router, we are ready to call the prepare
method. As we can see this method contains a lot of code, so I won't paste it here to not clutter the post. First, we should figure out if the response is informational or empty. The thing to know here is that response is informational if the status code is between 100 and 200, and is empty if the status code is 204 or 304. If it is informational or empty, remove any content and content-related headers. Otherwise set the content type and content length based on the Request also set the proper charset. Also, default the HTTP version to the v1.1. Finally, check if Cache-Control should be removed for SSL encrypted downloads for IE.
JSON & Redirect response
JSON and Redirect responses are just slightly modified versions of the base Response object. JSON response is represented by the Laravel's JsonResponse
class which inherits the Symfony's class and performs some JSON validation, encoding options, sets the JSON content-type header, etc. Redirect response does exactly what it says and it also inherits the Symfony's class. The only thing this class does is sets the Location
header to target URL.
Redirector
Redirector is a special class that wraps the redirect response behind some common functions: redirect back, redirect home, redirect to route, etc. So when you call the redirect facade/helper, it actually boots up the Redirector object. It also uses URLGenerator
class for generating URLs to named routes, previous URL from header/session, etc. So, for example, redirect()->back()
will actually call this little piece of code:
public function back($status = 302, $headers = [], $fallback = false)
{
return $this->createRedirect($this->generator->previous($fallback), $status, $headers);
}
If you take a look at the createRedirect
method, this method literally just creates a new instance of the RedirectResponse
class and sets the session and the request in the Response.
Response helper
It's also worth noting that, if you call the Laravel's amazing response()
helper, factory class ResponseFactory
is actually the one that is being resolved from the Container. This is a class that is just a wrapper around Response object and provides delegates for creating common response types (JSON, download, redirect, ...):
// Illuminate/Foundation/helpers.php
function response($content = '', $status = 200, array $headers = [])
{
$factory = app(ResponseFactory::class);
if (func_num_args() === 0) {
return $factory;
}
return $factory->make($content, $status, $headers);
}
Finish the lifecycle
We have now done every step through the lifecycle of Laravel application and reverted back through the stack to that famous index.php
file. The only thing left to do here to display (send) the response to the client and call any terminable middleware.
$response->send();
$kernel->terminate($request, $response);
Send the response
We prepared headers, compiled view into the HTML, done all business logic in our controllers and models and the only thing left to do is to send headers and show the content. This is what the send
method of the Symfony's Response class does.
public function send()
{
$this->sendHeaders();
$this->sendContent();
if (function_exists('fastcgi_finish_request')) {
fastcgi_finish_request();
} elseif (!\in_array(PHP_SAPI, array('cli', 'phpdbg'), true)) {
static::closeOutputBuffers(0, true);
}
return $this;
}
It's worth noting that this method also does some additional checks to flush the data to the client if server is using PHP-FPM. If you're using Laravel Valet and have PHP installed through brew, this function will most certainly be called. There is also a method to close output buffer and flush the content if using output control, but we won't cover it under this guide. If you're interested, take a look at the docs.
Send the headers
Taking a peek at the sendHeaders
method, we see that we're using two native PHP functions: headers_sent
and header
.
public function sendHeaders()
{
if (headers_sent()) {
return $this;
}
foreach ($this->headers->allPreserveCase() as $name => $values) {
foreach ($values as $value) {
header($name.': '.$value, false, $this->statusCode);
}
}
header(sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText), true, $this->statusCode);
return $this;
}
Remember those old errors that occurred if you messed up your CodeIgniter app -- Headers have already been sent? Well, conditional headers_sent
prevents that. If we have not already sent headers, we should send them now! Looping through the headers
property and calling the PHP's header
method on each item does exactly that. Also, we need one additional header call to set the status code and the version of the HTTP protocol. This step is done out of the loop because all other headers have a shape of "Name: Header" (e.g. header('Content-Type: application/json');
), while status header looks like "HTTP/version code text", e.g. header("HTTP/1.0 404 Not Found");
.
Send the content
After sending the headers, we need to send the content to the user. Remember when we said we can literally just echo out the content and get the HTML response? This is exactly what the sendContent
method does. It's just an echo
call:
public function sendContent()
{
echo $this->content;
return $this;
}
As of this moment, the response is sent and the browser displays the contents to the client. Or if you're using APIs, the JSON is responded!
Terminate the application
After sending the response, the only step in the lifecycle that needs to be done is to call any additional terminable middleware. We see that terminate
method on the Kernel in index.php
is called -- $kernel->terminate($request, $response);
, and that method delegates terminate
method on the Application instance but not before it calls any additional middleware.
public function terminate($request, $response)
{
$this->terminateMiddleware($request, $response);
$this->app->terminate();
}
Now that we know how middleware works, we can just take a quick look at the terminateMiddleware
method and figure out what it's doing:
protected function terminateMiddleware($request, $response)
{
$middlewares = $this->app->shouldSkipMiddleware() ? [] : array_merge(
$this->gatherRouteMiddleware($request),
$this->middleware
);
foreach ($middlewares as $middleware) {
if (! is_string($middleware)) {
continue;
}
list($name) = $this->parseMiddleware($middleware);
$instance = $this->app->make($name);
if (method_exists($instance, 'terminate')) {
$instance->terminate($request, $response);
}
}
}
After getting a list of middleware, loop through them and call the terminate
method on the Kernel -- but only if one exists. Then, delegate to terminate
method on the Application object which fires terminating event and calls any additional listeners that might have been created for that event. And with that, finally, the lifecycle of the entire application is done!
Summary
To summarize: we took a look at the first file that loads up when you make a request, how it registers core Laravel components, how it creates a HTTP kernel that registers and boots all service providers, creates the global request object from environment variables, runs the router and how that router executes your own code you provided in your controller method and finally responds back the response content to the client that made a request.
This will be it for the Lifecycle series in Laravel Behind the Scenes. I hope you enjoyed it and learned something new! Stay tuned because I'll dive deep into one cool Laravel component next. Also, make sure to subscribe to my newsletter to get a notification when I post something new. This is my first Laravel Behind the Scenes post and if you think I should change something or if you have any comments/critics or even if you have any ideas about future posts, feel free to comment below, DM me at Twitter or send me an email!