Completed
Pull Request — master (#447)
by Alexandru
02:04
created

RedisChannelManager::removeConnectionFromSet()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 7
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
1
<?php
2
3
namespace BeyondCode\LaravelWebSockets\ChannelManagers;
4
5
use BeyondCode\LaravelWebSockets\Channels\Channel;
6
use BeyondCode\LaravelWebSockets\Helpers;
7
use BeyondCode\LaravelWebSockets\Server\MockableConnection;
8
use Carbon\Carbon;
9
use Clue\React\Redis\Client;
10
use Clue\React\Redis\Factory;
11
use Illuminate\Cache\RedisLock;
12
use Illuminate\Support\Facades\Redis;
13
use Illuminate\Support\Str;
14
use Ratchet\ConnectionInterface;
15
use React\EventLoop\LoopInterface;
16
use React\Promise\PromiseInterface;
17
use stdClass;
18
19
class RedisChannelManager extends LocalChannelManager
20
{
21
    /**
22
     * The running loop.
23
     *
24
     * @var LoopInterface
25
     */
26
    protected $loop;
27
28
    /**
29
     * The unique server identifier.
30
     *
31
     * @var string
32
     */
33
    protected $serverId;
34
35
    /**
36
     * The pub client.
37
     *
38
     * @var Client
39
     */
40
    protected $publishClient;
41
42
    /**
43
     * The sub client.
44
     *
45
     * @var Client
46
     */
47
    protected $subscribeClient;
48
49
    /**
50
     * The Redis manager instance.
51
     *
52
     * @var \Illuminate\Redis\RedisManager
53
     */
54
    protected $redis;
55
56
    /**
57
     * The lock name to use on Redis to avoid multiple
58
     * actions that might lead to multiple processings.
59
     *
60
     * @var string
61
     */
62
    protected static $redisLockName = 'laravel-websockets:channel-manager:lock';
63
64
    /**
65
     * Create a new channel manager instance.
66
     *
67
     * @param  LoopInterface  $loop
68
     * @param  string|null  $factoryClass
69
     * @return void
0 ignored issues
show
Comprehensibility Best Practice introduced by
Adding a @return annotation to constructors is generally not recommended as a constructor does not have a meaningful return value.

Adding a @return annotation to a constructor is not recommended, since a constructor does not have a meaningful return value.

Please refer to the PHP core documentation on constructors.

Loading history...
70
     */
71
    public function __construct(LoopInterface $loop, $factoryClass = null)
72
    {
73
        $this->loop = $loop;
74
75
        $this->redis = Redis::connection(
0 ignored issues
show
Documentation Bug introduced by
It seems like \Illuminate\Support\Faca...onnection', 'default')) of type object<Illuminate\Redis\Connections\Connection> is incompatible with the declared type object<Illuminate\Redis\RedisManager> of property $redis.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
76
            config('websockets.replication.modes.redis.connection', 'default')
77
        );
78
79
        $connectionUri = $this->getConnectionUri();
80
81
        $factoryClass = $factoryClass ?: Factory::class;
82
        $factory = new $factoryClass($this->loop);
83
84
        $this->publishClient = $factory->createLazyClient($connectionUri);
85
        $this->subscribeClient = $factory->createLazyClient($connectionUri);
86
87
        $this->subscribeClient->on('message', function ($channel, $payload) {
88
            $this->onMessage($channel, $payload);
89
        });
90
91
        $this->serverId = Str::uuid()->toString();
92
    }
93
94
    /**
95
     * Get the local connections, regardless of the channel
96
     * they are connected to.
97
     *
98
     * @return \React\Promise\PromiseInterface
99
     */
100
    public function getLocalConnections(): PromiseInterface
101
    {
102
        return parent::getLocalConnections();
103
    }
104
105
    /**
106
     * Get all channels for a specific app
107
     * for the current instance.
108
     *
109
     * @param  string|int  $appId
110
     * @return \React\Promise\PromiseInterface[array]
0 ignored issues
show
Documentation introduced by
The doc-type \React\Promise\PromiseInterface[array] could not be parsed: Expected "]" at position 2, but found "array". (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
111
     */
112
    public function getLocalChannels($appId): PromiseInterface
113
    {
114
        return parent::getLocalChannels($appId);
115
    }
116
117
    /**
118
     * Get all channels for a specific app
119
     * across multiple servers.
120
     *
121
     * @param  string|int  $appId
122
     * @return \React\Promise\PromiseInterface[array]
0 ignored issues
show
Documentation introduced by
The doc-type \React\Promise\PromiseInterface[array] could not be parsed: Expected "]" at position 2, but found "array". (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
123
     */
124
    public function getGlobalChannels($appId): PromiseInterface
125
    {
126
        return $this->publishClient->smembers(
127
            $this->getRedisKey($appId, null, ['channels'])
128
        );
129
    }
130
131
    /**
132
     * Remove connection from all channels.
133
     *
134
     * @param  \Ratchet\ConnectionInterface  $connection
135
     * @return void
136
     */
137
    public function unsubscribeFromAllChannels(ConnectionInterface $connection)
138
    {
139
        $this->getGlobalChannels($connection->app->id)
0 ignored issues
show
Bug introduced by
Accessing app on the interface Ratchet\ConnectionInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
140
            ->then(function ($channels) use ($connection) {
141
                foreach ($channels as $channel) {
142
                    $this->unsubscribeFromChannel(
143
                        $connection, $channel, new stdClass
144
                    );
145
                }
146
            });
147
148
        parent::unsubscribeFromAllChannels($connection);
149
    }
150
151
    /**
152
     * Subscribe the connection to a specific channel.
153
     *
154
     * @param  \Ratchet\ConnectionInterface  $connection
155
     * @param  string  $channelName
156
     * @param  stdClass  $payload
157
     * @return void
158
     */
159
    public function subscribeToChannel(ConnectionInterface $connection, string $channelName, stdClass $payload)
160
    {
161
        $this->subscribeToTopic($connection->app->id, $channelName);
0 ignored issues
show
Bug introduced by
Accessing app on the interface Ratchet\ConnectionInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
162
163
        $this->addConnectionToSet($connection, Carbon::now());
164
165
        $this->addChannelToSet(
166
            $connection->app->id, $channelName
0 ignored issues
show
Bug introduced by
Accessing app on the interface Ratchet\ConnectionInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
167
        );
168
169
        $this->incrementSubscriptionsCount(
170
            $connection->app->id, $channelName, 1
0 ignored issues
show
Bug introduced by
Accessing app on the interface Ratchet\ConnectionInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
171
        );
172
173
        parent::subscribeToChannel($connection, $channelName, $payload);
174
    }
175
176
    /**
177
     * Unsubscribe the connection from the channel.
178
     *
179
     * @param  \Ratchet\ConnectionInterface  $connection
180
     * @param  string  $channelName
181
     * @param  stdClass  $payload
182
     * @return void
183
     */
184
    public function unsubscribeFromChannel(ConnectionInterface $connection, string $channelName, stdClass $payload)
185
    {
186
        $this->getGlobalConnectionsCount($connection->app->id, $channelName)
0 ignored issues
show
Bug introduced by
Accessing app on the interface Ratchet\ConnectionInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
187
            ->then(function ($count) use ($connection, $channelName) {
188
                if ($count === 0) {
189
                    // Make sure to not stay subscribed to the PubSub topic
190
                    // if there are no connections.
191
                    $this->unsubscribeFromTopic($connection->app->id, $channelName);
0 ignored issues
show
Bug introduced by
Accessing app on the interface Ratchet\ConnectionInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
192
                }
193
194
                $this->decrementSubscriptionsCount($connection->app->id, $channelName)
0 ignored issues
show
Bug introduced by
Accessing app on the interface Ratchet\ConnectionInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
195
                    ->then(function ($count) use ($connection, $channelName) {
196
                        // If the total connections count gets to 0 after unsubscribe,
197
                        // try again to check & unsubscribe from the PubSub topic if needed.
198
                        if ($count < 1) {
199
                            $this->unsubscribeFromTopic($connection->app->id, $channelName);
0 ignored issues
show
Bug introduced by
Accessing app on the interface Ratchet\ConnectionInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
200
                        }
201
                    });
202
203
                $this->removeChannelFromSet($connection->app->id, $channelName);
0 ignored issues
show
Bug introduced by
Accessing app on the interface Ratchet\ConnectionInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
204
205
                $this->removeConnectionFromSet($connection);
206
            });
207
208
        parent::unsubscribeFromChannel($connection, $channelName, $payload);
209
    }
210
211
    /**
212
     * Subscribe the connection to a specific channel.
213
     *
214
     * @param  string|int  $appId
215
     * @return void
216
     */
217
    public function subscribeToApp($appId)
218
    {
219
        $this->subscribeToTopic($appId);
220
221
        $this->incrementSubscriptionsCount($appId);
222
    }
223
224
    /**
225
     * Unsubscribe the connection from the channel.
226
     *
227
     * @param  string|int  $appId
228
     * @return void
229
     */
230
    public function unsubscribeFromApp($appId)
231
    {
232
        $this->unsubscribeFromTopic($appId);
233
234
        $this->incrementSubscriptionsCount($appId, null, -1);
235
    }
236
237
    /**
238
     * Get the connections count on the app
239
     * for the current server instance.
240
     *
241
     * @param  string|int  $appId
242
     * @param  string|null  $channelName
243
     * @return \React\Promise\PromiseInterface
244
     */
245
    public function getLocalConnectionsCount($appId, string $channelName = null): PromiseInterface
246
    {
247
        return parent::getLocalConnectionsCount($appId, $channelName);
248
    }
249
250
    /**
251
     * Get the connections count
252
     * across multiple servers.
253
     *
254
     * @param  string|int  $appId
255
     * @param  string|null  $channelName
256
     * @return \React\Promise\PromiseInterface
257
     */
258
    public function getGlobalConnectionsCount($appId, string $channelName = null): PromiseInterface
259
    {
260
        return $this->publishClient
261
            ->hget($this->getRedisKey($appId, $channelName, ['stats']), 'connections')
262
            ->then(function ($count) {
263
                return is_null($count) ? 0 : (int) $count;
264
            });
265
    }
266
267
    /**
268
     * Broadcast the message across multiple servers.
269
     *
270
     * @param  string|int  $appId
271
     * @param  string  $channel
272
     * @param  stdClass  $payload
273
     * @return bool
274
     */
275
    public function broadcastAcrossServers($appId, string $channel, stdClass $payload)
276
    {
277
        $payload->appId = $appId;
278
        $payload->serverId = $this->getServerId();
279
280
        $this->publishClient->publish($this->getRedisKey($appId, $channel), json_encode($payload));
281
282
        return true;
283
    }
284
285
    /**
286
     * Handle the user when it joined a presence channel.
287
     *
288
     * @param  \Ratchet\ConnectionInterface  $connection
289
     * @param  stdClass  $user
290
     * @param  string  $channel
291
     * @param  stdClass  $payload
292
     * @return void
293
     */
294
    public function userJoinedPresenceChannel(ConnectionInterface $connection, stdClass $user, string $channel, stdClass $payload)
295
    {
296
        $this->storeUserData(
297
            $connection->app->id, $channel, $connection->socketId, json_encode($user)
0 ignored issues
show
Bug introduced by
Accessing app on the interface Ratchet\ConnectionInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
Bug introduced by
Accessing socketId on the interface Ratchet\ConnectionInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
298
        );
299
300
        $this->addUserSocket(
301
            $connection->app->id, $channel, $user, $connection->socketId
0 ignored issues
show
Bug introduced by
Accessing app on the interface Ratchet\ConnectionInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
Bug introduced by
Accessing socketId on the interface Ratchet\ConnectionInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
302
        );
303
    }
304
305
    /**
306
     * Handle the user when it left a presence channel.
307
     *
308
     * @param  \Ratchet\ConnectionInterface  $connection
309
     * @param  stdClass  $user
310
     * @param  string  $channel
311
     * @param  stdClass  $payload
0 ignored issues
show
Bug introduced by
There is no parameter named $payload. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
312
     * @return void
313
     */
314
    public function userLeftPresenceChannel(ConnectionInterface $connection, stdClass $user, string $channel)
315
    {
316
        $this->removeUserData(
317
            $connection->app->id, $channel, $connection->socketId
0 ignored issues
show
Bug introduced by
Accessing app on the interface Ratchet\ConnectionInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
Bug introduced by
Accessing socketId on the interface Ratchet\ConnectionInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
318
        );
319
320
        $this->removeUserSocket(
321
            $connection->app->id, $channel, $user, $connection->socketId
0 ignored issues
show
Bug introduced by
Accessing app on the interface Ratchet\ConnectionInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
Bug introduced by
Accessing socketId on the interface Ratchet\ConnectionInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
322
        );
323
    }
324
325
    /**
326
     * Get the presence channel members.
327
     *
328
     * @param  string|int  $appId
329
     * @param  string  $channel
330
     * @return \React\Promise\PromiseInterface
331
     */
332
    public function getChannelMembers($appId, string $channel): PromiseInterface
333
    {
334
        return $this->publishClient
335
            ->hgetall($this->getRedisKey($appId, $channel, ['users']))
336
            ->then(function ($list) {
337
                return collect(Helpers::redisListToArray($list))
338
                    ->map(function ($user) {
339
                        return json_decode($user);
340
                    })
341
                    ->unique('user_id')
342
                    ->toArray();
343
            });
344
    }
345
346
    /**
347
     * Get a member from a presence channel based on connection.
348
     *
349
     * @param  \Ratchet\ConnectionInterface  $connection
350
     * @param  string  $channel
351
     * @return \React\Promise\PromiseInterface
352
     */
353
    public function getChannelMember(ConnectionInterface $connection, string $channel): PromiseInterface
354
    {
355
        return $this->publishClient->hget(
356
            $this->getRedisKey($connection->app->id, $channel, ['users']), $connection->socketId
0 ignored issues
show
Bug introduced by
Accessing app on the interface Ratchet\ConnectionInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
Bug introduced by
Accessing socketId on the interface Ratchet\ConnectionInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
357
        );
358
    }
359
360
    /**
361
     * Get the presence channels total members count.
362
     *
363
     * @param  string|int  $appId
364
     * @param  array  $channelNames
365
     * @return \React\Promise\PromiseInterface
366
     */
367
    public function getChannelsMembersCount($appId, array $channelNames): PromiseInterface
368
    {
369
        $this->publishClient->multi();
370
371
        foreach ($channelNames as $channel) {
372
            $this->publishClient->hlen(
373
                $this->getRedisKey($appId, $channel, ['users'])
374
            );
375
        }
376
377
        return $this->publishClient->exec()
378
            ->then(function ($data) use ($channelNames) {
379
                return array_combine($channelNames, $data);
380
            });
381
    }
382
383
    /**
384
     * Get the socket IDs for a presence channel member.
385
     *
386
     * @param  string|int  $userId
387
     * @param  string|int  $appId
388
     * @param  string  $channelName
389
     * @return \React\Promise\PromiseInterface
390
     */
391
    public function getMemberSockets($userId, $appId, $channelName): PromiseInterface
392
    {
393
        return $this->publishClient->smembers(
394
            $this->getRedisKey($appId, $channelName, [$userId, 'userSockets'])
395
        );
396
    }
397
398
    /**
399
     * Keep tracking the connections availability when they pong.
400
     *
401
     * @param  \Ratchet\ConnectionInterface  $connection
402
     * @return bool
403
     */
404
    public function connectionPonged(ConnectionInterface $connection): bool
405
    {
406
        // This will update the score with the current timestamp.
407
        $this->addConnectionToSet($connection, Carbon::now());
408
409
        return parent::connectionPonged($connection);
410
    }
411
412
    /**
413
     * Remove the obsolete connections that didn't ponged in a while.
414
     *
415
     * @return bool
416
     */
417
    public function removeObsoleteConnections(): bool
418
    {
419
        $this->lock()->get(function () {
420
            $this->getConnectionsFromSet(0, now()->subMinutes(2)->format('U'))
421
                ->then(function ($connections) {
422
                    foreach ($connections as $appId => $socketId) {
423
                        $this->unsubscribeFromAllChannels(
424
                            $this->fakeConnectionForApp($appId, $socketId)
425
                        );
426
                    }
427
                });
428
        });
429
430
        return parent::removeObsoleteConnections();
431
    }
432
433
    /**
434
     * Handle a message received from Redis on a specific channel.
435
     *
436
     * @param  string  $redisChannel
437
     * @param  string  $payload
438
     * @return void
439
     */
440
    public function onMessage(string $redisChannel, string $payload)
441
    {
442
        $payload = json_decode($payload);
443
444
        if (isset($payload->serverId) && $this->getServerId() === $payload->serverId) {
445
            return;
446
        }
447
448
        $payload->channel = Str::after($redisChannel, "{$payload->appId}:");
449
450
        if (! $channel = $this->find($payload->appId, $payload->channel)) {
451
            return;
452
        }
453
454
        $appId = $payload->appId ?? null;
455
        $socketId = $payload->socketId ?? null;
456
        $serverId = $payload->serverId ?? null;
0 ignored issues
show
Unused Code introduced by
$serverId is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
457
458
        unset($payload->socketId);
459
        unset($payload->serverId);
460
        unset($payload->appId);
461
462
        $channel->broadcastToEveryoneExcept($payload, $socketId, $appId, false);
463
    }
464
465
    /**
466
     * Build the Redis connection URL from Laravel database config.
467
     *
468
     * @return string
469
     */
470
    protected function getConnectionUri()
471
    {
472
        $name = config('websockets.replication.redis.connection', 'default');
473
        $config = config("database.redis.{$name}");
474
475
        $host = $config['host'];
476
        $port = $config['port'] ?: 6379;
477
478
        $query = [];
479
480
        if ($config['password']) {
481
            $query['password'] = $config['password'];
482
        }
483
484
        if ($config['database']) {
485
            $query['database'] = $config['database'];
486
        }
487
488
        $query = http_build_query($query);
489
490
        return "redis://{$host}:{$port}".($query ? "?{$query}" : '');
491
    }
492
493
    /**
494
     * Get the Subscribe client instance.
495
     *
496
     * @return Client
497
     */
498
    public function getSubscribeClient()
499
    {
500
        return $this->subscribeClient;
501
    }
502
503
    /**
504
     * Get the Publish client instance.
505
     *
506
     * @return Client
507
     */
508
    public function getPublishClient()
509
    {
510
        return $this->publishClient;
511
    }
512
513
    /**
514
     * Get the unique identifier for the server.
515
     *
516
     * @return string
517
     */
518
    public function getServerId()
519
    {
520
        return $this->serverId;
521
    }
522
523
    /**
524
     * Increment the subscribed count number.
525
     *
526
     * @param  string|int  $appId
527
     * @param  string|null  $channel
528
     * @param  int  $increment
529
     * @return PromiseInterface
530
     */
531
    public function incrementSubscriptionsCount($appId, string $channel = null, int $increment = 1)
532
    {
533
        return $this->publishClient->hincrby(
534
            $this->getRedisKey($appId, $channel, ['stats']), 'connections', $increment
535
        );
536
    }
537
538
    /**
539
     * Decrement the subscribed count number.
540
     *
541
     * @param  string|int  $appId
542
     * @param  string|null  $channel
543
     * @param  int  $decrement
0 ignored issues
show
Bug introduced by
There is no parameter named $decrement. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
544
     * @return PromiseInterface
545
     */
546
    public function decrementSubscriptionsCount($appId, string $channel = null, int $increment = 1)
547
    {
548
        return $this->incrementSubscriptionsCount($appId, $channel, $increment * -1);
549
    }
550
551
    /**
552
     * Add the connection to the sorted list.
553
     *
554
     * @param  \Ratchet\ConnectionInterface  $connection
555
     * @param  \DateTime|string|null  $moment
556
     * @return void
557
     */
558
    public function addConnectionToSet(ConnectionInterface $connection, $moment = null)
559
    {
560
        $moment = $moment ? Carbon::parse($moment) : Carbon::now();
561
562
        $this->publishClient->zadd(
563
            $this->getRedisKey(null, null, ['sockets']),
564
            $moment->format('U'), "{$connection->app->id}:{$connection->socketId}"
0 ignored issues
show
Bug introduced by
Accessing app on the interface Ratchet\ConnectionInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
Bug introduced by
Accessing socketId on the interface Ratchet\ConnectionInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
565
        );
566
    }
567
568
    /**
569
     * Remove the connection from the sorted list.
570
     *
571
     * @param  \Ratchet\ConnectionInterface  $connection
572
     * @return void
573
     */
574
    public function removeConnectionFromSet(ConnectionInterface $connection)
575
    {
576
        $this->publishClient->zrem(
577
            $this->getRedisKey(null, null, ['sockets']),
578
            "{$connection->app->id}:{$connection->socketId}"
0 ignored issues
show
Bug introduced by
Accessing app on the interface Ratchet\ConnectionInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
Bug introduced by
Accessing socketId on the interface Ratchet\ConnectionInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
579
        );
580
    }
581
582
    /**
583
     * Get the connections from the sorted list, with last
584
     * connection between certain timestamps.
585
     *
586
     * @param  int  $start
587
     * @param  int  $stop
588
     * @param  bool  $strict
589
     * @return PromiseInterface
590
     */
591
    public function getConnectionsFromSet(int $start = 0, int $stop = 0, bool $strict = true)
592
    {
593
        if ($strict) {
594
            $start = "({$start}";
595
            $stop = "({$stop}";
596
        }
597
598
        return $this->publishClient->zrangebyscore(
599
            $this->getRedisKey(null, null, ['sockets']),
600
            $start, $stop
601
        )
602
        ->then(function ($list) {
603
            return collect($list)->mapWithKeys(function ($appWithSocket) {
604
                [$appId, $socketId] = explode(':', $appWithSocket);
0 ignored issues
show
Bug introduced by
The variable $appId does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $socketId does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
605
606
                return [$appId => $socketId];
607
            })->toArray();
608
        });
609
    }
610
611
    /**
612
     * Add a channel to the set list.
613
     *
614
     * @param  string|int  $appId
615
     * @param  string $channel
616
     * @return PromiseInterface
617
     */
618
    public function addChannelToSet($appId, string $channel)
619
    {
620
        return $this->publishClient->sadd(
621
            $this->getRedisKey($appId, null, ['channels']),
622
            $channel
623
        );
624
    }
625
626
    /**
627
     * Remove a channel from the set list.
628
     *
629
     * @param  string|int  $appId
630
     * @param  string  $channel
631
     * @return PromiseInterface
632
     */
633
    public function removeChannelFromSet($appId, string $channel)
634
    {
635
        return $this->publishClient->srem(
636
            $this->getRedisKey($appId, null, ['channels']),
637
            $channel
638
        );
639
    }
640
641
    /**
642
     * Set data for a topic. Might be used for the presence channels.
643
     *
644
     * @param  string|int  $appId
645
     * @param  string|null  $channel
646
     * @param  string  $key
647
     * @param  string  $data
648
     * @return PromiseInterface
649
     */
650
    public function storeUserData($appId, string $channel = null, string $key, $data)
651
    {
652
        $this->publishClient->hset(
653
            $this->getRedisKey($appId, $channel, ['users']), $key, $data
654
        );
655
    }
656
657
    /**
658
     * Remove data for a topic. Might be used for the presence channels.
659
     *
660
     * @param  string|int  $appId
661
     * @param  string|null  $channel
662
     * @param  string  $key
663
     * @return PromiseInterface
664
     */
665
    public function removeUserData($appId, string $channel = null, string $key)
666
    {
667
        return $this->publishClient->hdel(
668
            $this->getRedisKey($appId, $channel, ['users']), $key
669
        );
670
    }
671
672
    /**
673
     * Subscribe to the topic for the app, or app and channel.
674
     *
675
     * @param  string|int  $appId
676
     * @param  string|null  $channel
677
     * @return void
678
     */
679
    public function subscribeToTopic($appId, string $channel = null)
680
    {
681
        $this->subscribeClient->subscribe(
682
            $this->getRedisKey($appId, $channel)
683
        );
684
    }
685
686
    /**
687
     * Unsubscribe from the topic for the app, or app and channel.
688
     *
689
     * @param  string|int  $appId
690
     * @param  string|null  $channel
691
     * @return void
692
     */
693
    public function unsubscribeFromTopic($appId, string $channel = null)
694
    {
695
        $this->subscribeClient->unsubscribe(
696
            $this->getRedisKey($appId, $channel)
697
        );
698
    }
699
700
    /**
701
     * Add the Presence Channel's User's Socket ID to a list.
702
     *
703
     * @param  string|int  $appId
704
     * @param  string  $channel
705
     * @param  stdClass  $user
706
     * @param  string  $socketId
707
     * @return void
708
     */
709
    protected function addUserSocket($appId, string $channel, stdClass $user, string $socketId)
710
    {
711
        $this->publishClient->sadd(
712
            $this->getRedisKey($appId, $channel, [$user->user_id, 'userSockets']),
713
            $socketId
714
        );
715
    }
716
717
    /**
718
     * Remove the Presence Channel's User's Socket ID from the list.
719
     *
720
     * @param  string|int  $appId
721
     * @param  string  $channel
722
     * @param  stdClass  $user
723
     * @param  string  $socketId
724
     * @return void
725
     */
726
    protected function removeUserSocket($appId, string $channel, stdClass $user, string $socketId)
727
    {
728
        $this->publishClient->srem(
729
            $this->getRedisKey($appId, $channel, [$user->user_id, 'userSockets']),
730
            $socketId
731
        );
732
    }
733
734
    /**
735
     * Get the Redis Keyspace name to handle subscriptions
736
     * and other key-value sets.
737
     *
738
     * @param  string|int|null  $appId
739
     * @param  string|null  $channel
740
     * @return string
741
     */
742
    public function getRedisKey($appId = null, string $channel = null, array $suffixes = []): string
743
    {
744
        $prefix = config('database.redis.options.prefix', null);
745
746
        $hash = "{$prefix}{$appId}";
747
748
        if ($channel) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $channel of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
749
            $suffixes = array_merge([$channel], $suffixes);
750
        }
751
752
        $suffixes = implode(':', $suffixes);
753
754
        if ($suffixes) {
755
            $hash .= ":{$suffixes}";
756
        }
757
758
        return $hash;
759
    }
760
761
    /**
762
     * Get a new RedisLock instance to avoid race conditions.
763
     *
764
     * @return \Illuminate\Cache\CacheLock
765
     */
766
    protected function lock()
767
    {
768
        return new RedisLock($this->redis, static::$redisLockName, 0);
0 ignored issues
show
Documentation introduced by
$this->redis is of type object<Illuminate\Redis\RedisManager>, but the function expects a object<Illuminate\Redis\Connections\Connection>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
769
    }
770
771
    /**
772
     * Create a fake connection for app that will mimick a connection
773
     * by app ID and Socket ID to be able to be passed to the methods
774
     * that accepts a connection class.
775
     *
776
     * @param  string|int  $appId
777
     * @param  string  $socketId
778
     * @return ConnectionInterface
779
     */
780
    public function fakeConnectionForApp($appId, string $socketId)
781
    {
782
        return new MockableConnection($appId, $socketId);
783
    }
784
}
785