Phluxor actors can change their behavior at any time, enabling the implementation of a State Machine.
Let’s model the audio player as an example.
<?php
declare(strict_types=1);
namespace Example;
use Phluxor\ActorSystem\Behavior;
use Phluxor\ActorSystem\Message\ActorInterface;
class AudioPlayer implements ActorInterface
{
private Behavior $behavior;
public function __construct()
{
$this->behavior = new Behavior();
}
}
Implement the Phluxor\ActorSystem\Message\ActorInterface
and call the receive
method of the Phluxor\ActorSystem\Behavior
class inside the actor’s receive method.
<?php
declare(strict_types=1);
namespace Example;
use Phluxor\ActorSystem\Behavior;
use Phluxor\ActorSystem\Message\ActorInterface;
class AudioPlayer implements ActorInterface
{
private Behavior $behavior;
public function __construct()
{
$this->behavior = new Behavior();
}
public function receive(ContextInterface $context): void
{
$this->behavior->receive($context);
}
|
The message objects used in this example are the following three
<?php
declare(strict_types=1);
namespace Example\Message;
class PowerOn
{
}
<?php
declare(strict_types=1);
namespace Example\Message;
class PowerOff
{
}
<?php
declare(strict_types=1);
namespace Example\Message;
class Touch
{
}
To change an actor’s behavior, three methods are used:
become: Replaces the current behavior with the one passed in, replacing the default receive method.
becomeStacked: Stacks the new behavior while retaining the previous one.
unbecomeStacked: Reverts to the previous behavior.
As the initial state of the audio player, let’s set the default behavior to Power Off.
<?php
declare(strict_types=1);
namespace Example;
use Phluxor\ActorSystem\Behavior;
use Phluxor\ActorSystem\Context\ContextInterface;
use Phluxor\ActorSystem\Message\ActorInterface;
use Phluxor\ActorSystem\Message\ReceiveFunction;
class AudioPlayer implements ActorInterface
{
private Behavior $behavior;
public function __construct()
{
$this->behavior = new Behavior();
$this->behavior->become(
new ReceiveFunction(
fn(ContextInterface $context) => $this->off($context)
)
);
}
}
When in the Power Off state, touching the player will not play music.
A message saying “doing nothing” will be output.
private function off(ContextInterface $context): void
{
$message = $context->message();
switch (true) {
case $message instanceof PowerOn:
$context->logger()->info('Powering on');
$this->behavior->become(
new ReceiveFunction(fn(ContextInterface $context) => $this->on($context))
);
break;
case $message instanceof Touch:
$context->logger()->info('doing nothing');
break;
}
}
When in the Power On state, touching the player will play music.
private function on(ContextInterface $context): void
{
$message = $context->message();
switch (true) {
case $message instanceof PowerOff:
$context->logger()->info('Powering off');
$this->behavior->become(
new ReceiveFunction(fn(ContextInterface $context) => $this->off($context))
);
break;
case $message instanceof Touch:
$context->logger()->info('starting to play');
break;
}
}
If the power is turned off again, the same “doing nothing” message will be output as before.
The complete implementation example is as follows.
<?php
declare(strict_types=1);
namespace Example;
use Example\Message\PowerOff;
use Example\Message\PowerOn;
use Example\Message\Touch;
use Phluxor\ActorSystem\Behavior;
use Phluxor\ActorSystem\Context\ContextInterface;
use Phluxor\ActorSystem\Message\ActorInterface;
use Phluxor\ActorSystem\Message\ReceiveFunction;
class AudioPlayer implements ActorInterface
{
private Behavior $behavior;
public function __construct()
{
$this->behavior = new Behavior();
$this->behavior->become(
new ReceiveFunction(
fn(ContextInterface $context) => $this->off($context)
)
);
}
public function receive(ContextInterface $context): void
{
$this->behavior->receive($context);
}
private function off(ContextInterface $context): void
{
$message = $context->message();
switch (true) {
case $message instanceof PowerOn:
$context->logger()->info('Powering on');
$this->behavior->become(
new ReceiveFunction(fn(ContextInterface $context) => $this->on($context))
);
break;
case $message instanceof Touch:
$context->logger()->info('doing nothing');
break;
}
}
private function on(ContextInterface $context): void
{
$message = $context->message();
switch (true) {
case $message instanceof PowerOff:
$context->logger()->info('Powering off');
$this->behavior->become(
new ReceiveFunction(fn(ContextInterface $context) => $this->off($context))
);
break;
case $message instanceof Touch:
$context->logger()->info('starting to play');
break;
}
}
}
Run the following code as the entry point.
<?php
declare(strict_types=1);
use Example\AudioPlayer;
use Example\Message\PowerOff;
use Example\Message\PowerOn;
use Example\Message\Touch;
use Phluxor\ActorSystem;
use Phluxor\ActorSystem\Props;
use function Swoole\Coroutine\run;
require_once __DIR__ . '/vendor/autoload.php';
run(function () {
\Swoole\Coroutine\go(function () {
$system = ActorSystem::create();
$ref = $system->root()->spawn(
Props::fromProducer(fn() => new AudioPlayer())
);
$system->root()->send($ref, new Touch());
$system->root()->send($ref, new PowerOn());
$system->root()->send($ref, new Touch());
$system->root()->send($ref, new PowerOff());
$system->root()->send($ref, new Touch());
});
});
The output will be as follows.
There may be cases where you want to process certain messages regardless of the current behavior.
Let’s consider what happens if you mosh while listening to music.
The audio player might get stepped on and break.
This can be handled by processing specific messages like the following before delegating to the Phluxor\ActorSystem\Behavior
class.
Let’s represent mosh using the following messages as an example.
<?php
declare(strict_types=1);
namespace Example\Message;
class Moshing
{
}
<?php
declare(strict_types=1);
namespace Example\Message;
class ReplaceAudioPlayer
{
}
The implementation is as follows.
public function receive(ContextInterface $context): void
{
$message = $context->message();
if ($message instanceof Moshing) {
$context->respond('moshing');
$this->behavior->become(
new ReceiveFunction(fn(ContextInterface $context) => $this->moshed($context))
);
}
$this->behavior->receive($context);
}
private function moshed(ContextInterface $context): void
{
$message = $context->message();
switch (true) {
case $message instanceof PowerOn:
case $message instanceof PowerOff:
$context->logger()->info('broken');
break;
case $message instanceof Touch:
$context->logger()->info('broken!!!');
break;
case $message instanceof ReplaceAudioPlayer:
$this->behavior->become(
new ReceiveFunction(fn(ContextInterface $context) => $this->off($context))
);
break;
}
}
The complete implementation example is as follows.
<?php
declare(strict_types=1);
namespace Example;
use Example\Message\Moshing;
use Example\Message\PowerOff;
use Example\Message\PowerOn;
use Example\Message\ReplaceAudioPlayer;
use Example\Message\Touch;
use Phluxor\ActorSystem\Behavior;
use Phluxor\ActorSystem\Context\ContextInterface;
use Phluxor\ActorSystem\Message\ActorInterface;
use Phluxor\ActorSystem\Message\ReceiveFunction;
class AudioPlayer implements ActorInterface
{
private Behavior $behavior;
public function __construct()
{
$this->behavior = new Behavior();
$this->behavior->become(
new ReceiveFunction(
fn(ContextInterface $context) => $this->off($context)
)
);
}
public function receive(ContextInterface $context): void
{
$message = $context->message();
if ($message instanceof Moshing) {
$context->respond('moshing');
$this->behavior->become(
new ReceiveFunction(fn(ContextInterface $context) => $this->moshed($context))
);
}
$this->behavior->receive($context);
}
private function off(ContextInterface $context): void
{
$message = $context->message();
switch (true) {
case $message instanceof PowerOn:
$context->logger()->info('Powering on');
$this->behavior->become(
new ReceiveFunction(fn(ContextInterface $context) => $this->on($context))
);
break;
case $message instanceof Touch:
$context->logger()->info('doing nothing');
break;
}
}
private function on(ContextInterface $context): void
{
$message = $context->message();
switch (true) {
case $message instanceof PowerOff:
$context->logger()->info('Powering off');
$this->behavior->become(
new ReceiveFunction(fn(ContextInterface $context) => $this->off($context))
);
break;
case $message instanceof Touch:
$context->logger()->info('starting to play');
break;
}
}
private function moshed(ContextInterface $context): void
{
$message = $context->message();
switch (true) {
case $message instanceof PowerOn:
case $message instanceof PowerOff:
$context->logger()->info('broken');
break;
case $message instanceof Touch:
$context->logger()->info('broken!!!');
break;
case $message instanceof ReplaceAudioPlayer:
$this->behavior->become(
new ReceiveFunction(fn(ContextInterface $context) => $this->off($context))
);
break;
}
}
}
Run the following code as the entry point.
<?php
declare(strict_types=1);
use Example\AudioPlayer;
use Example\Message\Moshing;
use Example\Message\PowerOff;
use Example\Message\PowerOn;
use Example\Message\ReplaceAudioPlayer;
use Example\Message\Touch;
use Phluxor\ActorSystem;
use Phluxor\ActorSystem\Props;
use function Swoole\Coroutine\run;
require_once __DIR__ . '/vendor/autoload.php';
run(function () {
\Swoole\Coroutine\go(function () {
$system = ActorSystem::create();
$ref = $system->root()->spawn(
Props::fromProducer(fn() => new AudioPlayer())
);
$system->root()->send($ref, new Touch());
$system->root()->send($ref, new PowerOn());
$system->root()->send($ref, new Touch());
$system->root()->send($ref, new PowerOff());
$future = $system->root()->requestFuture($ref, new Moshing(), 1);
$system->getLogger()->info($future->result()->value());
$system->root()->send($ref, new Touch());
$system->root()->send($ref, new PowerOn());
$system->root()->send($ref, new ReplaceAudioPlayer());
$system->root()->send($ref, new PowerOn());
$system->root()->send($ref, new Touch());
});
});
The becomeStacked
method stacks the new behavior while retaining the previous one.
The unbecome
method reverts to the previous behavior.
Let’s implement it as follows:
After moshing, use the becomeStacked
method to replace the broken audio player.
Execute the unbecome
method to go back to the broken audio player. :)
private function moshed(ContextInterface $context): void
{
$message = $context->message();
switch (true) {
case $message instanceof PowerOn:
case $message instanceof PowerOff:
$context->logger()->info('broken');
break;
case $message instanceof Touch:
$context->logger()->info('broken!!!');
break;
case $message instanceof ReplaceAudioPlayer:
$this->behavior->becomeStacked(
new ReceiveFunction(fn(ContextInterface $context) => $this->off($context))
);
$this->behavior->unbecome();
break;
}
}