Completed
Push — master ( 143878...f45699 )
by Taosikai
14:33
created

Server::createMessageHandler()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 22
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 22
rs 8.6737
cc 5
eloc 18
nc 5
nop 2

2 Methods

Rating   Name   Duplication   Size   Complexity  
A Server::initializeTimers() 0 6 1
A Server::getDefaultCommands() 0 6 1
1
<?php
2
3
/*
4
 * This file is part of the slince/spike package.
5
 *
6
 * (c) Slince <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Spike\Server;
13
14
use Doctrine\Common\Collections\ArrayCollection;
15
use Doctrine\Common\Collections\Collection;
16
use React\EventLoop\Factory;
17
use React\EventLoop\LoopInterface;
18
use React\Socket\ConnectionInterface;
19
use React\Socket\Server as Socket;
20
use function Slince\Common\jsonBuffer;
21
use Slince\Event\Dispatcher;
22
use Slince\Event\DispatcherInterface;
23
use Slince\Event\Event;
24
use Spike\Client\ClientInterface;
25
use Spike\Common\Logger\Logger;
26
use Spike\Common\Protocol\Spike;
27
use Spike\Common\Timer\MemoryWatcher;
28
use Spike\Common\Timer\TimersAware;
29
use Spike\Server\ChunkServer\ChunkServerCollection;
30
use Spike\Server\ChunkServer\ChunkServerInterface;
31
use Spike\Server\Event\Events;
32
use Spike\Server\Event\FilterActionHandlerEvent;
33
use Spike\Server\Listener\LoggerListener;
34
use Spike\Server\Listener\ServerListener;
35
use Symfony\Component\Console\Application;
36
use Symfony\Component\Console\Input\InputInterface;
37
use Symfony\Component\Console\Input\InputOption;
38
use Symfony\Component\Console\Output\OutputInterface;
39
40
class Server extends Application implements ServerInterface
41
{
42
    use TimersAware;
43
44
    /**
45
     * @var string
46
     */
47
    const LOGO = <<<EOT
48
 _____   _____   _   _   _    _____   _____  
49
/  ___/ |  _  \ | | | | / /  | ____| |  _  \ 
50
| |___  | |_| | | | | |/ /   | |__   | | | | 
51
\___  \ |  ___/ | | | |\ \   |  __|  | | | | 
52
 ___| | | |     | | | | \ \  | |___  | |_| | 
53
/_____/ |_|     |_| |_|  \_\ |_____| |_____/ 
54
55
56
EOT;
57
58
    const NAME = 'Spike Server';
59
60
    const VERSION = '0.0.1';
61
62
    /**
63
     * @var Configuration
64
     */
65
    protected $configuration;
66
67
    /**
68
     * @var LoopInterface
69
     */
70
    protected $eventLoop;
71
72
    /**
73
     * @var ChunkServerInterface[]|ChunkServerCollection
74
     */
75
    protected $chunkServers;
76
77
    /**
78
     * @var ClientInterface[]|Collection
79
     */
80
    protected $clients;
81
82
    /**
83
     * @var DispatcherInterface
84
     */
85
    protected $eventDispatcher;
86
87
    /**
88
     * @var Logger
89
     */
90
    protected $logger;
91
92
    public function __construct(Configuration $configuration, LoopInterface $eventLoop = null)
93
    {
94
        $this->configuration = $configuration;
95
        $this->eventLoop = $eventLoop ?: Factory::create();
96
        $this->eventDispatcher = new Dispatcher();
97
        $this->chunkServers = new ChunkServerCollection();
98
        $this->clients = new ArrayCollection();
99
        $this->initializeEvents();
100
        parent::__construct(static::NAME, static::VERSION);
101
    }
102
103
    /**
104
     * {@inheritdoc}
105
     */
106
    public function getHelp()
107
    {
108
        return static::LOGO.parent::getHelp();
109
    }
110
111
    /**
112
     * {@inheritdoc}
113
     */
114
    public function doRun(InputInterface $input, OutputInterface $output)
115
    {
116
        $this->logger = new Logger(
117
            $this->eventLoop,
118
            $this->getConfiguration()->getLogLevel(),
119
            $this->getConfiguration()->getLogFile(),
120
            $output
121
        );
122
        // Execute command if the command name is exists
123
        if ($this->getCommandName($input) ||
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->getCommandName($input) of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
124
            true === $input->hasParameterOption(array('--help', '-h'), true)
125
        ) {
126
            $exitCode = parent::doRun($input, $output);
127
        } else {
128
            $exitCode = $this->start();
129
        }
130
131
        return $exitCode;
132
    }
133
134
    /**
135
     * {@inheritdoc}
136
     *
137
     * @codeCoverageIgnore
138
     */
139
    public function start()
140
    {
141
        $server = new Socket($this->configuration->getAddress(), $this->eventLoop);
142
        $this->eventDispatcher->dispatch(Events::SERVER_RUN);
143
        $server->on('connection', [$this, 'handleControlConnection']);
144
145
        $this->initializeTimers();
146
        $this->eventLoop->run();
147
148
        return 0;
149
    }
150
151
    /**
152
     * {@inheritdoc}
153
     */
154
    public function stopClient(ClientInterface $client)
