Behaviors
A Behavior<T> defines how an actor processes messages. Behaviors are immutable value objects -- when an actor handles a message, it returns a new behavior that will be used for the next message. This model enables actors to change their message-processing logic over time without mutable state.
Behavior
Behavior<T> is a final readonly class with several static factory methods. The template parameter T represents the message protocol the actor handles.
Behavior::receive
The primary way to define a behavior. The closure receives the ActorContext and the message, and returns the next Behavior.
use Monadial\Nexus\Core\Actor\ActorContext;
use Monadial\Nexus\Core\Actor\Behavior;
readonly class Greet
{
public function __construct(public string $name) {}
}
/** @var Behavior<Greet> */
$behavior = Behavior::receive(
static function (ActorContext $ctx, Greet $msg): Behavior {
$ctx->log()->info("Hello, {$msg->name}!");
return Behavior::same();
},
);
Signature:
/**
* @template U of object
* @param \Closure(ActorContext<U>, U): Behavior<U> $handler
* @return Behavior<U>
*/
public static function receive(Closure $handler): self;
Behavior::withState
Creates a stateful behavior. The closure receives the context, message, and current state, and returns a BehaviorWithState that carries the updated state.
use Monadial\Nexus\Core\Actor\BehaviorWithState;
readonly class Increment {}
readonly class Decrement {}
/** @var Behavior<Increment|Decrement> */
$behavior = Behavior::withState(0, static function (
ActorContext $ctx,
object $msg,
int $count,
): BehaviorWithState {
return match (true) {
$msg instanceof Increment => BehaviorWithState::next($count + 1),
$msg instanceof Decrement => BehaviorWithState::next($count - 1),
default => BehaviorWithState::same(),
};
});
Signature:
/**
* @template U of object
* @template S
* @param S $initialState
* @param \Closure(ActorContext<U>, U, S): BehaviorWithState<U, S> $handler
* @return Behavior<U>
*/
public static function withState(mixed $initialState, Closure $handler): self;
Behavior::setup
Runs an initialization function before the actor starts processing messages. The factory closure receives the context and returns the behavior the actor will use. This is the right place to spawn children, start timers, or perform other setup work.
use Monadial\Nexus\Core\Actor\Props;
use Monadial\Nexus\Core\Duration;
readonly class Tick {}
$behavior = Behavior::setup(function (ActorContext $ctx): Behavior {
// Spawn a child during initialization
$child = $ctx->spawn(Props::fromBehavior($childBehavior), 'worker');
$ctx->watch($child);
// Start a periodic timer
$ctx->scheduleRepeatedly(
Duration::seconds(0),
Duration::seconds(10),
new Tick(),
);
return Behavior::receive(
static fn (ActorContext $c, object $msg): Behavior => Behavior::same(),
);
});
Signature:
/**
* @template U of object
* @param \Closure(ActorContext<U>): Behavior<U> $factory
* @return Behavior<U>
*/
public static function setup(Closure $factory): self;
Behavior::same
Tells the actor system to keep the current behavior unchanged. Use this when a message does not require a behavior change.
return Behavior::same();
Behavior::stopped
Tells the actor system to stop this actor. The actor will process its PostStop signal and then terminate.
readonly class Shutdown {}
$behavior = Behavior::receive(
static fn (ActorContext $ctx, object $msg): Behavior => match (true) {
$msg instanceof Shutdown => Behavior::stopped(),
default => Behavior::same(),
},
);
Behavior::unhandled
Signals that the actor does not handle this particular message. The message is forwarded to dead letters.
$behavior = Behavior::receive(
static fn (ActorContext $ctx, object $msg): Behavior => match (true) {
$msg instanceof SupportedMessage => Behavior::same(),
default => Behavior::unhandled(),
},
);
Behavior::empty
Creates a behavior with no handler. Useful as a placeholder or for actors that only respond to signals.
$behavior = Behavior::empty();
Signal handling
Signals are lifecycle events delivered to an actor outside the normal message flow. Attach a signal handler to any behavior using onSignal(). The method returns a new behavior (the original is unchanged, since behaviors are immutable).
use Monadial\Nexus\Core\Lifecycle\Signal;
use Monadial\Nexus\Core\Lifecycle\PostStop;
use Monadial\Nexus\Core\Lifecycle\Terminated;
use Monadial\Nexus\Core\Lifecycle\PreStart;
$behavior = Behavior::receive(
static fn (ActorContext $ctx, object $msg): Behavior => Behavior::same(),
)->onSignal(
static function (ActorContext $ctx, Signal $signal): Behavior {
return match (true) {
$signal instanceof PostStop => handlePostStop($ctx),
$signal instanceof Terminated => handleTerminated($ctx, $signal),
default => Behavior::same(),
};
},
);
Signature:
/**
* @param \Closure(ActorContext<T>, Signal): Behavior<T> $handler
* @return Behavior<T>
*/
public function onSignal(Closure $handler): self;
Built-in signal types:
| Signal | When it fires |
|---|---|
PreStart | After the actor is created, before it processes any messages |
PostStop | After the actor has stopped |
PreRestart | Before the actor restarts due to a failure |
PostRestart | After the actor restarts |
Terminated | When a watched actor stops (carries the stopped actor's ActorRef) |
ChildFailed | When a child actor fails with an exception |
BehaviorWithState
BehaviorWithState<T, S> is the return type of stateful behavior handlers. It is a final readonly class that tells the actor system what to do with both the behavior and the state after processing a message.
BehaviorWithState::next
Keep the current behavior, update the state to a new value.
// State was 5, now it becomes 6
return BehaviorWithState::next($count + 1);
BehaviorWithState::same
Keep both the current behavior and the current state unchanged.
return BehaviorWithState::same();