Passed
Pull Request — v3 (#593)
by
unknown
07:16
created

GameQ   B

Complexity

Total Complexity 51

Size/Duplication

Total Lines 606
Duplicated Lines 0 %

Test Coverage

Coverage 76.75%

Importance

Changes 9
Bugs 0 Features 1
Metric Value
wmc 51
eloc 168
c 9
b 0
f 1
dl 0
loc 606
ccs 142
cts 185
cp 0.7675
rs 7.92

17 Methods

Rating   Name   Duplication   Size   Complexity  
A clearServers() 0 7 1
A __construct() 0 6 2
A addFilter() 0 14 1
A __get() 0 4 2
A __set() 0 6 1
A factory() 0 8 1
A setOption() 0 7 1
A addServer() 0 7 1
A listFilters() 0 3 1
A removeFilter() 0 13 2
A doApplyFilters() 0 22 3
B doParseResponse() 0 46 8
B addServersFromFiles() 0 28 7
A addServers() 0 9 2
B doQueries() 0 98 9
B doChallenges() 0 80 7
A process() 0 40 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
     * @type self
55
     */
56
    protected static $instance = null;
57
58
    /**
59
     * Create a new instance of this class
60
     *
61
     * @return \GameQ\GameQ
62
     */
63 1
    public static function factory()
64
    {
65
66
        // Create a new instance
67 1
        self::$instance = new self();
68
69
        // Return this new instance
70 1
        return self::$instance;
71
    }
72
73
    /* Dynamic Section */
74
75
    /**
76
     * Default options
77
     *
78
     * @type 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
     * @type array
103
     */
104
    protected $servers = [];
105
106
    /**
107
     * The query library to use.  Default is Native
108
     *
109
     * @type string
110
     */
111
    protected $queryLibrary = 'GameQ\\Query\\Native';
112
113
    /**
114
     * Holds the instance of the queryLibrary
115
     *
116
     * @type \GameQ\Query\Core|null
117
     */
118
    protected $query = null;
119
120
    /**
121
     * GameQ constructor.
122
     *
123
     * Do some checks as needed so this will operate
124
     */
125 267
    public function __construct()
126
    {
127
        // Check for missing utf8_encode function
128 267
        if (!function_exists('utf8_encode')) {
129
            throw new \Exception("PHP's utf8_encode() function is required - "
130
                . "http://php.net/manual/en/function.utf8-encode.php.  Check your php installation.");
131
        }
132 267
    }
133
134
    /**
135
     * Get an option's value
136
     *
137
     * @param mixed $option
138
     *
139
     * @return mixed|null
140
     */
141 259
    public function __get($option)
142
    {
143
144 259
        return isset($this->options[$option]) ? $this->options[$option] : null;
145
    }
146
147
    /**
148
     * Set an option's value
149
     *
150
     * @param mixed $option
151
     * @param mixed $value
152
     *
153
     * @return bool
154
     */
155 260
    public function __set($option, $value)
156
    {
157
158 260
        $this->options[$option] = $value;
159
160 260
        return true;
161
    }
162
163
    /**
164
     * Chainable call to __set, uses set as the actual setter
165
     *
166
     * @param mixed $var
167
     * @param mixed $value
168
     *
169
     * @return $this
170
     */
171 260
    public function setOption($var, $value)
172
    {
173
174
        // Use magic
175 260
        $this->{$var} = $value;
176
177 260
        return $this; // Make chainable
178
    }
179
180
    /**
181
     * Add a single server
182
     *
183
     * @param array $server_info
184
     *
185
     * @return $this
186
     */
187 3
    public function addServer(array $server_info = [])
188
    {
189
190
        // Add and validate the server
191 3
        $this->servers[uniqid()] = new Server($server_info);
192
193 3
        return $this; // Make calls chainable
194
    }
195
196
    /**
197
     * Add multiple servers in a single call
198
     *
199
     * @param array $servers
200
     *
201
     * @return $this
202
     */
203 2
    public function addServers(array $servers = [])
204
    {
205
206
        // Loop through all the servers and add them
207 2
        foreach ($servers as $server_info) {
208 2
            $this->addServer($server_info);
209 2
        }
210
211 2
        return $this; // Make calls chainable
212
    }
213
214
    /**
215
     * Add a set of servers from a file or an array of files.
216
     * Supported formats:
217
     * JSON
218
     *
219
     * @param array $files
220
     *
221
     * @return $this
222
     * @throws \Exception
223
     */
224 1
    public function addServersFromFiles($files = [])
225
    {
226
227
        // Since we expect an array let us turn a string (i.e. single file) into an array
228 1
        if (!is_array($files)) {
0 ignored issues
show
introduced by
The condition is_array($files) is always true.
Loading history...
229 1
            $files = [$files];
230 1
        }
231
232
        // Iterate over the file(s) and add them
233 1
        foreach ($files as $file) {
234
            // Check to make sure the file exists and we can read it
235 1
            if (!file_exists($file) || !is_readable($file)) {
236 1
                continue;
237
            }
238
239
            // See if this file is JSON
240 1
            if (($servers = json_decode(file_get_contents($file), true)) === null
241 1
                && json_last_error() !== JSON_ERROR_NONE
242 1
            ) {
243
                // Type not supported
244 1
                continue;
245
            }
246
247
            // Add this list of servers
248 1
            $this->addServers($servers);
249 1
        }
250
251 1
        return $this;
252
    }
253
254
    /**
255
     * Clear all of the defined servers
256
     *
257
     * @return $this
258
     */
259 2
    public function clearServers()
260
    {
261
262
        // Reset all the servers
263 2
        $this->servers = [];
264
265 2
        return $this; // Make Chainable
266
    }
267
268
    /**
269
     * Add a filter to the processing list
270
     *
271
     * @param string $filterName
272
     * @param array  $options
273
     *
274
     * @return $this
275
     */
276 4
    public function addFilter($filterName, $options = [])
277
    {
278
        // Create the filter hash so we can run multiple versions of the same filter
279 4
        $filterHash = sprintf('%s_%s', strtolower($filterName), md5(json_encode($options)));
280
281
        // Add the filter
282 4
        $this->options['filters'][$filterHash] = [
283 4
            'filter'  => strtolower($filterName),
284 4
            'options' => $options,
285
        ];
286
287 4
        unset($filterHash);
288
289 4
        return $this;
290
    }
291
292
    /**
293
     * Remove an added filter
294
     *
295
     * @param string $filterHash
296
     *
297
     * @return $this
298
     */
299 260
    public function removeFilter($filterHash)
300
    {
301
        // Make lower case
302 260
        $filterHash = strtolower($filterHash);
303
304
        // Remove this filter if it has been defined
305 260
        if (array_key_exists($filterHash, $this->options['filters'])) {
306 3
            unset($this->options['filters'][$filterHash]);
307 3
        }
308
309 260
        unset($filterHash);
310
311 260
        return $this;
312
    }
313
314
    /**
315
     * Return the list of applied filters
316
     *
317
     * @return array
318
     */
319
    public function listFilters()
320
    {
321
        return $this->options['filters'];
322
    }
323
324
    /**
325
     * Main method used to actually process all of the added servers and return the information
326
     *
327
     * @return array
328
     * @throws \Exception
329
     */
330 1
    public function process()
331
    {
332
333
        // Initialize the query library we are using
334 1
        $class = new \ReflectionClass($this->queryLibrary);
335
336
        // Set the query pointer to the new instance of the library
337 1
        $this->query = $class->newInstance();
338
339 1
        unset($class);
340
341
        // Define the return
342 1
        $results = [];
343
344
        // @todo: Add break up into loop to split large arrays into smaller chunks
345
346
        // Do server challenge(s) first, if any
347 1
        $this->doChallenges();
348
349
        // Do packets for server(s) and get query responses
350 1
        $this->doQueries();
351
352
        // Now we should have some information to process for each server
353 1
        foreach ($this->servers as $server) {
354
            /* @var $server \GameQ\Server */
355
356
            // Parse the responses for this server
357 1
            $result = $this->doParseResponse($server);
358
359
            // Apply the filters
360 1
            $result = array_merge($result, $this->doApplyFilters($result, $server));
361
362
            // Sort the keys so they are alphabetical and nicer to look at
363 1
            ksort($result);
364
365
            // Add the result to the results array
366 1
            $results[$server->id()] = $result;
367 1
        }
368
369 1
        return $results;
370
    }
371
372
    /**
373
     * Do server challenges, where required
374
     */
375 1
    protected function doChallenges()
376
    {
377
378
        // Initialize the sockets for reading
379 1
        $sockets = [];
380
381
        // By default we don't have any challenges to process
382 1
        $server_challenge = false;
383
384
        // Do challenge packets
385 1
        foreach ($this->servers as $server_id => $server) {
386
            /* @var $server \GameQ\Server */
387
388
            // This protocol has a challenge packet that needs to be sent
389 1
            if ($server->protocol()->hasChallenge()) {
390
                // We have a challenge, set the flag
391
                $server_challenge = true;
392
393
                // Let's make a clone of the query class
394
                $socket = clone $this->query;
395
396
                // Set the information for this query socket
397
                $socket->set(
398
                    $server->protocol()->transport(),
399
                    $server->ip,
400
                    $server->port_query,
401
                    $this->timeout
402
                );
403
404
                try {
405
                    // Now write the challenge packet to the socket.
406
                    $socket->write($server->protocol()->getPacket(Protocol::PACKET_CHALLENGE));
0 ignored issues
show
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

406
                    $socket->write(/** @scrutinizer ignore-type */ $server->protocol()->getPacket(Protocol::PACKET_CHALLENGE));
Loading history...
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

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

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