Completed
Pull Request — master (#104)
by Maxime
02:47
created

WebSocketServer::getMessageHandler()   B

Complexity

Conditions 6
Paths 12

Size

Total Lines 22
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 0
Metric Value
dl 0
loc 22
c 0
b 0
f 0
ccs 0
cts 11
cp 0
rs 8.6737
cc 6
eloc 11
nc 12
nop 2
crap 42
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
30
class WebSocketServer
31
{
32
    /**
33
     * @var int
34
     */
35
    private $port;
36
37
    /**
38
     * @var string
39
     */
40
    private $host;
41
42
    /**
43
     * @var ServerHandshake
44
     */
45
    private $handshake;
46
47
    /**
48
     * @var MessageHandlerInterface[]
49
     */
50
    private $messageHandlers;
51
52
    /**
53
     * @var array
54
     */
55
    private $connections;
56
57
    /**
58
     * @var LoopInterface
59
     */
60
    private $loop;
61
62
    /**
63
     * @var MessageProcessor
64
     */
65
    private $messageProcessor;
66
67
    /**
68
     * @var array
69
     */
70
    private $config;
71
72
    /**
73
     * @var LoggerInterface
74
     */
75
    private $logger;
76
77
    /**
78
     * @param int    $port    The number of the port to bind
79
     * @param string $host    The host to listen on (by default 127.0.0.1)
80
     * @param array  $config
81
     */
82 3
    public function __construct($port, $host = '127.0.0.1', $config = [])
83
    {
84 3
        $this->setConfig($config);
85 3
        $this->host = $host;
86 3
        $this->port = $port;
87 3
        $this->handshake = new ServerHandshake();
88 3
        $this->connections = [];
89 3
        $this->buildMessageProcessor();
90
91
        // Some optimization
92 2
        \gc_enable();       // As the process never stops, the garbage collector will be usefull, you may need to call it manually sometimes for performance purpose
93 2
        \set_time_limit(0); // It's by default on most server for cli apps but better be sure of that fact
94 2
    }
95
96
    /**
97
     * @param MessageHandlerInterface|string $messageHandler An instance of a class as string
98
     * @param string                         $uri            The URI you want to bind on
99
     */
100
    public function setMessageHandler($messageHandler, $uri = '*')
101
    {
102
        if (!$messageHandler instanceof MessageHandlerInterface &&  !\is_string($messageHandler)) {
103
            throw new \InvalidArgumentException('The message handler must be an instance of MessageHandlerInterface or a string.');
104
        }
105
        if (\is_string($messageHandler)) {
106
            try {
107
                $reflection = new \ReflectionClass($messageHandler);
108
                if(!$reflection->implementsInterface('Nekland\Woketo\Message\MessageHandlerInterface')) {
109
                    throw new \InvalidArgumentException('The messageHandler must implement MessageHandlerInterface');
110
                }
111
            } catch (\ReflectionException $e) {
112
                throw new \InvalidArgumentException('The messageHandler must be a string representing a class.');
113
            }
114
        }
115
        $this->messageHandlers[$uri] = $messageHandler;
116
    }
117
118
    public function start()
119
    {
120
        if ($this->config['prod'] && \extension_loaded('xdebug')) {
121
            throw new \Exception('xdebug is enabled, it\'s a performance issue. Disable that extension or specify "prod" option to false.');
122
        }
123
        
124
        $this->loop = \React\EventLoop\Factory::create();
125
126
        $socket = new \React\Socket\Server($this->loop);
127
        $socket->on('connection', function ($socketStream) {
128
            $this->onNewConnection($socketStream);
129
        });
130
        $socket->listen($this->port);
131
132
        $this->getLogger()->info('Listening on ' . $this->host . ':' . $this->port);
133
134
        $this->loop->run();
135
    }
136
137
    /**
138
     * @param ConnectionInterface $socketStream
139
     */
140
    private function onNewConnection(ConnectionInterface $socketStream)
141
    {
142
        $connection = new Connection($socketStream, function ($uri, Connection $connection) {
143
            return $this->getMessageHandler($uri, $connection);
144
        }, $this->loop, $this->messageProcessor);
145
146
        $connection->setLogger($this->getLogger());
147
        $this->connections[] = $connection;
148
    }
149
150
    /**
151
     * @param string $uri
152
     * @param Connection $connection
153
     * @return MessageHandlerInterface|null
154
     */
155
    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...
156
    {
157
        $handler = null;
158
159
        if (!empty($this->messageHandlers[$uri])) {
160
            $handler = $this->messageHandlers[$uri];
161
        }
162
163
        if (null === $handler && !empty($this->messageHandlers['*'])) {
164
            $handler = $this->messageHandlers['*'];
165
        }
166
167
        if (null !== $handler) {
168
            if (\is_string($handler)) {
169
                $handler = new $handler;
170
            }
171
172
            return $handler;
173
        }
174
175
        return null;
176
    }
177
178
    /**
179
     * Build the message processor with configuration
180
     */
181 3
    private function buildMessageProcessor()
182
    {
183 3
        $this->messageProcessor = new MessageProcessor(
184 3
            new FrameFactory($this->config['frame']),
185 3
            new MessageFactory($this->config['message'])
186
        );
187 3
        $this->messageProcessor->addHandler(new PingFrameHandler());
188 3
        $this->messageProcessor->addHandler(new CloseFrameHandler());
189 3
        $this->messageProcessor->addHandler(new WrongOpcodeHandler());
190 3
        $this->messageProcessor->addHandler(new RsvCheckFrameHandler());
191
192 3
        foreach ($this->config['messageHandlers'] as $handler) {
193 1
            if (!$handler instanceof MessageHandlerInterface) {
194 1
                throw new RuntimeException(sprintf('%s is not an instance of MessageHandlerInterface but must be !', get_class($handler)));
195
            }
196
        }
197 2
    }
198
199
    /**
200
     * Sets the configuration
201
     *
202
     * @param array $config
203
     */
204 3
    private function setConfig(array $config)
205
    {
206 3
        $this->config = \array_merge([
207 3
            'frame' => [],
208
            'message' => [],
209
            'messageHandlers' => [],
210
            'prod' => true
211
        ], $config);
212 3
    }
213
214
    /**
215
     * @return SimpleLogger|LoggerInterface
216
     */
217
    public function getLogger()
218
    {
219
        if (null === $this->logger) {
220
            return $this->logger = new SimpleLogger(!$this->config['prod']);
221
        }
222
223
        return $this->logger;
224
    }
225
226
    /**
227
     * Allows you to set a custom logger
228
     *
229
     * @param LoggerInterface $logger
230
     * @return WebSocketServer
231
     */
232
    public function setLogger(LoggerInterface $logger)
233
    {
234
        $this->logger = $logger;
235
236
        return $this;
237
    }
238
}
239