phalanx/core

Expression-based async coordination for PHP 8.4+

Maintainers

Package info

github.com/havy-tech/phalanx-core

pkg:composer/phalanx/core

Statistics

Installs: 7

Dependents: 4

Suggesters: 0

Stars: 0

Open Issues: 0

v0.6.1 2026-04-07 15:21 UTC

This package is auto-updated.

Last update: 2026-04-26 06:12:53 UTC


README

Phalanx

Phalanx Core - Async PHP

PHP 8.4 changed what's possible. Phalanx is an attempt to make it accessible.

In my view, PHP's async foundations — built over years by the ReactPHP and AMPHP communities — represent some of the most underutilized work in the language's ecosystem. Fibers, property hooks, lazy objects, and asymmetric visibility have quietly made PHP a different language since 8.0. Phalanx is an attempt to bring all of this to bear through a unified coordination layer, using the most powerful tools available in the modern PHP ecosystem — particularly as AI workloads raise the bar on throughput and reactivity.

The framework coordinates async PHP through a centralized scope hierarchy — a single scheduler that manages concurrency, cancellation, and resource cleanup no matter how deeply nested the execution becomes. Developers write named computations. The scope handles the machinery. Every API decision is weighed against the balance between first-class DX and call-site explicitness — a tradeoff influenced by ideas from SICP, Rust's ownership model, .NET's task system, Clojure's value semantics, and Go's structured concurrency.

The design has been iterated on since late 2024, with intensive refinement through mid-2025 and resumed development earlier this year. Every abstraction has been questioned, discarded, and rebuilt — often more than once. Several proof-of-concept applications continue to evolve from their MVP baseline — a multi-agent AI terminal (aisentinel-cli), a subnet scanner, a Twilio control center, a Docker CLI, and a debug dashboard. Some of these will be released as they stabilize, and there are many ideas for what comes next that haven't been started yet.

The project is currently stabilizing through active iteration. Design decisions in one area sometimes require foundational refactoring in others, and some corners of the codebase will fall out of sync with others. This is expected.

Contributions are welcome from those who feel inspired to do so. See the contributing guide for how to get involved.

Substack write up

Table of Contents

Installation

composer require phalanx/core

Requires PHP 8.4+.

Quick Start

<?php

[$app, $scope] = Application::starting()
    ->providers(new AppBundle())
    ->compile()
    ->boot();

$result = $scope->execute(Task::of(static fn(ExecutionScope $s) =>
    $s->service(OrderService::class)->process(42)
));

$scope->dispose();
$app->shutdown();

Note: Service classes like OrderService, UserRepo, DatabasePool in these examples are illustrative. Phalanx Core provides the coordination primitives—your application brings the domain logic.

How It Works

Phalanx's model: Application -> Scope -> Tasks.

Application::starting($context)
    -> compile()           // Validate service graph, create app
    -> startup()           // Run startup hooks, enable shutdown handlers
    -> createScope()       // Create ExecutionScope
    -> execute(Task)       // Run typed tasks
    -> dispose()           // Cleanup scope resources
    -> shutdown()          // Cleanup app resources

Every task implements Scopeable or Executable—single-method interfaces:

<?php

// Tasks needing only service resolution
interface Scopeable {
    public function __invoke(Scope $scope): mixed;
}

// Tasks needing execution primitives (concurrency, cancellation)
interface Executable {
    public function __invoke(ExecutionScope $scope): mixed;
}

Scope Hierarchy

Phalanx decomposes scope into granular capability interfaces:

Scope                 service(), attribute(), trace()
Suspendable           await(PromiseInterface): mixed
Cancellable           isCancelled, throwIfCancelled(), cancellation()
Disposable            onDispose(), dispose()

TaskScope             extends Scope + Suspendable + Cancellable + Disposable
                      execute(), executeFresh()

TaskExecutor          concurrent(), race(), any(), map(), settle()
                      timeout(), retry(), delay(), defer(), singleflight(), inWorker()

ExecutionScope        extends TaskScope + TaskExecutor + StreamContext
Interface Use when...
Scope You only need service resolution (file loaders, middleware)
Suspendable A service needs await() (RedisClient, TwilioRest)
TaskScope You compose tasks and need cancellation/disposal (handlers, middleware chains)
ExecutionScope You orchestrate concurrent operations (scanners, pipelines, deployment tasks)

All fiber suspension goes through $scope->await(). Raw React\Async\await() is only used inside ExecutionLifecycleScope internals.

<?php

// Services type-hint what they actually need
final class RedisClient {
    public function __construct(
        private readonly Client $inner,
        private readonly Suspendable $scope,  // only needs await()
    ) {}

    public function get(string $key): mixed {
        return $this->scope->await($this->inner->__call('get', [$key]));
    }
}

Domain scopes extend ExecutionScope with typed properties:

Scope Package Adds
CommandScope phalanx-console $args, $options, $commandName
RequestScope phalanx-http $request, $params, $query, $body
WsScope phalanx-ws-server $connection, $request

The Task System

Two Ways to Define Tasks

Quick tasks for one-offs:

<?php