155
    {
156
        $chunkServers = $this->chunkServers->filter(function(ChunkServerInterface $chunkServer) use ($client){
157
            return $client === $chunkServer->getClient();
158
        });
159
        $this->eventDispatcher->dispatch(new Event(Events::CLIENT_CLOSE, $this, [
160
            'client' => $client,
161
            'chunkServers' => $chunkServers,
162
        ]));
163
        foreach ($chunkServers as $chunkServer) {
164
            //Close the tunnel server and removes it
165
            $chunkServer->stop();
166
            $this->chunkServers->removeElement($chunkServer);
167
        }
168
        $client->getControlConnection()->end(); //Distinct
169
        $this->clients->removeElement($client); //Removes the client
170
    }
171
172
    /**
173
     * Handle control connection.
174
     *
175
     * @param ConnectionInterface $connection
176
     */
177
    public function handleControlConnection(ConnectionInterface $connection)
178
    {
179
        jsonBuffer($connection, function($messages) use ($connection){
180
            foreach ($messages as $messageData) {
181
                if (!$messageData) {
182
                    continue;
183
                }
184
                $message = Spike::fromArray($messageData);
185
186
                //Fires filter action handler event
187
                $event = new FilterActionHandlerEvent($this, $message, $connection);
188
                $this->eventDispatcher->dispatch($event);
189
                if ($actionHandler = $event->getActionHandler()) {
190
                    $actionHandler->handle($message);
191
                }
192
            }
193
        }, function($exception) use ($connection){
194
            $this->eventDispatcher->dispatch(new Event(Events::CONNECTION_ERROR, $this, [
195
                'connection' => $connection,
196
                'exception' => $exception,
197
            ]));
198
        });
199
        //Distinct
200
        $connection->on('close', function() use($connection){
201
            //If client has been registered and then close it.
202
            $client = $this->clients->filter(function(ClientInterface $client) use ($connection){
203
                return $client->getControlConnection() === $connection;
204
            })->first();
205
            if ($client) {
206
                $this->stopClient($client);
207
            } else {
208
                $connection->end();
209
            }
210
            $this->eventDispatcher->dispatch(new Event(Events::CLIENT_CLOSE, $this, [
211
                'connection' => $connection,
212
            ]));
213
        });
214
    }
215
216
    /**
217
     * {@inheritdoc}
218
     */
219
    public function getChunkServers()
220
    {
221
        return $this->chunkServers;
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->chunkServers; of type Spike\Server\ChunkServer...r\ChunkServerCollection adds the type Spike\Server\ChunkServer\ChunkServerInterface[] to the return on line 221 which is incompatible with the return type declared by the interface Spike\Server\ServerInterface::getChunkServers of type Doctrine\Common\Collections\Collection.
Loading history...
222
    }
223
224
    /**
225
     * @return LoopInterface
226
     */
227
    public function getEventLoop()
228
    {
229
        return $this->eventLoop;
230
    }
231
232
    /**
233
     * {@inheritdoc}
234
     */
235
    public function getEventDispatcher()
236
    {
237
        return $this->eventDispatcher;
238
    }
239
240
    /**
241
     * @return Configuration
242
     */
243
    public function getConfiguration()
244
    {
245
        return $this->configuration;
246
    }
247
248
    /**
249
     * Gets all clients.
250
     *
251
     * @return Collection|ClientInterface[]
252
     */
253
    public function getClients()
254
    {
255
        return $this->clients;
256
    }
257
258
    /**
259
     * Gets the client by ID.
260
     *
261
     * @param string $id
262
     *
263
     * @return null|ClientInterface
264
     */
265
    public function getClientById($id)
266
    {
267
        return $this->clients->filter(function(Client $client) use ($id){
268
            return $client->getId() === $id;
269
        })->first();
270
    }
271
272
    /**
273
     * @return Logger
274
     */
275
    public function getLogger()
276
    {
277
        return $this->logger;
278
    }
279
280
    protected function initializeEvents()
281
    {
282
        $this->eventDispatcher->addSubscriber(new ServerListener());
283
        $this->eventDispatcher->addSubscriber(new LoggerListener($this));
284
    }
285
286
    /**
287
     * Creates default timers.
288
     *
289
     * @codeCoverageIgnore
290
     */
291
    protected function initializeTimers()
292
    {
293
        $this->addTimer(new Timer\ReviewClient($this));
294
        $this->addTimer(new Timer\SummaryWatcher($this));
295
        $this->addTimer(new MemoryWatcher($this->getLogger()));
296
    }
297
298
    /**
299
     * {@inheritdoc}
300
     */
301
    protected function getDefaultCommands()
302
    {
303
        return array_merge(parent::getDefaultCommands(), [
0 ignored issues
show
Best Practice introduced by
The expression return array_merge(paren...d\InitCommand($this))); seems to be an array, but some of its elements' types (Spike\Server\Command\InitCommand) are incompatible with the return type of the parent method Symfony\Component\Consol...ion::getDefaultCommands of type array<Symfony\Component\...le\Command\ListCommand>.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
304
            new Command\InitCommand($this),
305
        ]);
306
    }
307
308
    /**
309
     * {@inheritdoc}
310
     */
311
    protected function getDefaultInputDefinition()
312
    {
313
        $definition = parent::getDefaultInputDefinition();
314
        $definition->addOptions([
315
            new InputOption('config', 'c', InputOption::VALUE_OPTIONAL, 'The configuration file, support json,ini,xml and yaml format'),
316
            new InputOption('address', 'a', InputOption::VALUE_REQUIRED, 'The server address'),
317
        ]);
318
319
        return $definition;
320
    }
321
}
322