Completed
Pull Request — master (#447)
by Alexandru
01:46
created

RedisChannelManager::decrementSubscriptionsCount()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 3
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($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...
166
167
        $this->incrementSubscriptionsCount($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...
168
169
        parent::subscribeToChannel($connection, $channelName, $payload);
170
    }
171
172
    /**
173
     * Unsubscribe the connection from the channel.
174
     *
175
     * @param  \Ratchet\ConnectionInterface  $connection
176
     * @param  string  $channelName
177
     * @param  stdClass  $payload
178
     * @return void
179
     */
180
    public function unsubscribeFromChannel(ConnectionInterface $connection, string $channelName, stdClass $payload)
181
    {
182
        $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...
183
            ->then(function ($count) use ($connection, $channelName) {
184
                if ($count === 0) {
185
                    // Make sure to not stay subscribed to the PubSub topic
186
                    // if there are no connections.
187
                    $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...
188
                }
189
190
                $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...
191
                    ->then(function ($count) use ($connection, $channelName) {
192
                        // If the total connections count gets to 0 after unsubscribe,
193
                        // try again to check & unsubscribe from the PubSub topic if needed.
194
                        if ($count < 1) {
195
                            $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...
196
                        }
197
                    });
198
199
                $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...
200
201
                $this->removeConnectionFromSet($connection);
202
            });
203
204
        parent::unsubscribeFromChannel($connection, $channelName, $payload);
205
    }
206
207
    /**
208
     * Subscribe the connection to a specific channel.
209
     *
210
     * @param  string|int  $appId
211
     * @return void
212
     */
213
    public function subscribeToApp($appId)
214
    {
215
        $this->subscribeToTopic($appId);
216
217
        $this->incrementSubscriptionsCount($appId);
218
    }
219
220
    /**
221
     * Unsubscribe the connection from the channel.
222
     *
223
     * @param  string|int  $appId
224
     * @return void
225
     */
226
    public function unsubscribeFromApp($appId)
227
    {
228
        $this->unsubscribeFromTopic($appId);
229
230
        $this->incrementSubscriptionsCount($appId, null, -1);
231
    }
232
233
    /**
234
     * Get the connections count on the app
235
     * for the current server instance.
236
     *
237
     * @param  string|int  $appId
238
     * @param  string|null  $channelName
239
     * @return \React\Promise\PromiseInterface
240
     */
241
    public function getLocalConnectionsCount($appId, string $channelName = null): PromiseInterface
242
    {
243
        return parent::getLocalConnectionsCount($appId, $channelName);
244
    }
245
246
    /**
247
     * Get the connections count
248
     * across multiple servers.
249
     *
250
     * @param  string|int  $appId
251
     * @param  string|null  $channelName
252
     * @return \React\Promise\PromiseInterface
253
     */
254
    public function getGlobalConnectionsCount($appId, string $channelName = null): PromiseInterface
255
    {
256
        return $this->publishClient
257
            ->hget($this->getRedisKey($appId, $channelName, ['stats']), 'connections')
258
            ->then(function ($count) {
259
                return is_null($count) ? 0 : (int) $count;
260
            });
261
    }
262
263
    /**
264
     * Broadcast the message across multiple servers.
265
     *
266
     * @param  string|int  $appId
267
     * @param  string|null  $socketId
268
     * @param  string  $channel
269
     * @param  stdClass  $payload
270
     * @param  string|null  $serverId
271
     * @return bool
272
     */
273
    public function broadcastAcrossServers($appId, ?string $socketId, string $channel, stdClass $payload, string $serverId = null)
274
    {
275
        $payload->appId = $appId;
276
        $payload->socketId = $socketId;
277
        $payload->serverId = $serverId ?: $this->getServerId();
278
279
        $this->publishClient->publish($this->getRedisKey($appId, $channel), json_encode($payload));
280
281
        return true;
282
    }
283
284
    /**
285
     * Handle the user when it joined a presence channel.
286
     *
287
     * @param  \Ratchet\ConnectionInterface  $connection
288
     * @param  stdClass  $user
289
     * @param  string  $channel
290
     * @param  stdClass  $payload
291
     * @return void
292
     */
293
    public function userJoinedPresenceChannel(ConnectionInterface $connection, stdClass $user, string $channel, stdClass $payload)
294
    {
295
        $this->storeUserData(
296
            $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...
297
        );
298
299
        $this->addUserSocket(
300
            $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...
301
        );
302
    }
303
304
    /**
305
     * Handle the user when it left a presence channel.
306
     *
307
     * @param  \Ratchet\ConnectionInterface  $connection
308
     * @param  stdClass  $user
309
     * @param  string  $channel
310
     * @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...
311
     * @return void
312
     */
313
    public function userLeftPresenceChannel(ConnectionInterface $connection, stdClass $user, string $channel)
314
    {
315
        $this->removeUserData(
316
            $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...
317
        );
318
319
        $this->removeUserSocket(
320
            $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...
321
        );
322
    }
323
324
    /**
325
     * Get the presence channel members.
326
     *
327
     * @param  string|int  $appId
328
     * @param  string  $channel
329
     * @return \React\Promise\PromiseInterface
330
     */
331
    public function getChannelMembers($appId, string $channel): PromiseInterface
332
    {
333
        return $this->publishClient
334
            ->hgetall($this->getRedisKey($appId, $channel, ['users']))
335
            ->then(function ($list) {
336
                return collect(Helpers::redisListToArray($list))
337
                    ->map(function ($user) {
338
                        return json_decode($user);
339
                    })
340
                    ->unique('user_id')
341
                    ->toArray();
342
            });
343
    }
344
345
    /**
346
     * Get a member from a presence channel based on connection.
347
     *
348
     * @param  \Ratchet\ConnectionInterface  $connection
349
     * @param  string  $channel
350
     * @return \React\Promise\PromiseInterface
351
     */
352
    public function getChannelMember(ConnectionInterface $connection, string $channel): PromiseInterface
353
    {
354
        return $this->publishClient->hget(
355
            $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...
356
        );
357
    }
358
359
    /**
360
     * Get the presence channels total members count.
361
     *
362
     * @param  string|int  $appId
363
     * @param  array  $channelNames
364
     * @return \React\Promise\PromiseInterface
365
     */
366
    public function getChannelsMembersCount($appId, array $channelNames): PromiseInterface
367
    {
368
        $this->publishClient->multi();
369
370
        foreach ($channelNames as $channel) {
371
            $this->publishClient->hlen(
372
                $this->getRedisKey($appId, $channel, ['users'])
373
            );
374
        }
375
376
        return $this->publishClient->exec()
377
            ->then(function ($data) use ($channelNames) {
378
                return array_combine($channelNames, $data);
379
            });
380
    }
381
382
    /**
383
     * Get the socket IDs for a presence channel member.
384
     *
385
     * @param  string|int  $userId
386
     * @param  string|int  $appId
387
     * @param  string  $channelName
388
     * @return \React\Promise\PromiseInterface
389
     */
390
    public function getMemberSockets($userId, $appId, $channelName): PromiseInterface
391
    {
392
        return $this->publishClient->smembers(
393
            $this->getRedisKey($appId, $channelName, [$userId, 'userSockets'])
394
        );
395
    }
396
397
    /**
398
     * Keep tracking the connections availability when they pong.
399
     *
400
     * @param  \Ratchet\ConnectionInterface  $connection
401
     * @return bool
402
     */
403
    public function connectionPonged(ConnectionInterface $connection): bool
404
    {
405
        // This will update the score with the current timestamp.
406
        $this->addConnectionToSet($connection, Carbon::now());
407
408
        return parent::connectionPonged($connection);
409
    }
410
411
    /**
412
     * Remove the obsolete connections that didn't ponged in a while.
413
     *
414
     * @return bool
415
     */
416
    public function removeObsoleteConnections(): bool
417
    {
418
        $this->lock()->get(function () {
419
            $this->getConnectionsFromSet(0, now()->subMinutes(2)->format('U'))
420
                ->then(function ($connections) {
421
                    foreach ($connections as $socketId => $appId) {
422
                        $this->unsubscribeFromAllChannels(
423
                            $this->fakeConnectionForApp($appId, $socketId)
424
                        );
425
                    }
426
                });
427
        });
428
429
        return parent::removeObsoleteConnections();
430
    }
431
432
    /**
433
     * Handle a message received from Redis on a specific channel.
434
     *
435
     * @param  string  $redisChannel
436
     * @param  string  $payload
437
     * @return void
438
     */
439
    public function onMessage(string $redisChannel, string $payload)
440
    {
441
        $payload = json_decode($payload);
442
443
        if (isset($payload->serverId) && $this->getServerId() === $payload->serverId) {
444
            return;
445
        }
446
447
        $payload->channel = Str::after($redisChannel, "{$payload->appId}:");
448
449
        if (! $channel = $this->find($payload->appId, $payload->channel)) {
450
            return;
451
        }
452
453
        $appId = $payload->appId ?? null;
454
        $socketId = $payload->socketId ?? null;
455
        $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...
456
457
        unset($payload->socketId);
458
        unset($payload->serverId);
459
        unset($payload->appId);
460
461
        $channel->broadcastLocallyToEveryoneExcept($payload, $socketId, $appId);
462
    }
463
464
    /**
465
     * Build the Redis connection URL from Laravel database config.
466
     *
467
     * @return string
468
     */
469
    protected function getConnectionUri()
470
    {
471
        $name = config('websockets.replication.redis.connection', 'default');
472
        $config = config("database.redis.{$name}");
473
474
        $host = $config['host'];
475
        $port = $config['port'] ?: 6379;
476
477
        $query = [];
478
479
        if ($config['password']) {
480
            $query['password'] = $config['password'];
481
        }
482
483
        if ($config['database']) {
484
            $query['database'] = $config['database'];
485
        }
486
487
        $query = http_build_query($query);
488
489
        return "redis://{$host}:{$port}".($query ? "?{$query}" : '');
490
    }
491
492
    /**
493
     * Get the Subscribe client instance.
494
     *
495
     * @return Client
496
     */
497
    public function getSubscribeClient()
498
    {
499
        return $this->subscribeClient;
500
    }
501
502
    /**
503
     * Get the Publish client instance.
504
     *
505
     * @return Client
506
     */
507
    public function getPublishClient()
508
    {
509
        return $this->publishClient;
510
    }
511
512
    /**
513
     * Get the unique identifier for the server.
514
     *
515
     * @return string
516
     */
517
    public function getServerId()
518
    {
519
        return $this->serverId;
520
    }
521
522
    /**
523
     * Increment the subscribed count number.
524
     *
525
     * @param  string|int  $appId
526
     * @param  string|null  $channel
527
     * @param  int  $increment
528
     * @return PromiseInterface
529
     */
530
    public function incrementSubscriptionsCount($appId, string $channel = null, int $increment = 1)
531
    {
532
        return $this->publishClient->hincrby(
533
            $this->getRedisKey($appId, $channel, ['stats']), 'connections', $increment
534
        );
535
    }
536
537
    /**
538
     * Decrement the subscribed count number.
539
     *
540
     * @param  string|int  $appId
541
     * @param  string|null  $channel
542
     * @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...
543
     * @return PromiseInterface
544
     */
545
    public function decrementSubscriptionsCount($appId, string $channel = null, int $increment = 1)
546
    {
547
        return $this->incrementSubscriptionsCount($appId, $channel, $increment * -1);
548
    }
549
550
    /**
551
     * Add the connection to the sorted list.
552
     *
553
     * @param  \Ratchet\ConnectionInterface  $connection
554
     * @param  \DateTime|string|null  $moment
555
     * @return void
556
     */
557
    public function addConnectionToSet(ConnectionInterface $connection, $moment = null)
558
    {
559
        $moment = $moment ? Carbon::parse($moment) : Carbon::now();
560
561
        $this->publishClient->zadd(
562
            $this->getRedisKey(null, null, ['sockets']),
563
            $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...
564
        );
565
    }
566
567
    /**
568
     * Remove the connection from the sorted list.
569
     *
570
     * @param  \Ratchet\ConnectionInterface  $connection
571
     * @return void
572
     */
573
    public function removeConnectionFromSet(ConnectionInterface $connection)
574
    {
575
        $this->publishClient->zrem(
576
            $this->getRedisKey(null, null, ['sockets']),
577
            "{$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...
578
        );
579
    }
580
581
    /**
582
     * Get the connections from the sorted list, with last
583
     * connection between certain timestamps.
584
     *
585
     * @param  int  $start
586
     * @param  int  $stop
587
     * @param  bool  $strict
588
     * @return PromiseInterface
589
     */
590
    public function getConnectionsFromSet(int $start = 0, int $stop = 0, bool $strict = true)
591
    {
592
        if ($strict) {
593
            $start = "({$start}";
594
            $stop = "({$stop}";
595
        }
596
597
        return $this->publishClient
598
            ->zrangebyscore($this->getRedisKey(null, null, ['sockets']), $start, $stop)
599
            ->then(function ($list) {
600
                return collect($list)->mapWithKeys(function ($appWithSocket) {
601
                    [$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...
602
603
                    return [$socketId => $appId];
604
                })->toArray();
605
            });
606
    }
607
608
    /**
609
     * Add a channel to the set list.
610
     *
611
     * @param  string|int  $appId
612
     * @param  string $channel
613
     * @return PromiseInterface
614
     */
615
    public function addChannelToSet($appId, string $channel)
616
    {
617
        return $this->publishClient->sadd(
618
            $this->getRedisKey($appId, null, ['channels']), $channel
619
        );
620
    }
621
622
    /**
623
     * Remove a channel from the set list.
624
     *
625
     * @param  string|int  $appId
626
     * @param  string  $channel
627
     * @return PromiseInterface
628
     */
629
    public function removeChannelFromSet($appId, string $channel)
630
    {
631
        return $this->publishClient->srem(
632
            $this->getRedisKey($appId, null, ['channels']), $channel
633
        );
634
    }
635
636
    /**
637
     * Set data for a topic. Might be used for the presence channels.
638
     *
639
     * @param  string|int  $appId
640
     * @param  string|null  $channel
641
     * @param  string  $key
642
     * @param  string  $data
643
     * @return PromiseInterface
644
     */
645
    public function storeUserData($appId, string $channel = null, string $key, $data)
646
    {
647
        $this->publishClient->hset(
648
            $this->getRedisKey($appId, $channel, ['users']), $key, $data
649
        );
650
    }
651
652
    /**
653
     * Remove data for a topic. Might be used for the presence channels.
654
     *
655
     * @param  string|int  $appId
656
     * @param  string|null  $channel
657
     * @param  string  $key
658
     * @return PromiseInterface
659
     */
660
    public function removeUserData($appId, string $channel = null, string $key)
661
    {
662
        return $this->publishClient->hdel(
663
            $this->getRedisKey($appId, $channel, ['users']), $key
664
        );
665
    }
666
667
    /**
668
     * Subscribe to the topic for the app, or app and channel.
669
     *
670
     * @param  string|int  $appId
671
     * @param  string|null  $channel
672
     * @return void
673
     */
674
    public function subscribeToTopic($appId, string $channel = null)
675
    {
676
        $this->subscribeClient->subscribe(
677
            $this->getRedisKey($appId, $channel)
678
        );
679
    }
680
681
    /**
682
     * Unsubscribe from the topic for the app, or app and channel.
683
     *
684
     * @param  string|int  $appId
685
     * @param  string|null  $channel
686
     * @return void
687
     */
688
    public function unsubscribeFromTopic($appId, string $channel = null)
689
    {
690
        $this->subscribeClient->unsubscribe(
691
            $this->getRedisKey($appId, $channel)
692
        );
693
    }
694
695
    /**
696
     * Add the Presence Channel's User's Socket ID to a list.
697
     *
698
     * @param  string|int  $appId
699
     * @param  string  $channel
700
     * @param  stdClass  $user
701
     * @param  string  $socketId
702
     * @return void
703
     */
704
    protected function addUserSocket($appId, string $channel, stdClass $user, string $socketId)
705
    {
706
        $this->publishClient->sadd(
707
            $this->getRedisKey($appId, $channel, [$user->user_id, 'userSockets']), $socketId
708
        );
709
    }
710
711
    /**
712
     * Remove the Presence Channel's User's Socket ID from the list.
713
     *
714
     * @param  string|int  $appId
715
     * @param  string  $channel
716
     * @param  stdClass  $user
717
     * @param  string  $socketId
718
     * @return void
719
     */
720
    protected function removeUserSocket($appId, string $channel, stdClass $user, string $socketId)
721
    {
722
        $this->publishClient->srem(
723
            $this->getRedisKey($appId, $channel, [$user->user_id, 'userSockets']), $socketId
724
        );
725
    }
726
727
    /**
728
     * Get the Redis Keyspace name to handle subscriptions
729
     * and other key-value sets.
730
     *
731
     * @param  string|int|null  $appId
732
     * @param  string|null  $channel
733
     * @return string
734
     */
735
    public function getRedisKey($appId = null, string $channel = null, array $suffixes = []): string
736
    {
737
        $prefix = config('database.redis.options.prefix', null);
738
739
        $hash = "{$prefix}{$appId}";
740
741
        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...
742
            $suffixes = array_merge([$channel], $suffixes);
743
        }
744
745
        $suffixes = implode(':', $suffixes);
746
747
        if ($suffixes) {
748
            $hash .= ":{$suffixes}";
749
        }
750
751
        return $hash;
752
    }
753
754
    /**
755
     * Get a new RedisLock instance to avoid race conditions.
756
     *
757
     * @return \Illuminate\Cache\CacheLock
758
     */
759
    protected function lock()
760
    {
761
        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...
762
    }
763
764
    /**
765
     * Create a fake connection for app that will mimick a connection
766
     * by app ID and Socket ID to be able to be passed to the methods
767
     * that accepts a connection class.
768
     *
769
     * @param  string|int  $appId
770
     * @param  string  $socketId
771
     * @return ConnectionInterface
772
     */
773
    public function fakeConnectionForApp($appId, string $socketId)
774
    {
775
        return new MockableConnection($appId, $socketId);
776
    }
777
}
778