Completed
Push — v3 ( b16433...4e7576 )
by Austin
05:06
created

GameQ   B

Complexity

Total Complexity 51

Size/Duplication

Total Lines 608
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 43.78%

Importance

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