Passed
Push — v3 ( dc00fa...37cdd6 )
by
unknown
02:41
created

GameQ   B

Complexity

Total Complexity 51

Size/Duplication

Total Lines 602
Duplicated Lines 0 %

Test Coverage

Coverage 72.51%

Importance

Changes 5
Bugs 0 Features 1
Metric Value
wmc 51
eloc 167
c 5
b 0
f 1
dl 0
loc 602
ccs 124
cts 171
cp 0.7251
rs 7.92

18 Methods

Rating   Name   Duplication   Size   Complexity  
A factory() 0 8 1
A listFilters() 0 3 1
A clearServers() 0 7 1
A getServers() 0 3 1
A getOptions() 0 3 1
A addFilter() 0 14 1
A __get() 0 4 2
A __set() 0 6 1
A setOption() 0 7 1
A addServer() 0 7 1
B doQueries() 0 98 9
A removeFilter() 0 13 2
B doChallenges() 0 80 7
A doApplyFilters() 0 22 3
B doParseResponse() 0 46 8
B addServersFromFiles() 0 28 7
A process() 0 40 2
A addServers() 0 9 2

How to fix   Complexity   

Complex Class

Complex classes like GameQ often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use GameQ, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * This file is part of GameQ.
4
 *
5
 * GameQ is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU Lesser General Public License as published by
7
 * the Free Software Foundation; either version 3 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * GameQ is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
 * GNU Lesser General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU Lesser General Public License
16
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
17
 */
18
19
namespace GameQ;
20
21
use GameQ\Exception\Protocol as ProtocolException;
22
use GameQ\Exception\Query as QueryException;
23
24
/**
25
 * Base GameQ Class
26
 *
27
 * This class should be the only one that is included when you use GameQ to query
28
 * any games servers.
29
 *
30
 * Requirements: See wiki or README for more information on the requirements
31
 *  - PHP 5.4.14+
32
 *    * Bzip2 - http://www.php.net/manual/en/book.bzip2.php
33
 *
34
 * @author Austin Bischoff <[email protected]>
35
 *
36
 * @property bool   $debug
37
 * @property string $capture_packets_file
38
 * @property int    $stream_timeout
39
 * @property int    $timeout
40
 * @property int    $write_wait
41
 */
