Laravel Behind the Scenes: Lifecycle - Routing

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 will split the guide into couple parts. In this part, we will take a look at how Laravel runs through the middleware, how it matches the route against the request and how it executes the controller for your route. We will wrap up the "Lifecycle" series with creating a response, setting up headers, etc. But now, let's start with part 3.

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. We saw how Laravel registers it's necessary service providers, such as router, logger and event dispatcher into the Container. In part 2, we reviewed how the framework loads the configuration, sets up error handling, registers all service providers and resolves the facades. To freshen up your memory, that happened at the handle method of the Kernel class by calling bootstrap method. We stopped there, and the unexamined code was this:

return (new Pipeline($this->app))
    ->send($request)
    ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
    ->then($this->dispatchToRouter());

Let's try to debug this in part 3!

The stack of middleware

First of all, we can try to simplify this code by trying to figure out when does the shouldSkipMiddleware() evaluate to true. This method evaluates to true if string literal middleware.disable is bound to the Container and is true (think of it as a global configuration). Note that this string it's set when you import the WithoutMiddleware trait in your tests and only then. We won't go deeper as this isn't in the scope of this lesson, but if you want to know more, here's the code behind it. Now that we know that empty array is passed only in tests, we know what we're passing to the through method: a list of our global middleware:

array:6 [▼
  0 => "Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode"
  1 => "Illuminate\Foundation\Http\Middleware\ValidatePostSize"
  2 => "App\Http\Middleware\TrimStrings"
  3 => "Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull"
  4 => "App\Http\Middleware\TrustProxies"
  // other global middleware, such as InjectDebugbar from laravel-debugbar library
]

We can also inline the dispatchToRouter method as it only returns a closure with little to no logic. Let's take a look at our simplified code now:

return (new Pipeline($this->app))
    ->send($request)
    ->through($this->middleware)
    ->then(function ($request) {
        $this->app->instance('request', $request);

        return $this->router->dispatch($request);
    });

Now we can break this code down into few parts: creating a Pipeline, passing the object through the stack and finally resolving the closure after the request has passed through the stack.

The Pipeline

Think of Pipeline as a stack of classes. We take an object (in this case the request), modify it however we like it, then pass it onto the next class, and execute the same method as in the element before. This sounds familiar, right? When you create a new middleware in your App namespace, you have that handle method, remember? Well, this method accepts the request that was passed from the previous element in the stack and passes it on to the next element in that stack ($next($request)).

Use case

Perfect use-case for the pipeline is running a request through the middleware. We take a request, pass it onto the first middleware class, modify it, then do the same thing on the next middleware class. If anything along the way fails, we just push the client back to the top of the stack, show the error and don't let him through.

The implementation

If we open up the Pipeline class, we see that this class, that's in the Illuminate\Routing namespace, inherits the Pipeline from the Illuminate\Pipeline namespace. That is done because Routing Pipeline needs some modifications. After creating an instance of the class and passing the Container to the constructor, nothing yet has happened. Then, we assign the object that's going to be passed through the pipes using the ->send($passable), in this case that is our request. Then, we have to assign the stack our object will be sent through, in our case we pass on the global middleware list using ->through($pipes). Note that nothing has been sent through yet, we're just assigning class properties. This is where things get interesting -- then() method happens. Let's take a look at the source for that method:

public function then(Closure $destination)
{
    $pipeline = array_reduce(
        array_reverse($this->pipes), $this->carry(), $this->prepareDestination($destination)
    );

    return $pipeline($this->passable);
}

Note that $this->pipes is the list of middleware, $this->passable is the request and $destination is the final closure, in our case we accept what's left of the request and pass it onto the Router.

What we're doing in this function is running our request through each middleware, then evaluating final destination (dispatching to the router). We're utilising native PHP function array_reduce for iterating through array of middleware with a closure. Since this is pretty complex logic, it would take a long time for me to break down each component and describe what it does, and we still have routing to examine, so let's move onto the Routing.

Routing

Now that we have sliced through the middleware, we have the modified request passing onto the router. If you take a look at the ->router->dispatch($request) method (this is called in the closure of the then method), we see that we're delegating the dispatchToRoute method in the Router itself, and that method executes the runRoute method:

public function dispatchToRoute(Request $request)
{
    return $this->runRoute($request, $this->findRoute($request));
}

Before we execute the route action, we have to find route that matches our request URI. There are requestUri and method properties on our Request object that can help us do that. In findRoute method, we're calling the match method from RouteCollection object which finds the route, maps it to the Route object and after we have done that, we're binding that Route object into the Container. Note that RouteCollection is the class we instantiated in the constructor when we were registering core RoutingServiceProvider (look: part 1).

Matching the route

The contents of the match method in Illuminate\Routing\RouteCollection are as follows:

public function match(Request $request)
{
    $routes = $this->get($request->getMethod());

    $route = $this->matchAgainstRoutes($routes, $request);

    if (! is_null($route)) {
        return $route->bind($request);
    }

    $others = $this->checkForAlternateVerbs($request);

    if (count($others) > 0) {
        return $this->getRouteForMethods($request, $others);
    }

    throw new NotFoundHttpException;
}

$this->get($request->getMethod()) grabs all the defined routes for the request method, e.g. if the request is of GET type, we return an array of all routes that can be accessed with the GET method. The structure of this array is defined as uri => Route-object.

