Completed
Pull Request — v3 (#389)
by Soner
03:50
created

GameQ   B

Complexity

Total Complexity 49

Size/Duplication

Total Lines 607
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 97.33%

Importance

Changes 0
Metric Value
wmc 49
lcom 1
cbo 4
dl 0
loc 607
ccs 73
cts 75
cp 0.9733
rs 8.517
c 0
b 0
f 0

17 Methods

Rating   Name   Duplication   Size   Complexity  
A factory() 0 9 1
A __construct() 0 8 2
A __get() 0 5 2
A __set() 0 7 1
A setOption() 0 8 1
A addServer() 0 8 1
A addServers() 0 10 2
C addServersFromFiles() 0 29 7
A clearServers() 0 8 1
A addFilter() 0 15 1
A removeFilter() 0 14 2
A listFilters() 0 4 1
B process() 0 41 2
C doChallenges() 0 83 7
D doQueries() 0 99 9
B doParseResponse() 0 47 6
A doApplyFilters() 0 23 3

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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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
namespace GameQ;
19
20
use GameQ\Exception\Protocol as ProtocolException;
21
use GameQ\Exception\Query as QueryException;
22
23
/**
24
 * Base GameQ Class
25
 *
26
 * This class should be the only one that is included when you use GameQ to query
27
 * any games servers.
28
 *
29
 * Requirements: See wiki or README for more information on the requirements
30
 *  - PHP 5.4.14+
31
 *    * Bzip2 - http://www.php.net/manual/en/book.bzip2.php
32
 *
33
 * @author Austin Bischoff <[email protected]>
34
 *
35
 * @property bool   $debug
36
 * @property string $capture_packets_file
37
 * @property int    $stream_timeout
38
 * @property int    $timeout
39
 * @property int    $write_wait
40
 */
41
class GameQ
42
{
43
    /*
44
     * Constants
45
     */
46
47
    /* Static Section */
48
49
    /**
50
     * Holds the instance of itself
51
     *
52
     * @type self
53
     */
54
    protected static $instance = null;
55
56
    /**
57
     * Create a new instance of this class
58
     *
59
     * @return \GameQ\GameQ
60
     */
61 1
    public static function factory()
62
    {
63
64
        // Create a new instance
65 1
        self::$instance = new self();
66
67
        // Return this new instance
68 1
        return self::$instance;
69
    }
70
71
    /* Dynamic Section */
72
73
    /**
74
     * Default options
75
     *
76
     * @type array
77
     */
78
    protected $options = [
79
        'debug'                => false,
80
        'timeout'              => 3, // Seconds
81
        'filters'              => [
82
            // Default normalize
83
            'normalize_d751713988987e9331980363e24189ce' => [
84
                'filter'  => 'normalize',
85
                'options' => [],
86
            ],
87
        ],
88
        // Advanced settings
89
        'stream_timeout'       => 200000, // See http://www.php.net/manual/en/function.stream-select.php for more info
90
        'write_wait'           => 500,
91
        // How long (in micro-seconds) to pause between writing to server sockets, helps cpu usage
92
93
        // Used for generating protocol test data
94
        'capture_packets_file' => null,
95
    ];
96
97
    /**
98
     * Array of servers being queried
99
     *
100
     * @type array
101
     */
102
    protected $servers = [];
103
104
    /**
105
     * The query library to use.  Default is Native
106
     *
107
     * @type string
108
     */
109
    protected $queryLibrary = 'GameQ\\Query\\Native';
110
111
    /**
112
     * Holds the instance of the queryLibrary
113
     *
114
     * @type \GameQ\Query\Core|null
115
     */
116
    protected $query = null;
117
118
    /**
119
     * GameQ constructor.
120
     *
121
     * Do some checks as needed so this will operate
122
     */
123 186
    public function __construct()
124
    {
125
        // Check for missing utf8_encode function
126 186
        if (!function_exists('utf8_encode')) {
127
            throw new \Exception("PHP's utf8_encode() function is required - "
128
                . "http://php.net/manual/en/function.utf8-encode.php.  Check your php installation.");
129
        }
130 186
    }
131
132
    /**
133
     * Get an option's value
134
     *
135
     * @param mixed $option
136
     *
137
     * @return mixed|null
138
     */
139 179
    public function __get($option)
140
    {
141
142 179
        return isset($this->options[$option]) ? $this->options[$option] : null;
143
    }
144
145
    /**
146
     * Set an option's value
147
     *
148
     * @param mixed $option
149
     * @param mixed $value
150
     *
151
     * @return bool
152
     */
153 181
    public function __set($option, $value)
154
    {
155
156 181
        $this->options[$option] = $value;
157
158 181
        return true;
159
    }
160
161
    /**
162
     * Chainable call to __set, uses set as the actual setter
163
     *
164
     * @param mixed $var
165
     * @param mixed $value
166
     *
167
     * @return $this
168
     */
169 181
    public function setOption($var, $value)
170
    {
171
172
        // Use magic
173 181
        $this->{$var} = $value;
174
175 181
        return $this; // Make chainable
176
    }
177
178
    /**
179
     * Add a single server
180
     *
181
     * @param array $server_info
182
     *
183
     * @return $this
184
     */
185 2
    public function addServer(array $server_info = [])
186
    {
187
188
        // Add and validate the server
189 2
        $this->servers[uniqid()] = new Server($server_info);
190
191 2
        return $this; // Make calls chainable
192
    }
193
194
    /**
195
     * Add multiple servers in a single call
196
     *
197
     * @param array $servers
198
     *
199
     * @return $this
200
     */
201 2
    public function addServers(array $servers = [])
202
    {
203
204
        // Loop through all the servers and add them
205 2
        foreach ($servers as $server_info) {
206 2
            $this->addServer($server_info);
207
        }
208
209 2
        return $this; // Make calls chainable
210
    }
211
212
    /**
213
     * Add a set of servers from a file or an array of files.
214
     * Supported formats:
215
     * JSON
216
     *
217
     * @param array $files
218
     *
219
     * @return $this
220
     * @throws \Exception
221
     */
222 1
    public function addServersFromFiles($files = [])
223
    {
224
225
        // Since we expect an array let us turn a string (i.e. single file) into an array
226 1
        if (!is_array($files)) {
227 1
            $files = [$files];
228
        }
229
230
        // Iterate over the file(s) and add them
231 1
        foreach ($files as $file) {
232
            // Check to make sure the file exists and we can read it
233 1
            if (!file_exists($file) || !is_readable($file)) {
234 1
                continue;
235
            }
236
237
            // See if this file is JSON
238 1
            if (($servers = json_decode(file_get_contents($file), true)) === null
239 1
                && json_last_error() !== JSON_ERROR_NONE
240
            ) {
241
                // Type not supported
242 1
                continue;
243
            }
244
245
            // Add this list of servers
246 1
            $this->addServers($servers);
247
        }
248
249 1
        return $this;
250
    }
251
252
    /**
253
     * Clear all of the defined servers
254
     *
255
     * @return $this
256
     */
257 2
    public function clearServers()
258
    {
259
260
        // Reset all the servers
261 2
        $this->servers = [];
262
263 2
        return $this; // Make Chainable
264
    }
265
266
    /**
267
     * Add a filter to the processing list
268
     *
269
     * @param string $filterName
270
     * @param array  $options
271
     *
272
     * @return $this
273
     */
274 3
    public function addFilter($filterName, $options = [])
275
    {
276
        // Create the filter hash so we can run multiple versions of the same filter
277 3
        $filterHash = sprintf('%s_%s', strtolower($filterName), md5(json_encode($options)));
278
279
        // Add the filter
280 3
        $this->options['filters'][$filterHash] = [
281 3
            'filter'  => strtolower($filterName),
282 3
            'options' => $options,
283
        ];
284
285 3
        unset($filterHash);
286
287 3
        return $this;
288
    }
289
290
    /**
291
     * Remove an added filter
292
     *
293
     * @param string $filterHash
294
     *
295
     * @return $this
296
     */
297 179
    public function removeFilter($filterHash)
298
    {
299
        // Make lower case
300 179
        $filterHash = strtolower($filterHash);
301
302
        // Remove this filter if it has been defined
303 179
        if (array_key_exists($filterHash, $this->options['filters'])) {
304 3
            unset($this->options['filters'][$filterHash]);
305
        }
306
307 179
        unset($filterHash);
308
309 179
        return $this;
310
    }
311
312
    /**
313
     * Return the list of applied filters
314
     *
315
     * @return array
316
     */
317
    public function listFilters()
318
    {
319
        return $this->options['filters'];
320
    }
321
322
    /**
323
     * Main method used to actually process all of the added servers and return the information
324
     *
325
     * @return array
326
     * @throws \Exception
327
     */
328
    public function process()
329
    {
330
331
        // Initialize the query library we are using
332
        $class = new \ReflectionClass($this->queryLibrary);
333
334
        // Set the query pointer to the new instance of the library
335
        $this->query = $class->newInstance();
336
337
        unset($class);
338
339
        // Define the return
340
        $results = [];
341
342
        // @todo: Add break up into loop to split large arrays into smaller chunks
343
344
        // Do server challenge(s) first, if any
345
        $this->doChallenges();
346
347
        // Do packets for server(s) and get query responses
348
        $this->doQueries();
349
350
        // Now we should have some information to process for each server
351
        foreach ($this->servers as $server) {
352
            /* @var $server \GameQ\Server */
353
354
            // Parse the responses for this server
355
            $result = $this->doParseResponse($server);
356
357
            // Apply the filters
358
            $result = array_merge($result, $this->doApplyFilters($result, $server));
359
360
            // Sort the keys so they are alphabetical and nicer to look at
361
            ksort($result);
362
363
            // Add the result to the results array
364
            $results[$server->id()] = $result;
365
        }
366
367
        return $results;
368
    }
369
370
    /**
371
     * Do server challenges, where required
372
     */
373
    protected function doChallenges()
374
    {
375
376
        // Initialize the sockets for reading
377
        $sockets = [];
378
379
        // By default we don't have any challenges to process
380
        $server_challenge = false;
381
382
        // Do challenge packets
383
        foreach ($this->servers as $server_id => $server) {
384
            /* @var $server \GameQ\Server */
385
386
            // This protocol has a challenge packet that needs to be sent
387
            if ($server->protocol()->hasChallenge()) {
388
                // We have a challenge, set the flag
389
                $server_challenge = true;
390
391
                // Let's make a clone of the query class
392
                $socket = clone $this->query;
393
394
                // Set the information for this query socket
395
                $socket->set(
396
                    $server->protocol()->transport(),
397
                    $server->ip,
398
                    $server->port_query,
399
                    $this->timeout
400
                );
401
402
                try {
403
                    // Now write the challenge packet to the socket.
404
                    $socket->write($server->protocol()->getPacket(Protocol::PACKET_CHALLENGE));
405
406
                    // Add the socket information so we can reference it easily
407
                    $sockets[(int)$socket->get()] = [
408
                        'server_id' => $server_id,
409
                        'socket'    => $socket,
410
                    ];
411
                } catch (QueryException $e) {
412
                    // Check to see if we are in debug, if so bubble up the exception
413
                    if ($this->debug) {
414
                        throw new \Exception($e->getMessage(), $e->getCode(), $e);
415
                    }
416
                }
417
418
                unset($socket);
419
420
                // Let's sleep shortly so we are not hammering out calls rapid fire style hogging cpu
421
                usleep($this->write_wait);
422
            }
423
        }
424
425
        // We have at least one server with a challenge, we need to listen for responses
426
        if ($server_challenge) {
427
            // Now we need to listen for and grab challenge response(s)
428
            $responses = call_user_func_array(
429
                [$this->query, 'getResponses'],
430
                [$sockets, $this->timeout, $this->stream_timeout]
431
            );
432
433
            // Iterate over the challenge responses
434
            foreach ($responses as $socket_id => $response) {
435
                // Back out the server_id we need to update the challenge response for
436
                $server_id = $sockets[$socket_id]['server_id'];
437
438
                // Make this into a buffer so it is easier to manipulate
439
                $challenge = new Buffer(implode('', $response));
440
441
                // Grab the server instance
442
                /* @var $server \GameQ\Server */
443
                $server = $this->servers[$server_id];
444
445
                // Apply the challenge
446
                $server->protocol()->challengeParseAndApply($challenge);
447
448
                // Add this socket to be reused, has to be reused in GameSpy3 for example
449
                $server->socketAdd($sockets[$socket_id]['socket']);
450
451
                // Clear
452
                unset($server);
453
            }
454
        }
455
    }
456
457
    /**
458
     * Run the actual queries and get the response(s)
459
     */
460
    protected function doQueries()
461
    {
462
463
        // Initialize the array of sockets
464
        $sockets = [];
465
466
        // Iterate over the server list
467
        foreach ($this->servers as $server_id => $server) {
468
            /* @var $server \GameQ\Server */
469
470
            // Invoke the beforeSend method
471
            $server->protocol()->beforeSend($server);
472
473
            // Get all the non-challenge packets we need to send
474
            $packets = $server->protocol()->getPacket('!' . Protocol::PACKET_CHALLENGE);
475
476
            if (count($packets) == 0) {
477
                // Skip nothing else to do for some reason.
478
                continue;
479
            }
480
481
            // Try to use an existing socket
482
            if (($socket = $server->socketGet()) === null) {
483
                // Let's make a clone of the query class
484
                $socket = clone $this->query;
485
486
                // Set the information for this query socket
487
                $socket->set(
488
                    $server->protocol()->transport(),
489
                    $server->ip,
490
                    $server->port_query,
491
                    $this->timeout
492
                );
493
            }
494
495
            try {
496
                // Iterate over all the packets we need to send
497
                foreach ($packets as $packet_data) {
498
                    // Now write the packet to the socket.
499
                    $socket->write($packet_data);
500
501
                    // Let's sleep shortly so we are not hammering out calls rapid fire style
502
                    usleep($this->write_wait);
503
                }
504
505
                unset($packets);
506
507
                // Add the socket information so we can reference it easily
508
                $sockets[(int)$socket->get()] = [
509
                    'server_id' => $server_id,
510
                    'socket'    => $socket,
511
                ];
512
            } catch (QueryException $e) {
513
                // Check to see if we are in debug, if so bubble up the exception
514
                if ($this->debug) {
515
                    throw new \Exception($e->getMessage(), $e->getCode(), $e);
516
                }
517
518
                break;
519
            }
520
521
            // Clean up the sockets, if any left over
522
            $server->socketCleanse();
523
        }
524
525
        // Now we need to listen for and grab response(s)
526
        $responses = call_user_func_array(
527
            [$this->query, 'getResponses'],
528
            [$sockets, $this->timeout, $this->stream_timeout]
529
        );
530
531
        // Iterate over the responses
532
        foreach ($responses as $socket_id => $response) {
533
            // Back out the server_id
534
            $server_id = $sockets[$socket_id]['server_id'];
535
536
            // Grab the server instance
537
            /* @var $server \GameQ\Server */
538
            $server = $this->servers[$server_id];
539
540
            // Save the response from this packet
541
            $server->protocol()->packetResponse($response);
542
543
            unset($server);
544
        }
545
546
        // Now we need to close all of the sockets
547
        foreach ($sockets as $socketInfo) {
548
            /* @var $socket \GameQ\Query\Core */
549
            $socket = $socketInfo['socket'];
550
551
            // Close the socket
552
            $socket->close();
553
554
            unset($socket);
555
        }
556
557
        unset($sockets);
558
    }
559
560
    /**
561
     * Parse the response for a specific server
562
     *
563
     * @param \GameQ\Server $server
564
     *
565
     * @return array
566
     * @throws \Exception
567
     */
568 178
    protected function doParseResponse(Server $server)
569
    {
570
571
        try {
572
            // @codeCoverageIgnoreStart
573
            // We want to save this server's response to a file (useful for unit testing)
574
            if (!is_null($this->capture_packets_file)) {
575
                file_put_contents(
576
                    $this->capture_packets_file,
577
                    implode(PHP_EOL . '||' . PHP_EOL, $server->protocol()->packetResponse())
578
                );
579
            }
580
            // @codeCoverageIgnoreEnd
581
582
            // Get the server response
583 178
            $results = $server->protocol()->processResponse();
584
585
            // Check for online before we do anything else
586 150
            $results['gq_online'] = (count($results) > 0);
587 28
        } catch (ProtocolException $e) {
588
            // Check to see if we are in debug, if so bubble up the exception
589 28
            if ($this->debug) {
590 17
                throw new \Exception($e->getMessage(), $e->getCode(), $e);
591
            }
592
593
            // We ignore this server
594
            $results = [
595 11
                'gq_online' => false,
596
            ];
597
        }
598
599
        // Now add some default stuff
600 161
        $results['gq_address'] = $server->ip();
601 161
        $results['gq_port_client'] = $server->portClient();
602 161
        $results['gq_port_query'] = $server->portQuery();
603 161
        $results['gq_protocol'] = $server->protocol()->getProtocol();
604 161
        $results['gq_type'] = (string)$server->protocol();
605 161
        $results['gq_name'] = $server->protocol()->nameLong();
606 161
        $results['gq_transport'] = $server->protocol()->transport();
607
608
        // Process the join link
609 161
        if (!isset($results['gq_joinlink']) || empty($results['gq_joinlink'])) {
610 161
            $results['gq_joinlink'] = $server->getJoinLink();
611
        }
612
613 161
        return $results;
614
    }
615
616
    /**
617
     * Apply any filters to the results
618
     *
619
     * @param array         $results
620
     * @param \GameQ\Server $server
621
     *
622
     * @return array
623
     */
624 2
    protected function doApplyFilters(array $results, Server $server)
625
    {
626
627
        // Loop over the filters
628 2
        foreach ($this->options['filters'] as $filterOptions) {
629
            // Try to do this filter
630
            try {
631
                // Make a new reflection class
632 2
                $class = new \ReflectionClass(sprintf('GameQ\\Filters\\%s', ucfirst($filterOptions['filter'])));
633
634
                // Create a new instance of the filter class specified
635 1
                $filter = $class->newInstanceArgs([$filterOptions['options']]);
636
637
                // Apply the filter to the data
638 1
                $results = $filter->apply($results, $server);
639 1
            } catch (\ReflectionException $e) {
640
                // Invalid, skip it
641 2
                continue;
642
            }
643
        }
644
645 2
        return $results;
646
    }
647
}
648