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):

Response Image

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!

👋 I'm available for software development contract work and consulting. You can find me at [email protected]