Completed
Push — v3 ( 1321d2...d4735a )
by Austin
04:09
created

GameQ::doQueries()   D

Complexity

Conditions 9
Paths 34

Size

Total Lines 97
Code Lines 39

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 90

Importance

Changes 3
Bugs 2 Features 1
Metric Value
c 3
b 2
f 1
dl 0
loc 97
ccs 0
cts 46
cp 0
rs 4.9219
cc 9
eloc 39
nc 34
nop 0
crap 90

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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