42
class GameQ
43
{
44
    /*
45
     * Constants
46
     */
47
    const PROTOCOLS_DIRECTORY = __DIR__ . '/Protocols';
48
49
    /* Static Section */
50
51
    /**
52
     * Holds the instance of itself
53
     *
54
     * @var self
55
     */
56
    protected static $instance = null;
57
58
    /**
59
     * Create a new instance of this class
60
     *
61
     * @return \GameQ\GameQ
62
     */
63 6
    public static function factory()
64
    {
65
66
        // Create a new instance
67 6
        self::$instance = new self();
68
69
        // Return this new instance
70 6
        return self::$instance;
71
    }
72
73
    /* Dynamic Section */
74
75
    /**
76
     * Default options
77
     *
78
     * @var array
79
     */
80
    protected $options = [
81
        'debug'                => false,
82
        'timeout'              => 3, // Seconds
83
        'filters'              => [
84
            // Default normalize
85
            'normalize_d751713988987e9331980363e24189ce' => [
86
                'filter'  => 'normalize',
87
                'options' => [],
88
            ],
89
        ],
90
        // Advanced settings
91
        'stream_timeout'       => 200000, // See http://www.php.net/manual/en/function.stream-select.php for more info
92
        'write_wait'           => 500,
93
        // How long (in micro-seconds) to pause between writing to server sockets, helps cpu usage
94
95
        // Used for generating protocol test data
96
        'capture_packets_file' => null,
97
    ];
98
99
    /**
100
     * Array of servers being queried
101
     *
102
     * @var array
103
     */
104
    protected $servers = [];
105
106
    /**
107
     * The query library to use.  Default is Native
108
     *
109
     * @var string
110
     */
111
    protected $queryLibrary = 'GameQ\\Query\\Native';
112
113
    /**
114
     * Holds the instance of the queryLibrary
115
     *
116
     * @var \GameQ\Query\Core|null
117
     */
118
    protected $query = null;
119
120
    /**
121
     * Get an option's value
122
     *
123
     * @param mixed $option
124
     *
125
     * @return mixed|null
126
     */
127 1878
    public function __get($option)
128
    {
129
130 1878
        return isset($this->options[$option]) ? $this->options[$option] : null;
131
    }
132
133
    /**
134
     * Set an option's value
135
     *
136
     * @param mixed $option
137
     * @param mixed $value
138
     *
139
     * @return bool
140
     */
141 1890
    public function __set($option, $value)
142
    {
143
144 1890
        $this->options[$option] = $value;
145
146 1890
        return true;
147
    }
148
149 18
    public function getServers()
150
    {
151 18
        return $this->servers;
152
    }
153
154
    public function getOptions()
155
    {
156
        return $this->options;
157
    }
158
159
    /**
160
     * Chainable call to __set, uses set as the actual setter
161
     *
162
     * @param mixed $var
163
     * @param mixed $value
164
     *
165
     * @return $this
166
     */
167 1890
    public function setOption($var, $value)
168
    {
169
170
        // Use magic
171 1890
        $this->{$var} = $value;
172
173 1890
        return $this; // Make chainable
174
    }
175
176
    /**
177
     * Add a single server
178
     *
179
     * @param array $server_info
180
     *
181
     * @return $this
182
     */
183 48
    public function addServer(array $server_info = [])
184
    {
185
186
        // Add and validate the server
187 48
        $this->servers[uniqid()] = new Server($server_info);
188
189 48
        return $this; // Make calls chainable
190
    }
191
192
    /**
193
     * Add multiple servers in a single call
194
     *
195
     * @param array $servers
196
     *
197
     * @return $this
198
     */
199 12
    public function addServers(array $servers = [])
200
    {
201
202
        // Loop through all the servers and add them
203 12
        foreach ($servers as $server_info) {
204 12
            $this->addServer($server_info);
205
        }
206
207 12
        return $this; // Make calls chainable
208
    }
209
210
    /**
211
     * Add a set of servers from a file or an array of files.
212
     * Supported formats:
213
     * JSON
214
     *
215
     * @param array $files
216
     *
217
     * @return $this
218
     * @throws \Exception
219
     */
220 6
    public function addServersFromFiles($files = [])
221
    {
222
223
        // Since we expect an array let us turn a string (i.e. single file) into an array
224 6
        if (!is_array($files)) {
0 ignored issues
show
introduced by
The condition is_array($files) is always true.
Loading history...
225 6
            $files = [$files];
226
        }
227
228
        // Iterate over the file(s) and add them
229 6
        foreach ($files as $file) {
230
            // Check to make sure the file exists and we can read it
231 6
            if (!file_exists($file) || !is_readable($file)) {
232 6
                continue;
233
            }
234
235
            // See if this file is JSON
236 6
            if (($servers = json_decode(file_get_contents($file), true)) === null
237 6
                && json_last_error() !== JSON_ERROR_NONE
238
            ) {
239
                // Type not supported
240 6
                continue;
241
            }
242
243
            // Add this list of servers
244 6
            $this->addServers($servers);
245
        }
246
247 6
        return $this;
248
    }
249
250
    /**
251
     * Clear all of the defined servers
252
     *
253
     * @return $this
254
     */
255 12
    public function clearServers()
256
    {
257
258
        // Reset all the servers
259 12
        $this->servers = [];
260
261 12
        return $this; // Make Chainable
262
    }
263
264
    /**
265
     * Add a filter to the processing list
266
     *
267
     * @param string $filterName
268
     * @param array  $options
269
     *
270
     * @return $this
271
     */
272 18
    public function addFilter($filterName, $options = [])
273
    {
274
        // Create the filter hash so we can run multiple versions of the same filter
275 18
        $filterHash = sprintf('%s_%s', strtolower($filterName), md5(json_encode($options)));
276
277
        // Add the filter
278 18
        $this->options['filters'][$filterHash] = [
279 18
            'filter'  => strtolower($filterName),
280 18
            'options' => $options,
281 15
        ];
282
283 18
        unset($filterHash);
284
285 18
        return $this;
286
    }
287
288
    /**
289
     * Remove an added filter
290
     *
291
     * @param string $filterHash
292
     *
293
     * @return $this
294
     */
295 1890
    public function removeFilter($filterHash)
296
    {
297
        // Make lower case
298 1890
        $filterHash = strtolower($filterHash);
299
300
        // Remove this filter if it has been defined
301 1890
        if (array_key_exists($filterHash, $this->options['filters'])) {
302 18
            unset($this->options['filters'][$filterHash]);
303
        }
304
305 1890
        unset($filterHash);
306
307 1890
        return $this;
308
    }
309
310
    /**
311
     * Return the list of applied filters
312
     *
313
     * @return array
314
     */
315 6
    public function listFilters()
316
    {
317 6
        return $this->options['filters'];
318
    }
319
320
    /**
321
     * Main method used to actually process all of the added servers and return the information
322
     *
323
     * @return array
324
     * @throws \Exception
325
     */
326 30
    public function process()
327
    {
328
329
        // Initialize the query library we are using
330 30
        $class = new \ReflectionClass($this->queryLibrary);
331
332
        // Set the query pointer to the new instance of the library
333 30
        $this->query = $class->newInstance();
334
335 30
        unset($class);
336
337
        // Define the return
338 30
        $results = [];
339
340
        // @todo: Add break up into loop to split large arrays into smaller chunks
341
342
        // Do server challenge(s) first, if any
343 30
        $this->doChallenges();
344
345
        // Do packets for server(s) and get query responses
346 30
        $this->doQueries();
347
348
        // Now we should have some information to process for each server
349 30
        foreach ($this->servers as $server) {
350
            /* @var $server \GameQ\Server */
351
352
            // Parse the responses for this server
353 30
            $result = $this->doParseResponse($server);
354
355
            // Apply the filters
356 30
            $result = array_merge($result, $this->doApplyFilters($result, $server));
357
358
            // Sort the keys so they are alphabetical and nicer to look at
359 30
            ksort($result);
360
361
            // Add the result to the results array
362 30
            $results[$server->id()] = $result;
363
        }
364
365 30
        return $results;
366
    }
367
368
    /**
369
     * Do server challenges, where required
370
     */
371 30
    protected function doChallenges()
372
    {
373
374
        // Initialize the sockets for reading
375 30
        $sockets = [];
376
377
        // By default we don't have any challenges to process
378 30
        $server_challenge = false;
379
380
        // Do challenge packets
381 30
        foreach ($this->servers as $server_id => $server) {
382
            /* @var $server \GameQ\Server */
383
384
            // This protocol has a challenge packet that needs to be sent
385 30
            if ($server->protocol()->hasChallenge()) {
386
                // We have a challenge, set the flag
387
                $server_challenge = true;
388
389
                // Let's make a clone of the query class
390
                $socket = clone $this->query;
391
392
                // Set the information for this query socket
393
                $socket->set(
394
                    $server->protocol()->transport(),
395
                    $server->ip,
396
                    $server->port_query,
397
                    $this->timeout
398
                );
399
400
                try {
401
                    // Now write the challenge packet to the socket.
402
                    $socket->write($server->protocol()->getPacket(Protocol::PACKET_CHALLENGE));
0 ignored issues
show
Bug introduced by
GameQ\Protocol::PACKET_CHALLENGE of type string is incompatible with the type array expected by parameter $type of GameQ\Protocol::getPacket(). ( Ignorable by Annotation )

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

402
                    $socket->write($server->protocol()->getPacket(/** @scrutinizer ignore-type */ Protocol::PACKET_CHALLENGE));
Loading history...
Bug introduced by
$server->protocol()->get...ocol::PACKET_CHALLENGE) of type array is incompatible with the type string expected by parameter $data of GameQ\Query\Core::write(). ( Ignorable by Annotation )

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

402
                    $socket->write(/** @scrutinizer ignore-type */ $server->protocol()->getPacket(Protocol::PACKET_CHALLENGE));
Loading history...
403
404
                    // Add the socket information so we can reference it easily
405
                    $sockets[(int)$socket->get()] = [
406
                        'server_id' => $server_id,
407
                        'socket'    => $socket,
408
                    ];
409
                } catch (QueryException $exception) {
410
                    // Check to see if we are in debug, if so bubble up the exception
411
                    if ($this->debug) {
412
                        throw new \Exception($exception->getMessage(), $exception->getCode(), $exception);
413
                    }
414
                }
415
416
                unset($socket);
417
418
                // Let's sleep shortly so we are not hammering out calls rapid fire style hogging cpu
419
                usleep($this->write_wait);
420
            }
421
        }
422
423
        // We have at least one server with a challenge, we need to listen for responses
424 30
        if ($server_challenge) {
425
            // Now we need to listen for and grab challenge response(s)
426
            $responses = call_user_func_array(
427
                [$this->query, 'getResponses'],
428
                [$sockets, $this->timeout, $this->stream_timeout]
429
            );
430
431
            // Iterate over the challenge responses
432
            foreach ($responses as $socket_id => $response) {
433
                // Back out the server_id we need to update the challenge response for
434
                $server_id = $sockets[$socket_id]['server_id'];
435
436
                // Make this into a buffer so it is easier to manipulate
437
                $challenge = new Buffer(implode('', $response));
438
439
                // Grab the server instance
440
                /* @var $server \GameQ\Server */
441
                $server = $this->servers[$server_id];
442
443
                // Apply the challenge
444
                $server->protocol()->challengeParseAndApply($challenge);
445
446
                // Add this socket to be reused, has to be reused in GameSpy3 for example
447
                $server->socketAdd($sockets[$socket_id]['socket']);
448
449
                // Clear
450
                unset($server);
451
            }
452
        }
453 5
    }
