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:

Conveyor SubProtocol Workflow

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:

  1. Function: server

    • Dependencies: Starts from the started state.
    • Next State: Transitions to the server_set state.
  2. Function: fd

    • Dependencies: Starts from the server_set state.
    • Next State: Transitions to the fd_set state.
  3. 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.
  4. 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.
  5. 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.
  6. 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.

At this stage, there is a sequence of transitions that happens without functions:

  1. Transition: prepare_pipeline

    • Dependencies: Starts from the action_prepared state.
    • Next State: Transitions to the pipeline_prepared state.
  2. Transition: process_message

    • Dependencies: Starts from the pipeline_prepared state.
    • Next State: Transitions to the message_processed state.
  3. Transition: finalize

    • Dependencies: Starts from the message_processed state.
    • Next State: Transitions to the finalized state.

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.

  1. 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();
  1. 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();
Previous
Conveyor