Phluxorのアクターはいつでも動作を変更できるため、ステートマシンの実装が簡単に実現できます。
例としてオーディオ プレーヤーをモデル化してみましょう。
<?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();
}
}
Phluxor\ActorSystem\Message\ActorInterface
を実装し、
アクターのreceive
メソッド内で Phluxor\ActorSystem\Behavior
クラスの receive
メソッドを呼び出します。
<?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);
}
|
この例で使用されているメッセージオブジェクトは次の3つです。
<?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
{
}
アクターの動作を変更するには、次の3つのメソッドを使用します:
become: 現在の動作を渡された動作に置き換え、デフォルトの受信メソッドを置き換えます。
becomeStacked: 以前の動作を保持したまま、新しい動作をスタックします。
unbecomeStacked: 以前の動作に戻します。
オーディオ プレーヤーの初期状態として、デフォルトの動作を電源オフに設定しましょう。
<?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)
)
);
}
}
電源オフの状態では、プレーヤーに触れても音楽は再生されません。
「doing nothing(なにもしていません)」というメッセージが出力されます。
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;
}
}
再度電源を切ると、以前と同じ「何もしていません」というメッセージが出力されます。
完全な実装例は次のとおりです。
<?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;
}
}
}
エントリポイントとして次のコードを実行します。
<?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());
});
});
出力は次のようになります。
現在の動作に関係なく、特定のメッセージを処理したい場合があります。
音楽を聴きながらモッシュするとどうなるか考えてみましょう。
オーディオ プレーヤーが踏まれて壊れる場合があります。
これは、Phluxor\ActorSystem\Behavior
クラスに動作を委譲する前に、次のような特定のメッセージを処理することで対処できます。
次のメッセージを例として、モッシュを表現してみましょう。
<?php
declare(strict_types=1);
namespace Example\Message;
class Moshing
{
}
<?php
declare(strict_types=1);
namespace Example\Message;
class ReplaceAudioPlayer
{
}
実装は以下のとおりです。
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;
}
}
動作を確認したいですか?
完全な実装例は次のとおりです。
<?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;
}
}
}
エントリポイントとして次のコードを実行します。
<?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());
});
});
becomeStacked
メソッドは、以前の動作を保持しながら新しい動作をスタックします。
unbecome
メソッドは、以前の動作に戻ります。
以下の実装をしてみましょう。
モッシュのあとに becomeStacked
メソッドを使用して、壊れたオーディオ プレーヤーを置き換えます。
unbecome
メソッドを実行すると、壊れたオーディオ プレーヤーに戻ります。 :)
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;
}
}