454
455
    /**
456
     * Run the actual queries and get the response(s)
457
     */
458 30
    protected function doQueries()
459
    {
460
461
        // Initialize the array of sockets
462 30
        $sockets = [];
463
464
        // Iterate over the server list
465 30
        foreach ($this->servers as $server_id => $server) {
466
            /* @var $server \GameQ\Server */
467
468
            // Invoke the beforeSend method
469 30
            $server->protocol()->beforeSend($server);
470
471
            // Get all the non-challenge packets we need to send
472 30
            $packets = $server->protocol()->getPacket('!' . Protocol::PACKET_CHALLENGE);
0 ignored issues
show
Bug introduced by
'!' . GameQ\Protocol::PACKET_CHALLENGE of type string is incompatible with the type array expected by parameter $type of GameQ\Protocol::getPacket(). ( Ignorable by Annotation )

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

472
            $packets = $server->protocol()->getPacket(/** @scrutinizer ignore-type */ '!' . Protocol::PACKET_CHALLENGE);
Loading history...
473
474 30
            if (count($packets) == 0) {
475
                // Skip nothing else to do for some reason.
476
                continue;
477
            }
478
479
            // Try to use an existing socket
480 30
            if (($socket = $server->socketGet()) === null) {
481
                // Let's make a clone of the query class
482 30
                $socket = clone $this->query;
483
484
                // Set the information for this query socket
485 30
                $socket->set(
486 30
                    $server->protocol()->transport(),
487 30
                    $server->ip,
488 30
                    $server->port_query,
489 30
                    $this->timeout
490 25
                );
491
            }
492
493
            try {
494
                // Iterate over all the packets we need to send
495 30
                foreach ($packets as $packet_data) {
496
                    // Now write the packet to the socket.
497 30
                    $socket->write($packet_data);
498
499
                    // Let's sleep shortly so we are not hammering out calls rapid fire style
500
                    usleep($this->write_wait);
501
                }
502
503
                unset($packets);
504
505
                // Add the socket information so we can reference it easily
506
                $sockets[(int)$socket->get()] = [
507
                    'server_id' => $server_id,
508
                    'socket'    => $socket,
509
                ];
510 30
            } catch (QueryException $exception) {
511
                // Check to see if we are in debug, if so bubble up the exception
512 30
                if ($this->debug) {
513
                    throw new \Exception($exception->getMessage(), $exception->getCode(), $exception);
514
                }
515
516 30
                continue;
517
            }
518
519
            // Clean up the sockets, if any left over
520
            $server->socketCleanse();
521
        }
522
523
        // Now we need to listen for and grab response(s)
524 30
        $responses = call_user_func_array(
525 30
            [$this->query, 'getResponses'],
526 30
            [$sockets, $this->timeout, $this->stream_timeout]
527 25
        );
528
529
        // Iterate over the responses
530 30
        foreach ($responses as $socket_id => $response) {
531
            // Back out the server_id
532
            $server_id = $sockets[$socket_id]['server_id'];
533
534
            // Grab the server instance
535
            /* @var $server \GameQ\Server */
536
            $server = $this->servers[$server_id];
537
538
            // Save the response from this packet
539
            $server->protocol()->packetResponse($response);
540
541
            unset($server);
542
        }
543
544
        // Now we need to close all of the sockets
545 30
        foreach ($sockets as $socketInfo) {
546
            /* @var $socket \GameQ\Query\Core */
547
            $socket = $socketInfo['socket'];
548
549
            // Close the socket
550
            $socket->close();
551
552
            unset($socket);
553
        }
554
555 30
        unset($sockets);
556 5
    }
