Laravel Octane — Bootstrapping the Application and Handling Requests

Updated: Apr 7, 2021 — 6 min Read

In a typical LEMP stack setup, the Laravel application is booted on every new request, container bindings are registered, fresh instances are created, middleware run, your route actions are invoked, and then a response is generated to be sent to the browser.

When running your application under Laravel Octane, one major thing changes; your application is booted only once when the Octane workers start, and that same instance will be used for all requests.

To understand how this works, let's take a look at what happens when an Octane worker starts:

$app = require BASE_PATH . '/bootstrap/app.php'

$app->bootstrapWith([
    LoadEnvironmentVariables::class,
    LoadConfiguration::class,
    HandleExceptions::class,
    RegisterFacades::class,
    SetRequestForConsole::class,
    RegisterProviders::class,
    BootProviders::class,
]);

$app->loadDeferredProviders();

foreach ($app->make('config')->get('octane.warm') as $service) {
    $app->make($service);
}

As you can see, a Laravel application instance is created when a worker starts. This instance is then bootstrapped by doing a few things like loading configuration files, registering facades, and registering the service providers.

At this point all the register and boot methods of non-deferred service providers has been called, which means all your application services are now bound to the container. In other words, the container now knows how to resolve all your application service bindings. If we call app('config') at this point, the container will know how to resolve the configuration repository for you.

Next, calling the loadDeferredProviders() method will ensure all your deferred providers are also registered and booted. Since the application boots only a single time, deferring provider won't have any performance benefits. Octane will load all of them so all services they provide will be bound to the container and can be resolved on any request.

Bindings in the container can be registered as singletons, these special bindings are only going to be resolved once in an application lifetime. The resolved instances will be stored in the container cache and the same instances will be re-used during the lifetime of the application. This gives your application a performance boost since you won't need to construct these instances again and again each time you need to resolve them from the container.

When running on Octane, you should pay special attention to singletons. Since they're only going to be resolved a single time, any state stored in these instances will persist for as long as the Octane server is running.

Instances are resolved by calling $app->resolve('singleton') or $app->make('singleton'). So if you resolve any singletons inside your service providers' boot or register methods. These singletons will persist. Singletons that are resolved while handling requests won't persist tho, they will get constructed on every request when running under Octane. You don't need to worry about those.

In case you want to resolve some singletons while the worker is starting and you don't explicitly resolve them in your service provider, Octane allow you to set an array of services to pre-warm for you. That's what the last lines of code in the example above do:

foreach ($app->make('config')->get('octane.warm') as $service) {
    $app->make($service);
}

Octane loops over these services and resolves them for you. That way they will persist in the container memory for as long as Octane is running.

Handling Requests

Now that we have an application instance with a service container that knows how to resolve all our bindings that also has some pre-resolved (warmed) singletons, we are ready to handle requests.

Here's how Octane handles incoming requests:

$server->on('request', function($request) use ($app){
    $sandbox = clone $app;

    Container::setInstance($sandbox);

    $sandbox->make('events')->dispatch(new RequestReceived);

    $response = $sandbox->make(Kernel::class)->handle($request);

    Container::setInstance($app);

    return $response;
});

Octane clones the original application instance and uses the clone to handle the incoming request. Having a sandbox instance for each request allows Octane to clean some of the services that got mutated during handling a request so the next request receives a clean state.

As we explained earlier, singletons that persist for the lifetime of the Octane server can be useful to boost performance. But our applications cannot tolerate mutations of these instances leaking between requests. For example, the config service can get mutated during a request by calling something like:

app('config')->set('services.aws.key', AWS_KEY_HERE);

Now on handling the next request, the services.aws.key configuration key will still hold the value set by the previous request. Tracking these leaks can be very tricky, specially if they were mutated by third party packages not built by the developer. For that reason, Laravel gives the sandbox app a separate config service that get pruned after every request while keeping the original config service immutable inside the original app instance.

$sandbox->instance('config', clone $sandbox['config']);

The code above shows how Octane gives each sandbox a cloned instance of the original config service.

Now back to how Octane handles requests. Before the sandbox instance is used to handle the request, Octane dispatches a RequestReceived event. Octane listens to this event and performs several steps to prepare the sandbox instance to handle the request.

Here are some of the important things that Octane does when the RequestReceived event is dispatched:

$sandbox->instance('config', clone $sandbox['config']);

$sandbox[Kernel::class]->setApplication($sandbox);

$sandbox['cache']->store('array')->flush();

