The Integration of Laravel with Swoole (Part 2)

In this article we're going to talk about how we can integrate Laravel with Swoole's structure in different solutions and their pros and cons.

Integration Solutions

Let's define our goals first:

  • The integration solution should be friendly for current Laravel developers. It has to be easy enough for them to use it.
  • Zero hard-code modifications for core Laravel code.
  • Performance must be enhanced at least 5x faster.

To achieve our goals, we decide to make our solution packed as a Laravel package. Developers only need to install this package, and they're good to go.

I list four different solutions below:

  1. Only use Swoole like PHP-FPM.
  2. Preload and share single Laravel application.
  3. Reset necessary classes/variables based on number 2.
  4. Build sandbox app for request process based on number 3.

The code below is only for explanation. Don't use them in production directly.

Only Use Swoole like PHP-FPM

The simplest solution is to use Swoole like PHP-FPM.

public function onRequest($swooleRequest, $swooleResponse)  
{
    $app = require $this->basePath . 'bootstrap/app.php';
    $kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);

    $illuminateRequest = Request::transform($swooleRequest);
    $illuminateResponse = $kernel->handle($illuminateRequest);

    $swooleResponse->status($illuminateResponse->getStatusCode());
    $swooleResponse->end($illuminateResponse->getContent());

    $kernel->terminate($illuminateRequest, $illuminateResponse);
}

Once the request is received by Swoole's reactor, it will require the Laravel application and handle the request in onRequst callback function.

However, obviously, it's simple yet it's the most ineffective solution. Because we still need to require files when we need to process the request every time.

Swoole's request has its own format, so we need to transform the original request into illuminate request for Laravel.

Preload And Share Single Laravel Application

To reuse the created resource more effectively, we can make Laravel app singleton. By that way all the requests can share one app container without additional I/O consumption.

And the lifecycle will then become what it shows below:

In this lifecycle, all the bootstrap and init stuffs will only be executed at the first time. The rest of next coming requests will reuse the created Laravel app to handle the request directly.

Everything looks great but is this the fact?

Now assume we have some protected resources. Users need to authenticate before accessing them. User A authenticates first and access the resource. What will happen to user B if he tries to access the same protected resource? Will he be redirected to Login page?

Session is the default driver for authentication in Laravel, so let's take a look at the partial code in SessionGuard.php.

public function user()  
{
    if (! is_null($this->user)) {
        return $this->user;
    }

    $id = $this->session->get($this->getName());

    // skip unimportant code...

As what you see, user will be cached once it has been resolved by the session guard. And the auth and auth.driver instances are all registered as singletons in auth service provider. So when user B tries to get user via auth()->user(), he will get the same result as user A even if he's not authenticated.

protected function registerAuthenticator()  
{
    $this->app->singleton('auth', function ($app) {
        $app['auth.loaded'] = true;
        return new AuthManager($app);
    });

    $this->app->singleton('auth.driver', function ($app) {
        return $app['auth']->guard();
    });
}

Because Laravel app is now stored in the memory and all the singleton resources in Laravel app won't be reset unless you do it manually.

Reset Necessary Classes/Variables

To avoid getting wrong results from the app container, we need to reset the affected singleton class before using them like below:

public function onRequest($swooleRequest, $swooleResponse)  
{
    $app = $this->getApplication();
    $kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);

    $app->forget('auth');
    Facade::clearResolvedInstance('auth');

    $app->forget('request');
    Facade::clearResolvedInstance('request');

    $app->forget('session');
    Facade::clearResolvedInstance('session');

    // skip unimportant code...
}

All the static and global properties will also be kept in the memory.

Developers should keep away from using them because there's no way to clean those declared static or global variables. You can only reset them individually.

Nevertheless, it's impossible for developers to reset every affected singleton instances by themselves. It's a strenuous and tedious job.

Build Sandbox App for Request Process

In order to solve the problem of variable pollutions in Laravel app container, if we design a sandbox app container that variables in different requests can be isolated, singleton classes will no more be affected each other.

This is also the final solution I choose to apply in swooletw/laravel-swoole package. The implementation details of the sandbox will be introduced in the next article.