Skip to content
This repository was archived by the owner on Feb 7, 2024. It is now read-only.

Commit 97e215b

Browse files
committed
Making channels easily extendable by replacing contents with traits.
1 parent 1de554e commit 97e215b

File tree

6 files changed

+453
-420
lines changed

6 files changed

+453
-420
lines changed

src/Concerns/Channelable.php

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
<?php
2+
3+
namespace BeyondCode\LaravelWebSockets\Concerns;
4+
5+
use BeyondCode\LaravelWebSockets\Dashboard\DashboardLogger;
6+
use BeyondCode\LaravelWebSockets\PubSub\ReplicationInterface;
7+
use BeyondCode\LaravelWebSockets\WebSockets\Exceptions\InvalidSignature;
8+
use Illuminate\Support\Str;
9+
use Ratchet\ConnectionInterface;
10+
use stdClass;
11+
12+
trait Channelable
13+
{
14+
/**
15+
* The channel name.
16+
*
17+
* @var string
18+
*/
19+
protected $channelName;
20+
21+
/**
22+
* The replicator client.
23+
*
24+
* @var ReplicationInterface
25+
*/
26+
protected $replicator;
27+
28+
/**
29+
* The connections that got subscribed.
30+
*
31+
* @var array
32+
*/
33+
protected $subscribedConnections = [];
34+
35+
/**
36+
* Create a new instance.
37+
*
38+
* @param string $channelName
39+
* @return void
40+
*/
41+
public function __construct(string $channelName)
42+
{
43+
$this->channelName = $channelName;
44+
$this->replicator = app(ReplicationInterface::class);
45+
}
46+
47+
/**
48+
* Get the channel name.
49+
*
50+
* @return string
51+
*/
52+
public function getChannelName(): string
53+
{
54+
return $this->channelName;
55+
}
56+
57+
/**
58+
* Check if the channel has connections.
59+
*
60+
* @return bool
61+
*/
62+
public function hasConnections(): bool
63+
{
64+
return count($this->subscribedConnections) > 0;
65+
}
66+
67+
/**
68+
* Get all subscribed connections.
69+
*
70+
* @return array
71+
*/
72+
public function getSubscribedConnections(): array
73+
{
74+
return $this->subscribedConnections;
75+
}
76+
77+
/**
78+
* Check if the signature for the payload is valid.
79+
*
80+
* @param \Ratchet\ConnectionInterface $connection
81+
* @param \stdClass $payload
82+
* @return void
83+
* @throws InvalidSignature
84+
*/
85+
protected function verifySignature(ConnectionInterface $connection, stdClass $payload)
86+
{
87+
$signature = "{$connection->socketId}:{$this->channelName}";
88+
89+
if (isset($payload->channel_data)) {
90+
$signature .= ":{$payload->channel_data}";
91+
}
92+
93+
if (! hash_equals(
94+
hash_hmac('sha256', $signature, $connection->app->secret),
95+
Str::after($payload->auth, ':'))
96+
) {
97+
throw new InvalidSignature();
98+
}
99+
}
100+
101+
/**
102+
* Subscribe to the channel.
103+
*
104+
* @see https://pusher.com/docs/pusher_protocol#presence-channel-events
105+
* @param \Ratchet\ConnectionInterface $connection
106+
* @param \stdClass $payload
107+
* @return void
108+
*/
109+
public function subscribe(ConnectionInterface $connection, stdClass $payload)
110+
{
111+
$this->saveConnection($connection);
112+
113+
$connection->send(json_encode([
114+
'event' => 'pusher_internal:subscription_succeeded',
115+
'channel' => $this->channelName,
116+
]));
117+
118+
$this->replicator->subscribe($connection->app->id, $this->channelName);
119+
120+
event(new )
121+
}
122+
123+
/**
124+
* Unsubscribe connection from the channel.
125+
*
126+
* @param \Ratchet\ConnectionInterface $connection
127+
* @return void
128+
*/
129+
public function unsubscribe(ConnectionInterface $connection)
130+
{
131+
unset($this->subscribedConnections[$connection->socketId]);
132+
133+
$this->replicator->unsubscribe($connection->app->id, $this->channelName);
134+
135+
if (! $this->hasConnections()) {
136+
DashboardLogger::log($connection->app->id, DashboardLogger::TYPE_VACATED, [
137+
'socketId' => $connection->socketId,
138+
'channel' => $this->channelName,
139+
]);
140+
}
141+
}
142+
143+
/**
144+
* Store the connection to the subscribers list.
145+
*
146+
* @param \Ratchet\ConnectionInterface $connection
147+
* @return void
148+
*/
149+
protected function saveConnection(ConnectionInterface $connection)
150+
{
151+
$hadConnectionsPreviously = $this->hasConnections();
152+
153+
$this->subscribedConnections[$connection->socketId] = $connection;
154+
155+
if (! $hadConnectionsPreviously) {
156+
DashboardLogger::log($connection->app->id, DashboardLogger::TYPE_OCCUPIED, [
157+
'channel' => $this->channelName,
158+
]);
159+
}
160+
161+
DashboardLogger::log($connection->app->id, DashboardLogger::TYPE_SUBSCRIBED, [
162+
'socketId' => $connection->socketId,
163+
'channel' => $this->channelName,
164+
]);
165+
}
166+
167+
/**
168+
* Broadcast a payload to the subscribed connections.
169+
*
170+
* @param \stdClass $payload
171+
* @return void
172+
*/
173+
public function broadcast($payload)
174+
{
175+
foreach ($this->subscribedConnections as $connection) {
176+
$connection->send(json_encode($payload));
177+
}
178+
}
179+
180+
/**
181+
* Broadcast the payload, but exclude the current connection.
182+
*
183+
* @param \Ratchet\ConnectionInterface $connection
184+
* @param \stdClass $payload
185+
* @return void
186+
*/
187+
public function broadcastToOthers(ConnectionInterface $connection, stdClass $payload)
188+
{
189+
$this->broadcastToEveryoneExcept(
190+
$payload, $connection->socketId, $connection->app->id
191+
);
192+
}
193+
194+
/**
195+
* Broadcast the payload, but exclude a specific socket id.
196+
*
197+
* @param \stdClass $payload
198+
* @param string|null $socketId
199+
* @param mixed $appId
200+
* @param bool $publish
201+
* @return void
202+
*/
203+
public function broadcastToEveryoneExcept(stdClass $payload, ?string $socketId, $appId, bool $publish = true)
204+
{
205+
// Also broadcast via the other websocket server instances.
206+
// This is set false in the Redis client because we don't want to cause a loop
207+
// in this case. If this came from TriggerEventController, then we still want
208+
// to publish to get the message out to other server instances.
209+
if ($publish) {
210+
$this->replicator->publish($appId, $this->channelName, $payload);
211+
}
212+
213+
// Performance optimization, if we don't have a socket ID,
214+
// then we avoid running the if condition in the foreach loop below
215+
// by calling broadcast() instead.
216+
if (is_null($socketId)) {
217+
$this->broadcast($payload);
218+
219+
return;
220+
}
221+
222+
foreach ($this->subscribedConnections as $connection) {
223+
if ($connection->socketId !== $socketId) {
224+
$connection->send(json_encode($payload));
225+
}
226+
}
227+
}
228+
229+
/**
230+
* Convert the channel to array.
231+
*
232+
* @param mixed $appId
233+
* @return array
234+
*/
235+
public function toArray($appId = null)
236+
{
237+
return [
238+
'occupied' => count($this->subscribedConnections) > 0,
239+
'subscription_count' => count($this->subscribedConnections),
240+
];
241+
}
242+
}

0 commit comments

Comments
 (0)