Completed
Push — master ( b8e1fc...0b3013 )
by Ryosuke
03:19
created

Co::getDefaultOptions()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 4
rs 10
cc 1
eloc 2
nc 1
nop 0
1
<?php
2
3
namespace mpyw\Co;
4
5
/**
6
 * Asynchronous cURL executor simply based on resource and Generator.
7
 * http://github.com/mpyw/co
8
 *
9
 * @author mpyw
10
 * @license MIT
11
 */
12
13
class Co
14
{
15
16
    /**
17
     * Special constants used for Generator yielding keys.
18
     *
19
     * @const Co::RETURN_WITH  Treat yielded value as returned value.
20
     *                         This is for PHP 5.5 ~ 5.6.
21
     * @const Co::UNSAFE       Allow current yield to throw Exceptions.
22
     * @const Co::SAFE         Forbid current yield to throw Exceptions.
23
     *                         Exceptions are just to be returned.
24
     */
25
    const RETURN_WITH = '__RETURN_WITH__';
26
    const RETURN_ = '__RETURN_WITH__'; // alias
27
    const RET = '__RETURN_WITH__'; // alias
28
    const RTN = '__RETURN_WITH__'; // alias
29
    const UNSAFE = '__UNSAFE__';
30
    const SAFE = '__SAFE__';
31
32
    /**
33
     * Static default options.
34
     */
35
    private static $defaults = array(
36
        'throw' => true, // Throw CURLExceptions?
37
        'pipeline' => false, // Use HTTP/1.1 pipelining?
38
        'multiplex' => true, // Use HTTP/2 multiplexing?
39
        'interval' => 0.5, // curl_multi_select() timeout
40
        'concurrency' => 6, // Limit of TCP connections
41
    );
42
43
    /**
44
     * Execution instance is stored here.
45
     */
46
    private static $self;
47
48
    /**
49
     * Instance properties
50
     *
51
     * *Stack ID* means...
52
     *   - Generator ID
53
     *   - "wait" (Co::wait calls)
54
     *   - "async" (Co::async calls)
55
     */
56
    private $options = array();
57
    private $mh;                          // curl_multi_init()
58
    private $count = 0;                   // count(curl_multi_add_handle called)
59
    private $queue = array();             // cURL resources over concurrency limits are temporalily stored here
60
    private $tree = array();              // array<*Stack ID*, mixed>
61
    private $values = array();            // array<*Stack ID*|*cURL ID*, Generator|resource<cURL>>
62
    private $value_to_parent = array();   // array<*Stack ID*|*cURL ID*, *Stack ID*>
63
    private $value_to_children = array(); // array<*Stack ID*, array<*Stack ID*|*cURL ID*, true>>
64
    private $value_to_keylist = array();  // array<*Stack ID*|*cURL ID*, array<mixed>>
65
66
    /**
67
     * Override or get default settings.
68
     *
69
     * @access public
70
     * @static
71
     * @param array<string, mixed> $options
72
     */
73
    public static function setDefaultOptions(array $options)
74
    {
75
        self::$defaults = self::validateOptions($options);
76
    }
77
    public static function getDefaultOptions()
78
    {
79
        return self::$defaults;
80
    }
81
82
    /**
83
     * Wait all cURL requests to be completed.
84
     * Options override static defaults.
85
     *
86
     *
87
     * @access public
88
     * @static
89
     * @param mixed $value
90
     * @param array<string, mixed> $options
91
     * @see self::__construct()
92
     */
93
    public static function wait($value, array $options = array())
94
    {
95
96
        $options = self::validateOptions($options) + self::$defaults;
97
        // This function call must be atomic.
98
        try {
99
            if (self::$self) {
100
                throw new \BadMethodCallException(
101
                    'Co::wait() is already running. Use Co::async() instead.'
102
                );
103
            }
104
            self::$self = new self($options);
105
            $enqueued = self::$self->initialize($value, 'wait');
106
            if ($enqueued) {
107
                self::$self->run();
108
            }
109
            $result = self::$self->tree['wait'];
110
            self::$self = null;
111
            return $result;
112
        } catch (\Throwable $e) { // for PHP 7+
113
            self::$self = null;
114
            throw $e;
115
        } catch (\Exception $e) { // for PHP 5
116
            self::$self = null;
117
            throw $e;
118
        }
119
    }
120
121
    /**
122
     * Parallel execution along with Co::async().
123
     * This method is mainly expected to be used in CURLOPT_WRITEFUNCTION callback.
124
     *
125
     * @access public
126
     * @static
127
     * @param mixed $value
128
     * @see self::__construct()
129
     */
130
    public static function async($value)
131
    {
132
        // This function must be called along with Co::wait().
133
        if (!self::$self) {
134
            throw new \BadMethodCallException(
135
                'Co::async() must be called along with Co::wait(). ' .
136
                'This method is mainly expected to be used in CURLOPT_WRITEFUNCTION callback.'
137
            );
138
        }
139
        self::$self->initialize($value, 'async');
140
    }
141
142
    /**
143
     * Internal constructor.
144
     *
145
     * @access private
146
     * @param array<string, mixed> $options
147
     * @see self::initialize(), self::run()
148
     */
149
    private function __construct(array $options)
150
    {
151
        $this->mh = curl_multi_init();
152
        if (function_exists('curl_multi_setopt')) {
153
            $flags = ($options['pipeline'] ? 1 : 0) | ($options['multiplex'] ? 2 : 0);
154
            curl_multi_setopt($this->mh, CURLMOPT_PIPELINING, $flags);
155
        }
156
        $this->options = $options;
157
    }
158
159
    /**
160
     * Call curl_multi_add_handle or push into waiting queue.
161
     *
162
     * @access private
163
     * @param resource<cURL> $curl
1 ignored issue
show
Documentation introduced by
The doc-type resource<cURL> could not be parsed: Expected "|" or "end of type", but got "<" at position 8. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
164
     */
165
    private function enqueue($curl)
166
    {
167
        if (!$this->options['concurrency'] || $this->count < $this->options['concurrency']) {
168
            // If within concurrency limit...
169
            if (CURLM_OK !== $errno = curl_multi_add_handle($this->mh, $curl)) {
170
                $msg = curl_multi_strerror($errno) . ": $curl";
171
                if ($errno === 7 || $errno === CURLE_FAILED_INIT) {
172
                    // These errors are caused by users mistake.
173
                    throw new \InvalidArgumentException($msg);
174
                } else {
175
                    // These errors are by internal reason.
176
                    throw new \RuntimeException($msg);
177
                }
178
            }
179
            ++$this->count;
180
        } else {
181
            // Else...
182
            if (isset($this->queue[(string)$curl])) {
183
                throw new \InvalidArgumentException("The cURL resource is already enqueued: $curl");
184
            }
185
            $this->queue[(string)$curl] = $curl;
186
        }
187
    }
188
189
    /**
190
     * Set or overwrite tree of return values.
191
     *
192
     * @access private
193
     * @param mixed $value mixed
194
     * @param string $parent_hash      *Stack ID*
195
     * @param array<string>? $keylist  Queue of keys for its hierarchy.
1 ignored issue
show
Documentation introduced by
The doc-type array<string>? could not be parsed: Expected "|" or "end of type", but got "?" at position 13. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
196
     */
197
    private function setTree($value, $parent_hash, array $keylist = array())
198
    {
199
        $current = &$this->tree[$parent_hash];
200
        while (null !== $key = array_shift($keylist)) {
201
            if (!is_array($current)) {
202
                $current = array();
203
            }
204
            $current = &$current[$key];
205
        }
206
        $current = $value;
207
    }
208
209
    /**
210
     * Unset tree of return values.
211
     *
212
     * @access private
213
     * @param string $hash *Stack ID* or *cURL ID*
214
     */
215
    private function unsetTree($hash)
216
    {
217
        if (isset($this->tree[$hash])) {
218
            foreach (self::flatten($this->tree[$hash]) as $v) {
1 ignored issue
show
Bug introduced by
The expression self::flatten($this->tree[$hash]) of type array|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
219
                if (self::isGenerator($v)) {
220
                    $this->unsetTree(spl_object_hash($v));
221
                }
222
            }
223
            unset($this->tree[$hash]);
224
        }
225
    }
226
227
    /**
228
     * Set table of dependencies.
229
     *
230
     * @access private
231
     * @param Generator|resource<cURL> $value
1 ignored issue
show
Documentation introduced by
The doc-type Generator|resource<cURL> could not be parsed: Expected "|" or "end of type", but got "<" at position 18. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
232
     * @param string $parent_hash              *Stack ID* or *cURL ID*
233
     * @param array? $keylist                  Queue of keys for its hierarchy.
1 ignored issue
show
Documentation introduced by
The doc-type array? could not be parsed: Unknown type name "array?" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
234
     */
235
    private function setTable($value, $parent_hash, array $keylist = array())
236
    {
237
        $hash = is_object($value) ? spl_object_hash($value) : (string)$value;
238
        $this->values[$hash] = $value;
239
        $this->value_to_parent[$hash] = $parent_hash;
240
        $this->value_to_children[$parent_hash][$hash] = true;
241
        $this->value_to_keylist[$hash] = $keylist;
242
        $parent =
0 ignored issues
show
Unused Code introduced by
$parent is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
243
            isset($this->values[$parent_hash]) // Is in Generator stack?
244
            ? $this->values[$parent_hash]
245
            : null
246
        ;
247
    }
248
249
    /**
250
     * Unset table of dependencies.
251
     *
252
     * @access private
253
     * @param string $hash *Stack ID* or *cURL ID*
254
     */
255
    private function unsetTable($hash)
256
    {
257
        $parent_hash = $this->value_to_parent[$hash];
258
        $parent =
0 ignored issues
show
Unused Code introduced by
$parent is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
259
            isset($this->values[$parent_hash])
260
            ? $this->values[$parent_hash]
261
            : null
262
        ;
263
        // Clear self table.
264
        if (isset($this->queues[$hash])) {
0 ignored issues
show
Bug introduced by
The property queues does not seem to exist. Did you mean queue?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
265
            unset($this->queues[$hash]);
266
        }
267
        unset($this->values[$hash]);
268
        unset($this->value_to_parent[$hash]);
269
        unset($this->value_to_keylist[$hash]);
270
        // Clear descendants tables.
271
        // (This is required for cases that
272
        //  some cURL resources are abondoned because of Exceptions thrown)
273
        if (isset($this->value_to_children[$hash])) {
274
            foreach ($this->value_to_children[$hash] as $child => $_) {
275
                $this->unsetTable($child);
276
            }
277
            unset($this->value_to_children[$hash]);
278
        }
279
        // Clear reference from ancestor table.
280
        if (isset($this->value_to_children[$parent_hash][$hash])) {
281
            unset($this->value_to_children[$parent_hash][$hash]);
282
        }
283
    }
284
285
    /**
286
     * Run curl_multi_exec() loop.
287
     *
288
     * @access private
289
     * @see self::updateCurl(), self::enqueue()
290
     */
291
    private function run()
292
    {
293
        curl_multi_exec($this->mh, $active); // Start requests.
294
        do {
295
            curl_multi_select($this->mh, $this->options['interval']); // Wait events.
296
            curl_multi_exec($this->mh, $active); // Update resources.
297
            // NOTE: DO NOT call curl_multi_remove_handle
298
            //       or curl_multi_add_handle while looping curl_multi_info_read!
299
            $entries = array();
300
            do if ($entry = curl_multi_info_read($this->mh, $remains)) {
301
                $entries[] = $entry;
302
            } while ($remains);
1 ignored issue
show
Bug Best Practice introduced by
The expression $remains of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
303
            // Remove done and consume queue.
304
            foreach ($entries as $entry) {
305
                curl_multi_remove_handle($this->mh, $entry['handle']);
306
                --$this->count;
307
                if ($curl = array_shift($this->queue)) {
308
                    $this->enqueue($curl);
309
                }
310
            }
311
            // Update cURL and Generator stacks.
312
            foreach ($entries as $entry) {
313
                $this->updateCurl($entry['handle'], $entry['result']);
314
            }
315
        } while ($this->count > 0 || $this->queue);
1 ignored issue
show
Bug Best Practice introduced by
The expression $this->queue of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
316
        // All request must be done when reached here.
317
        if ($active) {
318
            throw new \LogicException('Unreachable statement.');
319
        }
320
    }
321
322
    /**
323
     * Unset table of dependencies.
324
     *
325
     * @access private
326
     * @param mixed $value
327
     * @param string $parent_hash  *Stack ID* or *cURL ID*
328
     * @param array? $keylist      Queue of keys for its hierarchy.
1 ignored issue
show
Documentation introduced by
The doc-type array? could not be parsed: Unknown type name "array?" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
329
     * @return bool                Enqueued?
330
     */
331
    private function initialize($value, $parent_hash, array $keylist = array())
332
    {
333
        $value = self::normalize($value);
334
        // Array or Traversable
335
        if (self::isArrayLike($value)) {
336
            $this->setTree($value, $parent_hash, $keylist);
337
            $enqueued = false;
338
            foreach ($value as $k => $v) {
339
                // Append current key and call recursively
340
                $tmp_keylist = $keylist;
341
                $tmp_keylist[] = $k;
342
                $enqueued = $this->initialize($v, $parent_hash, $tmp_keylist) || $enqueued;
343
            }
344
            return $enqueued;
345
        }
346
        // Generator
347
        if (self::isGenerator($value)) {
348
            $hash = spl_object_hash($value);
349
            if (isset($this->values[$hash])) {
350
                throw new \InvalidArgumentException("The Genertor is already running: #$hash");
351
            }
352
            $this->setTree($value, $parent_hash, $keylist);
353
            while (self::isGeneratorRunning($value)) {
354
                $current = self::normalize($value->current());
355
                // Call recursively
356
                $enqueued = $this->initialize($current, $hash);
357
                if ($enqueued) { // If cURL resource found?
358
                    $this->setTable($value, $parent_hash, $keylist);
359
                    return true;
360
                }
361
                // Search more...
362
                $value->send($current);
363
            }
364
            $value = self::getGeneratorReturn($value);
365
            // Replace current tree with new value
366
            $this->unsetTree($hash);
367
            return $this->initialize($value, $parent_hash, $keylist);
368
        }
369
        // cURL resource
370
        if (self::isCurl($value)) {
371
            $hash = (string)$value;
0 ignored issues
show
Unused Code introduced by
$hash is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
372
            $this->enqueue($value);
373
            $this->setTree($value, $parent_hash, $keylist);
374
            $this->setTable($value, $parent_hash, $keylist);
375
            return true;
376
        }
377
        // Other
378
        $this->setTree($value, $parent_hash, $keylist);
379
        return false;
380
    }
381
382
    /**
383
     * Update tree with cURL result.
384
     *
385
     * @access private
386
     * @param resource<cURL> $value
1 ignored issue
show
Documentation introduced by
The doc-type resource<cURL> could not be parsed: Expected "|" or "end of type", but got "<" at position 8. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
387
     * @param int $errno
388
     * @see self::updateGenerator()
389
     */
390
    private function updateCurl($value, $errno)
391
    {
392
        $hash = (string)$value;
393
        if (!isset($this->values[$hash])) {
394
            return;
395
        }
396
        $parent_hash = $this->value_to_parent[$hash]; // *Stack ID*
397
        $parent = isset($this->values[$parent_hash]) ? $this->values[$parent_hash] : null; // Generator or null
398
        $keylist = $this->value_to_keylist[$hash];
399
        $result =
400
            $errno === CURLE_OK
401
            ? curl_multi_getcontent($value)
402
            : new CURLException(curl_error($value), $errno, $value)
403
        ;
404
        $this->setTree($result, $parent_hash, $keylist);
405
        $this->unsetTable($hash);
406
        if ($errno !== CURLE_OK && $parent && $this->canThrow($parent)) {// Error and is to be thrown into Generator?
407
            $this->unsetTree($hash); // No more needed
408
            $parent->throw($result);
409
            $this->updateGenerator($parent);
410
        } elseif ($errno !== CURLE_OK && !$parent && $this->options['throw']) { // Error and is to be thrown globally?
411
            $this->unsetTree($hash); // No more needed
412
            throw $result;
413
        } elseif ($parent_hash === 'async') { // Co::async() complete?
1 ignored issue
show
Unused Code Comprehensibility introduced by
45% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
414
            $this->unsetTree($hash); // No more needed
415 View Code Duplication
        } elseif ($parent && !$this->value_to_children[$parent_hash]) { // Generator complete?
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
416
            $this->unsetTree($hash); // No more needed
417
            $result = $this->tree[$parent_hash];
418
            $parent->send($result);
419
            $this->updateGenerator($parent);
420
        }
421
    }
422
423
    /**
424
     * Check current Generator can throw a CURLException.
425
     *
426
     * @access private
427
     * @param Generator $value
428
     * @return bool
429
     */
430
    private function canThrow(\Generator $value)
431
    {
432
        while (true) {
433
            $key = $value->key();
434
            if ($key === self::SAFE) {
435
                return false;
436
            }
437
            if ($key === self::UNSAFE) {
438
                return true;
439
            }
440
            $parent_hash = $this->value_to_parent[spl_object_hash($value)];
441
            if (!isset($this->values[$parent_hash])) {
442
                return $this->options['throw'];
443
            }
444
            $value = $this->values[$parent_hash];
445
        }
446
    }
447
448
    /**
449
     * Update tree with updateCurl() result.
450
     *
451
     * @access private
452
     * @param Generator $value
453
     */
454
    private function updateGenerator(\Generator $value)
455
    {
456
        $hash = spl_object_hash($value);
457
        if (!isset($this->values[$hash])) {
458
            return;
459
        }
460
        while (self::isGeneratorRunning($value)) {
461
            $current = self::normalize($value->current());
462
            $enqueued = $this->initialize($current, $hash);
463
            if ($enqueued) { // cURL resource found?
464
                return;
465
            }
466
            // Search more...
467
            $value->send($current);
468
        }
469
        $value = self::getGeneratorReturn($value);
470
        $parent_hash = $this->value_to_parent[$hash];
471
        $parent = isset($this->values[$parent_hash]) ? $this->values[$parent_hash] : null;
472
        $keylist = $this->value_to_keylist[$hash];
473
        $this->unsetTable($hash);
474
        $this->unsetTree($hash);
475
        $enqueued = $this->initialize($value, $parent_hash, $keylist);
476 View Code Duplication
        if (!$enqueued && $parent && !$this->value_to_children[$parent_hash]) { // Generator complete?
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
477
            // Traverse parent stack.
478
            $next = $this->tree[$parent_hash];
479
            $this->unsetTree($parent_hash);
480
            $parent->send($next);
481
            $this->updateGenerator($parent);
482
        }
483
    }
484
485
    /**
486
     * Validate options.
487
     *
488
     * @access private
489
     * @static
490
     * @param array<string, mixed> $options
491
     * @return array<string, mixed>
1 ignored issue
show
Documentation introduced by
The doc-type array<string, could not be parsed: Expected ">" at position 5, but found "end of type". (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
492
     */
493
    private static function validateOptions($options)
494
    {
495
        foreach ($options as $key => $value) {
496
            if (in_array($key, array('throw', 'pipeline', 'multiplex'), true)) {
497
                $value = filter_var($value, FILTER_VALIDATE_BOOLEAN, array(
498
                    'flags' => FILTER_NULL_ON_FAILURE,
499
                ));
500
                if ($value === null) {
501
                    throw new \InvalidArgumentException("Option[$key] must be boolean.");
502
                }
503
            } elseif ($key === 'interval') {
504
                $value = filter_var($value, FILTER_VALIDATE_FLOAT);
505
                if ($value === false || $value < 0.0) {
506
                    throw new \InvalidArgumentException("Option[interval] must be positive float or zero.");
507
                }
508
            } elseif ($key === 'concurrency') {
509
                $value = filter_var($value, FILTER_VALIDATE_INT);
510
                if ($value === false || $value < 0) {
511
                    throw new \InvalidArgumentException("Option[concurrency] must be positive integer or zero.");
512
                }
513
            } else {
514
                throw new \InvalidArgumentException("Unknown option: $key");
515
            }
516
            $options[$key] = $value;
517
        }
518
        return $options;
519
    }
520
521
    /**
522
     * Normalize value.
523
     *
524
     * @access private
525
     * @static
526
     * @param mixed $value
527
     * @return miexed
528
     */
529
    private static function normalize($value)
530
    {
531
        while ($value instanceof \Closure) {
532
            $value = $value();
533
        }
534
        if (self::isArrayLike($value)
535
            && !is_array($value)
536
            && !$value->valid()) {
537
            $value = array();
538
        }
539
        return $value;
540
    }
541
542
    /**
543
     * Check if a Generator is running.
544
     * This method supports psuedo return with Co::RETURN_WITH.
545
     *
546
     * @access private
547
     * @static
548
     * @param Generator $value
549
     * @return bool
550
     */
551
    private static function isGeneratorRunning(\Generator $value)
552
    {
553
        $value->current();
554
        return $value->valid() && $value->key() !== self::RETURN_WITH; // yield Co::RETURN_WITH => XX
555
    }
556
557
    /**
558
     * Get return value from a Generator.
559
     * This method supports psuedo return with Co::RETURN_WITH.
560
     *
561
     * @access private
562
     * @static
563
     * @param Generator $value
564
     * @return bool
565
     */
566
    private static function getGeneratorReturn(\Generator $value)
567
    {
568
        $value->current();
569
        if ($value->valid() && $value->key() === self::RETURN_WITH) {  // yield Co::RETURN_WITH => XX
570
            return $value->current();
571
        }
572
        if ($value->valid()) {
573
            throw new \LogicException('Unreachable statement.');
574
        }
575
        return method_exists($value, 'getReturn') ? $value->getReturn() : null;
576
    }
577
578
    /**
579
     * Check if value is a valid cURL resource.
580
     *
581
     * @access private
582
     * @static
583
     * @param mixed $value
584
     * @return bool
585
     */
586
    private static function isCurl($value)
587
    {
588
        return is_resource($value) && get_resource_type($value) === 'curl';
589
    }
590
591
    /**
592
     * Check if value is a valid Generator.
593
     *
594
     * @access private
595
     * @static
596
     * @param mixed $value
597
     * @return bool
598
     */
599
    private static function isGenerator($value)
600
    {
601
        return $value instanceof \Generator;
1 ignored issue
show
Bug introduced by
The class Generator does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
602
    }
603
604
    /**
605
     * Check if value is a valid array or Traversable, not a Generator.
606
     *
607
     * @access private
608
     * @static
609
     * @param mixed $value
610
     * @return bool
611
     */
612
    private static function isArrayLike($value)
613
    {
614
        return $value instanceof \Traversable && !$value instanceof \Generator
1 ignored issue
show
Bug introduced by
The class Generator does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
615
               || is_array($value);
616
    }
617
618
    /**
619
     * Flatten an array or a Traversable.
620
     *
621
     * @access private
622
     * @static
623
     * @param mixed $value
624
     * @param array &$carry
625
     * @return array<mixed>
626
     */
627
    private static function flatten($value, &$carry = array())
628
    {
629
        if (!self::isArrayLike($value)) {
630
            $carry[] = $value;
631
        } else {
632
            foreach ($value as $v) {
633
                self::flatten($v, $carry);
634
            }
635
        }
636
        return func_num_args() <= 1 ? $carry : null;
637
    }
638
639
}
640