Completed
Push — v3 ( 6185ec...827840 )
by Austin
09:21
created

GameQ   B

Complexity

Total Complexity 48

Size/Duplication

Total Lines 584
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 97.14%

Importance

Changes 12
Bugs 8 Features 4
Metric Value
wmc 48
c 12
b 8
f 4
lcom 1
cbo 4
dl 0
loc 584
ccs 68
cts 70
cp 0.9714
rs 8.4864

16 Methods

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

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