Completed
Pull Request — master (#447)
by Marcel
03:00 queued 01:29
created

RedisChannelManager   F

Complexity

Total Complexity 60

Size/Duplication

Total Lines 769
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 11

Importance

Changes 0
Metric Value
wmc 60
lcom 1
cbo 11
dl 0
loc 769
rs 3.431
c 0
b 0
f 0

41 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 22 2
A getLocalConnections() 0 4 1
A getLocalChannels() 0 4 1
A getGlobalChannels() 0 6 1
A unsubscribeFromAllChannels() 0 13 2
A subscribeToChannel() 0 16 1
A unsubscribeFromChannel() 0 26 3
A subscribeToApp() 0 6 1
A unsubscribeFromApp() 0 6 1
A getLocalConnectionsCount() 0 4 1
A getGlobalConnectionsCount() 0 8 2
A broadcastAcrossServers() 0 10 2
A userJoinedPresenceChannel() 0 10 1
A userLeftPresenceChannel() 0 10 1
A getChannelMembers() 0 13 1
A getChannelMember() 0 6 1
A getChannelsMembersCount() 0 15 2
A getMemberSockets() 0 6 1
A connectionPonged() 0 7 1
A removeObsoleteConnections() 0 15 2
A onMessage() 0 24 4
A getConnectionUri() 0 22 5
A getSubscribeClient() 0 4 1
A getPublishClient() 0 4 1
A getServerId() 0 4 1
A incrementSubscriptionsCount() 0 6 1
A decrementSubscriptionsCount() 0 4 1
A addConnectionToSet() 0 9 2
A removeConnectionFromSet() 0 7 1
A getConnectionsFromSet() 0 19 2
A addChannelToSet() 0 7 1
A removeChannelFromSet() 0 7 1
A storeUserData() 0 6 1
A removeUserData() 0 6 1
A subscribeToTopic() 0 6 1
A unsubscribeFromTopic() 0 6 1
A addUserSocket() 0 7 1
A removeUserSocket() 0 7 1
A getRedisKey() 0 18 3
A lock() 0 4 1
A fakeConnectionForApp() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like RedisChannelManager often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use RedisChannelManager, and based on these observations, apply Extract Interface, too.

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