Completed
Push — v3 ( 03b878...ef2d7e )
by Austin
07:38
created

GameQ   B

Complexity

Total Complexity 48

Size/Duplication

Total Lines 589
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 97.14%

Importance

Changes 13
Bugs 9 Features 5
Metric Value
wmc 48
c 13
b 9
f 5
lcom 1
cbo 4
dl 0
loc 589
ccs 68
cts 70
cp 0.9714
rs 8.4864

16 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 clearServers() 0 8 1
A addFilter() 0 8 1
A addServers() 0 10 2
C addServersFromFiles() 0 29 7
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

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