$task = Task::of(static fn(ExecutionScope $s) => $s->service(UserRepo::class)->find($id));
$user = $scope->execute($task);

Invokable classes for everything else:

<?php

final readonly class FetchUser implements Scopeable
{
    public function __construct(private int $id) {}

    public function __invoke(Scope $scope): User
    {
        return $scope->service(UserRepo::class)->find($this->id);
    }
}

$user = $scope->execute(new FetchUser(42));

The invokable approach gives you:

  • Traceable: Stack traces show FetchUser::__invoke, not Closure@handler.php:47
  • Testable: Mock the scope, invoke the task, assert the result
  • Serializable: Constructor args are data—queue jobs, distribute across workers
  • Inspectable: The class name is the identity; constructor args are the inputs

Behavior via Interfaces

Tasks declare behavior through PHP 8.4 property hooks:

<?php

final class DatabaseQuery implements Scopeable, Retryable, HasTimeout
{
    public RetryPolicy $retryPolicy {
        get => RetryPolicy::exponential(3);
    }

    public float $timeout {
        get => 5.0;
    }

    public function __invoke(Scope $scope): array
    {
        return $scope->service(Database::class)->query($this->sql);
    }
}

The behavior pipeline applies automatically: timeout wraps retry wraps trace wraps work.

Interface Property Purpose
Retryable RetryPolicy $retryPolicy { get; } Automatic retry with policy
HasTimeout float $timeout { get; } Automatic timeout in seconds
HasPriority int $priority { get; } Priority queue ordering
UsesPool UnitEnum $pool { get; } Pool-aware scheduling
Traceable string $traceName { get; } Custom trace label

Self-Description

Two opt-in interfaces let tasks and components carry human-readable metadata. The execution layer does not read these — they are consumed by tooling that introspects the component graph at registration time.

Interface Property Purpose
SelfDescribed string $description { get; } Human-readable description of what the component does
Tagged list<string> $tags { get; } Classification labels for grouping and filtering

Consumers: OpenAPI generation in phalanx-http uses $description to populate operation summaries. phalanx-ai reads both when registering agent tools. CLI inspection commands use them to build help output.

<?php

final class SummarizeDocument implements Executable, SelfDescribed, Tagged
{
    public string $description {
        get => 'Fetches a document by ID and returns an AI-generated summary.';
    }

    /** @return list<string> */
    public array $tags {
        get => ['ai', 'documents', 'read'];
    }

    public function __construct(private int $documentId) {}

    public function __invoke(ExecutionScope $scope): string
    {
        return $scope->service(Summarizer::class)->summarize($this->documentId);
    }
}

Concurrency Primitives

Method Behavior Returns
concurrent($tasks) Run all concurrently, wait for all Array of results
race($tasks) First to settle (success or failure) Single result
any($tasks) First success (ignores failures) Single result
map($items, $fn, $limit) Bounded concurrency over collection Array of results
settle($tasks) Run all, collect outcomes including failures SettlementBag
timeout($seconds, $task) Run with deadline Result or throws
series($tasks) Sequential execution Array of results
waterfall($tasks) Sequential, passing result forward Final result
<?php

// Concurrent fetch
[$customer, $inventory] = $scope->concurrent([
    new FetchCustomer($customerId),
    new ValidateInventory($items),
]);

// First successful response wins (fallback pattern)
$data = $scope->any([
    new FetchFromPrimary($key),
    new FetchFromFallback($key),
]);

// 10,000 items. 10 concurrent fibers.
$results = $scope->map($items, fn($item) => new ProcessItem($item), limit: 10);

Lazy Sequences

LazySequence processes large datasets through generator-based pipelines. Values flow one at a time—memory stays flat regardless of dataset size.

<?php

use Phalanx\Task\LazySequence;

$seq = LazySequence::from(static function (ExecutionScope $scope) {
    foreach ($scope->service(OrderRepo::class)->cursor() as $order) {
        yield $order;
    }
});

$totals = $seq
    ->filter(fn(Order $o) => $o->total > 100_00)
    ->map(fn(Order $o) => new OrderSummary($o))
    ->take(50)
    ->toArray();

$result = $scope->execute($totals);

Operators (map, filter, take, chunk) are lazy—nothing runs until a terminal (toArray, reduce, first, consume) triggers execution. Two mapping modes handle different workloads:

Method Execution Model
mapConcurrent($fn, $concurrency) Fibers in the current process
mapParallel($fn, $concurrency) Worker processes via IPC

Route Groups

Typed collections of HTTP routes with RouteGroup. Route handlers receive RequestScope—a scope decorator with typed route parameters, query strings, and request body access:

<?php
// routes/api.php

use Phalanx\Http\RouteGroup;

return RouteGroup::of([
    'GET /users'      => ListUsers::class,
    'GET /users/{id}' => ShowUser::class,
    'POST /users'     => CreateUser::class,
]);

Loading Routes

<?php

use Phalanx\Http\Runner;

$runner = Runner::from($app, requestTimeout: 30.0)
    ->withRoutes(__DIR__ . '/routes');
$runner->run('0.0.0.0:8080');

Composing Route Groups

