Passed
Push — master ( cbb991...62e47c )
by Albert
07:16 queued 05:08
created

InteractsWithWebsocket::isServerWebsocket()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 2
nc 2
nop 1
dl 0
loc 5
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace SwooleTW\Http\Concerns;
4
5
use Throwable;
6
use Illuminate\Support\Arr;
7
use Illuminate\Pipeline\Pipeline;
8
use SwooleTW\Http\Server\Sandbox;
9
use SwooleTW\Http\Websocket\Push;
10
use SwooleTW\Http\Websocket\Parser;
11
use SwooleTW\Http\Websocket\Websocket;
12
use SwooleTW\Http\Transformers\Request;
13
use SwooleTW\Http\Server\Facades\Server;
14
use SwooleTW\Http\Websocket\HandlerContract;
15
use Illuminate\Contracts\Container\Container;
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
     * Websocket server events.
45
     *
46
     * @var array
47
     */
48
    protected $wsEvents = ['open', 'message', 'close'];
49
50
    /**
51
     * "onOpen" listener.
52
     *
53
     * @param \Swoole\Websocket\Server $server
54
     * @param \Swoole\Http\Request $swooleRequest
55
     */
56
    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

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

79
            $this->/** @scrutinizer ignore-call */ 
80
                   logServerError($e);
Loading history...
80
        } finally {
81
            // disable and recycle sandbox resource
82
            $sandbox->disable();
83
        }
84
    }
85
86
    /**
87
     * "onMessage" listener.
88
     *
89
     * @param \Swoole\Websocket\Server $server
90
     * @param \Swoole\Websocket\Frame $frame
91
     */
92
    public function onMessage($server, $frame)
93
    {
94
        // execute parser strategies and skip non-message packet
95
        if ($this->payloadParser->execute($server, $frame)) {
96
            return;
97
        }
98
99
        $websocket = $this->app->make(Websocket::class);
100
        $sandbox = $this->app->make(Sandbox::class);
101
102
        try {
103
            // decode raw message via parser
104
            $payload = $this->payloadParser->decode($frame);
105
106
            $websocket->reset(true)->setSender($frame->fd);
107
108
            // enable sandbox
109
            $sandbox->enable();
110
111
            // dispatch message to registered event callback
112
            ['event' => $event, 'data' => $data] = $payload;
113
            $websocket->eventExists($event)
114
                ? $websocket->call($event, $data)
115
                : $this->websocketHandler->onMessage($frame);
116
        } catch (Throwable $e) {
117
            $this->logServerError($e);
118
        } finally {
119
            // disable and recycle sandbox resource
120
            $sandbox->disable();
121
        }
122
    }
123
124
    /**
125
     * "onClose" listener.
126
     *
127
     * @param \Swoole\Websocket\Server $server
128
     * @param int $fd
129
     * @param int $reactorId
130
     */
131
    public function onClose($server, $fd, $reactorId)
132
    {
133
        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...
134
            return;
135
        }
136
137
        $websocket = $this->app->make(Websocket::class);
138
139
        try {
140
            $websocket->reset(true)->setSender($fd);
141
            // trigger 'disconnect' websocket event
142
            if ($websocket->eventExists('disconnect')) {
143
                $websocket->call('disconnect');
144
            } else {
145
                $this->websocketHandler->onClose($fd, $reactorId);
146
            }
147
            // leave all rooms
148
            $websocket->leave();
149
        } catch (Throwable $e) {
150
            $this->logServerError($e);
151
        }
152
    }
153
154
    /**
155
     * Push websocket message to clients.
156
     *
157
     * @param \Swoole\Websocket\Server $server
158
     * @param mixed $data
159
     */
160
    public function pushMessage($server, array $data)
161
    {
162
        $push = Push::new($data);
163
        $payload = $this->payloadParser->encode($push->getEvent(), $push->getMessage());
164
165
        // attach sender if not broadcast
166
        if (! $push->isBroadcast() && $push->getSender() && ! $push->hasOwnDescriptor()) {
167
            $push->addDescriptor($push->getSender());
168
        }
169
170
        // check if to broadcast all clients
171
        if ($push->isBroadcastToAllDescriptors()) {
172
            $push->mergeDescriptor($this->filterWebsocket($server->connections));
0 ignored issues
show
Bug introduced by
$server->connections of type Swoole\Connection\Iterator is incompatible with the type array expected by parameter $descriptors of SwooleTW\Http\Concerns\I...cket::filterWebsocket(). ( Ignorable by Annotation )

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

172
            $push->mergeDescriptor($this->filterWebsocket(/** @scrutinizer ignore-type */ $server->connections));
Loading history...
173
        }
174
175
        // push message to designated fds
176
        foreach ($push->getDescriptors() as $descriptor) {
177
            if ($server->exist($descriptor) || ! $push->isBroadcastToDescriptor((int) $descriptor)) {
178
                $server->push($descriptor, $payload, $push->getOpcode());
0 ignored issues
show
Bug introduced by
$push->getOpcode() of type integer is incompatible with the type boolean expected by parameter $binary_data of Swoole\WebSocket\Server::push(). ( Ignorable by Annotation )

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

178
                $server->push($descriptor, $payload, /** @scrutinizer ignore-type */ $push->getOpcode());
Loading history...
179
            }
180
        }
181
    }
182
183
    /**
184
     * Set frame parser for websocket.
185
     *
186
     * @param \SwooleTW\Http\Websocket\Parser $payloadParser
187
     *
188
     * @return \SwooleTW\Http\Concerns\InteractsWithWebsocket
189
     */
190
    public function setPayloadParser(Parser $payloadParser)