$sandbox['session']->driver()->flush();
$sandbox['session']->driver()->regenerate();

$sandbox['translator']->setLocale($sandbox['config']['app.locale']);
$sandbox['translator']->setFallback($sandbox['config']['app.fallback_locale']);

$sandbox['auth']->forgetGuards();

$app->instance('request', $request);
$sandbox->instance('request', $request);

First, it clones a clean copy of the configuration repository from the fresh sandbox instance that was just cloned. It then binds this fresh configuration repository to the sandbox application. From now on, any mutation to the configurations should only affect the sandbox.

Next, Octane passed the sandbox instance to several container services. That way when $this->app is called inside these services, the sandbox instance will be returned not the original application instance. Octane does that to several services but we only show the Kernel service in our example.

Next, Octane flushes the array cache store and the session state so they don't persist between requests. It also sets the locales inside the translator to the original locales for the same reason.

After that, Octane deletes all instances of authentication guards so fresh onces are created for each request. This is very important since these guard instances cache the authenticated user inside them and we need to set the user for every request since it could be a different user.

Finally, Octane passes the incoming request instance to the original application instance as well as the sandbox instance. The reason for passing the request to the original application instance is that we want any services that hold reference to the original app instance to still be able to access the request. Even tho passing references to the application instance is highly discouraged by Octane, we still pass the request to handle cases where this happens until all applications and packages can adapt their code.

Things to Consider

Now that we understand how Octane boots our application and uses it to handle several incoming requests, I want to highlight a few gotchas for application developers and package maintainers:

Passing the application instance to services

If a service needs to interact with the application instance and you pass it in the constructor, make sure you use the application instance passed to the callback not the one passed to the service provider:

// Instead of...
$this->app->bind(Service::class, function () {
    return new Service($this->app);
});

// Do...
$this->app->bind(Service::class, function ($app) {
    return new Service($app);
});

The reason is that $this->app holds a reference to the original instance of the application since it was the one used to register the providers on booting. While the sandbox is resolving the service, you want the sandbox instance to be passed to the service constructor not the original instance.

Another option is to not pass the application instance to the constructor and use the app() helper inside the service instead, or Container::getInstance(). These helpers will always hold reference to the sandbox instance.

Passing the application instance to singletons

Don't pass the application instance to a singleton, pass a callback that returns the sandbox instance instead.

// Instead of...
$this->app->singleton(Service::class, function ($app) {
    return new Service($app);
});

// Do...
$this->app->singleton(Service::class, function ($app) {
    return new Service(fn () => Container::getInstance());
});

Singletons can persist between requests, that means the application instance passed when the singleton was resolved the first time will be used when the service is used on every request.

Passing the request instance to singletons

Same as the case with the application instance, pass a callback instead:

// Instead of...
$this->app->singleton(Service::class, function ($app) {
    return new Service($app['request']);
});

// Do...
$this->app->singleton(Service::class, function ($app) {
    return new Service(fn () => Container::getInstance()['request']);
});

Or use the request() helper inside the service and don't inject the request as a dependency.

Passing the configuration repository instance to singletons

Same as the two cases above, pass a callback instead or use the config() helper:

// Instead of...
$this->app->singleton(Service::class, function ($app) {
    return new Service($app['config']);
});

// Do...
$this->app->singleton(Service::class, function ($app) {
    return new Service(fn() => Container::getInstance()['config']);
});

Persisting Singletons

Only singletons that are resolved during the application bootstrapping will persist between requests. Singletons that are resolved during request handling will be registered in the sandbox container, this container is destroyed after handling the request.

To persist singletons between requests, you can either resolve them in your service providers or add them to the warm array inside the Octane configuration file:

'warm' => [
    ...Octane::defaultServicesToWarm(),
    Service::class
],

On the other hand, if you have a package that registers and resolves a singleton inside a service provider and you want to flush that instance before every request, add it to the flush array inside the configuration file:

'flush' => [
    Service::class
],

Octane will remove these singletons from the container after handling each request.

Hey! 👋 If you want to receive updates on what I'm up to, I host a newsletter on my website themsaid.com and would love to have you.

You can also follow me on Twitter, I regularly post about all things Laravel including my latest video tutorials and blog posts.

By Mohamed Said

Hello! I'm a full-stack web developer working at Laravel. In this publication, I share everything I know about Laravel's core, packages, and tools.

You can find me on Twitter and Github.

This site was built using Wink. Follow the RSS Feed.