<?php

use Phalanx\Http\RouteGroup;

$api = $publicRoutes
    ->merge($adminRoutes)
    ->mount('/admin', $adminRoutes)
    ->wrap(AuthMiddleware::class);

Command Groups

Typed collections of CLI commands with CommandGroup. Command handlers receive CommandScope—a scope decorator with typed arguments and options:

<?php
// commands/db.php

use Phalanx\Console\CommandConfig;
use Phalanx\Console\CommandGroup;
use Phalanx\Console\Opt;

return CommandGroup::of([
    'migrate' => [RunMigrations::class, new CommandConfig(
        description: 'Run database migrations',
    )],
    'db:seed' => [SeedDatabase::class, new CommandConfig(
        description: 'Seed the database',
        options: [Opt::flag('fresh', 'f', 'Truncate tables first')],
    )],
]);

Running Commands

<?php

use Phalanx\Console\ConsoleRunner;

$runner = ConsoleRunner::withCommands($app, __DIR__ . '/commands');
exit($runner->run($argv));

Services

<?php

use Phalanx\Service\ServiceBundle;
use Phalanx\Service\Services;

class AppBundle implements ServiceBundle
{
    public function services(Services $services, array $context): void
    {
        $services->singleton(DatabasePool::class)
            ->factory(fn() => new DatabasePool($context['db_url']))
            ->onStartup(fn($pool) => $pool->warmUp(5))
            ->onShutdown(fn($pool) => $pool->drain());

        $services->scoped(RequestLogger::class)
            ->lazy()
            ->onDispose(fn($log) => $log->flush());
    }
}
Method Lifecycle
singleton() One instance per application
scoped() One instance per scope, disposed with scope
lazy() Defer creation until first access (PHP 8.4 lazy ghosts)

Cancellation & Retry

<?php

use Phalanx\Concurrency\CancellationToken;
use Phalanx\Concurrency\RetryPolicy;

// Timeout for entire scope
$scope = $app->createScope(CancellationToken::timeout(30.0));

// Task-level timeout
$result = $scope->timeout(5.0, new SlowApiCall($id));

// Retry with exponential backoff
$result = $scope->retry(
    new FetchFromApi($url),
    RetryPolicy::exponential(attempts: 3)
);

// Check cancellation within tasks (use Executable when you need ExecutionScope)
final class LongRunningTask implements Executable
{
    public function __invoke(ExecutionScope $scope): mixed
    {
        foreach ($this->chunks as $chunk) {
            $scope->throwIfCancelled();
            $this->process($chunk);
        }
        return $this->result;
    }
}

Tracing

PHALANX_TRACE=1 php server.php
    0ms  STRT  compiling
    4ms  STRT  startup
    6ms  CON>    concurrent(2)
    7ms  EXEC    FetchCustomer
    8ms  DONE    FetchCustomer  +0.61ms
   19ms  CON<    concurrent(2) joined  +12.8ms

0 svc  4.0MB peak  0 gc  39.8ms total

Authentication

Phalanx provides core auth primitives that transport packages (phalanx/http, phalanx/ws-server) build on.

Guard Interface

Implement Guard to extract identity from a request:

<?php

use Phalanx\Auth\AuthContext;
use Phalanx\Auth\Guard;
use Psr\Http\Message\ServerRequestInterface;

final class BearerTokenGuard implements Guard
{
    public function resolve(ServerRequestInterface $request): ?AuthContext
    {
        $header = $request->getHeaderLine('Authorization');
        if (!str_starts_with($header, 'Bearer ')) {
            return null;
        }

        $user = $this->validateToken(substr($header, 7));
        return $user !== null
            ? AuthContext::authenticated($user, substr($header, 7))
            : null;
    }
}

Identity Interface

Your user model implements Identity:

<?php

use Phalanx\Auth\Identity;

final class AppUser implements Identity
{
    public string|int $id { get => $this->userId; }

    public function __construct(private readonly int $userId) {}
}

AuthContext

The resolved auth state, carrying identity, token, and abilities:

<?php

use Phalanx\Auth\AuthContext;

$auth = AuthContext::authenticated($user, $token, ['admin', 'write']);

$auth->isAuthenticated;      // true
$auth->identity->id;         // user ID
$auth->can('admin');         // true
$auth->token();              // the raw token string

$guest = AuthContext::guest();
$guest->isAuthenticated;     // false

Authenticate Middleware

Use the built-in Authenticate middleware with any Guard:

<?php

use Phalanx\Http\Auth\Authenticate;

$routes = RouteGroup::of([...])->wrap(new Authenticate(new BearerTokenGuard()));

Deterministic Cleanup

<?php

$scope = $app->createScope();
$scope->onDispose(fn() => $connection->close());

// Your task code...

$scope->dispose();  // Cleanup fires in reverse order

Examples

Docker CLI

A practical CLI app demonstrating CommandScope with typed arguments and options, ServiceBundle wiring, and invokable task classes.

php examples/docker-cli/docker-cli.php ps -a
php examples/docker-cli/docker-cli.php images
php examples/docker-cli/docker-cli.php logs nginx