Concurrent PHP,
done right.
Type-safe actors, supervision trees, event sourcing, multi-worker scaling, pluggable runtimes. Erlang/OTP and Akka patterns — in the PHP you already know.
composer require nexus-actors/nexus // Messages are plain readonly classes
readonly class Increment {}
// Stateful actor: receives messages, returns next state
$counter = Behavior::withState(0, function (
ActorContext $ctx, object $msg, int $count,
): BehaviorWithState {
if ($msg instanceof Increment) {
$ctx->log()->info("count: " . ($count + 1));
return BehaviorWithState::next($count + 1);
}
return BehaviorWithState::same();
});
// Create system, spawn actor, send messages
$system = ActorSystem::create('app', new FiberRuntime());
$ref = $system->spawn(Props::fromBehavior($counter), 'counter');
$ref->tell(new Increment());
$ref->tell(new Increment());
$ref->tell(new Increment());
$system->run();Why Nexus, not the alternatives?
Every other PHP concurrency approach leaves critical gaps. Nexus closes them all.
| Capability | Queue workers | Plain Fibers | Threads ext | Nexus |
|---|---|---|---|---|
| Typed messages | ✗ | ✗ | ✗ | ✓ |
| Supervision trees | ✗ | ✗ | ✗ | ✓ |
| Cross-worker messaging | partial | ✗ | partial | ✓ |
| Deterministic testing | ✗ | ✗ | ✗ | ✓ |
| Event sourcing | ✗ | ✗ | ✗ | ✓ |
| Backpressure / mailbox limits | partial | ✗ | ✗ | ✓ |
| Runtime swap (dev → prod) | ✗ | ✗ | ✗ | ✓ |
Anatomy of an actor.
Three files. Complete working example.
1<?php23declare(strict_types=1);45namespace App\Order\Actor;67use App\Order\Message\PlaceOrder;8use App\Order\Message\OrderPlaced;9use Monadial\Nexus\Core\Actor\ActorContext;10use Monadial\Nexus\Core\Actor\Behavior;1112final class OrderActor13{14 public static function behavior(): Behavior15 {16 return Behavior::receive(17 static function (ActorContext $ctx, object $msg): Behavior {18 if ($msg instanceof PlaceOrder) {19 $ctx->log()->info('order received', ['id' => $msg->orderId]);2021 $msg->replyTo->tell(22 new OrderPlaced($msg->orderId),23 );24 }2526 return Behavior::same();27 },28 );29 }30}HTTP, the actor way.
Plug an actor system into PSR-15. Requests become messages; responses come back through ask.
1<?php23declare(strict_types=1);45namespace App\Order;67use App\Order\Message\PlaceOrder;8use App\Order\Message\OrderPlaced;9use Monadial\Nexus\Core\Actor\ActorRef;10use Monadial\Nexus\Http\Handler\Attribute\FromBody;11use Monadial\Nexus\Http\Routing\Attribute\Route;12use Monadial\Nexus\Runtime\Duration;13use Monadial\Nexus\Http\Response\JsonResponse;1415#[Route('POST', '/orders')]16final readonly class OrderController17{18 public function __construct(19 private ActorRef $orders,20 ) {}2122 public function __invoke(23 #[FromBody] PlaceOrderRequest $body,24 ): JsonResponse {25 /** @var OrderPlaced $result */26 $result = $this->orders->ask(27 new PlaceOrder(orderId: $body->id, items: $body->items),28 Duration::seconds(2),29 )->await();3031 return JsonResponse::ok(['status' => 'placed', 'id' => $result->orderId]);32 }33}Event sourcing, built in.
Persist events, not state. Recovery is automatic. Snapshots and retention are policy, not boilerplate.
1<?php23declare(strict_types=1);45namespace App\Order\Actor;67use App\Order\Event\OrderPlaced;8use App\Order\Event\OrderShipped;9use App\Order\Message\PlaceOrder;10use App\Order\Message\ShipOrder;11use App\Order\State\OrderState;12use Monadial\Nexus\Core\Actor\ActorContext;13use Monadial\Nexus\Persistence\PersistenceId;14use Monadial\Nexus\Persistence\EventSourced\EventSourcedBehavior;15use Monadial\Nexus\Persistence\EventSourced\Effect;16use Monadial\Nexus\Persistence\EventSourced\SnapshotStrategy;1718final class OrderActor19{20 public static function behavior(string $orderId): EventSourcedBehavior21 {22 return EventSourcedBehavior::create(23 persistenceId: PersistenceId::of('Order', $orderId),24 emptyState: OrderState::empty(),25 commandHandler: static function (OrderState $state, ActorContext $ctx, object $cmd): Effect {26 return match (true) {27 $cmd instanceof PlaceOrder => Effect::persist(28 new OrderPlaced($cmd->orderId, $cmd->items),29 ),30 $cmd instanceof ShipOrder => Effect::persist(31 new OrderShipped($cmd->orderId, $cmd->tracking),32 ),33 };34 },35 eventHandler: static fn (OrderState $state, object $event): OrderState => match (true) {36 $event instanceof OrderPlaced => $state->placed($event->items),37 $event instanceof OrderShipped => $state->shipped($event->tracking),38 },39 )->withSnapshotStrategy(SnapshotStrategy::everyN(50));40 }41}Every message leaves a trace.
OpenTelemetry traces, RED metrics, and trace-correlated logs — propagated across every actor, HTTP request, and worker thread. Off by default, zero overhead when disabled.
1<?php23declare(strict_types=1);45use Monadial\Nexus\App\NexusApp;6use Monadial\Nexus\Observability\Config\ObservabilityConfig;7use Monadial\Nexus\Observability\Otel\ObservabilityFactory;8use Monadial\Nexus\Runtime\Swoole\SwooleRuntime;910// One call wires traces, metrics, and logs into every actor.11NexusApp::create('payments')12 ->withObservability(ObservabilityFactory::fromConfig(13 ObservabilityConfig::fromEnv($_SERVER),14 ))15 ->actor('payments', Props::fromBehavior($paymentBehavior))16 ->run(new SwooleRuntime());1718// No call to withObservability()? A no-op provider — zero overhead.Security by isolation.
Actor patterns eliminate entire classes of vulnerability. Type checking enforces the contract.
No shared mutable state
Actors cannot corrupt each other's data. TOCTOU races and data races are removed at the language level — not by convention.
Explicit overflow policy
Mailbox capacity and overflow strategy are explicit configuration. DoS-by-flooding becomes a tunable policy decision, not an incident.
Single-writer guarantee
Each ActorSystem owns a unique ULID. Persistence stores reject foreign writes. Split-brain corruption cannot happen silently.
Verified by Psalm
Custom Psalm rules enforce readonly messages and immutable actor state. Security invariants are checked at analysis time, not runtime.
One codebase, three runtimes.
Same actor logic. Swap one line to change how it runs.
1use Monadial\Nexus\Core\Actor\ActorSystem;2use Monadial\Nexus\Runtime\Fiber\FiberRuntime;34// Cooperative multitasking — zero extensions required.5// Each actor runs in its own PHP Fiber. Fibers suspend6// when waiting for messages and resume on delivery.78$system = ActorSystem::create('app', new FiberRuntime());9$ref = $system->spawn($props, 'orders');10$ref->tell(new PlaceOrder('ORD-1'));11$system->run(); // blocks; fibers cooperate insideHow it fits together.
One model. Three runtimes. Production-ready supervision.
Try it in five minutes.
Install, create demo.php, and run it. That's the whole setup.
composer require nexus-actors/nexus