Parallel Resource Execution Alpha

BEAR.Async enables transparent parallel execution of #[Embed] resources. Embedded resources are fetched in parallel without changing any application code. Resource classes written 10 years ago can benefit from parallel execution just by adding a Module.

Overview

In standard BEAR.Sunday, #[Embed] resources are fetched sequentially. With BEAR.Async, they are fetched in parallel.

[Sequential]                     [Parallel]
Request                          Request
    │                                │
    ├── Embed 1 ──── 50ms            ├── Embed 1 ──┬── 50ms
    ├── Embed 2 ──── 50ms            ├── Embed 2 ──┤
    ├── Embed 3 ──── 50ms            ├── Embed 3 ──┤
    └── Embed 4 ──── 50ms            └── Embed 4 ──┘
    │                                │
Response (200ms)                 Response (50ms)

Design Philosophy

URL as Intent

In BEAR.Sunday, a URI expresses intent, not just a location.

#[Embed(rel: 'profile', src: 'query://self/user_profile{?id}')]

The query://self/user_profile expresses only the intent: “I want the user’s profile information.” This separation of “What” from “How” allows the same code to work in both sync and parallel execution. Debug with Xdebug in development, then switch Module in production to enable parallel execution.

Solving the Function Coloring Problem

Async programming has the “Function Coloring” problem—functions calling async functions must themselves be async, causing “async contamination” throughout the codebase.

In BEAR.Sunday, the “resource” boundary cuts through this problem. No async-specific code is required—resource classes don’t need to know how they were invoked.

Installation

composer require bear/async

Configuration

Choose the appropriate module based on your server environment.

Environment Module Features
PHP-FPM / Apache AsyncParallelModule Uses ext-parallel, requires ZTS PHP
Swoole HTTP Server AsyncSwooleModule Uses coroutines, requires connection pool

AsyncParallelModule

use BEAR\Async\Module\AsyncParallelModule;

class AppModule extends AbstractModule
{
    protected function configure(): void
    {
        $this->install(new AsyncParallelModule(
            namespace: 'MyVendor\MyApp',
            context: 'prod-app',
            appDir: dirname(__DIR__),
        ));
    }
}

AsyncSwooleModule

use BEAR\Async\Module\AsyncSwooleModule;
use BEAR\Async\Module\PdoPoolEnvModule;

class AppModule extends AbstractModule
{
    protected function configure(): void
    {
        $this->install(new AsyncSwooleModule());
        $this->install(new PdoPoolEnvModule('PDO_DSN', 'PDO_USER', 'PDO_PASSWORD'));
    }
}

Swoole coroutines share memory, so PdoPoolEnvModule is required for connection pooling.

Usage

Once the module is installed, existing #[Embed] resources are automatically executed in parallel.

class Dashboard extends ResourceObject
{
    #[Embed(rel: 'user', src: '/user{?id}')]
    #[Embed(rel: 'notifications', src: '/notifications{?user_id}')]
    #[Embed(rel: 'stats', src: '/stats{?user_id}')]
    public function onGet(string $id): static
    {
        $this->body['id'] = $id;
        return $this;
    }
}

By not installing the async module in development and only enabling it in production, you can debug in sync mode and run parallel in production.

BEAR.Projection Integration

BEAR.Projection transforms SQL query results into typed Projection objects and exposes them as resources via the query:// scheme. Combined with #[Embed], multiple SQL queries execute in parallel.

Projection classes are defined as immutable value objects.

final class UserProfile
{
    public function __construct(
        public readonly string $id,
        public readonly string $name,
        public readonly int $age,
        public readonly string $avatarUrl,
    ) {}
}

Factory classes transform raw SQL data into Projections. Dependencies can be injected via DI, enabling business logic like age calculation or URL resolution.

final class UserProfileFactory
{
    public function __construct(
        private readonly AgeCalculator $ageCalculator,
        private readonly ImageUrlResolver $imageResolver,
    ) {}

    public function __invoke(
        string $id,
        string $name,
        string $birthDate,
        string $avatarPath,
    ): UserProfile {
        return new UserProfile(
            id: $id,
            name: $name,
            age: $this->ageCalculator->fromBirthDate($birthDate),
            avatarUrl: $this->imageResolver->resolve($avatarPath),
        );
    }
}

SQL files return columns corresponding to Factory parameter names.

-- var/sql/query/user_profile.sql
SELECT id, name, birth_date, avatar_path FROM users WHERE id = :id

When used with #[Embed], multiple Projections execute in parallel.

class User extends ResourceObject
{
    #[Embed(rel: 'profile', src: 'query://self/user_profile{?id}')]
    #[Embed(rel: 'orders', src: 'query://self/user_orders{?id}')]
    public function onGet(string $id): static
    {
        return $this;
    }
}

SQL Batch Execution

Parallel SQL query execution using mysqli’s native async support is also provided.

use BEAR\Async\Module\MysqliBatchEnvModule;

$this->install(new MysqliBatchEnvModule('MYSQL_HOST', 'MYSQL_USER', 'MYSQL_PASS', 'MYSQL_DB'));
use BEAR\Async\SqlBatch;
use BEAR\Async\SqlBatchExecutorInterface;

class MyService
{
    public function __construct(
        private SqlBatchExecutorInterface $executor,
    ) {}

    public function getData(int $userId): array
    {
        return (new SqlBatch($this->executor, [
            'user' => ['SELECT * FROM users WHERE id = ?', [$userId]],
            'posts' => ['SELECT * FROM posts WHERE user_id = ?', [$userId]],
        ]))();
    }
}

References