Passed
Push — master ( e0b916...9184fe )
by Albert
06:29 queued 04:25
created

InteractsWithWebsocket   A

Complexity

Total Complexity 36

Size/Duplication

Total Lines 326
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 100
dl 0
loc 326
rs 9.52
c 0
b 0
f 0
wmc 36

17 Methods

Rating   Name   Duplication   Size   Complexity  
A getWebsocketHandler() 0 3 1
A onOpen() 0 27 4
A getPayloadParser() 0 3 1
A prepareWebsocketHandler() 0 9 2
A isServerWebsocket() 0 4 1
A bindRoom() 0 16 1
A pushMessage() 0 6 1
A setWebsocketHandler() 0 5 1
A loadWebsocketRoutes() 0 10 2
A bindWebsocket() 0 7 1
A setPayloadParser() 0 5 1
A prepareWebsocket() 0 10 2
A createRoom() 0 3 1
A isWebsocketPushPacket() 0 9 4
A onMessage() 0 29 4
A onClose() 0 20 5
A isWebsocketPushPayload() 0 9 4
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 SwooleTW\Http\Websocket\Rooms\RoomContract;
16
use SwooleTW\Http\Exceptions\WebsocketNotSetInConfigException;
17
18
/**
19
 * Trait InteractsWithWebsocket
20
 *
21
 * @property \Illuminate\Contracts\Container\Container $container
22
 * @property \Illuminate\Contracts\Container\Container $app
23
 * @property array $types
24
 */
