Core concepts
Conveyor SubProtocol
“Patterns are the language of our craft. They provide a shared vocabulary for us to communicate, innovate, and efficiently solve common challenges in software development.”
Erich Gamma
This implementation is about the basic usage of Socket Conveyor, without the Conveyor Server.
The basic usage
Conveyor processes every incoming message at the “message” event of the WebSocket server. For that, do as follows:
1<?php
2
3use OpenSwoole\WebSocket\Server;
4use OpenSwoole\WebSocket\Frame;
5use Conveyor\ConveyorServer;
6
7$websocket = new Server('0.0.0.0', 8080);
8$websocket->on('message', fn (Server $server, Frame $frame) =>
9 Conveyor::init()
10 ->server($server)
11 ->fd($frame->fd)
12 ->run($frame->data));
13$websocket->on('request', fn(Request $rq, Response $rp) => $rp->end($html));
14$websocket->start();
This solution will handle every incoming message and process it, providing all Socket Conveyor's features. Thats it!
The Workflow
When using Socket Conveyor within an existing OpenSwoole server, you need to pay attention to its workflow. Socket Conveyor has a Workflow to make sure a sequence of pieces are in place at any given moment. This is a part that os always evolving, so having some visualizable structure makes the work easier.
Here is a diagram of the workflow in place:
This diagram represents the Workflow implemented in Socket Conveyor. This workflow happens in chain in the code, we will check that later. For now, let's see the list of functions that the developer has access to in the actual usage of this class to accomplish the workflow:
Full example:
1<?php
2
3Conveyor\Conveyor::init()
4 ->server($server)
5 ->fd($fd)
6 // here we replace any default persistence in Conveyor (must implement ConveyorPersistenceInterfacesGenericPersistenceInterface)
7 ->persistence()
8 // here we add extra actions (must implement ConveyorActionsInterfacesActionInterface)
9 ->addActions([new SampleAction()])
10 // here we add middlewares to actions (must be callables)
11 ->addMiddlewareToAction(SampleAction::NAME, new SampleMiddleware())
12 ->run(json_encode([
13 'action' => SampleAction::NAME,
14 'token' => 'invalid-token',
15 'second-verification' => 'valid',
16 ]));
Explanation:
Function:
server
- Dependencies: Starts from the
started
state. - Next State: Transitions to the
server_set
state.
- Dependencies: Starts from the
Function:
fd
- Dependencies: Starts from the
server_set
state. - Next State: Transitions to the
fd_set
state.
- Dependencies: Starts from the
Function:
persistence
- Dependencies: Can start from any of the following states:
fd_set
actions_added
middleware_added
- Next State: Transitions to the
persistence_set
state.
- Dependencies: Can start from any of the following states:
Function:
addActions
- Dependencies: Can start from any of the following states:
fd_set
persistence_set
middleware_added
actions_added
(self-transition)
- Next State: Transitions to the actions_added state.
- Dependencies: Can start from any of the following states:
Function:
addMiddlewareToAction
- Dependencies: Can start from any of the following states:
fd_set
persistence_set
actions_added
middleware_added
(self-transition)
- Next State: Transitions to the middleware_added state.
- Dependencies: Can start from any of the following states:
Function:
run
- Dependencies: Can start from any of the following states:
persistence_set
actions_added
middleware_added
- Next State: Transitions to the action_prepared state.
- Dependencies: Can start from any of the following states:
At this stage, there is a sequence of transitions that happens without functions:
Transition:
prepare_pipeline
- Dependencies: Starts from the
action_prepared
state. - Next State: Transitions to the
pipeline_prepared
state.
- Dependencies: Starts from the
Transition:
process_message
- Dependencies: Starts from the
pipeline_prepared
state. - Next State: Transitions to the
message_processed
state.
- Dependencies: Starts from the
Transition:
finalize
- Dependencies: Starts from the
message_processed
state. - Next State: Transitions to the
finalized
state.
- Dependencies: Starts from the
Each function represents a transition in the workflow, and then, the final few transitions happen after the "run" function call. This documentation can serve as a guide for understanding the flow and dependencies of the system.
Available Actions
This package comes with some out-of-the-box Actions, but you can (and probably will need) build your own for your own needs by extending the existent ones or creating new. To learn more how to create your own, check here: Creating your own Actions.
Associate User to Fd
Conveyor\Actions\AssocUserToFdAction
Action responsible for associating users to connections.
Structure:
1{
2 "action": "assoc-user-to-fd-action",
3 "userId": 1
4}
Base (default)
Conveyor\Actions\BaseAction
This is the base action. Works like a ping pong, returning the message to the client who sent it.
Structure:
1{
2 "action": "base-action",
3 "data": "message"
4}
Notice that this action also works if you don’t send a JSON, forcing the action to be the base one.
Broadcast
Conveyor\Actions\BroadcastAction
This is for messages to be broadcasted on the context of the connection that dispatches it.
Structure:
1{
2 "action": "broadcast-action",
3 "data": "message"
4}
Channel Connect
Conveyor\Actions\ChannelConnectAction
Action used to connect to a channel.
Structure:
1{
2 "action": "channel-connect",
3 "channel": "channel-name"
4}
Channel Disconnect
Conveyor\Actions\ChannelDisconnectAction
Action used to disconnect from a channel.
Structure:
1{
2 "action": "channel-disconnect"
3}
Fanout
Conveyor\Actions\FanoutAction
Action used to broadcast without context borders (to every client in the server).
Structure:
1{
2 "action": "fanout-action",
3 "data": "message"
4}
Creating your own Actions
Creating your own actions need 2 steps. (1) is about creating your action’s class. The other is injecting that action in Socket Conveyor’s workflow.
Action's Class
You can create your own actions by extending any action, or by extending the action abstraction Conveyor\Actions\Abstractions\AbstractAction
.
To get up to speed with this, let’s look at the BaseAction’s code:
1<?php
2
3namespace Conveyor\Actions;
4
5use Conveyor\Actions\Abstractions\AbstractAction;
6use InvalidArgumentException;
7
8class BaseAction extends AbstractAction
9{
10 public const NAME = 'base-action';
11
12 protected string $name = self::NAME;
13
14 public function validateData(array $data): void
15 {
16 if (!isset($data['data'])) {
17 throw new InvalidArgumentException('BaseAction required \'data\' field to be created!');
18}
19 }
20
21 public function execute(array $data): mixed
22 {
23 $this->send($data['data'], $this->fd);
24 return null;
25 }
26}
In this, you can see the 2 abstract functions implementations: validateData
and execute
.
Almost any logic can be implemented by looking at the AbstractAction
code and customizing it as well.
Adding an Action to Socket Conveyor
You can add extra actions to Socket Conveyor in 2 different ways: (1) at the Conveyor class, (2) at the Conveyor Server.
- Conveyor:
During the declaration of Socket Conveyor’s code, you can add your own actions like follows:
1<?php
2
3use Conveyor\Conveyor;
4use OpenSwoole\WebSocket\Frame;
5use OpenSwoole\WebSocket\Server;
6use MyNamespace\MyCustomAction;
7
8$websocket = new Server('0.0.0.0', 8080);
9
10$websocket->on('message', function (Server $server, Frame $frame) use ($persistenceService) {
11 Conveyor::init()
12 ->server($server)
13 ->fd($frame->fd)
14 ->addActions([
15 new MyCustomAction,
16 ])
17 ->closeConnections()
18 ->run($frame->data);
19});
20
21$websocket->start();
- Conveyor Server:
During the declaration of Conveyor Server, you can add your actions as a “conveyorOptions” parameter:
1<?php
2
3use Conveyor\ConveyorServer;
4use Conveyor\ConveyorWorker;
5use Conveyor\Actions\Interfaces\ActionInterface;
6
7/** @var array<array-key, ActionInterface> $actions */
8$actions = [new MyCustomAction];
9
10(new ConveyorServer())
11 ->conveyorOptions([
12 ConveyorWorker::ACTIONS => $actions,
13 ])
14 ->start();