Completed
Pull Request — master (#104)
by Maxime
02:53 queued 23s
created

WebSocketServer::getMessageHandler()   B

Complexity

Conditions 6
Paths 12

Size

Total Lines 22
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 6.2163

Importance

Changes 0
Metric Value
dl 0
loc 22
ccs 9
cts 11
cp 0.8182
rs 8.6737
c 0
b 0
f 0
cc 6
eloc 11
nc 12
nop 2
crap 6.2163
1
<?php
2
3
/**
4
 * This file is a part of Woketo package.
5
 *
6
 * (c) Nekland <[email protected]>
7
 *
8
 * For the full license, take a look to the LICENSE file
9
 * on the root directory of this project
10
 */
11
12
namespace Nekland\Woketo\Server;
13
14
use Nekland\Woketo\Exception\RuntimeException;
15
use Nekland\Woketo\Message\MessageHandlerInterface;
16
use Nekland\Woketo\Rfc6455\FrameFactory;
17
use Nekland\Woketo\Rfc6455\MessageFactory;
18
use Nekland\Woketo\Rfc6455\MessageHandler\CloseFrameHandler;
19
use Nekland\Woketo\Rfc6455\MessageHandler\RsvCheckFrameHandler;
20
use Nekland\Woketo\Rfc6455\MessageHandler\WrongOpcodeHandler;
21
use Nekland\Woketo\Rfc6455\MessageHandler\PingFrameHandler;
22
use Nekland\Woketo\Rfc6455\MessageProcessor;
23
use Nekland\Woketo\Rfc6455\ServerHandshake;
24
use Nekland\Woketo\Utils\SimpleLogger;
25
use Psr\Log\LoggerInterface;
26
use Psr\Log\LogLevel;
27
use React\EventLoop\LoopInterface;
28
use React\Socket\ConnectionInterface;
29
use React\Socket\ServerInterface;
30
31
class WebSocketServer
32
{
33
    /**
34
     * @var int
35
     */
36
    private $port;
37
38
    /**
39
     * @var string
40
     */
41
    private $host;
42
43
    /**
44
     * @var ServerHandshake
45
     */
46
    private $handshake;
47
48
    /**
49
     * @var MessageHandlerInterface[]
50
     */
51
    private $messageHandlers;
52
53
    /**
54
     * @var array
55
     */
56
    private $connections;
57
58
    /**
59
     * @var LoopInterface
60
     */
61
    private $loop;
62
63
    /**
64
     * @var ServerInterface
65
     */
66
    private $server;
67
68
    /**
69
     * @var MessageProcessor
70
     */
71
    private $messageProcessor;
72
73
    /**
74
     * @var array
75
     */
76
    private $config;
77
78
    /**
79
     * @var LoggerInterface
80
     */
81
    private $logger;
82
83
    /**
84
     * @param int    $port    The number of the port to bind
85
     * @param string $host    The host to listen on (by default 127.0.0.1)
86
     * @param array  $config
87
     */
88 6
    public function __construct($port, $host = '127.0.0.1', $config = [])
89
    {
90 6
        $this->setConfig($config);
91 6
        $this->host = $host;
92 6
        $this->port = $port;
93 6
        $this->handshake = new ServerHandshake();
94 6
        $this->connections = [];
95 6
        $this->buildMessageProcessor();
96
97
        // Some optimization
98 5
        \gc_enable();       // As the process never stops, the garbage collector will be usefull, you may need to call it manually sometimes for performance purpose
99 5
        \set_time_limit(0); // It's by default on most server for cli apps but better be sure of that fact
100 5
    }
101
102
    /**
103
     * @param MessageHandlerInterface|string $messageHandler An instance of a class as string
104
     * @param string                         $uri            The URI you want to bind on
105
     */
106 3
    public function setMessageHandler($messageHandler, $uri = '*')
107
    {
108 3
        if (!$messageHandler instanceof MessageHandlerInterface &&  !\is_string($messageHandler)) {
109
            throw new \InvalidArgumentException('The message handler must be an instance of MessageHandlerInterface or a string.');
110
        }
111 3
        if (\is_string($messageHandler)) {
112
            try {
113
                $reflection = new \ReflectionClass($messageHandler);
114
                if(!$reflection->implementsInterface('Nekland\Woketo\Message\MessageHandlerInterface')) {
115
                    throw new \InvalidArgumentException('The messageHandler must implement MessageHandlerInterface');
116
                }
117
            } catch (\ReflectionException $e) {
118
                throw new \InvalidArgumentException('The messageHandler must be a string representing a class.');
119
            }
120
        }
121 3
        $this->messageHandlers[$uri] = $messageHandler;
122 3
    }
123
124
    /**
125
     * Launch the WebSocket server and an infinite loop that act on event.
126
     *
127
     * @throws \Exception
128
     */
129 3
    public function start()
130
    {
131 3
        if ($this->config['prod'] && \extension_loaded('xdebug')) {
132
            throw new \Exception('xdebug is enabled, it\'s a performance issue. Disable that extension or specify "prod" option to false.');
133
        }
134
135 3
        $this->loop = $this->loop ?? \React\EventLoop\Factory::create();
136 3
        $this->server = $this->server ?? new \React\Socket\Server($this->loop);
137
138
        $this->server->on('connection', function ($socketStream) {
139 3
            $this->onNewConnection($socketStream);
140 3
        });
141 3
        $this->server->listen($this->port);
142
143 3
        $this->getLogger()->info('Listening on ' . $this->host . ':' . $this->port);
144
145 3
        $this->loop->run();
146 3
    }
147
148
    /**
149
     * @param ConnectionInterface $socketStream
150
     */
151
    private function onNewConnection(ConnectionInterface $socketStream)
152
    {
153 3
        $connection = new Connection($socketStream, function ($uri, Connection $connection) {
154 3
            return $this->getMessageHandler($uri, $connection);
155 3
        }, $this->loop, $this->messageProcessor);
156
157 3
        $connection->setLogger($this->getLogger());
158 3
        $this->connections[] = $connection;
159 3
    }
160
161
    /**
162
     * @param string $uri
163
     * @param Connection $connection
164
     * @return MessageHandlerInterface|null
165
     */
166 3
    private function getMessageHandler(string $uri, Connection $connection)
0 ignored issues
show
Unused Code introduced by
The parameter $connection is not used and could be removed.

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

Loading history...
167
    {
168 3
        $handler = null;
169
170 3
        if (!empty($this->messageHandlers[$uri])) {
171 2
            $handler = $this->messageHandlers[$uri];
172
        }
173
174 3
        if (null === $handler && !empty($this->messageHandlers['*'])) {
175
            $handler = $this->messageHandlers['*'];
176
        }
177
178 3
        if (null !== $handler) {
179 2
            if (\is_string($handler)) {
180
                $handler = new $handler;
181
            }
182
183 2
            return $handler;
184
        }
185
186 1
        return null;
187
    }
188
189
    /**
190
     * Build the message processor with configuration
191
     */
192 6
    private function buildMessageProcessor()
193
    {
194 6
        $this->messageProcessor = new MessageProcessor(
195 6
            new FrameFactory($this->config['frame']),
196 6
            new MessageFactory($this->config['message'])
197
        );
198 6
        $this->messageProcessor->addHandler(new PingFrameHandler());
199 6
        $this->messageProcessor->addHandler(new CloseFrameHandler());
200 6
        $this->messageProcessor->addHandler(new WrongOpcodeHandler());
201 6
        $this->messageProcessor->addHandler(new RsvCheckFrameHandler());
202
203 6
        foreach ($this->config['messageHandlers'] as $handler) {
204 1
            if (!$handler instanceof MessageHandlerInterface) {
205 1
                throw new RuntimeException(sprintf('%s is not an instance of MessageHandlerInterface but must be !', get_class($handler)));
206
            }
207
        }
208 5
    }
209
210
    /**
211
     * Sets the configuration
212
     *
213
     * @param array $config
214
     */
215 6
    private function setConfig(array $config)
216
    {
217 6
        $this->config = \array_merge([
218 6
            'frame' => [],
219
            'message' => [],
220
            'messageHandlers' => [],
221
            'prod' => true
222
        ], $config);
223 6
    }
224
225
    /**
226
     * @return SimpleLogger|LoggerInterface
227
     */
228 3
    public function getLogger()
229
    {
230 3
        if (null === $this->logger) {
231 3
            return $this->logger = new SimpleLogger(!$this->config['prod']);
232
        }
233
234 3
        return $this->logger;
235
    }
236
237
    /**
238
     * Allows you to set a custom logger
239
     *
240
     * @param LoggerInterface $logger
241
     * @return WebSocketServer
242
     */
243
    public function setLogger(LoggerInterface $logger)
244
    {
245
        $this->logger = $logger;
246
247
        return $this;
248
    }
249
250
    /**
251
     * Allows to specify a loop that will be used instead of the reactphp generated loop.
252
     *
253
     * @param LoopInterface $loop
254
     * @return WebSocketServer
255
     */
256 3
    public function setLoop(LoopInterface $loop)
257
    {
258 3
        $this->loop = $loop;
259
260 3
        return $this;
261
    }
262
263
    /**
264
     * @param ServerInterface $server
265
     * @return WebSocketServer
266
     */
267 3
    public function setSocketServer(ServerInterface $server)
268
    {
269 3
        $this->server = $server;
270
271 3
        return $this;
272
    }
273
}
274