191
    {
192
        $this->payloadParser = $payloadParser;
193
194
        return $this;
195
    }
196
197
    /**
198
     * Get frame parser for websocket.
199
     */
200
    public function getPayloadParser()
201
    {
202
        return $this->payloadParser;
203
    }
204
205
    /**
206
     * Prepare settings if websocket is enabled.
207
     */
208
    protected function prepareWebsocket()
209
    {
210
        $config = $this->container->make('config');
211
        $isWebsocket = $config->get('swoole_http.websocket.enabled');
212
        $parser = $config->get('swoole_websocket.parser');
213
214
        if ($isWebsocket) {
215
            $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...
216
            $this->isServerWebsocket = true;
217
            $this->setPayloadParser(new $parser);
218
        }
219
    }
220
221
    /**
222
     * Check if it's a websocket fd.
223
     *
224
     * @param int $fd
225
     *
226
     * @return bool
227
     */
228
    protected function isServerWebsocket(int $fd): bool
229
    {
230
        $info = $this->container->make(Server::class)->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

230
        $info = $this->container->make(Server::class)->/** @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...
231
232
        return Arr::has($info, 'websocket_status') && Arr::get($info, 'websocket_status');
233
    }
234
235
    /**
236
     * Returns all descriptors that are websocket
237
     *
238
     * @param array $descriptors
239
     *
240
     * @return array
241
     */
242
    protected function filterWebsocket(array $descriptors): array
243
    {
244
        $callback = function ($descriptor) {
245
            return $this->isServerWebsocket($descriptor);
246
        };
247
248
        return collect($descriptors)->filter($callback)->toArray();
249
    }
250
251
    /**
252
     * Prepare websocket handler for onOpen and onClose callback.
253
     *
254
     * @throws \Exception
255
     */
256
    protected function prepareWebsocketHandler()
257
    {
258
        $handlerClass = $this->container->make('config')->get('swoole_websocket.handler');
259
260
        if (! $handlerClass) {
261
            throw new WebsocketNotSetInConfigException;
262
        }
263
264
        $this->setWebsocketHandler($this->app->make($handlerClass));
265
    }
266
267
    /**
268
     * Set websocket handler.
269
     *
270
     * @param \SwooleTW\Http\Websocket\HandlerContract $handler
271
     *
272
     * @return \SwooleTW\Http\Concerns\InteractsWithWebsocket
273
     */
274
    public function setWebsocketHandler(HandlerContract $handler)
275
    {
276
        $this->websocketHandler = $handler;
277
278
        return $this;
279
    }
280
281
    /**
282
     * Get websocket handler.
283
     *
284
     * @return \SwooleTW\Http\Websocket\HandlerContract
285
     */
286
    public function getWebsocketHandler(): HandlerContract
287
    {
288
        return $this->websocketHandler;
289
    }
290
291
    /**
292
     * @param string $class
293
     * @param array $settings
294
     *
295
     * @return \SwooleTW\Http\Websocket\Rooms\RoomContract
296
     */
297
    protected function createRoom(string $class, array $settings): RoomContract
298
    {
299
        return new $class($settings);
300
    }
301
302
    /**
303
     * Bind room instance to Laravel app container.
304
     */
305
    protected function bindRoom(): void
306
    {
307
        $this->app->singleton(RoomContract::class, function (Container $container) {
308
            $config = $container->make('config');
309
            $driver = $config->get('swoole_websocket.default');
310
            $settings = $config->get("swoole_websocket.settings.{$driver}");
311
            $className = $config->get("swoole_websocket.drivers.{$driver}");
312
313
            return $this->createRoom($className, $settings);
314
        });
315
316
        $this->app->alias(RoomContract::class, 'swoole.room');
317
    }
318
319
    /**
320
     * Bind websocket instance to Laravel app container.
321
     */
322
    protected function bindWebsocket()
323
    {
324
        $this->app->singleton(Websocket::class, function (Container $app) {
325
            return new Websocket($app->make(RoomContract::class), new Pipeline($app));
326
        });
327
328
        $this->app->alias(Websocket::class, 'swoole.websocket');
329
    }
330
331
    /**
332
     * Load websocket routes file.
333
     */
334
    protected function loadWebsocketRoutes()
335
    {
336
        $routePath = $this->container->make('config')
337
            ->get('swoole_websocket.route_file');
338
339
        if (! file_exists($routePath)) {
340
            $routePath = __DIR__ . '/../../routes/websocket.php';
341
        }
342
343
        return require $routePath;
344
    }
345
346
    /**
347
     * Normalize data for message push.
348
     *
349
     * @param array $data
350
     *
351
     * @return array
352
     */
353
    public function normalizePushData(array $data)
354
    {
355
        $opcode = Arr::get($data, 'opcode', 1);
356
        $sender = Arr::get($data, 'sender', 0);
357
        $fds = Arr::get($data, 'fds', []);
358
        $broadcast = Arr::get($data, 'broadcast', false);
359
        $assigned = Arr::get($data, 'assigned', false);
360
        $event = Arr::get($data, 'event', null);
361
        $message = Arr::get($data, 'message', null);
362
363
        return [$opcode, $sender, $fds, $broadcast, $assigned, $event, $message];
364
    }
365
366
    /**
367
     * Indicates if the payload is websocket push.
368
     *
369
     * @param mixed $payload
370
     *
371
     * @return boolean
372
     */
373
    protected function isWebsocketPushPayload($payload): bool
374
    {
375
        if (! is_array($payload)) {
376
            return false;
377
        }
378
379
        return $this->isServerWebsocket
380
            && array_key_exists('action', $payload)
381
            && $payload['action'] === Websocket::PUSH_ACTION;
382
    }
383
}
384