Back to Home

Concurrency in Laravel with TrueAsync — technical guide

The article describes the integration of Laravel with TrueAsync to achieve true concurrency. Mechanisms of contextual isolation, adaptation of stateful services, and work with PDO Pool are discussed in detail. The solution allows handling hundreds of requests in one thread without Swoole.

TrueAsync instead of Swoole: concurrency architecture in Laravel
Advertisement 728x90

# TrueAsync in Laravel: Architecture for True Concurrency Without Swoole

Traditional solutions for asynchrony in PHP, such as Swoole or RoadRunner, require deep code modifications and the introduction of colored functions. TrueAsync offers a fundamentally different approach—modifying the Zend core for transparent asynchrony. We'll break down how to adapt Laravel to this model, overcoming issues with stateful services and enabling concurrent handling of I/O-bound operations.

TrueAsync Basics: How Transparent Asynchrony Works

TrueAsync is an alternative PHP core that modifies the Zend Engine, I/O libraries, and database operations. Its key advantage is eliminating function coloring problems. Unlike Node.js or Python asyncio, standard PHP functions (PDO::query(), file_get_contents()) automatically yield control to the scheduler during I/O waits. Coroutines suspend at the C-function level, allowing hundreds of requests to be handled in a single thread.

Important to understand: this isn't multithreading. The model relies on cooperative multitasking—context switching while awaiting external resources. It's inefficient for CPU-bound tasks (heavy computations) but ideal for typical web scenarios involving database and API calls. Here's a basic usage example:

Google AdInline article slot
use function Async\spawn;
use function Async\await;
use function Async\delay;

$a = spawn(function() {
    delay(100);
    return 'result A';
});

$b = spawn(function() {
    delay(100);
    return 'result B';
});

// Obschee time vypolneniya ~100ms
 echo await($a); 
echo await($b);

The Problem of Stateful Services in Laravel

Laravel was originally designed for the FPM model, where each request is handled in an isolated process. Stateful services (AuthManager, Session, Router) store state in object properties, which are destroyed after request processing. In async mode, the process lives indefinitely, leading to state leaks:

// Coroutine A: request from user_1
Auth::loginUsingId(1);
delay(200);

// Coroutine B: request from user_2  
Auth::loginUsingId(2);

// Return in korutinu A
echo Auth::id(); // Vozvraschaet 2 vmesto 1

Static properties (Model::$dispatcher, Facade::$resolvedInstance) and services in the IoC container become shared across all requests. Octane addresses this by cloning the container, but that introduces overhead and doesn't cover static properties.

Contextual Isolation: The Solution Architecture

TrueAsync provides current_context()—a data store tied to the request lifecycle. When a coroutine completes, the context is automatically cleared. The architecture divides Laravel into two zones:

Google AdInline article slot
  • Static core: router, config, IoC container. Loaded once, shared across all requests.
  • Per-request services: Auth, Session, Cookie. Created in each coroutine's context.

The key mechanism is intercepting service resolution via AsyncApplication::resolve():

protected function resolve($abstract, ...)
{
    if ($this->asyncMode && $this->isScoped($abstract)) {
        $ctx = current_context();
        
        if ($instance = $ctx->find($abstract)) {
            return $instance;
        }
        
        $instance = $this->buildFresh($abstract);
        $ctx->set($abstract, $instance);
        return $instance;
    }
    
    return parent::resolve($abstract, ...);
}

Contextual Keys via Enum and the Facade Solution

To avoid collisions and ensure safe access, enum keys are used:

enum ScopedService: string
{
    case REQUEST     = 'request';
    case SESSION     = 'session';
    case AUTH        = 'auth';
    case AUTH_DRIVER = 'auth.driver';
    case COOKIE      = 'cookie';
}

This provides:

Google AdInline article slot
  • Access isolation (only the enum owner can read/write data)
  • Collision-free guarantees (even with identical string values)
  • Static analysis support (PHPStan/IDE track usage)

Facades required a separate solution due to caching resolvedInstance in a static array. Enter ScopedServiceProxy:

class ScopedServiceProxy
{
    public function __call($method, $args)
    {
        return ($this->resolver)()->$method(...$args);
    }
}

The proxy forwards calls to the current coroutine's context while maintaining cache at the proxy level.

Critical Adaptations and PDO Pool

After basic isolation, services storing per-request state internally were identified, requiring specialized async versions:

  • AsyncRouter (current route isolation)
  • AsyncDispatcher (deferred per-coroutine events)
  • AsyncTranslator (translation locale)
  • AsyncViewFactory (View::share())
  • AsyncConfig (runtime config changes)
  • AsyncDatabaseSessionHandler (atomic session writes)
  • Async*Connection (transaction counters)

Special attention went to database handling. Without a connection pool, concurrent coroutine queries break the PostgreSQL protocol. TrueAsync solves this with a built-in PDO Pool:

$pdo = new PDO($dsn, $user, $password, [
    PDO::ATTR_POOL_ENABLED              => true,
    PDO::ATTR_POOL_MIN                  => 0,
    PDO::ATTR_POOL_MAX                  => 10,
    PDO::ATTR_POOL_HEALTHCHECK_INTERVAL => 30,
]);

The pool dynamically allocates connections: after a SELECT result, the connection returns to the pool, serving more coroutines than the pool's max size. For transactions, the connection is locked until commit/rollback.

Key Takeaways

  • TrueAsync eliminates function coloring via PHP core modifications
  • Laravel's stateful services require contextual isolation via current_context()
  • PDO Pool is essential for correct database operation in concurrent environments
  • Enum keys ensure safety and traceability of contextual data
  • The solution doesn't replace multithreading for CPU-bound tasks

— Editorial Team

Advertisement 728x90

Read Next