chipslays / porter
Requires
- php: ^8.1
- chipslays/collection: ^1.1
- chipslays/sauce: ^1.0
- respect/validation: ^2.2
- workerman/workerman: ^4.0
Requires (Dev)
- pestphp/pest: ^1.21
- symfony/var-dumper: ^6.0
- dev-master
- 2.x-dev
- 1.2.9
- 1.2.8
- 1.2.7
- 1.2.6
- 1.2.5
- 1.2.4
- 1.2.3
- 1.2.2
- 1.2.1
- 1.2.0
- 1.1.50
- 1.1.49
- 1.1.48
- 1.1.47
- 1.1.46
- 1.1.45
- 1.1.44
- 1.1.43
- 1.1.42
- 1.1.41
- 1.1.40
- 1.1.39
- 1.1.38
- 1.1.37
- 1.1.36
- 1.1.35
- 1.1.34
- 1.1.33
- 1.1.32
- 1.1.31
- 1.1.30
- 1.1.29
- 1.1.28
- 1.1.27
- 1.1.26
- 1.1.25
- 1.1.24
- 1.1.23
- 1.1.22
- 1.1.21
- 1.1.20
- 1.1.19
- 1.1.18
- 1.1.17
- 1.1.16
- 1.1.15
- 1.1.14
- 1.1.13
- 1.1.12
- 1.1.11
- 1.1.10
- 1.1.9
- 1.1.8
- 1.1.7
- 1.1.6
- 1.1.5
- 1.1.4
- 1.1.3
- 1.1.2
- 1.1.1
- 1.1.0
- 1.0.48
- 1.0.47
- 1.0.46
- 1.0.45
- 1.0.44
- 1.0.43
- 1.0.42
- 1.0.41
- 1.0.40
- 1.0.39
- 1.0.38
- 1.0.37
- 1.0.36
- 1.0.35
- 1.0.34
- 1.0.33
- 1.0.32
- 1.0.31
- 1.0.30
- 1.0.29
- 1.0.28
- 1.0.27
- 1.0.26
- 1.0.25
- 1.0.24
- 1.0.23
- 1.0.22
- 1.0.21
- 1.0.20
- 1.0.19
- 1.0.18
- 1.0.17
- 1.0.16
- 1.0.15
- 1.0.14
- 1.0.13
- 1.0.12
- 1.0.11
- 1.0.10
- 1.0.9
- 1.0.8
- 1.0.7
- 1.0.6
- 1.0.5
- 1.0.4
- 1.0.3
- 1.0.2
- 1.0.1
- 1.0.0
This package is auto-updated.
Last update: 2024-10-19 13:50:19 UTC
README
A simple PHP 8 websocket server and client wrapper over Workerman with events, channels and other stuff, can say this is a Socket IO alternative for PHP.
Note
Latest version 1.2 is production ready, maintenance only small features, fix bugs and has no breaking changes updates.
🧰 Installation
- Install Porter via Composer:
composer require chipslays/porter
- Put javascript code in views:
<script src="https://cdn.jsdelivr.net/gh/chipslays/porter@latest/dist/porter.min.js"></script>
- All done.
Laravel integration can be found here.
👨💻 Usage
Server (PHP)
Simplest ping-pong server.
use Porter\Events\Event; require __DIR__ . '/vendor/autoload.php'; server()->create('0.0.0.0:3737'); server()->on('ping', function (Event $event) { $event->reply('pong'); }); server()->start();
Run server.
php server.php start
Or run server in background as daemon process.
php server.php start -d
List of all available commands
php server.php start
php server.php start -d
php server.php status
php server.php status -d
php server.php connections
php server.php stop
php server.php stop -g
php server.php restart
php server.php reload
php server.php reload -g
Client (Javascript)
Send ping
event on established connection.
<script src="https://cdn.jsdelivr.net/gh/chipslays/porter@latest/dist/porter.min.js"></script> <script> const client = new Porter(`ws://${location.hostname}:3737`); client.connected = () => { client.send('ping'); } client.on('pong', payload => { console.log(payload); }); client.listen(); </script>
💡 Examples
Examples can be found here.
📚 Documentation
NOTE: The documentation may not contain the latest updates or may be out of date in places. See examples, code and comments on methods. The code is well documented.
Basics
Local development
use Workerman\Worker; $worker = new Worker('websocket://0.0.0.0:3737');
On server with SSL
use Workerman\Worker; $context = [ // More see http://php.net/manual/en/context.ssl.php 'ssl' => [ 'local_cert' => '/path/to/cert.pem', 'local_pk' => '/path/to/privkey.pem', 'verify_peer' => false, // 'allow_self_signed' => true, ], ]; $worker = new Worker('websocket://0.0.0.0:3737', $context); $worker->transport = 'ssl';
🔹 Server
Can be used anywhere as function server()
or Server::getInstance()
.
use Porter\Server; $server = Server::getInstance(); $server->on(...);
server()->on(...);
boot(Worker $worker): self
Booting websocket server. It method init all needle classes inside.
Use this method instead of constructor.
$server = Server::getInstance(); $server->boot($worker); // by helper function server()->boot($worker);
setWorker(Worker $worker): void
Set worker instance.
use Workerman\Worker; $worker = new Worker('websocket://0.0.0.0:3737'); server()->boot($worker); // booting server $worker = new Worker('websocket://0.0.0.0:3737'); $worker->... // configure new worker // change worker in already booted server server()->setWorker($worker);
setWorker(Worker $worker): void
Set worker instance.
use Workerman\Worker; $worker = new Worker('websocket://0.0.0.0:3737'); server()->setWorker($worker);
getWorker(): Worker
Get worker instance.
server()->getWorker();
addEvent(AbstractEvent|string $event): self
Add event class handler.
use Porter\Server; use Porter\Payload; use Porter\Connection; use Porter\Events\AbstractEvent; class Ping extends AbstractEvent { public static string $id = 'ping'; public function handle(Connection $connection, Payload $payload, Server $server): void { $this->reply('pong'); } } server()->addEvent(Ping::class);
autoloadEvents(string $path, string|array $masks = ['*.php', '**/*.php']): void
Autoload all events inside passed path.
Note: Use it instead manual add events by
addEvent
method.
server()->autoloadEvents(__DIR__ . '/Events');
on(string $type, callable $handler): void
Note
Event $event
class extends and have all methods & properties ofAbstractEvent
.
$server->on('ping', function (Event $event) { $event->reply('pong'); });
start(): void
Start server.
server()->start();
onConnected(callable $handler): void
Emitted when a socket connection is successfully established.
In this method available vars:
$_GET
,$_COOKIE
,$_SERVER
.
use Porter\Terminal; use Porter\Connection; server()->onConnected(function (Connection $connection, string $header) { Terminal::print('{text:darkGreen}Connected: ' . $connection->getRemoteAddress()); // Here also available vars: $_GET, $_COOKIE, $_SERVER. Terminal::print("Query from client: {text:darkYellow}foo={$_GET['foo']}"); });
onDisconnected(callable $handler): void
Emitted when the other end of the socket sends a FIN packet.
NOTICE: On disconnect client connection will leave of all the channels where he was.
use Porter\Terminal; use Porter\Connection; server()->onDisconnected(function (Connection $connection) { Terminal::print('{text:darkGreen}Connected: ' . $connection->getRemoteAddress()); });
onError(callable $handler): void
Emitted when an error occurs with connection.
use Porter\Terminal; use Porter\Connection; server()->onError(function (Connection $connection, $code, $message) { Terminal::print("{bg:red}{text:white}Error {$code} {$message}"); });
onStart(callable $handler): void
Emitted when worker processes start.
use Porter\Terminal; use Workerman\Worker; server()->onStart(function (Worker $worker) { // });
onStop(callable $handler): void
Emitted when worker processes stoped.
use Porter\Terminal; use Workerman\Worker; server()->onStop(function (Worker $worker) { // });
onReload(callable $handler): void
Emitted when worker processes get reload signal.
use Porter\Terminal; use Workerman\Worker; server()->onReload(function (Worker $worker) { // });
onRaw(callable $handler): void
Handle non event messages (raw data).
server()->onRaw(function (string $payload, Connection $connection) { if ($payload == 'ping') { $connection->send('pong'); } });
to(TcpConnection|Connection|array $connection, string $event, array $data = []): self
Send event to connection.
server()->to($connection, 'ping');
broadcast(string $event, array $data = [], array $excepts = []): void
Send event to all connections.
Yes, to all connections on server.
server()->broadcast('chat message', [ 'nickname' => 'John Doe', 'message' => 'Hello World!', ]);
storage(): Storage
Getter for Storage class.
server()->storage(); server()->storage()->put('foo', 'bar'); $storage = server()->storage(); $storage->get('foo'); // can also be a get as propperty server()->storage()->put('foo', 'bar'); $storage = server()->storage;
channels(): Channels
Getter for Channels class.
server()->channels(); server()->channels()->create('secret channel'); $channels = server()->channels(); $channels->get('secret channel');
connection(int $connectionId): ?Connection
Get connection instance by id.
$connection = server()->connection(1); server()->to($connection, 'welcome message', [ 'text' => 'Hello world!' ]); // also can get like $connection = server()->getWorker()->connections[1337] ?? null;
connections(): Collection[]
Get collection of all connections on server.
$connections = server()->connections(); server()->broadcast('update users count', ['count' => $connections->count()]); // also can get like $connections = server()->getWorker()->connections;
validator(): Validator
Create validator instance.
See documenation & examples how to use.
$v = server()->validator(); if ($v->email()->validate('john.doe@example.com')) { // } // available as helper if (validator()->contains('example.com')->validate('john.doe@example.com')) { // }
🔹 Channels
This is a convenient division of connected connections into channels.
One connection can consist of an unlimited number of channels.
Channels also support broadcasting and their own storage.
Channel can be access like:
// by method server()->channels(); // by property server()->channels;
create(string $id, array $data = []): Channel
Create new channel.
$channel = server()->channels()->create('secret channel', [ 'foo' => 'bar', ]); $channel->join($connection)->broadcast('welcome message', [ 'foo' => $channel->data->get('foo'), ]);
get(string $id): ?Channel
Get a channel.
Returns
NULL
if channel not exists.
$channel = server()->channels()->get('secret channel');
all(): Channel[]
Get array of channels (Channel
instances).
foreach (server()->channels()->all() as $id => $channel) { echo $channel->connections()->count() . ' connection(s) in channel: ' . $id . PHP_EOL; }
count(): int
Get count of channels.
$count = server()->channels()->count(); echo "Total channels: {$count}";
delete(string $id): void
Delete channel.
server()->channels()->delete('secret channel');
exists(string $id): bool
Checks if given channel id exists already.
$channelId = 'secret channel'; if (!server()->channels()->exists($channelId)) { server()->channels()->create($channelId); }
join(string $id, Connection|Connection[]|int[] $connections): Channel
Join or create and join to channel.
server()->channels()->join($connection); server()->channels()->join([$connection1, $connection2, $connection3, ...]);
🔹 Channel
join(TcpConnection|Connection|array $connections): self
Join given connections to channel.
$channel = server()->channel('secret channel'); $channel->join($connection); $channel->join([$connection1, $connection2, $connection3, ...]);
leave(TcpConnection|Connection $connection): self
Remove given connection from channel.
$channel = server()->channel('secret channel'); $channel->leave($connection);
exists(TcpConnection|Connection|int $connection): bool
Checks if given connection exists in channel.
$channel = server()->channel('secret channel'); $channel->exists($connection);
connections(): Connections
A array of connections in this channel. Key is a id
of connection, and value is a instance of connection Connection
.
$channel = server()->channel('secret channel'); foreach($channel->connections()->all()) as $connection) { $connection->lastMessageAt = time(); }
$channel = server()->channel('secret channel'); $connection = $channel->connections()->get([1337]); // get connection with 1337 id
broadcast(string $event, array $data = [], array $excepts = []): void
Send an event to all connection on this channel.
TcpConnection[]|Connection[]|int[] $excepts
Connection instance or connection id.
$channel = server()->channel('secret channel'); $channel->broadcast('welcome message', [ 'text' => 'Hello world', ]);
For example, you need to send to all participants in the room except yourself, or other connections.
$channel->broadcast('welcome message', [ 'text' => 'Hello world', ], [$connection]); $channel->broadcast('welcome message', [ 'text' => 'Hello world', ], [$connection1, $connection2, ...]);
destroy(): void
Delete this channel from channels.
$channel = server()->channel('secret channel'); $channel->destroy(); // now if use $channel, you get an error $channel->data->get('foo');
Lifehack for Channel
You can add channel to current user as property to $connection
instance and get it anywhere.
$channel = channel('secret channel'); $connection->channel = &$channel;
Properties
$channel->data
Data is a simple implement of box for storage your data.
Data is a object of powerful chipslays/collection.
See documentation for more information how to manipulate this data.
NOTICE: All this data will be deleted when the server is restarted.
Two of simple-short examples:
$channel->data->set('foo'); $channel->data->get('foo', 'default value'); $channel->data->has('foo', 'default value'); $channel->data['foo']; $channel->data['foo'] ?? 'default value'; isset($channel->data['foo']); // see more examples here: https://github.com/chipslays/collection
🔹 Payload
The payload is the object that came from the client.
payload(string $key, mixed $default = null): mixed
Get value from data.
$payload->get('foo', 'default value'); // can also use like: $payload->data->get('foo', 'default value'); $payload->data['foo'] ?? 'default value';
is(string|array $rule, string $key): bool
Validate payload data.
See documenation & examples how to use.
$payload->is('StringType', 'username'); // return true if username is string $payload->is(['contains', 'john'], 'username'); // return true if $payload->data['username'] contains 'john'
Properties
$payload->type
Is a id of event, for example, welcome message
.
$payload->type; // string
$payload->data
An object of values passed from the client.
Object of chipslays/collection.
See documentation for more information how to manipulate this data.
$payload->data; // Collection $payload->data->set('foo'); $payload->data->get('foo', 'default value'); $payload->data->has('foo', 'default value'); $payload->data['foo']; $payload->data['foo'] ?? 'default value'; isset($payload->data['foo']); // see more examples here: https://github.com/chipslays/collection
$payload->rules
[protected]
Auto validate payload data on incoming event.
Available only in events as class
.
use Porter\Server; use Porter\Payload; use Porter\Connection; use Porter\Events\AbstractEvent; return new class extends AbstractEvent { public static string $type = 'hello to'; protected array $rules = [ 'username' => ['stringType', ['length', [3, 18]]], ]; public function handle(Connection $connection, Payload $payload, Server $server): void { if (!$this->validate()) { $this->reply('bad request', ['errors' => $this->errors]); return; } $username = $this->payload->data['username']; $this->reply(data: ['message' => "Hello, {$username}!"]); } };
🔹 Events
Events can be as a separate class or as an anonymous function.
Event class
Basic ping-pong example:
use Porter\Server; use Porter\Payload; use Porter\Events\AbstractEvent; use Porter\Connection; class Ping extends AbstractEvent { public static string $type = 'ping'; public function handle(Connection $connection, Payload $payload, Server $server): void { $this->reply('pong'); } } // and next you need add (register) this class to events: server()->addEvent(Ping::class);
NOTICE: The event class must have a
handle()
method.This method handles the event. You can also create other methods.
AbstractEvent
Properties
Each child class get following properties:
Connection $connection
- from whom the event came;Payload $payload
- contain data from client;Server $server
- server instance;Collection $data
- short cut for payload data (as &link).;
Magic properties & methods.
If client pass in data channel_i_d
with channel id or target_id
with id of connection, we got a magic properties and methods.
// this is a object of Channel, getted by `channel_id` from client. $this->channel; $this->channel(); $this->channel()->broadcast('new message', [ 'text' => $this->payload->get('text'), 'from' => $this->connection->nickname, ]);
// this is a object of Channel, getted by `target_id` from client. $this->target; $this->target(); $this->to($this->target, 'new message', [ 'text' => $this->payload->get('text'), 'from' => $this->connection->nickname, ]);
Methods
to(TcpConnection|Connection|array $connection, string $event, array $data = []): self
Send event to connection.
$this->to($connection, 'ping');
reply(string $event, array $data = []): ?bool
Reply event to incoming connection.
$this->reply('ping'); // analog for: $this->to($this->connection, 'ping');
To reply with the current type
, pass only the $data
parameter.
On front-end:
client.send('hello to', {username: 'John Doe'}, payload => { console.log(payload.data.message); // Hello, John Doe! });
On back-end:
$username = $this->payload->data['username']; $this->reply(data: ['message' => "Hello, {$username}!"]);
raw(string $string): bool|null
Send raw data to connection. Not a event object.
$this->raw('ping'); // now client will receive just a 'ping', not event object.
broadcast(string $event, array $data = [], TcpConnection|Connection|array $excepts = []): void
Send event to all connections.
Yes, to all connections on server.
$this->broadcast('chat message', [ 'nickname' => 'John Doe', 'message' => 'Hello World!', ]);
Send event to all except for the connection from which the event came.
$this->broadcast('user join', [ 'text' => 'New user joined to chat.', ], [$this->connection]);
validate(): bool
Validate payload data.
Pass custom rules. Default use $rules class attribute.
Returns false
if has errors.
if (!$this->validate()) { return $this->reply(/** ... */); }
hasErrors(): bool
Returns true
if has errors on validate payload data.
if ($this->hasErrors()) { return $this->reply('bad request', ['errors' => $this->errors]); }
// $this->errors contains:
^ array:1 [
"username" => array:1 [
"length" => "username failed validation: length"
]
]
payload(string $key, mixed $default = null): mixed
Yet another short cut for payload data.
public function handle(Connection $connection, Payload $payload, Server $server) { $this->get('nickname'); // as property $this->data['nickname']; $this->data->get('nickname'); // form payload instance $payload->data['nickname']; $payload->data->get('nickname'); $this->payload->data['nickname']; $this->payload->data->get('nickname'); }
Anonymous function
In anonymous function instead of $this
, use $event
.
use Porter\Events\Event; server()->on('new message', function (Event $event) { // $event has all the same property && methods as in the example above $event->to($event->target, 'new message', [ 'text' => $this->payload->get('text'), 'from' => $this->connection->nickname, ]); $event->channel()->broadcast('new message', [ 'text' => $this->payload->get('text'), 'from' => $this->connection->nickname, ]); });
🔹 TcpConnection|Connection $connection
It is a global object, changing in one place, it will contain the changed data in another place.
This object has already predefined properties:
See all $connection
methods here.
You can set different properties, functions to this object.
$connection->firstName = 'John'; $connection->lastName = 'Doe'; $connection->getFullName = fn () => $connection->firstName . ' ' . $connection->lastName; call_user_func($connection->getFullname); // John Doe
Custom property channels
$connection->channels; // object of Porter\Connection\Channels
List of methods Porter\Connection\Channels
/** * Get connection channels. * * @return Channel[] */ public function all(): array
/** * Get channels count. * * @return int */ public function count(): int
/** * Method for when connection join to channel should detach channel id from connection. * * @param string $channelId * @return void */ public function delete(string $channelId): void
/** * Leave all channels for this connection. * * @return void */ public function leaveAll(): void
NOTICE: On disconnect client connection will leave of all the channels where he was.
/** * When connection join to channel should attach channel id to connection. * * You don't need to use this method, it will automatically fire inside the class. * * @param string $channelId * @return void */ public function add(string $channelId): void
🔹 Client (PHP)
Simple implementation of client.
See basic example of client here.
__construct(string $host, array $context = [])
Create client.
$client = new Client('ws://localhost:3737'); $client = new Client('wss://example.com:3737');
setWorker(Worker $worker): void
Set worker.
NOTICE: Worker instance auto init in constructor. Use this method if you need to define worker with specific settings.
getWorker(): Worker
Get worker.
send(string $type, array $data = []): ?bool
Send event to server.
$client->on('ping', function (AsyncTcpConnection $connection, Payload $payload, Client $client) { $client->send('pong', ['time' => time()]); });
raw(string $payload): ?bool
Send raw payload to server.
$client->raw('simple message');
onConnected(callable $handler): void
Emitted when a socket connection is successfully established.
$client->onConnected(function (AsynTcpConnection $connection) { // });
onDisconnected(callable $handler): void
Emitted when the server sends a FIN packet.
$client->onDisconnected(function (AsynTcpConnection $connection) { // });
onError(callable $handler): void
Emitted when an error occurs with connection.
$client->onError(function (AsyncTcpConnection $connection, $code, $message) { // });
onRaw(callable $handler): void
Handle non event messages (raw data).
$client->onRaw(function (string $payload, AsyncTcpConnection $connection) { if ($payload == 'ping') { $connection->send('pong'); } });
on(string $type, callable $handler): void
Event handler as callable.
$client->on('pong', function (AsyncTcpConnection $connection, Payload $payload, Client $client) { // });
listen(): void
Connect to server and listen.
$client->listen();
🔹 Storage
Storage is a part of server, all data stored in flat files.
To get started you need set a path where files will be stored.
server()->storage()->load(__DIR__ . '/server-storage.data'); // you can use any filename
You can get access to storage like property or method:
server()->storage();
NOTICE: Set path only after if you booting server by (
server()->boot($worker)
method,Storage::class
can use anywhere and before booting server.
WARNING: If you not provide path or an incorrect path, data will be stored in RAM. After server restart you lose your data.
Storage::class
// as standalone use without server $store1 = new Porter\Storage(__DIR__ . '/path/to/file1'); $store2 = new Porter\Storage(__DIR__ . '/path/to/file2'); $store3 = new Porter\Storage(__DIR__ . '/path/to/file3');
load(?string $path = null): self
server()->storage()->load(__DIR__ . '/path/to/file'); // you can use any filename
put(string $key, mixed $value): void
server()->storage()->put('foo', 'bar');
get(string $key, mixed $default = null): mixed
server()->storage()->get('foo', 'default value'); // foo server()->storage()->get('baz', 'default value'); // default value
remove(string ...$keys): self
server()->storage()->remove('foo'); // true
has(string $key): bool
server()->storage()->has('foo'); // true server()->storage()->has('baz'); // false
filename(): string
Returns path to file.
server()->storage()->getPath();
🔹 Helpers (functions)
server(): Server
server()->on(...); // will be like: use Porter\Server; Server::getInstance()->on(...);
worker(): Worker
worker()->connections; // will be like: use Porter\Server; Server::getInstance()->getWorker()->connections;
channel(string $id, string|array $key = null, mixed $default = null): mixed
$channel = channel('secret channel'); // get channel instance $channel = server()->channel('secret channel'); $channel = server()->channels()->get('secret channel');
💡 See all helpers here.
🔹 Mappable methods (Macros)
You can extend the class and map your own methods on the fly..
Basic method:
server()->map('sum', fn(...$args) => array_sum($args)); echo server()->sum(1000, 300, 30, 5, 2); // 1337 echo server()->sum(1000, 300, 30, 5, 3); // 1338
As singletone method:
server()->mapOnce('timestamp', fn() => time()); echo server()->timestamp(); // e.g. 1234567890 sleep(1); echo server()->timestamp(); // e.g. 1234567890
🔹 Front-end
There is also a small class for working with websockets on the client side.
if (location.hostname == '127.0.0.1' || location.hostname == 'localhost') { const ws = new WebSocket(`ws://${location.hostname}:3737`); // on local dev } else { const ws = new WebSocket(`wss://${location.hostname}:3737`); // on vps with ssl certificate } // options (optional, below default values) let options = { pingInterval: 30000, // 30 sec. maxBodySize: 1e+6, // 1 mb. } const client = new Porter(ws, options); // on client connected to server client.connected = () => { // code... } // on client disconected from server client.disconnected = () => { // code... } // on error client.error = () => { // code... } // on raw `pong` event (if you needed it) client.pong = () => { // code... } // close connection client.close(); // event handler client.on('ping', payload => { // available properties payload.type; payload.data; console.log(payload.data.foo) // bar }); // send event to server client.send('pong', { foo: 'bar', }); // chain methods client.send('ping').on('pong', payload => console.log(payload.type)); // send event and handle answer in one method client.send('get online users', {/* ... */}, payload => { console.log(payload.type); // contains same event type 'get online users' console.log(payload.data.online); // and server answer e.g. '1337 users' }); // pass channel_id and target_id for magic properties on back-end server client.send('magical properties example', { channel_id: 'secret channel', target_id: 1337, // on backend php websocket server we can use $this->channel and $this->target magical properties. }); // send raw websocket data client.raw.send('hello world'); // send raw websocket data as json client.raw.send(JSON.stringify({ foo: 'bar', })); // handle raw websocket data from server client.raw.on('hello from server', data => { console.log(data); // hello from server }); // dont forget start listen websocket server! client.listen();
Used by
- naplenke.online — The largest online cinema in Russia. Watching movies together.
Credits
License
MIT