array:27 [
  "api/user" => Route {#298 ▶}
  "/" => Route {#300 ▶}
  "login" => Route {#307 ▶}
  "register" => Route {#310 ▶}
  "password/reset" => Route {#312 ▶}
  "password/reset/{token}" => Route {#314 ▶}
]

Then, we try to match request against $routes using the matchAgainstRoute method, and that method essentially loops through the array of routes and calls the matches method on Route object to see if the requests matches this route. The first route hit is returned back and stops the iteration (utilizing Collection@first method). Note that we're pushing the fallback routes to the end of the collection using partition then merge. This is done because we first want to check regular routes, then fallback routes in case no regular has been found.

protected function matchAgainstRoutes(array $routes, $request, $includingMethod = true)
{
    list($fallbacks, $routes) = collect($routes)->partition(function ($route) {
        return $route->isFallback;
    });

    return $routes->merge($fallbacks)->first(function ($value) use ($request, $includingMethod) {
        return $value->matches($request, $includingMethod);
    });
}

The matches method of the Route class resolves and executes different route validators: URIValidator, SchemeValidator, HostValidator & MethodValidator. Those validator classes try to validate the route for its specific purpose. If you want to know more, take a look at the source code. Since this is pretty complex, maybe I'll dedicate a complete series of Laravel Behind the Scenes to Routing. Essentially, we're just mapping the URI of the request to any of the compiled routes, but note that we also need to match the request method. If we have found any routes that match, we bind the request to the route and return it back. However, if no route was matched in the process, we will loop through the route collection to see if that URI exists for different HTTP methods using checkForAlternateVerbs($request). If none exist, throw back an Exception.

Executing the route

Now that we have found our route, we are ready to execute it. This is what runRoute method does. If we take a look at the definition of that method, we see a bunch:

protected function runRoute(Request $request, Route $route)
{
    $request->setRouteResolver(function () use ($route) {
        return $route;
    });

    $this->events->dispatch(new Events\RouteMatched($route, $request));

    return $this->prepareResponse($request,
        $this->runRouteWithinStack($route, $request)
    );
}

Note that we initially need to set the route resolver in the Request class, so when you use the $request->route() method in your classes, that resolver finds that route and returns it back to you. Afterwards we're just firing off the event that we have matched the route, nothing special. Then, we "run route within stack". What does that mean? We need to take a look at the code to make more sense.

Route middleware

protected function runRouteWithinStack(Route $route, Request $request)
{
    $shouldSkipMiddleware = $this->container->bound('middleware.disable') &&
                            $this->container->make('middleware.disable') === true;

    $middleware = $shouldSkipMiddleware ? [] : $this->gatherRouteMiddleware($route);

    return (new Pipeline($this->container))
                    ->send($request)
                    ->through($middleware)
                    ->then(function ($request) use ($route) {
                        return $this->prepareResponse(
                            $request, $route->run()
                        );
                    });
}

Yep, that code looks familiar -- the middleware. We again create a pipeline, but this time on we run the request through middleware defined for this route. Since we already know what this code does, we should only take a look at the closure in then method, in this case:

function ($request) use ($route) {
    return $this->prepareResponse(
        $request, $route->run()
    );
}

Dispatching the Controller

This is where we execute the route, dispatch the controller and return back the response to prepareResponse.

public function run()
{
    $this->container = $this->container ?: new Container;

    try {
        if ($this->isControllerAction()) {
            return $this->runController();
        }

        return $this->runCallable();
    } catch (HttpResponseException $e) {
        return $e->getResponse();
    }
}

Looking at this method, we see that we just try to dispatch the controller (if the route was defined as a controller action), or execute the callback (if route was defined as a closure), or throw an error. isControllerAction checks if uses parameter in your routes is a string (e.g. Route::get('/', 'SomeController@someMethod')). If it is, execute the runController method. If it's not, execute the runCallable method which executes the closure-defined route. Looking at the runController, we see that it does exactly that:

protected function runController()
{
    return $this->controllerDispatcher()->dispatch(
        $this, $this->getController(), $this->getControllerMethod()
    );
}

A lot of code happens in the background of this, so we won't go that deep. Note: this is where all your controller dependencies are automatically injected.

So in essence, $route->run() executed your controller method defined for this route and got back some response (note that this is the response you return in your controller action) and that response output is passed on the prepareResponse method.

Response

The prepareResponse delegates the static toResponse method whose purpose is to create a new Response class and prepare the response for sending it back to the client. That looks like this:

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);
}

Note that $response variable in the argument is the exact output we returned back from our controller. This method is ready to modify that response however is necessary. We returned back the Model instance? No problem, check if $response is a model?, new-up the JsonResponse class and it's done. We returned the string? No problem, new up regular Response class and voila. We returned anything that's JSON serializable? Convert it to the JsonResponse object. Also note that if you returned back Eloquent model AND that resource was just created (in this very request), Laravel will set status code of the response to 201.


The only thing left in our lifecycle is to examine this prepare($request) method of our Response object and finish up the remaining code in index.php -- which sends the response to the client and executes any terminable middleware. In the next part, we will take a look at the Response class and dive deeper into the Symfony's Response component and probably wrap up the "Lifecycle" series.

I have updated the series with part 4 so make sure to check it out!

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