The Integration of Laravel with Swoole (Part 3)

The core concept of Laravel is its IoC container. If you don't know much about Laravel's IoC Container, I highly suggest you read Laravel's Dependency Injection Container in Depth before reading this article.

The Sandbox Container

Basically a sandbox container is just a cloned class from fresh app container. The binding of related instances is mainly determined by resolved, bindings and instances properties. All the requests use their independent sandbox containers to handle their own requests. Every time the worker receives a request, it will get a fresh new sandbox app cloned from clean container.

The clone function here is just native shallow clone in PHP.

Now, even if auth instance has been resolved in request A, auth instance in request B will not get affected because there's no binding record of auth in request B's container, and it will call auth's binding closure function to get a new auth instance.

However, in fact, not all the instances need to be isolated. Some instances can be possibly shared by different requests. Therefore we can make this kind of instances pre-resolved in the original container (where the sandbox app clones from). These instances will not be resolved again in the sandbox app.

Now every time when the worker receives a request from reactor, it will clone a sandbox container from the original app first, and then make this sandbox container handle the request.

Challenges

The design of sandbox container is nearly the perfect solution of what we're looking for. Nonetheless, there are still some challenges we need to overcome.

Redirection of App Container

Our goal is to replace all the original apps with sandbox container while handling a request. But it's difficult to replace all the containers in some shared instances, for example, router.

This is how nested container dependencies exist in router instance. How do you replace all the container bindings in this case, especially when container is private in that class?

We need some hacking skills to substitute these non-public properties. In PHP, there are two ways you can choose to use.

  1. Reflection
  2. Closure::bindTo

Here's the example how you can do it with closure's bindTo method:

protected function rebindRouterContainer(Container $app)  
{
    $router = $app->make('router');
    $request = $this->getRequest();

    $closure = function () use ($app, $request) {
        $this->container = $app;
    };

    $resetRouter = $closure->bindTo($router, $router);
    $resetRouter();
}

This kind of skills are heavily used in this package to replace related container bindings.

Static Mappings

In Laravel you can get an authenticated user in several ways:

  1. app('auth')->user()
  2. app()->make('auth')->user()
  3. Container::getInstance()->make('auth')
  4. Auth::user()

You can get specific instance via app() helper or use Facade. Both of them use static properties to store the mapped app container, so we can use container anywhere we want.

Hence we also need to replace static's container bindings:

Container::setInstance($app);  
Facade::clearResolvedInstances();  
Facade::setFacadeApplication($app);  

Incorrect Apps in Service Providers

Because all the service providers will be only registered and booted at the beginning. Take PaginationServiceProvider in Laravel for instance, request instance in paginator's resolver function is retrieved by $this->app['request'].

public function register()  
{
    Paginator::currentPageResolver(function ($pageName = 'page') {
        $page = $this->app['request']->input($pageName);

        if (filter_var($page, FILTER_VALIDATE_INT) !== false && (int) $page >= 1) {
            return (int) $page;
        }

        return 1;
    });
}

What will we eventually get with $this->app here? The app injected into service providers is the original app instead of sandbox container. However, we only bind request instance to sandbox container after the worker receives a request. So $this->app['request'] will also be empty, and the paginator can't get correct page either.

To fix this kind of issues, this package remains a config for developers to customized their must-to-reload service providers. And the app in these service providers will be automatically replaced by sandbox container.

public function resetProviders(Container $app)  
{
    foreach ($this->providers as $provider) {
        $this->rebindProviderContainer($app, $provider);
        if (method_exists($provider, 'register')) {
            $provider->register();
        }
        if (method_exists($provider, 'boot')) {
            $app->call([$provider, 'boot']);
        }
    }
}

New Lifecycle

This is what new lifecycle looks like. Before sandbox handles the request, some reset logics will be applied for ensuring all the apps we get has been substituted with sandbox container.