557
558
    /**
559
     * Parse the response for a specific server
560
     *
561
     * @param \GameQ\Server $server
562
     *
563
     * @return array
564
     * @throws \Exception
565
     */
566 1872
    protected function doParseResponse(Server $server)
567
    {
568
569
        try {
570
            // @codeCoverageIgnoreStart
571
            // We want to save this server's response to a file (useful for unit testing)
572
            if (!is_null($this->capture_packets_file)) {
0 ignored issues
show
introduced by
The condition is_null($this->capture_packets_file) is always false.
Loading history...
573
                file_put_contents(
574
                    $this->capture_packets_file,
575
                    implode(PHP_EOL . '||' . PHP_EOL, $server->protocol()->packetResponse())
576
                );
577
            }
578
            // @codeCoverageIgnoreEnd
579
580
            // Get the server response
581 1872
            $results = $server->protocol()->processResponse();
582
583
            // Check for online before we do anything else
584 1626
            $results['gq_online'] = (count($results) > 0);
585 258
        } catch (ProtocolException $e) {
586
            // Check to see if we are in debug, if so bubble up the exception
587 258
            if ($this->debug) {
588 144
                throw new \Exception($e->getMessage(), $e->getCode(), $e);
589
            }
590
591
            // We ignore this server
592 95
            $results = [
593 114
                'gq_online' => false,
594 95
            ];
595
        }
596
597
        // Now add some default stuff
598 1734
        $results['gq_address'] = (isset($results['gq_address'])) ? $results['gq_address'] : $server->ip();
599 1734
        $results['gq_port_client'] = $server->portClient();
600 1734
        $results['gq_port_query'] = (isset($results['gq_port_query'])) ? $results['gq_port_query'] : $server->portQuery();
601 1734
        $results['gq_protocol'] = $server->protocol()->getProtocol();
602 1734
        $results['gq_type'] = (string)$server->protocol();
603 1734
        $results['gq_name'] = $server->protocol()->nameLong();
604 1734
        $results['gq_transport'] = $server->protocol()->transport();
605
606
        // Process the join link
607 1734
        if (!isset($results['gq_joinlink']) || empty($results['gq_joinlink'])) {
608 1734
            $results['gq_joinlink'] = $server->getJoinLink();
609
        }
610
611 1734
        return $results;
612
    }
613
614
    /**
615
     * Apply any filters to the results
616
     *
617
     * @param array         $results
618
     * @param \GameQ\Server $server
619
     *
620
     * @return array
621
     */
622 42
    protected function doApplyFilters(array $results, Server $server)
623
    {
624
625
        // Loop over the filters
626 42
        foreach ($this->options['filters'] as $filterOptions) {
627
            // Try to do this filter
628
            try {
629
                // Make a new reflection class
630 42
                $class = new \ReflectionClass(sprintf('GameQ\\Filters\\%s', ucfirst($filterOptions['filter'])));
631
632
                // Create a new instance of the filter class specified
633 36
                $filter = $class->newInstanceArgs([$filterOptions['options']]);
634
635
                // Apply the filter to the data
636 36
                $results = $filter->apply($results, $server);
637 6
            } catch (\ReflectionException $exception) {
638
                // Invalid, skip it
639 6
                continue;
640
            }
641
        }
642
643 42
        return $results;
644
    }
645
}
646