Initial commit
This commit is contained in:
		
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
/vendor/
 | 
			
		||||
/.phpunit.*
 | 
			
		||||
/composer.lock
 | 
			
		||||
							
								
								
									
										52
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,52 @@
 | 
			
		||||
# Command Bus for ReactPHP
 | 
			
		||||
 | 
			
		||||
* Can run monolithic (create a bus and use as is), or distributed (create bus and use clients), or a hybrid.
 | 
			
		||||
* All commands called asynchronously using promises and deferreds.
 | 
			
		||||
 | 
			
		||||
## Installing
 | 
			
		||||
 | 
			
		||||
```shell
 | 
			
		||||
$ composer require noccylabs/react-command-bus:^0.1.0
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Usage
 | 
			
		||||
 | 
			
		||||
```php
 | 
			
		||||
// This is enough to setup a local bus.
 | 
			
		||||
$bus = new CommandBus();
 | 
			
		||||
$bus->register('hello', function (Context $context) {
 | 
			
		||||
    return new Promise(function (callable $resolve) use ($context) {
 | 
			
		||||
        return $resolve([ 'message' => "Hello, {$context->name}" ]);
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// You can call it as expected
 | 
			
		||||
$bus->execute('hello', [ 'name' => "Bob" ])
 | 
			
		||||
    ->then(function (array $result) {
 | 
			
		||||
        echo "Result: {$result['message']}\n";
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
// Add a listener, and you can now connect to it!
 | 
			
		||||
$bus->addServer($server);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
// So using this in another script works as expected, if you consider
 | 
			
		||||
// the async flow. See the examples for working examples.
 | 
			
		||||
 | 
			
		||||
$client = new CommandBusClient();
 | 
			
		||||
$client->connect($socket);
 | 
			
		||||
 | 
			
		||||
$client->execute('hello', [ 'name' => "Bob" ])
 | 
			
		||||
    ->then(function (array $result) {
 | 
			
		||||
        echo "Result: {$result['message']}\n";
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
// The bus can also notify all clients about important events
 | 
			
		||||
$bus->notify('updateCompleted', [ 'info' => [] ]);
 | 
			
		||||
 | 
			
		||||
// Listening on the bus or client will yield the event
 | 
			
		||||
$bus->on('notify', function (string $event, array $data) {});
 | 
			
		||||
$client->on('notify', function (string $event, array $data) {});
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
							
								
								
									
										29
									
								
								composer.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								composer.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,29 @@
 | 
			
		||||
{
 | 
			
		||||
    "name": "noccylabs/react-command-bus",
 | 
			
		||||
    "description": "A command bus for ReactPHP applications",
 | 
			
		||||
    "type": "library",
 | 
			
		||||
    "license": "GPL-3.0-or-later",
 | 
			
		||||
    "autoload": {
 | 
			
		||||
        "psr-4": {
 | 
			
		||||
            "NoccyLabs\\React\\CommandBus\\": "src/"
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    "authors": [
 | 
			
		||||
        {
 | 
			
		||||
            "name": "NoccyLabs",
 | 
			
		||||
            "email": "labs@noccy.com"
 | 
			
		||||
        }
 | 
			
		||||
    ],
 | 
			
		||||
    "require": {
 | 
			
		||||
        "react/event-loop": "^1.5",
 | 
			
		||||
        "react/promise": "^3.1",
 | 
			
		||||
        "react/socket": "^1.15",
 | 
			
		||||
        "react/stream": "^1.3",
 | 
			
		||||
        "react/promise-timer": "^1.10",
 | 
			
		||||
        "symfony/uid": "^6.0|^7.0"
 | 
			
		||||
    },
 | 
			
		||||
    "require-dev": {
 | 
			
		||||
        "phpunit/phpunit": "^11.0",
 | 
			
		||||
        "phpstan/phpstan": "^1.10"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										33
									
								
								examples/local.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								examples/local.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,33 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
require_once __DIR__."/../vendor/autoload.php";
 | 
			
		||||
 | 
			
		||||
use NoccyLabs\React\CommandBus\CommandBus;
 | 
			
		||||
use NoccyLabs\React\CommandBus\CommandRegistry;
 | 
			
		||||
use NoccyLabs\React\CommandBus\Context;
 | 
			
		||||
use React\EventLoop\Loop;
 | 
			
		||||
use React\Promise\Promise;
 | 
			
		||||
 | 
			
		||||
$commands = new CommandRegistry();
 | 
			
		||||
$commands->register("hello", function (Context $context) {
 | 
			
		||||
    return new Promise(function (callable $resolve) use ($context) {
 | 
			
		||||
        Loop::addTimer(1, function () use ($context, $resolve) {
 | 
			
		||||
            $resolve("Hello, {$context->name}");
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
$commands->register("hello2", function (Context $context) {
 | 
			
		||||
    return "Hello2, {$context->name}";
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
$bus = new CommandBus($commands);
 | 
			
		||||
 | 
			
		||||
$bus->execute('hello', ['name'=>'Bob'])
 | 
			
		||||
->then(function ($result) {
 | 
			
		||||
    var_dump($result);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
$bus->execute('hello2', ['name'=>'Bob'])
 | 
			
		||||
->then(function ($result) {
 | 
			
		||||
    var_dump($result);
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										45
									
								
								examples/server.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								examples/server.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,45 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
require_once __DIR__."/../vendor/autoload.php";
 | 
			
		||||
 | 
			
		||||
use NoccyLabs\React\CommandBus\CommandBus;
 | 
			
		||||
use NoccyLabs\React\CommandBus\CommandBusClient;
 | 
			
		||||
use NoccyLabs\React\CommandBus\CommandRegistry;
 | 
			
		||||
use NoccyLabs\React\CommandBus\Context;
 | 
			
		||||
use React\Promise\Promise;
 | 
			
		||||
use React\Socket\SocketServer;
 | 
			
		||||
 | 
			
		||||
$commands = new CommandRegistry();
 | 
			
		||||
// Register some function to call. The name here is "hello", and it will
 | 
			
		||||
// receive a Context holding the call context. Any passed data will be
 | 
			
		||||
// available as properties on the Context object.
 | 
			
		||||
$commands->register("hello", function (Context $context) {
 | 
			
		||||
    // You don't have to, but you should return a promise from your 
 | 
			
		||||
    // commands.
 | 
			
		||||
    return new Promise(function (callable $resolve) use ($context) {
 | 
			
		||||
        $resolve("Hello, {$context->name}");
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Create the CommandBus and pass the CommandRegistry
 | 
			
		||||
$bus = new CommandBus($commands);
 | 
			
		||||
 | 
			
		||||
$server = new SocketServer("tcp://127.0.0.1:9999");
 | 
			
		||||
$bus->addServer($server);
 | 
			
		||||
 | 
			
		||||
// The server is sorted, now for the client!
 | 
			
		||||
 | 
			
		||||
$client = new CommandBusClient();
 | 
			
		||||
$client->on(CommandBusClient::EVENT_CONNECTED, 
 | 
			
		||||
    function () use ($client, $bus) {
 | 
			
		||||
        $client->execute('hello', ['name'=>'Bob'])
 | 
			
		||||
        ->then(function ($result) use ($client,$bus) {
 | 
			
		||||
            // Result from the call
 | 
			
		||||
            var_dump($result);
 | 
			
		||||
            // Shut down after receiving the response
 | 
			
		||||
            $bus->close();
 | 
			
		||||
            $client->close();
 | 
			
		||||
        });        
 | 
			
		||||
    }
 | 
			
		||||
);
 | 
			
		||||
$client->connect("tcp://127.0.0.1:9999");
 | 
			
		||||
							
								
								
									
										12
									
								
								phpstan.neon
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								phpstan.neon
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
			
		||||
parameters:
 | 
			
		||||
  level: 5
 | 
			
		||||
    
 | 
			
		||||
  excludePaths:
 | 
			
		||||
    - doc
 | 
			
		||||
    - vendor
 | 
			
		||||
    - tests
 | 
			
		||||
 | 
			
		||||
  # Paths to include in the analysis
 | 
			
		||||
  paths:
 | 
			
		||||
    - src
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										40
									
								
								src/Command.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/Command.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,40 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace NoccyLabs\React\CommandBus;
 | 
			
		||||
 | 
			
		||||
use React\Promise\Deferred;
 | 
			
		||||
use React\Promise\Promise;
 | 
			
		||||
use React\Promise\PromiseInterface;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 *
 | 
			
		||||
 */
 | 
			
		||||
class Command
 | 
			
		||||
{
 | 
			
		||||
    /** @var string $name The command name */
 | 
			
		||||
    private string $name;
 | 
			
		||||
 | 
			
		||||
    /** @var callable $handler The handler */
 | 
			
		||||
    private $handler;
 | 
			
		||||
 | 
			
		||||
    public function __construct(string $name, callable $handler)
 | 
			
		||||
    {
 | 
			
		||||
        $this->name = $name;
 | 
			
		||||
        $this->handler = $handler;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getName(): string
 | 
			
		||||
    {
 | 
			
		||||
        return $this->name;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function call(Context $context): PromiseInterface
 | 
			
		||||
    {
 | 
			
		||||
        return new Promise(function (callable $resolve) use ($context) {
 | 
			
		||||
            $resolve(call_user_func($this->handler, $context));
 | 
			
		||||
            return;
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										115
									
								
								src/CommandBus.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								src/CommandBus.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,115 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace NoccyLabs\React\CommandBus;
 | 
			
		||||
use Evenement\EventEmitterTrait;
 | 
			
		||||
use React\Promise\PromiseInterface;
 | 
			
		||||
use React\Promise\Promise;
 | 
			
		||||
use React\Promise\Deferred;
 | 
			
		||||
use React\Socket\ServerInterface;
 | 
			
		||||
use React\Stream\DuplexStreamInterface;
 | 
			
		||||
use SplObjectStorage;
 | 
			
		||||
 | 
			
		||||
class CommandBus implements CommandBusInterface
 | 
			
		||||
{
 | 
			
		||||
    use EventEmitterTrait;
 | 
			
		||||
 | 
			
		||||
    private CommandRegistry $commandRegistry;
 | 
			
		||||
 | 
			
		||||
    private SplObjectStorage $connections;
 | 
			
		||||
 | 
			
		||||
    private SplObjectStorage $servers;
 | 
			
		||||
 | 
			
		||||
    public function __construct(CommandRegistry $commandRegistry)
 | 
			
		||||
    {
 | 
			
		||||
        $this->commandRegistry = $commandRegistry;
 | 
			
		||||
        $this->connections = new SplObjectStorage();
 | 
			
		||||
        $this->servers = new SplObjectStorage();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function addServer(ServerInterface $server): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->servers->attach($server);
 | 
			
		||||
        $server->on('connection', $this->onServerConnection(...));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function removeServer(ServerInterface $server): void
 | 
			
		||||
    {
 | 
			
		||||
        $server->close();
 | 
			
		||||
        $server->removeListener('connection', $this->onServerConnection(...));
 | 
			
		||||
        $this->servers->detach($server);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function close(): void
 | 
			
		||||
    {
 | 
			
		||||
        foreach ($this->servers as $server) {
 | 
			
		||||
            $this->removeServer($server);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function onServerConnection(DuplexStreamInterface $client): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->connections->attach($client);
 | 
			
		||||
        $client->on('data', function ($data) use ($client) {
 | 
			
		||||
            try {
 | 
			
		||||
                $message = Message::fromString($data);
 | 
			
		||||
                $this->onClientMessage($client, $message);
 | 
			
		||||
            } catch (MessageException $e) {
 | 
			
		||||
                $client->end('{"msg":"error","data":{"error":"Bad message format"}}');
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        $client->on('close', function () use ($client) {
 | 
			
		||||
            $this->connections->detach($client);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function onClientMessage(DuplexStreamInterface $client, Message $message): void
 | 
			
		||||
    {
 | 
			
		||||
        switch ($message->getType()) {
 | 
			
		||||
            case Message::MSGTYPE_EXECUTE: // Client call to execute command
 | 
			
		||||
                $data = $message->getData();
 | 
			
		||||
                $context = new Context($data['command'],$data['context']);
 | 
			
		||||
                $this->executeContext($context)->then(
 | 
			
		||||
                    function ($result) use ($message, $client) {
 | 
			
		||||
                        $client->write($message->asResult($result)->toJson()."\n");
 | 
			
		||||
                    }
 | 
			
		||||
                );
 | 
			
		||||
                break;
 | 
			
		||||
            case Message::MSGTYPE_RESULT: // Result from execution on client
 | 
			
		||||
                // TODO implement me
 | 
			
		||||
                break;
 | 
			
		||||
                case Message::MSGTYPE_REGISTRY: // command registry actions
 | 
			
		||||
                // TODO implement me
 | 
			
		||||
                break;
 | 
			
		||||
            default:
 | 
			
		||||
                $client->end('{"msg":"error","data":{"error":"Unexpected message type"}}');
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * {@inheritDoc}
 | 
			
		||||
     */
 | 
			
		||||
    public function execute(string $command, array $context): PromiseInterface
 | 
			
		||||
    {
 | 
			
		||||
        $context = new Context($command, $context);
 | 
			
		||||
        return $this->executeContext($context);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function executeContext(Context $context): PromiseInterface
 | 
			
		||||
    {
 | 
			
		||||
        $command = $this->commandRegistry->find($context->getCommandName());
 | 
			
		||||
        if (!$command) return new Promise(function (callable $resolve) use ($context) {
 | 
			
		||||
            throw new \RuntimeException("Unable to resolve command: ".$context->getCommandName());
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return $command->call($context);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * {@inheritDoc}
 | 
			
		||||
     */
 | 
			
		||||
    public function notify(string $event, array $data): void
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										150
									
								
								src/CommandBusClient.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										150
									
								
								src/CommandBusClient.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,150 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace NoccyLabs\React\CommandBus;
 | 
			
		||||
use Evenement\EventEmitterTrait;
 | 
			
		||||
use React\EventLoop\Loop;
 | 
			
		||||
use React\Promise\PromiseInterface;
 | 
			
		||||
use React\Promise\Promise;
 | 
			
		||||
use React\Promise\Deferred;
 | 
			
		||||
use React\Socket\ConnectorInterface;
 | 
			
		||||
use React\Socket\TcpConnector;
 | 
			
		||||
use React\Stream\DuplexStreamInterface;
 | 
			
		||||
use Throwable;
 | 
			
		||||
 | 
			
		||||
class CommandBusClient implements CommandBusInterface
 | 
			
		||||
{
 | 
			
		||||
    use EventEmitterTrait;
 | 
			
		||||
 | 
			
		||||
    const EVENT_ERROR = 'error';
 | 
			
		||||
    const EVENT_CONNECTED = 'connected';
 | 
			
		||||
    const EVENT_DISCONNECTED = 'disconnected';
 | 
			
		||||
 | 
			
		||||
    private ?CommandRegistry $commandRegistry = null;
 | 
			
		||||
 | 
			
		||||
    private ConnectorInterface $connector;
 | 
			
		||||
 | 
			
		||||
    private ?DuplexStreamInterface $connection = null;
 | 
			
		||||
 | 
			
		||||
    /** @var array<string,Deferred> */
 | 
			
		||||
    private array $pending = [];
 | 
			
		||||
 | 
			
		||||
    private string $address;
 | 
			
		||||
 | 
			
		||||
    public function __construct(?CommandRegistry $commandRegistry = null, ?ConnectorInterface $connector = null)
 | 
			
		||||
    {
 | 
			
		||||
        $this->commandRegistry = $commandRegistry;
 | 
			
		||||
        if ($commandRegistry) {
 | 
			
		||||
            $commandRegistry->on(CommandRegistry::EVENT_REGISTERED, $this->onCommandRegistered(...));
 | 
			
		||||
            $commandRegistry->on(CommandRegistry::EVENT_UNREGISTERED, $this->onCommandUnregistered(...));
 | 
			
		||||
        }
 | 
			
		||||
        $this->connector = $connector??new TcpConnector();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function connect(string $address): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->address = $address;
 | 
			
		||||
        $this->reconnect();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function close(): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->connection->close();
 | 
			
		||||
        $this->connection->removeAllListeners();
 | 
			
		||||
        $this->connection = null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function reconnect(): void
 | 
			
		||||
    {
 | 
			
		||||
        if ($this->connection) {
 | 
			
		||||
            $this->close();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->connector->connect($this->address)->then(
 | 
			
		||||
            function (DuplexStreamInterface $connection) {
 | 
			
		||||
                $this->connection = $connection;
 | 
			
		||||
                $this->emit(self::EVENT_CONNECTED);
 | 
			
		||||
                $connection->on('close', function () {
 | 
			
		||||
                    $this->emit(self::EVENT_DISCONNECTED);
 | 
			
		||||
                });
 | 
			
		||||
                $connection->on('data', function ($data) use ($connection) {
 | 
			
		||||
                    try {
 | 
			
		||||
                        $message = Message::fromString($data);
 | 
			
		||||
                        $this->onServerMessage($message);
 | 
			
		||||
                    } catch (MessageException $e) {
 | 
			
		||||
                        $connection->end('{"msg":"error","data":{"error":"Bad message format"}}');
 | 
			
		||||
                    }        
 | 
			
		||||
                });
 | 
			
		||||
            },
 | 
			
		||||
            function (Throwable $e) {
 | 
			
		||||
                $this->emit(self::EVENT_ERROR, [ $e->getMessage(), $e ]);
 | 
			
		||||
                Loop::addTimer(5, $this->reconnect(...));
 | 
			
		||||
            }
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function onServerMessage(Message $message): void
 | 
			
		||||
    {
 | 
			
		||||
        // fprintf(STDERR, "onServerMessage: %s\n", $message->toJson());
 | 
			
		||||
        switch ($message->getType()) {
 | 
			
		||||
            case Message::MSGTYPE_EXECUTE: // server call to execute command
 | 
			
		||||
                // TODO implement me
 | 
			
		||||
                break;
 | 
			
		||||
            case Message::MSGTYPE_RESULT: // result from server
 | 
			
		||||
                $uuid = $message->getUuid();
 | 
			
		||||
                $result = $message->getData()['result'];
 | 
			
		||||
                if (array_key_exists($uuid, $this->pending)) {
 | 
			
		||||
                    $this->pending[$uuid]->resolve($result);
 | 
			
		||||
                    unset($this->pending[$uuid]);
 | 
			
		||||
                }
 | 
			
		||||
                break;
 | 
			
		||||
            case Message::MSGTYPE_REGISTRY: // command registry actions
 | 
			
		||||
                // TODO implement me
 | 
			
		||||
                break;
 | 
			
		||||
            default:
 | 
			
		||||
                $this->connection->end('{"msg":"error","data":{"error":"Unexpected message type"}}');
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function onCommandRegistered(string $command): void
 | 
			
		||||
    {
 | 
			
		||||
        if ($this->connection && $this->connection->isWritable()) {
 | 
			
		||||
            $msg = new Message(Message::MSGTYPE_REGISTRY, [ 'register' => [ $command ]]);
 | 
			
		||||
            $this->connection->write($msg->toJson());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function onCommandUnregistered(string $command): void
 | 
			
		||||
    {
 | 
			
		||||
        if ($this->connection && $this->connection->isWritable()) {
 | 
			
		||||
            $msg = new Message(Message::MSGTYPE_REGISTRY, [ 'unregister' => [ $command ]]);
 | 
			
		||||
            $this->connection->write($msg->toJson());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * {@inheritDoc}
 | 
			
		||||
     */
 | 
			
		||||
    public function execute(string $command, array $context): PromiseInterface
 | 
			
		||||
    {
 | 
			
		||||
        $deferred = new Deferred();
 | 
			
		||||
 | 
			
		||||
        $message = new Message(Message::MSGTYPE_EXECUTE, [
 | 
			
		||||
            'command' => $command,
 | 
			
		||||
            'context' => $context
 | 
			
		||||
        ]);
 | 
			
		||||
        $this->pending[$message->getUuid()] = $deferred;
 | 
			
		||||
 | 
			
		||||
        // fprintf(STDERR, "write: %s\n", $message->toJson());
 | 
			
		||||
        $this->connection->write($message->toJson()."\n");
 | 
			
		||||
 | 
			
		||||
        return $deferred->promise();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * {@inheritDoc}
 | 
			
		||||
     */
 | 
			
		||||
    public function notify(string $event, array $data): void
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										8
									
								
								src/CommandBusException.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/CommandBusException.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,8 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace NoccyLabs\React\CommandBus;
 | 
			
		||||
 | 
			
		||||
interface CommandBusException
 | 
			
		||||
{
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										33
									
								
								src/CommandBusInterface.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/CommandBusInterface.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,33 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace NoccyLabs\React\CommandBus;
 | 
			
		||||
use Evenement\EventEmitterInterface;
 | 
			
		||||
use React\Promise\PromiseInterface;
 | 
			
		||||
 | 
			
		||||
interface CommandBusInterface extends EventEmitterInterface
 | 
			
		||||
{
 | 
			
		||||
    /** @var string Emitted to distribrute notifications */
 | 
			
		||||
    const EVENT_NOTIFY = "notify";
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Execute a command on the command bus, and return a promise that will be
 | 
			
		||||
     * resolved once the command has been handled.
 | 
			
		||||
     *
 | 
			
		||||
     * @param string $command The name of the command to execute
 | 
			
		||||
     * @param array $context Data to pass in the call context
 | 
			
		||||
     * @®eturn PromiseInterface A promise that will be resolved with the result
 | 
			
		||||
     */
 | 
			
		||||
    public function execute(string $command, array $context): PromiseInterface;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Notify all connected clients to emit the notify event, and also be
 | 
			
		||||
     * emitted locally. These messages are unsolicited, and can not be
 | 
			
		||||
     * responded to. They are fire-and-forget.
 | 
			
		||||
     *
 | 
			
		||||
     * @param string $event The application-specific event name
 | 
			
		||||
     * @param array $data Event-specific data
 | 
			
		||||
     * @return void
 | 
			
		||||
     */
 | 
			
		||||
    public function notify(string $event, array $data): void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										53
									
								
								src/CommandRegistry.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								src/CommandRegistry.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,53 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace NoccyLabs\React\CommandBus;
 | 
			
		||||
use Evenement\EventEmitterInterface;
 | 
			
		||||
use Evenement\EventEmitterTrait;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A collection of commands that can be executed via CommandBusInterface
 | 
			
		||||
 *
 | 
			
		||||
 */
 | 
			
		||||
class CommandRegistry implements EventEmitterInterface
 | 
			
		||||
{
 | 
			
		||||
    use EventEmitterTrait;
 | 
			
		||||
 | 
			
		||||
    const EVENT_REGISTERED = 'registered';
 | 
			
		||||
    const EVENT_UNREGISTERED = 'unregistered';
 | 
			
		||||
 | 
			
		||||
    /** @var array<string,Command> */
 | 
			
		||||
    private array $commands = [];
 | 
			
		||||
 | 
			
		||||
    public function register(string $command, callable $handler): void
 | 
			
		||||
    {
 | 
			
		||||
        $isNew = !array_key_exists($command, $this->commands);
 | 
			
		||||
 | 
			
		||||
        $this->commands[$command] = new Command($command, $handler);
 | 
			
		||||
 | 
			
		||||
        if ($isNew) {
 | 
			
		||||
            $this->emit(self::EVENT_REGISTERED, [ $command ]);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function unregister(string $command): void
 | 
			
		||||
    {
 | 
			
		||||
        if (!array_key_exists($command, $this->commands)) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        unset($this->commands[$command]);
 | 
			
		||||
 | 
			
		||||
        $this->emit(self::EVENT_UNREGISTERED, [ $command ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function find(string $command): ?Command
 | 
			
		||||
    {
 | 
			
		||||
        return $this->commands[$command] ?? null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getNames(): array
 | 
			
		||||
    {
 | 
			
		||||
        return array_keys($this->commands);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										41
									
								
								src/Context.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/Context.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,41 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace NoccyLabs\React\CommandBus;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * The context contains all the data relating to calling a command. It has an
 | 
			
		||||
 * unique identifier, the command to call, and the payload data.
 | 
			
		||||
 *
 | 
			
		||||
 */
 | 
			
		||||
class Context
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    /** @var string $commandName The command name to call */
 | 
			
		||||
    private string $commandName;
 | 
			
		||||
 | 
			
		||||
    /** @var array<string,mixed> The payload data */
 | 
			
		||||
    private array $payload = [];
 | 
			
		||||
 | 
			
		||||
    public function __construct(string $commandName, array $payload)
 | 
			
		||||
    {
 | 
			
		||||
        $this->commandName = $commandName;
 | 
			
		||||
        $this->payload = $payload;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getCommandName(): string
 | 
			
		||||
    {
 | 
			
		||||
        return $this->commandName;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getPayload(): array
 | 
			
		||||
    {
 | 
			
		||||
        return $this->payload;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function __get($name)
 | 
			
		||||
    {
 | 
			
		||||
        return $this->payload[$name] ?? null;   
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										88
									
								
								src/Message.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								src/Message.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,88 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace NoccyLabs\React\CommandBus;
 | 
			
		||||
 | 
			
		||||
use JsonSerializable;
 | 
			
		||||
use Symfony\Component\Uid\Uuid;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A serializable message frame
 | 
			
		||||
 *
 | 
			
		||||
 */
 | 
			
		||||
class Message implements JsonSerializable
 | 
			
		||||
{
 | 
			
		||||
    /** @var string Execute request */
 | 
			
		||||
    const MSGTYPE_EXECUTE = 'execute';
 | 
			
		||||
    /** @var string Execute result */
 | 
			
		||||
    const MSGTYPE_RESULT = 'result';
 | 
			
		||||
    /** @var string Notify event */
 | 
			
		||||
    const MSGTYPE_NOTIFY = 'notify';
 | 
			
		||||
    /** @var string Registry update (command list set and update) */
 | 
			
		||||
    const MSGTYPE_REGISTRY = 'registry';
 | 
			
		||||
 | 
			
		||||
    /** @var string $uuid The message identifier */
 | 
			
		||||
    private string $uuid;
 | 
			
		||||
 | 
			
		||||
    private string $messageType;
 | 
			
		||||
 | 
			
		||||
    private array $messageData;
 | 
			
		||||
 | 
			
		||||
    public function __construct(string $messageType, array $messageData = [], ?string $uuid = null)
 | 
			
		||||
    {
 | 
			
		||||
        $this->uuid = ($uuid && Uuid::isValid($uuid)) ? $uuid : (string)Uuid::v7();
 | 
			
		||||
        $this->messageType = $messageType;
 | 
			
		||||
        $this->messageData = $messageData;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getUuid(): string
 | 
			
		||||
    {
 | 
			
		||||
        return $this->uuid;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getType(): string
 | 
			
		||||
    {
 | 
			
		||||
        return $this->messageType;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getData(): array
 | 
			
		||||
    {
 | 
			
		||||
        return $this->messageData;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     *
 | 
			
		||||
     * @param string $data The JSON-encoded message data
 | 
			
		||||
     * @return Message
 | 
			
		||||
     * @throws MessageException if the message can not be parsed
 | 
			
		||||
     */
 | 
			
		||||
    public static function fromString(string $data): Message
 | 
			
		||||
    {
 | 
			
		||||
        $json = @json_decode($data, true);
 | 
			
		||||
        if (!$json || empty($json['msg'])) {
 | 
			
		||||
            throw new MessageException("Invalid data");
 | 
			
		||||
        }
 | 
			
		||||
        return new Message($json['msg'], $json['data'], $json['uuid']);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function asResult($result): Message
 | 
			
		||||
    {
 | 
			
		||||
        return new Message(self::MSGTYPE_RESULT, [
 | 
			
		||||
            'result' => $result
 | 
			
		||||
        ], $this->uuid);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function toJson(): string
 | 
			
		||||
    {
 | 
			
		||||
        return json_encode($this, JSON_UNESCAPED_SLASHES);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function jsonSerialize(): array
 | 
			
		||||
    {
 | 
			
		||||
        return [
 | 
			
		||||
            'uuid' => $this->uuid,
 | 
			
		||||
            'msg' => $this->messageType,
 | 
			
		||||
            'data' => $this->messageData
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										9
									
								
								src/MessageException.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/MessageException.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace NoccyLabs\React\CommandBus;
 | 
			
		||||
use RuntimeException;
 | 
			
		||||
 | 
			
		||||
class MessageException extends RuntimeException implements CommandBusException
 | 
			
		||||
{
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user