InteractsWithWebsocket   A
last analyzed

Complexity

Total Complexity 41

Size/Duplication

Total Lines 375
Duplicated Lines 0 %

Importance

Changes 5
Bugs 3 Features 1
Metric Value
eloc 117
c 5
b 3
f 1
dl 0
loc 375
rs 9.1199
wmc 41

19 Methods

Rating   Name   Duplication   Size   Complexity  
A onHandShake() 0 6 1
A onMessage() 0 29 4
B onOpen() 0 33 6
A getWebsocketHandler() 0 3 1
A prepareWebsocketRoom() 0 9 1
A getPayloadParser() 0 3 1
A prepareWebsocketHandler() 0 9 2
A setWebsocketHandler() 0 5 1
A isServerWebsocket() 0 6 1
A bindRoom() 0 7 1
A pushMessage() 0 6 1
A loadWebsocketRoutes() 0 10 2
A bindWebsocket() 0 7 1
A setPayloadParser() 0 5 1
A prepareWebsocket() 0 16 3
A createRoom() 0 3 1
A isWebsocketPushPacket() 0 9 4
A onClose() 0 28 5
A isWebsocketPushPayload() 0 9 4

How to fix   Complexity   

Complex Class

Complex classes like InteractsWithWebsocket 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.

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 InteractsWithWebsocket, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace SwooleTW\Http\Concerns;
4
5
use Throwable;
6
use Illuminate\Pipeline\Pipeline;
7
use SwooleTW\Http\Server\Sandbox;
8
use SwooleTW\Http\Websocket\Parser;
9
use SwooleTW\Http\Websocket\Pusher;
10
use SwooleTW\Http\Websocket\Websocket;
11
use SwooleTW\Http\Transformers\Request;
12
use SwooleTW\Http\Server\Facades\Server;
13
use SwooleTW\Http\Websocket\HandlerContract;
14
use Illuminate\Contracts\Container\Container;
15
use Swoole\WebSocket\Server as WebsocketServer;
16
use SwooleTW\Http\Websocket\Rooms\RoomContract;
17
use SwooleTW\Http\Exceptions\WebsocketNotSetInConfigException;
18
19
/**
20
 * Trait InteractsWithWebsocket
21
 *
22
 * @property \Illuminate\Contracts\Container\Container $container
23
 * @property \Illuminate\Contracts\Container\Container $app
24
 * @property array $types
25
 */
