Completed
Push — v3 ( cb3f17...c6ed7d )
by Austin
05:44
created

GameQ   B

Complexity

Total Complexity 48

Size/Duplication

Total Lines 588
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 98.55%

Importance

Changes 12
Bugs 8 Features 5
Metric Value
wmc 48
c 12
b 8
f 5
lcom 1
cbo 4
dl 0
loc 588
ccs 68
cts 69
cp 0.9855
rs 8.4864

16 Methods

Rating   Name   Duplication   Size   Complexity  
A factory() 0 9 1
A __construct() 0 7 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 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

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