25
trait InteractsWithWebsocket
26
{
27
    /**
28
     * @var boolean
29
     */
30
    protected $isServerWebsocket = false;
31
32
    /**
33
     * @var \SwooleTW\Http\Websocket\HandlerContract
34
     */
35
    protected $websocketHandler;
36
37
    /**
38
     * @var \SwooleTW\Http\Websocket\Parser
39
     */
40
    protected $payloadParser;
41
42
    /**
43
     * Websocket server events.
44
     *
45
     * @var array
46
     */
47
    protected $wsEvents = ['open', 'message', 'close'];
48
49
    /**
50
     * "onOpen" listener.
51
     *
52
     * @param \Swoole\Websocket\Server $server
53
     * @param \Swoole\Http\Request $swooleRequest
54
     */
55
    public function onOpen($server, $swooleRequest)
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

55
    public function onOpen(/** @scrutinizer ignore-unused */ $server, $swooleRequest)

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...
56
    {
57
        $illuminateRequest = Request::make($swooleRequest)->toIlluminate();
58
        $websocket = $this->app->make(Websocket::class);
59
        $sandbox = $this->app->make(Sandbox::class);
60
61
        try {
62
            $websocket->reset(true)->setSender($swooleRequest->fd);
63
            // set currnt request to sandbox
64
            $sandbox->setRequest($illuminateRequest);
65
            // enable sandbox
66
            $sandbox->enable();
67
            // check if socket.io connection established
68
            if (! $this->websocketHandler->onOpen($swooleRequest->fd, $illuminateRequest)) {
69
                return;
70
            }
71
            // trigger 'connect' websocket event
72
            if ($websocket->eventExists('connect')) {
73
                // set sandbox container to websocket pipeline
74
                $websocket->setContainer($sandbox->getApplication());
75
                $websocket->call('connect', $illuminateRequest);
76
            }
77
        } catch (Throwable $e) {
78
            $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

78
            $this->/** @scrutinizer ignore-call */ 
79
                   logServerError($e);
Loading history...
79
        } finally {
80
            // disable and recycle sandbox resource
81
            $sandbox->disable();
82
        }
83
    }
84
85
    /**
86
     * "onMessage" listener.
87
     *
88
     * @param \Swoole\Websocket\Server $server
89
     * @param \Swoole\Websocket\Frame $frame
90
     */
91
    public function onMessage($server, $frame)
92
    {
93
        // execute parser strategies and skip non-message packet
94
        if ($this->payloadParser->execute($server, $frame)) {
95
            return;
96
        }
97
98
        $websocket = $this->app->make(Websocket::class);
99
        $sandbox = $this->app->make(Sandbox::class);
100
101
        try {
102
            // decode raw message via parser
103
            $payload = $this->payloadParser->decode($frame);
104
105
            $websocket->reset(true)->setSender($frame->fd);
106
107
            // enable sandbox
108
            $sandbox->enable();
109
110
            // dispatch message to registered event callback
111
            ['event' => $event, 'data' => $data] = $payload;
112
            $websocket->eventExists($event)
113
                ? $websocket->call($event, $data)
114
                : $this->websocketHandler->onMessage($frame);
115
        } catch (Throwable $e) {
116
            $this->logServerError($e);
117
        } finally {
118
            // disable and recycle sandbox resource
119
            $sandbox->disable();
120
        }
121
    }
122
123
    /**
124
     * "onClose" listener.
125
     *
126
     * @param \Swoole\Websocket\Server $server
127
     * @param int $fd
128
     * @param int $reactorId
129
     */
130
    public function onClose($server, $fd, $reactorId)
131
    {
132
        if (! $this->isServerWebsocket($fd) || ! $server instanceof Websocket) {
0 ignored issues
show
introduced by
$server is never a sub-type of SwooleTW\Http\Websocket\Websocket.
Loading history...
133
            return;
134
        }
135
136
        $websocket = $this->app->make(Websocket::class);
137
138
        try {
139
            $websocket->reset(true)->setSender($fd);
140
            // trigger 'disconnect' websocket event
141
            if ($websocket->eventExists('disconnect')) {
142
                $websocket->call('disconnect');
143
            } else {
144
                $this->websocketHandler->onClose($fd, $reactorId);
145
            }
146
            // leave all rooms
147
            $websocket->leave();
148
        } catch (Throwable $e) {
149
            $this->logServerError($e);
150
        }
151
    }
152
153
    /**
154
     * Indicates if a packet is websocket push action.
155
     *
156
     * @param mixed
157
     */
158
    protected function isWebsocketPushPacket($packet)
159
    {
160
        if (! is_array($packet)) {
161
            return false;
162
        }
163
164
        return $this->isWebsocket
165
            && array_key_exists('action', $packet)
166
            && $packet['action'] === Websocket::PUSH_ACTION;
167
    }
168
169
170
    /**
171
     * Push websocket message to clients.
172
     *
173
     * @param \Swoole\Websocket\Server $server
174
     * @param mixed $data
175
     */
176
    public function pushMessage($server, array $data)
177
    {
178
        $pusher = Pusher::make($data, $server);
179
        $pusher->push($this->payloadParser->encode(
180
            $pusher->getEvent(),
181
            $pusher->getMessage()
182
        ));
183
    }
184
185
    /**
186
     * Set frame parser for websocket.
187
     *
188
     * @param \SwooleTW\Http\Websocket\Parser $payloadParser
189
     *
190
     * @return \SwooleTW\Http\Concerns\InteractsWithWebsocket
191
     */
192
    public function setPayloadParser(Parser $payloadParser)
193
    {
194
        $this->payloadParser = $payloadParser;
195
196
        return $this;
197
    }
198
199
    /**
200
     * Get frame parser for websocket.
201
     */
202
    public function getPayloadParser()
203
    {
204
        return $this->payloadParser;
205
    }
206
207
    /**
208
     * Prepare settings if websocket is enabled.
209
     */
210
    protected function prepareWebsocket()
211
    {
212
        $config = $this->container->make('config');
213
        $isWebsocket = $config->get('swoole_http.websocket.enabled');
214
        $parser = $config->get('swoole_websocket.parser');
215
216
        if ($isWebsocket) {
217
            $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...
218
            $this->isServerWebsocket = true;
219
            $this->setPayloadParser(new $parser);
220
        }
221
    }
222
223
    /**
224
     * Check if it's a websocket fd.
225
     *
226
     * @param int $fd
227
     *
228
     * @return bool
229
     */
230
    protected function isServerWebsocket(int $fd): bool
231
    {
232
        return $this->container->make(Server::class)
233
            ->connection_info($fd)['websocket_status'] ?? false;
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

233
            ->/** @scrutinizer ignore-call */ connection_info($fd)['websocket_status'] ?? false;

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...
234
    }
235
236
    /**
237
     * Prepare websocket handler for onOpen and onClose callback.
238
     *
239
     * @throws \Exception
240
     */
241
    protected function prepareWebsocketHandler()
242
    {
243
        $handlerClass = $this->container->make('config')->get('swoole_websocket.handler');
244
245
        if (! $handlerClass) {
246
            throw new WebsocketNotSetInConfigException;
247
        }
248
249
        $this->setWebsocketHandler($this->app->make($handlerClass));
250
    }
251
252
    /**
253
     * Set websocket handler.
254
     *
255
     * @param \SwooleTW\Http\Websocket\HandlerContract $handler
256
     *
257
     * @return \SwooleTW\Http\Concerns\InteractsWithWebsocket
258
     */
259
    public function setWebsocketHandler(HandlerContract $handler)
260
    {
261
        $this->websocketHandler = $handler;
262
263
        return $this;
264
    }
265
266
    /**
267
     * Get websocket handler.
268
     *
269
     * @return \SwooleTW\Http\Websocket\HandlerContract
270
     */
271
    public function getWebsocketHandler(): HandlerContract
272
    {
273
        return $this->websocketHandler;
274
    }
275
276
    /**
277
     * @param string $class
278
     * @param array $settings
279
     *
280
     * @return \SwooleTW\Http\Websocket\Rooms\RoomContract
281
     */
282
    protected function createRoom(string $class, array $settings): RoomContract
283
    {
284
        return new $class($settings);
285
    }
286
287
    /**
288
     * Bind room instance to Laravel app container.
289
     */
290
    protected function bindRoom(): void
291
    {
292
        $this->app->singleton(RoomContract::class, function (Container $container) {
293
            $config = $container->make('config');
294
            $driver = $config->get('swoole_websocket.default');
295
            $settings = $config->get("swoole_websocket.settings.{$driver}");
296
            $className = $config->get("swoole_websocket.drivers.{$driver}");
297
298
            // create room instance and initialize
299
            $room = $this->createRoom($className, $settings);
300
            $room->prepare();
301
302
            return $room;
303
        });
304
305
        $this->app->alias(RoomContract::class, 'swoole.room');
306
    }
307
308
    /**
309
     * Bind websocket instance to Laravel app container.
310
     */
311
    protected function bindWebsocket()
312
    {
313
        $this->app->singleton(Websocket::class, function (Container $app) {
314
            return new Websocket($app->make(RoomContract::class), new Pipeline($app));
315
        });
316
317
        $this->app->alias(Websocket::class, 'swoole.websocket');
318
    }
319
320
    /**
321
     * Load websocket routes file.
322
     */
323
    protected function loadWebsocketRoutes()
324
    {
325
        $routePath = $this->container->make('config')
326
            ->get('swoole_websocket.route_file');
327
328
        if (! file_exists($routePath)) {
329
            $routePath = __DIR__ . '/../../routes/websocket.php';
330
        }
331
332
        return require $routePath;
333
    }
334
335
    /**
336
     * Indicates if the payload is websocket push.
337
     *
338
     * @param mixed $payload
339
     *
340
     * @return boolean
341
     */
342
    public function isWebsocketPushPayload($payload): bool
343
    {
344
        if (! is_array($payload)) {
345
            return false;
346
        }
347
348
        return $this->isServerWebsocket
349
            && ($payload['action'] ?? null) === Websocket::PUSH_ACTION
350
            && array_key_exists('data', $payload);
351
    }
352
}
353