26
trait InteractsWithWebsocket
27
{
28
    /**
29
     * @var boolean
30
     */
31
    protected $isServerWebsocket = false;
32
33
    /**
34
     * @var \SwooleTW\Http\Websocket\HandlerContract
35
     */
36
    protected $websocketHandler;
37
38
    /**
39
     * @var \SwooleTW\Http\Websocket\Parser
40
     */
41
    protected $payloadParser;
42
43
    /**
44
     * @var \SwooleTW\Http\Websocket\Rooms\RoomContract
45
     */
46
    protected $websocketRoom;
47
48
    /**
49
     * Websocket server events.
50
     *
51
     * @var array
52
     */
53
    protected $wsEvents = ['open', 'message', 'close'];
54
55
    /**
56
     * "onHandShake" listener.
57
     * @param \Swoole\Http\Request $swooleRequest
58
     * @param \Swoole\Http\Response $response
59
     */
60
    public function onHandShake($swooleRequest, $response)
61
    {
62
        $this->onOpen(
63
            $this->app->make(Server::class),
0 ignored issues
show
Bug introduced by
$this->app->make(SwooleT...\Facades\Server::class) of type SwooleTW\Http\Server\Facades\Server is incompatible with the type Swoole\Websocket\Server expected by parameter $server of SwooleTW\Http\Concerns\I...WithWebsocket::onOpen(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

63
            /** @scrutinizer ignore-type */ $this->app->make(Server::class),
Loading history...
64
            $swooleRequest,
65
            $response
66
        );
67
    }
68
69
    /**
70
     * "onOpen" listener.
71
     *
72
     * @param \Swoole\Websocket\Server $server
73
     * @param \Swoole\Http\Request $swooleRequest
74
     * @param \Swoole\Http\Response $response (optional)
75
     */
76
    public function onOpen($server, $swooleRequest, $response = null)
0 ignored issues
show
Unused Code introduced by
The parameter $server is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

76
    public function onOpen(/** @scrutinizer ignore-unused */ $server, $swooleRequest, $response = null)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
77
    {
78
        $illuminateRequest = Request::make($swooleRequest)->toIlluminate();
79
        $websocket = $this->app->make(Websocket::class);
80
        $sandbox = $this->app->make(Sandbox::class);
81
        $handshakeHandler = $this->app->make('config')
82
            ->get('swoole_websocket.handshake.handler');
83
84
        try {
85
            $websocket->reset(true)->setSender($swooleRequest->fd);
86
            // set currnt request to sandbox
87
            $sandbox->setRequest($illuminateRequest);
88
            // enable sandbox
89
            $sandbox->enable();
90
            // call customized handshake handler
91
            if ($response && ! $this->app->make($handshakeHandler)->handle($swooleRequest, $response)) {
92
                return;
93
            }
94
            // check if socket.io connection established
95
            if (! $this->websocketHandler->onOpen($swooleRequest->fd, $illuminateRequest)) {
96
                return;
97
            }
98
            // trigger 'connect' websocket event
99
            if ($websocket->eventExists('connect')) {
100
                // set sandbox container to websocket pipeline
101
                $websocket->setContainer($sandbox->getApplication());
102
                $websocket->call('connect', $illuminateRequest);
103
            }
104
        } catch (Throwable $e) {
105
            $this->logServerError($e);
0 ignored issues
show
Bug introduced by
It seems like logServerError() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

105
            $this->/** @scrutinizer ignore-call */ 
106
                   logServerError($e);
Loading history...
106
        } finally {
107
            // disable and recycle sandbox resource
108
            $sandbox->disable();
109
        }
110
    }
111
112
    /**
113
     * "onMessage" listener.
114
     *
115
     * @param \Swoole\Websocket\Server $server
116
     * @param \Swoole\Websocket\Frame $frame
117
     */
118
    public function onMessage($server, $frame)
119
    {
120
        // execute parser strategies and skip non-message packet
121
        if ($this->payloadParser->execute($server, $frame)) {
122
            return;
123
        }
124
125
        $websocket = $this->app->make(Websocket::class);
126
        $sandbox = $this->app->make(Sandbox::class);
127
128
        try {
129
            // decode raw message via parser
130
            $payload = $this->payloadParser->decode($frame);
131
132
            $websocket->reset(true)->setSender($frame->fd);
133
134
            // enable sandbox
135
            $sandbox->enable();
136
137
            // dispatch message to registered event callback
138
            ['event' => $event, 'data' => $data] = $payload;
139
            $websocket->eventExists($event)
140
                ? $websocket->call($event, $data)
141
                : $this->websocketHandler->onMessage($frame);
142
        } catch (Throwable $e) {
143
            $this->logServerError($e);
144
        } finally {
145
            // disable and recycle sandbox resource
146
            $sandbox->disable();
147
        }
148
    }
149
150
    /**
151
     * "onClose" listener.
152
     *
153
     * @param \Swoole\Websocket\Server $server
154
     * @param int $fd
155
     * @param int $reactorId
156
     */
157
    public function onClose($server, $fd, $reactorId)
158
    {
159
        if (! $this->isServerWebsocket($fd) || ! $server instanceof WebsocketServer) {
160
            return;
161
        }
162
163
        $websocket = $this->app->make(Websocket::class);
164
        $sandbox = $this->app->make(Sandbox::class);
165
166
        try {
167
            $websocket->reset(true)->setSender($fd);
168
            
169
            // enable sandbox
170
            $sandbox->enable();
171
            
172
            // trigger 'disconnect' websocket event
173
            if ($websocket->eventExists('disconnect')) {
174
                $websocket->call('disconnect');
175
            } else {
176
                $this->websocketHandler->onClose($fd, $reactorId);
177
            }
178
            // leave all rooms
179
            $websocket->leave();
180
        } catch (Throwable $e) {
181
            $this->logServerError($e);
182
        } finally {
183
            // disable and recycle sandbox resource
184
            $sandbox->disable();
185
        }
186
    }
187
188
    /**
189
     * Indicates if a packet is websocket push action.
190
     *
191
     * @param mixed
192
     *
193
     * @return bool
194
     */
195
    protected function isWebsocketPushPacket($packet)
196
    {
197
        if (! is_array($packet)) {
198
            return false;
199
        }
200
201
        return $this->isServerWebsocket
202
            && array_key_exists('action', $packet)
203
            && $packet['action'] === Websocket::PUSH_ACTION;
204
    }
205
206
207
    /**
208
     * Push websocket message to clients.
209
     *
210
     * @param \Swoole\Websocket\Server $server
211
     * @param mixed $data
212
     */
213
    public function pushMessage($server, array $data)
214
    {
215
        $pusher = Pusher::make($data, $server);
216
        $pusher->push($this->payloadParser->encode(
217
            $pusher->getEvent(),
218
            $pusher->getMessage()
219
        ));
220
    }
221
222
    /**
223
     * Set frame parser for websocket.
224
     *
225
     * @param \SwooleTW\Http\Websocket\Parser $payloadParser
226
     *
227
     * @return \SwooleTW\Http\Concerns\InteractsWithWebsocket
228
     */
229
    public function setPayloadParser(Parser $payloadParser)
230
    {
231
        $this->payloadParser = $payloadParser;
232
233
        return $this;
234
    }
235
236
    /**
237
     * Get frame parser for websocket.
238
     */
239
    public function getPayloadParser()
240
    {
241
        return $this->payloadParser;
242
    }
243
244
    /**
245
     * Prepare settings if websocket is enabled.
246
     */
247
    protected function prepareWebsocket()
248
    {
249
        $config = $this->container->make('config');
250
        $parser = $config->get('swoole_websocket.parser');
251
252
        if (! $this->isServerWebsocket = $config->get('swoole_http.websocket.enabled')) {
253
            return;
254
        }
255
256
        if ($config->get('swoole_websocket.handshake.enabled')) {
257
            $this->wsEvents = array_merge($this->wsEvents, ['handshake']);
258
        }
259
260
        $this->events = array_merge($this->events ?? [], $this->wsEvents);
0 ignored issues
show
Bug Best Practice introduced by
The property events does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
261
        $this->prepareWebsocketRoom();
262
        $this->setPayloadParser(new $parser);
263
    }
264
265
    /**
266
     * Check if it's a websocket fd.
267
     *
268
     * @param int $fd
269
     *
270
     * @return bool
271
     */
272
    protected function isServerWebsocket(int $fd): bool
273
    {
274
        return array_key_exists(
275
            'websocket_status',
276
            $this->container->make(Server::class)
277
                ->connection_info($fd)
0 ignored issues
show
Bug introduced by
The method connection_info() does not exist on SwooleTW\Http\Server\Facades\Server. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

277
                ->/** @scrutinizer ignore-call */ connection_info($fd)

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
278
        );
279
    }
280
281
    /**
282
     * Prepare websocket handler for onOpen and onClose callback.
283
     *
284
     * @throws \Exception
285
     */
286
    protected function prepareWebsocketHandler()
287
    {
288
        $handlerClass = $this->container->make('config')->get('swoole_websocket.handler');
289
290
        if (! $handlerClass) {
291
            throw new WebsocketNotSetInConfigException;
292
        }
293
294
        $this->setWebsocketHandler($this->app->make($handlerClass));
295
    }
296
297
    /**
298
     * Prepare websocket room.
299
     */
300
    protected function prepareWebsocketRoom()
301
    {
302
        $config = $this->container->make('config');
303
        $driver = $config->get('swoole_websocket.default');
304
        $websocketConfig = $config->get("swoole_websocket.settings.{$driver}");
305
        $className = $config->get("swoole_websocket.drivers.{$driver}");
306
307
        $this->websocketRoom = new $className($websocketConfig);
308
        $this->websocketRoom->prepare();
309
    }
310
311
    /**
312
     * Set websocket handler.
313
     *
314
     * @param \SwooleTW\Http\Websocket\HandlerContract $handler
315
     *
316
     * @return \SwooleTW\Http\Concerns\InteractsWithWebsocket
317
     */
318
    public function setWebsocketHandler(HandlerContract $handler)
319
    {
320
        $this->websocketHandler = $handler;
321
322
        return $this;
323
    }
324
325
    /**
326
     * Get websocket handler.
327
     *
328
     * @return \SwooleTW\Http\Websocket\HandlerContract
329
     */
330
    public function getWebsocketHandler(): HandlerContract
331
    {
332
        return $this->websocketHandler;
333
    }
334
335
    /**
336
     * @param string $class
337
     * @param array $settings
338
     *
339
     * @return \SwooleTW\Http\Websocket\Rooms\RoomContract
340
     */
341
    protected function createRoom(string $class, array $settings): RoomContract
342
    {
343
        return new $class($settings);
344
    }
345
346
    /**
347
     * Bind room instance to Laravel app container.
348
     */
349
    protected function bindRoom(): void
350
    {
351
        $this->app->singleton(RoomContract::class, function () {
352
            return $this->websocketRoom;
353
        });
354
355
        $this->app->alias(RoomContract::class, 'swoole.room');
356
    }
357
358
    /**
359
     * Bind websocket instance to Laravel app container.
360
     */
361
    protected function bindWebsocket()
362
    {
363
        $this->app->singleton(Websocket::class, function (Container $app) {
364
            return new Websocket($app->make(RoomContract::class), new Pipeline($app));
365
        });
366
367
        $this->app->alias(Websocket::class, 'swoole.websocket');
368
    }
369
370
    /**
371
     * Load websocket routes file.
372
     */
373
    protected function loadWebsocketRoutes()
374
    {
375
        $routePath = $this->container->make('config')
376
            ->get('swoole_websocket.route_file');
377
378
        if (! file_exists($routePath)) {
379
            $routePath = __DIR__ . '/../../routes/websocket.php';
380
        }
381
382
        return require $routePath;
383
    }
384
385
    /**
386
     * Indicates if the payload is websocket push.
387
     *
388
     * @param mixed $payload
389
     *
390
     * @return boolean
391
     */
392
    public function isWebsocketPushPayload($payload): bool
393
    {
394
        if (! is_array($payload)) {
395
            return false;
396
        }
397
398
        return $this->isServerWebsocket
399
            && ($payload['action'] ?? null) === Websocket::PUSH_ACTION
400
            && array_key_exists('data', $payload);
401
    }
402
}
403