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!