Completed
Push — master ( 4e9b37...0f36ee )
by Albert
13s queued 11s
created

InteractsWithWebsocket::onHandShake()   A

Complexity

Conditions 4
Paths 48

Size

Total Lines 30
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 18
nc 48
nop 2
dl 0
loc 30
rs 9.6666
c 0
b 0
f 0
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
     * "onOpen" listener.
57
     *
58
     * @param \Swoole\Websocket\Server $server
59
     * @param \Swoole\Http\Request $swooleRequest
60
     */
61
    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

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

84
            $this->/** @scrutinizer ignore-call */ 
85
                   logServerError($e);
Loading history...
85
        } finally {
86
            // disable and recycle sandbox resource
87
            $sandbox->disable();
88
        }
89
    }
90
91
    /**
92
     * @param \Swoole\Http\Request $swooleRequest
93
     * @param \Swoole\Http\Response $response
94
     *
95
     * @return bool
96
     * @throws \Illuminate\Contracts\Container\BindingResolutionException
97
     */
98
    public function onHandShake($swooleRequest, $response)
99
    {
100
        $illuminateRequest = Request::make($swooleRequest)->toIlluminate();
101
        $websocket = $this->app->make(Websocket::class);
102
        $sandbox = $this->app->make(Sandbox::class);
103
        $handler = $this->container->make('config')->get('swoole_websocket.handshake.handler');
104
105
        try {
106
            $websocket->reset(true)->setSender($swooleRequest->fd);
107
            // set currnt request to sandbox
108
            $sandbox->setRequest($illuminateRequest);
109
            // enable sandbox
110
            $sandbox->enable();
111
112
            if (! $this->app->make($handler)->handle($swooleRequest, $response)) {
113
                return false;
114
            }
115
            // trigger 'connect' websocket event
116
            if ($websocket->eventExists('connect')) {
117
                // set sandbox container to websocket pipeline
118
                $websocket->setContainer($sandbox->getApplication());
119
                $websocket->call('connect', $illuminateRequest);
120
            }
121
122
            return true;
123
        } catch (Throwable $e) {
124
            $this->logServerError($e);
125
        } finally {
126
            // disable and recycle sandbox resource
127
            $sandbox->disable();
128
        }
129
    }
130
131
    /**
132
     * "onMessage" listener.
133
     *
134
     * @param \Swoole\Websocket\Server $server
135
     * @param \Swoole\Websocket\Frame $frame
136
     */
137
    public function onMessage($server, $frame)
138
    {
139
        // execute parser strategies and skip non-message packet
140
        if ($this->payloadParser->execute($server, $frame)) {
141
            return;
142
        }
143
144
        $websocket = $this->app->make(Websocket::class);
145
        $sandbox = $this->app->make(Sandbox::class);
146
147
        try {
148
            // decode raw message via parser
149
            $payload = $this->payloadParser->decode($frame);
150
151
            $websocket->reset(true)->setSender($frame->fd);
152
153
            // enable sandbox
154
            $sandbox->enable();
155
156
            // dispatch message to registered event callback
157
            ['event' => $event, 'data' => $data] = $payload;
158
            $websocket->eventExists($event)
159
                ? $websocket->call($event, $data)
160
                : $this->websocketHandler->onMessage($frame);
161
        } catch (Throwable $e) {
162
            $this->logServerError($e);
163
        } finally {
164
            // disable and recycle sandbox resource
165
            $sandbox->disable();
166
        }
167
    }
168
169
    /**
170
     * "onClose" listener.
171
     *
172
     * @param \Swoole\Websocket\Server $server
173
     * @param int $fd
174
     * @param int $reactorId
175
     */
176
    public function onClose($server, $fd, $reactorId)
177
    {
178
        if (! $this->isServerWebsocket($fd) || ! $server instanceof WebsocketServer) {
179
            return;
180
        }
181
182
        $websocket = $this->app->make(Websocket::class);
183
184
        try {
185
            $websocket->reset(true)->setSender($fd);
186
            // trigger 'disconnect' websocket event
187
            if ($websocket->eventExists('disconnect')) {
188
                $websocket->call('disconnect');
189
            } else {
190
                $this->websocketHandler->onClose($fd, $reactorId);
191
            }
192
            // leave all rooms
193
            $websocket->leave();
194
        } catch (Throwable $e) {
195
            $this->logServerError($e);
196
        }
197
    }
198
199
    /**
200
     * Indicates if a packet is websocket push action.
201
     *
202
     * @param mixed
203
     *
204
     * @return bool
205
     */
206
    protected function isWebsocketPushPacket($packet)
207
    {
208
        if (! is_array($packet)) {
209
            return false;
210
        }
211
212
        return $this->isServerWebsocket
213
            && array_key_exists('action', $packet)
214
            && $packet['action'] === Websocket::PUSH_ACTION;
215
    }
216
217
218
    /**
219
     * Push websocket message to clients.
220
     *
221
     * @param \Swoole\Websocket\Server $server
222
     * @param mixed $data
223
     */
224
    public function pushMessage($server, array $data)
225
    {
226
        $pusher = Pusher::make($data, $server);
227
        $pusher->push($this->payloadParser->encode(
228
            $pusher->getEvent(),
229
            $pusher->getMessage()
230
        ));
231
    }
