Laravel Behind the Scenes: Lifecycle - Boot the framework
I'm Josip and I've been working with Laravel for quite some time and I've decided to help out others 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 2, I will try to explain how request is created, how global variables are read, and how service providers and facades are registered. Let's begin with part 2.
This guide covers the source code of version 5.6 of the Laravel framework.
Binding the kernel
In part 1, we reviewed what happens as soon as the Application instance is created and which core service providers are loaded. Next up is Kernel. As you can see in bootstrap/app.php
, after we new-up the Application, we try to bind a singleton of the HTTP and Console kernels to the Container.
$app->singleton(
Illuminate\Contracts\Http\Kernel::class,
App\Http\Kernel::class
);
$app->singleton(
Illuminate\Contracts\Console\Kernel::class,
App\Console\Kernel::class
);
Note that we're actually binding OUR own Kernel implementation that can be found in app/Http/
. However, you may not have noticed that our kernel inherits the Illuminate\Foundation\Http\Kernel
class. After we have stored our kernel classes and exception handler in the Container, nothing yet has been changed. We have just saved them for later use. This is being done in the app.php
instead of somewhere hidden in the vendor
directory so we can change which Kernel implementation is bound. You can (but probably never will) swap App\Http\Kernel
with any Kernel implementation you want. No kernel has yet been instantiated, no Kernel object has been created. This is what happens next. Also note that we're binding the interface as a key to fetch from the Container, but we're actually resolving App\Http\Kernel
.
Resolving the kernel
The very next line in index.php
is as follows:
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
This is the perfect example of interface binding, we bound Kernel interface, but actually are resolving concrete class implementation. This is where things get interesting.
After we call make()
, we can see that a new instance of the Kernel has been created and stored in the $kernel
variable. Let's take a look at the constructor of the Kernel class (the class that App\Http\Kernel
inherits from).
// Illuminate\Foundation\Http\Kernel
public function __construct(Application $app, Router $router)
{
$this->app = $app;
$this->router = $router;
$router->middlewarePriority = $this->middlewarePriority;
foreach ($this->middlewareGroups as $key => $middleware) {
$router->middlewareGroup($key, $middleware);
}
foreach ($this->routeMiddleware as $key => $middleware) {
$router->aliasMiddleware($key, $middleware);
}
}
Note that $app
and $router
parameters in the constructor are being resolved and injected automatically. We will take a look at the make()
command in the Who wants to know more segment.
Also note that the $router
here is just a new instance of the Router class and does not contain any routes or any configuration, just an Event dispatcher, that as you can remember, we bound to the Container in part 1. If we dump the contents of the $router
class, we can prove that to ourselves:
Router {#36 ▼
#events: Dispatcher {#37 ▶}
#container: Application {#7 ▶}
#routes: RouteCollection {#39 ▼
#routes: []
#allRoutes: []
#nameList: []
#actionList: []
}
#current: null
#currentRequest: null
#middleware: []
#middlewareGroups: []
+middlewarePriority: []
#binders: []
#patterns: []
#groupStack: []
}
Now, onto the code. After we have injected the Container and the Router, we're getting the list of global middleware and assigning them to the Router.
Note: if you're wondering how can $router->property = $contents
change the $router
class contents (doesn't work with regular variables), remember that PHP passes classes by reference (meaning that if you modify the $router
variable, the real class contents are being modified).
We're also just getting middleware groups and route middleware defined in the app/Http/Kernel.php
and assigning them to the class properties in Router, nothing more. Basically, Kernel constructor reads middleware list from your own Kernel class and passes them onto the Router, so Router can operate on it. Note that route collection is still empty.
Capture the request
This is where real magic happens. Let's examine the following code snippet:
$response = $kernel->handle(
$request = Illuminate\Http\Request::capture()
);
You may think that handle()
method is being called first, but no. First thing we do now is capture the request. Let's take a look at that capturing.
public static function capture()
{
static::enableHttpMethodParameterOverride();
return static::createFromBase(SymfonyRequest::createFromGlobals());
}
As we can see, this method creates a new Request instance from global variables and returns that object back. If we examine that, you notice this long-ass-named method called enableHttpMethodParameterOverride
. You have probably used method_field('DELETE')
in your forms in view files before, right? In enableHttpMethodParameterOverride()
, we just set the same-named property to true
to allow the Laravel to read _method
from the request data to see what type of the request we actually want. Next up, we're actually utilizing Symfony's Request class from the HttpFoundation component to create a new Symfony Request from global variables ($_SERVER, $_POST, $_GET, $_COOKIE, ...
). Taking a look at the createFromGlobals()
from the Symfony's Request class, we see the following:
public static function createFromGlobals()
{
$request = self::createRequestFromFactory($_GET, $_POST, array(), $_COOKIE, $_FILES, $_SERVER);
if (0 === strpos($request->headers->get('CONTENT_TYPE'), 'application/x-www-form-urlencoded')
&& in_array(strtoupper($request->server->get('REQUEST_METHOD', 'GET')), array('PUT', 'DELETE', 'PATCH'))
) {
parse_str($request->getContent(), $data);
$request->request = new ParameterBag($data);
}
return $request;
}
Basically, this code calls another static method called createRequestFromFactory
, which in essence just returns the new instance of self:
private static function createRequestFromFactory($bunchOfServerVariables)
{
// ... something we don't care about at the moment
return new static($bunchOfServerVariables);
}
This is all being done in the Symfony's Request component and I will probably sometime dig into that. Symfony's Request class reads all the global variables (such as cookies, headers, server vars, ...) on initialization (in a constructor and stores them. Remaining code from the createFromGlobals
checks if our request type is PUT, DELETE or PATCH and just reads the input parameters. As of this moment, headers have been read, server variables have been read, cookies have been read. Let's dive back into our Laravel's capture
method. Next thing is createFromBase
, which accepts the request Symfony just created, duplicates all of its contents and creates a new instance of Laravel's implementation of the Request class.
public static function createFromBase(SymfonyRequest $request)
{
// Right now we don't care about this
if ($request instanceof static) {
return $request;
}
$content = $request->content;
$request = (new static)->duplicate(
$request->query->all(), $request->request->all(), $request->attributes->all(),
$request->cookies->all(), $request->files->all(), $request->server->all()
);
$request->content = $content;
$request->request = $request->getInputSource();
return $request;
}
As of this moment, we have a request information, such as request type, URI, headers, cookies, etc. You can examine that Request class by dd($request)
when capturing.
Kernel handling
Next up, we see that we call handle()
method on the request we just captured. Let's examine the handle
method from our core Kernel class.
public function handle($request)
{
try {
$request->enableHttpMethodParameterOverride();
$response = $this->sendRequestThroughRouter($request);
} catch (Exception $e) {
$this->reportException($e);
$response = $this->renderException($request, $e);
} catch (Throwable $e) {
$this->reportException($e = new FatalThrowableError($e));
$response = $this->renderException($request, $e);
}
$this->app['events']->dispatch(
new Events\RequestHandled($request, $response)
);
return $response;
}
Note that we are calling enableHttpMethodParameterOverride()
once again, just to be sure that _method
parameter can be read (in case Symfony changes theirs implementation). After that marked that flag, we pass the request through the router to create a response, we dispatch an event that we handled the request and return that response back to the user. If something along the way fails or throws an Exception, Laravel's Exception handler (that we bound in the app.php
) will pick up and report/render that exception to the user. Taking a look at the sendRequestThroughRouter
we see some cool things:
protected function sendRequestThroughRouter($request)
{
$this->app->instance('request', $request);
Facade::clearResolvedInstance('request');
$this->bootstrap();
return (new Pipeline($this->app))
->send($request)
->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
->then($this->dispatchToRouter());
}
First things first, we bind an instance of the Request class to the Container (we have only one request object per HTTP request) and clear already resolved instances of the Request class. Then we call this bootstrap()
method that performs everything we were waiting for. Taking a look at that method, we see that we're essentially calling $this->app->bootstrapWith($bootstrappers)
method on our Container, passing through list of bootstrappers from the Kernel@$bootstrappers
property:
protected $bootstrappers = [
\Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class,
\Illuminate\Foundation\Bootstrap\LoadConfiguration::class,
\Illuminate\Foundation\Bootstrap\HandleExceptions::class,
\Illuminate\Foundation\Bootstrap\RegisterFacades::class,
\Illuminate\Foundation\Bootstrap\RegisterProviders::class,
\Illuminate\Foundation\Bootstrap\BootProviders::class,
];
Bootstrapping core components
bootstrapWith
method just fires an event that we're bootstrapping a component, calls bootstrap()
method on each component and fires an event that we have bootstrapped a component. I won't copy and examine code of all bootstrappers as their names clearly explain what they do, but I will try to summarize the code in this list. You can also take a look at the code of those components here.
LoadEnvironmentVariables
literally creates a new instance of theDotEnv
class and reads from.env
file- Also checks if configuration is cached, so it does not read
.env
again - Also checks if we're in specific environment to read that
.env.{environment}
file
- Also checks if configuration is cached, so it does not read
LoadConfiguration
checks the cached configuration and loads it (if it exists), otherwise loops through using Symfony's Finder component through all files in your config directory- It also sets the default timezone you specified in your
app.php
config:date_default_timezone_set($config->get('app.timezone', 'UTC'));
- It also sets the default UTF-8 encoding:
mb_internal_encoding('UTF-8');
- It also sets the default timezone you specified in your
HandleExceptions
uses PHP's error reporting functions to essentialy disable PHP error handling and setting up our custom error handler, so we can use libraries such as Whoops! to show stack tracesRegisterFacades
registers all the facades by storing their implementation into the Container. So when you callFacade::method()
, Laravel will actually resolve that class out of the Container and call non-staticmethod()
on them- It also registers real-time facades
- It also uses Facades from packages
RegisterProviders
literally delegates to$app->registerConfiguredProviders();
. This method loops through theproviders
array in theconfig/app.php
file and registers those service providers into the Container- It also reads all package service providers and binds them
- Note: this is where contents of the
register()
method of your service providers are evaluated
BootProviders
literally delegates to$app->boot();
, which loops through all registered service providers and callsboot()
method on each.- Note: this is where contents of the
boot()
method of your service providers are evaluated
- Note: this is where contents of the
I believe you have now noticed the differences between register()
and boot()
. If you need some core framework services, wait until framework registers all service providers, then store your code in boot()
method. If you need to get something done on provider registrations, put that code into register()
method.
After this code has been evaluted, all that is left is send the request through the router and get back the response. Now, if you dump the contents of the Application class, we see that all our routes have been loaded, database connection has been instantied and we're ready to go. In this course, we will also take a look at some of the core components to see where exactly the connection is created, where the routes are loaded, etc. For now, this will be the end of part 2 and I hope you now know how your HTTP request is captured and how the framework is booted.
I have just updated the series with part 3, so take a look at it!
Who wants to know more
As I said, we will take a look at the make()
method on the Container. Well, make()
just delegates to resolve()
method. Taking a look at the resolve()
, we see bunch of code (I have removed comments):
protected function resolve($abstract, $parameters = [])
{
$abstract = $this->getAlias($abstract);
$needsContextualBuild = ! empty($parameters) || ! is_null(
$this->getContextualConcrete($abstract)
);
if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
return $this->instances[$abstract];
}
$this->with[] = $parameters;
$concrete = $this->getConcrete($abstract);
if ($this->isBuildable($concrete, $abstract)) {
$object = $this->build($concrete);
} else {
$object = $this->make($concrete);
}
foreach ($this->getExtenders($abstract) as $extender) {
$object = $extender($object, $this);
}
if ($this->isShared($abstract) && ! $needsContextualBuild) {
$this->instances[$abstract] = $object;
}
$this->fireResolvingCallbacks($abstract, $object);
$this->resolved[$abstract] = true;
array_pop($this->with);
return $object;
}
As we see, we try to load the class name for the alias we provided (e.g. resolve Illuminate\Container\Container
for app
alias), then we check to see if we need contextual binding. If we don't need contextual binding and we already have an instance of the class bound in the $instances
property, just return that class instance. Otherwise, if we don't have an already bound instance, we should just build up the class and use Reflection to resolve all dependencies. We see that we try to get concrete implementation of the alias (remember interface-binding) and see if the class is buildable (can be instantiated). If the class is buildable, we call build()
method where we use Reflection to get the constructor of the class, get all of it's parameters and resolve them recursively as well. We see that if the constructor has no parameters, just return new $class
and you're done. If we have dependencies, just recursively call resolve()
on them! There is some additional checks to see if the class should be singleton (shared), then just resolve one instance and keep resolving that same class. Then we just fire some additional events, and we're done!
I have just updated the series with part 3, so take a look at it!