Passed
Push — master ( 92d93f...feff91 )
by Albert
04:27 queued 02:14
created

InteractsWithWebsocket::isWebsocketPushPacket()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 5
nc 4
nop 1
dl 0
loc 9
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
     * Indicates if a packet is websocket push action.
156
     *
157
     * @param mixed
158
     */
159
    protected function isWebsocketPushPacket($packet)
160
    {
161
        if ( !is_array($packet)) {
162
            return false;
163
        }
164
165
        return $this->isWebsocket
166
            && array_key_exists('action', $packet)
167
            && $packet['action'] === Websocket::PUSH_ACTION;
168
    }
169
170
171
    /**
172
     * Push websocket message to clients.
173
     *
174
     * @param \Swoole\Websocket\Server $server
175
     * @param mixed $data
176
     */
177
    public function pushMessage($server, array $data)
178
    {
179
        $push = Push::new($data);
180
        $payload = $this->payloadParser->encode($push->getEvent(), $push->getMessage());
181
182
        // attach sender if not broadcast
183
        if (! $push->isBroadcast() && $push->getSender() && ! $push->hasOwnDescriptor()) {
184
            $push->addDescriptor($push->getSender());
185
        }
186
187
        // check if to broadcast all clients
188
        if ($push->isBroadcastToAllDescriptors()) {
189
            $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

189
            $push->mergeDescriptor($this->filterWebsocket(/** @scrutinizer ignore-type */ $server->connections));
Loading history...
190
        }
191
192
        // push message to designated fds
193
        foreach ($push->getDescriptors() as $descriptor) {
194
            if ($server->exist($descriptor) || ! $push->isBroadcastToDescriptor((int) $descriptor)) {
195
                $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

195
                $server->push($descriptor, $payload, /** @scrutinizer ignore-type */ $push->getOpcode());
Loading history...
196
            }
197
        }
198
    }
199
200
    /**
201
     * Set frame parser for websocket.
202
     *
203
     * @param \SwooleTW\Http\Websocket\Parser $payloadParser
204
     *
205
     * @return \SwooleTW\Http\Concerns\InteractsWithWebsocket
206
     */
207
    public function setPayloadParser(Parser $payloadParser)
208
    {
209
        $this->payloadParser = $payloadParser;
210
211
        return $this;
212
    }
213
214
    /**
215
     * Get frame parser for websocket.
216
     */
217
    public function getPayloadParser()
218
    {
219
        return $this->payloadParser;
220
    }
221
222
    /**
223
     * Prepare settings if websocket is enabled.
224
     */
225
    protected function prepareWebsocket()
226
    {
227
        $config = $this->container->make('config');
228
        $isWebsocket = $config->get('swoole_http.websocket.enabled');
229
        $parser = $config->get('swoole_websocket.parser');
230
231
        if ($isWebsocket) {
232
            $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...
233
            $this->isServerWebsocket = true;
234
            $this->setPayloadParser(new $parser);
235
        }
236
    }
237
238
    /**
239
     * Check if it's a websocket fd.
240
     *
241
     * @param int $fd
242
     *
243
     * @return bool
244
     */
245
    protected function isServerWebsocket(int $fd): bool
246
    {
247
        $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

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