232
233
    /**
234
     * Set frame parser for websocket.
235
     *
236
     * @param \SwooleTW\Http\Websocket\Parser $payloadParser
237
     *
238
     * @return \SwooleTW\Http\Concerns\InteractsWithWebsocket
239
     */
240
    public function setPayloadParser(Parser $payloadParser)
241
    {
242
        $this->payloadParser = $payloadParser;
243
244
        return $this;
245
    }
246
247
    /**
248
     * Get frame parser for websocket.
249
     */
250
    public function getPayloadParser()
251
    {
252
        return $this->payloadParser;
253
    }
254
255
    /**
256
     * Prepare settings if websocket is enabled.
257
     */
258
    protected function prepareWebsocket()
259
    {
260
        $config = $this->container->make('config');
261
        $isWebsocket = $config->get('swoole_http.websocket.enabled');
262
        $parser = $config->get('swoole_websocket.parser');
263
264
        if ($isWebsocket) {
265
            $handshake = $config->get('swoole_websocket.handshake.enabled');
266
267
            $this->events = array_merge($this->events ?? [], array_merge($this->wsEvents, $handshake ? ['handshake'] : []));
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...
268
            $this->isServerWebsocket = true;
269
            $this->prepareWebsocketRoom();
270
            $this->setPayloadParser(new $parser);
271
        }
272
    }
273
274
    /**
275
     * Check if it's a websocket fd.
276
     *
277
     * @param int $fd
278
     *
279
     * @return bool
280
     */
281
    protected function isServerWebsocket(int $fd): bool
282
    {
283
        return (bool) $this->container->make(Server::class)
284
            ->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

284
            ->/** @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...
285
    }
286
287
    /**
288
     * Prepare websocket handler for onOpen and onClose callback.
289
     *
290
     * @throws \Exception
291
     */
292
    protected function prepareWebsocketHandler()
293
    {
294
        $handlerClass = $this->container->make('config')->get('swoole_websocket.handler');
295
296
        if (! $handlerClass) {
297
            throw new WebsocketNotSetInConfigException;
298
        }
299
300
        $this->setWebsocketHandler($this->app->make($handlerClass));
301
    }
302
303
    /**
304
     * Prepare websocket room.
305
     */
306
    protected function prepareWebsocketRoom()
307
    {
308
        $config = $this->container->make('config');
309
        $driver = $config->get('swoole_websocket.default');
310
        $websocketConfig = $config->get("swoole_websocket.settings.{$driver}");
311
        $className = $config->get("swoole_websocket.drivers.{$driver}");
312
313
        $this->websocketRoom = new $className($websocketConfig);
314
        $this->websocketRoom->prepare();
315
    }
316
317
    /**
318
     * Set websocket handler.
319
     *
320
     * @param \SwooleTW\Http\Websocket\HandlerContract $handler
321
     *
322
     * @return \SwooleTW\Http\Concerns\InteractsWithWebsocket
323
     */
324
    public function setWebsocketHandler(HandlerContract $handler)
325
    {
326
        $this->websocketHandler = $handler;
327
328
        return $this;
329
    }
330
331
    /**
332
     * Get websocket handler.
333
     *
334
     * @return \SwooleTW\Http\Websocket\HandlerContract
335
     */
336
    public function getWebsocketHandler(): HandlerContract
337
    {
338
        return $this->websocketHandler;
339
    }
340
341
    /**
342
     * @param string $class
343
     * @param array $settings
344
     *
345
     * @return \SwooleTW\Http\Websocket\Rooms\RoomContract
346
     */
347
    protected function createRoom(string $class, array $settings): RoomContract
348
    {
349
        return new $class($settings);
350
    }
351
352
    /**
353
     * Bind room instance to Laravel app container.
354
     */
355
    protected function bindRoom(): void
356
    {
357
        $this->app->singleton(RoomContract::class, function (Container $app) {
0 ignored issues
show
Unused Code introduced by
The parameter $app 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

357
        $this->app->singleton(RoomContract::class, function (/** @scrutinizer ignore-unused */ Container $app) {

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...
358
            return $this->websocketRoom;
359
        });
360
361
        $this->app->alias(RoomContract::class, 'swoole.room');
362
    }
363
364
    /**
365
     * Bind websocket instance to Laravel app container.
366
     */
367
    protected function bindWebsocket()
368
    {
369
        $this->app->singleton(Websocket::class, function (Container $app) {
370
            return new Websocket($app->make(RoomContract::class), new Pipeline($app));
371
        });
372
373
        $this->app->alias(Websocket::class, 'swoole.websocket');
374
    }
375
376
    /**
377
     * Load websocket routes file.
378
     */
379
    protected function loadWebsocketRoutes()
380
    {
381
        $routePath = $this->container->make('config')
382
            ->get('swoole_websocket.route_file');
383
384
        if (! file_exists($routePath)) {
385
            $routePath = __DIR__ . '/../../routes/websocket.php';
386
        }
387
388
        return require $routePath;
389
    }
390
391
    /**
392
     * Indicates if the payload is websocket push.
393
     *
394
     * @param mixed $payload
395
     *
396
     * @return boolean
397
     */
398
    public function isWebsocketPushPayload($payload): bool
399
    {
400
        if (! is_array($payload)) {
401
            return false;
402
        }
403
404
        return $this->isServerWebsocket
405
            && ($payload['action'] ?? null) === Websocket::PUSH_ACTION
406
            && array_key_exists('data', $payload);
407
    }
408
}
409