Skip to content

Commit 8180feb

Browse files
author
Micilini Roll
committed
phase 03: implement server runtime and events
1 parent f86c96b commit 8180feb

21 files changed

Lines changed: 988 additions & 0 deletions

README.zip

20.9 KB
Binary file not shown.

src/Connection/Connection.php

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Micilini\PhpSockets\Connection;
6+
7+
use Micilini\PhpSockets\Protocol\CloseCode;
8+
use Micilini\PhpSockets\Protocol\Frame;
9+
use Micilini\PhpSockets\Protocol\FrameCodec;
10+
use RuntimeException;
11+
use Socket;
12+
13+
final class Connection
14+
{
15+
private ConnectionState $state;
16+
private ?string $userId = null;
17+
18+
public function __construct(
19+
private readonly string $id,
20+
private readonly Socket $socket,
21+
private readonly FrameCodec $codec,
22+
private readonly ?string $remoteAddress = null,
23+
ConnectionState $state = ConnectionState::OPEN,
24+
) {
25+
$this->state = $state;
26+
}
27+
28+
public function id(): string
29+
{
30+
return $this->id;
31+
}
32+
33+
public function socket(): Socket
34+
{
35+
return $this->socket;
36+
}
37+
38+
public function socketId(): int
39+
{
40+
return spl_object_id($this->socket);
41+
}
42+
43+
public function state(): ConnectionState
44+
{
45+
return $this->state;
46+
}
47+
48+
public function remoteAddress(): ?string
49+
{
50+
return $this->remoteAddress;
51+
}
52+
53+
public function userId(): ?string
54+
{
55+
return $this->userId;
56+
}
57+
58+
public function setUserId(?string $userId): void
59+
{
60+
$this->userId = $userId;
61+
}
62+
63+
public function markOpen(): void
64+
{
65+
$this->state = ConnectionState::OPEN;
66+
}
67+
68+
public function markClosing(): void
69+
{
70+
$this->state = ConnectionState::CLOSING;
71+
}
72+
73+
public function markClosed(): void
74+
{
75+
$this->state = ConnectionState::CLOSED;
76+
}
77+
78+
public function send(string|Frame $message): void
79+
{
80+
if ($this->state === ConnectionState::CLOSED) {
81+
throw new RuntimeException('Cannot send data to a closed WebSocket connection.');
82+
}
83+
84+
$frame = is_string($message) ? Frame::text($message) : $message;
85+
$this->writeAll($this->codec->encode($frame));
86+
}
87+
88+
public function close(int $code = CloseCode::NORMAL_CLOSURE->value, string $reason = ''): void
89+
{
90+
if ($this->state === ConnectionState::CLOSED) {
91+
return;
92+
}
93+
94+
$this->state = ConnectionState::CLOSING;
95+
$payload = pack('n', $code) . $reason;
96+
97+
try {
98+
$this->send(Frame::close($payload));
99+
} catch (RuntimeException) {
100+
}
101+
102+
socket_close($this->socket);
103+
$this->state = ConnectionState::CLOSED;
104+
}
105+
106+
private function writeAll(string $bytes): void
107+
{
108+
$length = strlen($bytes);
109+
$written = 0;
110+
111+
while ($written < $length) {
112+
$result = socket_write($this->socket, substr($bytes, $written));
113+
114+
if ($result === false || $result === 0) {
115+
throw new RuntimeException('Failed to write data to the WebSocket connection.');
116+
}
117+
118+
$written += $result;
119+
}
120+
}
121+
}

src/Connection/ConnectionId.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Micilini\PhpSockets\Connection;
6+
7+
final class ConnectionId
8+
{
9+
public static function generate(): string
10+
{
11+
return 'conn_' . bin2hex(random_bytes(16));
12+
}
13+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Micilini\PhpSockets\Connection;
6+
7+
use Micilini\PhpSockets\Contracts\ConnectionRegistryInterface;
8+
9+
final class ConnectionRegistry implements ConnectionRegistryInterface
10+
{
11+
/**
12+
* @var array<string, Connection>
13+
*/
14+
private array $connections = [];
15+
16+
/**
17+
* @var array<int, string>
18+
*/
19+
private array $connectionIdsBySocketId = [];
20+
21+
public function add(Connection $connection): void
22+
{
23+
$this->connections[$connection->id()] = $connection;
24+
$this->connectionIdsBySocketId[$connection->socketId()] = $connection->id();
25+
}
26+
27+
public function remove(string $connectionId): void
28+
{
29+
$connection = $this->connections[$connectionId] ?? null;
30+
31+
if (!$connection instanceof Connection) {
32+
return;
33+
}
34+
35+
unset($this->connections[$connectionId], $this->connectionIdsBySocketId[$connection->socketId()]);
36+
}
37+
38+
public function get(string $connectionId): ?Connection
39+
{
40+
return $this->connections[$connectionId] ?? null;
41+
}
42+
43+
public function findBySocketId(int $socketId): ?Connection
44+
{
45+
$connectionId = $this->connectionIdsBySocketId[$socketId] ?? null;
46+
47+
if ($connectionId === null) {
48+
return null;
49+
}
50+
51+
return $this->get($connectionId);
52+
}
53+
54+
public function count(): int
55+
{
56+
return count($this->connections);
57+
}
58+
59+
public function all(): array
60+
{
61+
return array_values($this->connections);
62+
}
63+
}

src/Connection/ConnectionState.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Micilini\PhpSockets\Connection;
6+
7+
enum ConnectionState: string
8+
{
9+
case CONNECTING = 'connecting';
10+
case OPEN = 'open';
11+
case CLOSING = 'closing';
12+
case CLOSED = 'closed';
13+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Micilini\PhpSockets\Contracts;
6+
7+
use Micilini\PhpSockets\Connection\Connection;
8+
9+
interface ConnectionRegistryInterface
10+
{
11+
public function add(Connection $connection): void;
12+
13+
public function remove(string $connectionId): void;
14+
15+
public function get(string $connectionId): ?Connection;
16+
17+
public function findBySocketId(int $socketId): ?Connection;
18+
19+
public function count(): int;
20+
21+
/**
22+
* @return list<Connection>
23+
*/
24+
public function all(): array;
25+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Micilini\PhpSockets\Contracts;
6+
7+
use Micilini\PhpSockets\Events\Event;
8+
9+
interface EventDispatcherInterface
10+
{
11+
public function listen(string $eventName, callable $listener): void;
12+
13+
public function dispatch(Event $event): void;
14+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Micilini\PhpSockets\Events;
6+
7+
final class CallbackEventDispatcher extends EventDispatcher
8+
{
9+
}

src/Events/ConnectionClosed.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Micilini\PhpSockets\Events;
6+
7+
use Micilini\PhpSockets\Connection\Connection;
8+
9+
final class ConnectionClosed extends Event
10+
{
11+
public function __construct(
12+
public readonly Connection $connection,
13+
public readonly int $code = 1000,
14+
public readonly string $reason = '',
15+
) {
16+
}
17+
18+
public function name(): string
19+
{
20+
return 'close';
21+
}
22+
}

src/Events/ConnectionOpened.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Micilini\PhpSockets\Events;
6+
7+
use Micilini\PhpSockets\Connection\Connection;
8+
9+
final class ConnectionOpened extends Event
10+
{
11+
public function __construct(public readonly Connection $connection)
12+
{
13+
}
14+
15+
public function name(): string
16+
{
17+
return 'open';
18+
}
19+
}

0 commit comments

Comments
 (0)