Completed
Push — master ( bbd993...4e81f5 )
by Ryosuke
02:58
created

Co   D

Complexity

Total Complexity 110

Size/Duplication

Total Lines 616
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 2

Test Coverage

Coverage 28.24%

Importance

Changes 16
Bugs 6 Features 2
Metric Value
wmc 110
c 16
b 6
f 2
lcom 1
cbo 2
dl 0
loc 616
ccs 76
cts 269
cp 0.2824
rs 4.8078

23 Methods

Rating   Name   Duplication   Size   Complexity  
A setDefaultOptions() 0 4 1
A getDefaultOptions() 0 4 1
B wait() 0 27 5
A async() 0 11 2
A __construct() 0 9 4
C enqueue() 0 23 7
A setTree() 0 11 3
A unsetTree() 0 11 4
A setTable() 0 8 2
B unsetTable() 0 24 5
D run() 0 30 9
C initialize() 0 49 9
C updateCurl() 0 32 13
B canThrow() 0 17 5
C updateGenerator() 0 30 8
D validateOptions() 0 27 10
B normalize() 0 12 5
A isGeneratorRunning() 0 5 2
B getGeneratorReturn() 0 11 5
A isCurl() 0 4 2
A isGenerator() 0 4 1
A isArrayLike() 0 5 3
A flatten() 0 11 4

How to fix   Complexity   

Complex Class

Complex classes like Co 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 Co, and based on these observations, apply Extract Interface, too.

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 2
    private function __construct(array $options)
150 2
    {
151 2
        $this->mh = curl_multi_init();
152 2
        if (function_exists('curl_multi_setopt')) {
153 2
            $flags = ($options['pipeline'] ? 1 : 0) | ($options['multiplex'] ? 2 : 0);
154 2
            curl_multi_setopt($this->mh, CURLMOPT_PIPELINING, $flags);
155
        }
156 2
        $this->options = $options;
157 2
    }
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 1
    private function enqueue($curl)
166 1
    {
167 1
        if (!$this->options['concurrency'] || $this->count < $this->options['concurrency']) {
168
            // If within concurrency limit...
169 1
            if (CURLM_OK !== $errno = curl_multi_add_handle($this->mh, $curl)) {
170 1
                $msg = curl_multi_strerror($errno) . ": $curl";
171 1
                if ($errno === 7 || $errno === CURLE_FAILED_INIT) {
172
                    // These errors are caused by users mistake.
173 1
                    throw new \InvalidArgumentException($msg);
174
                } else {
175
                    // These errors are by internal reason.
176
                    throw new \RuntimeException($msg);
177
                }
178
            }
179 1
            ++$this->count;
180
        } else {
181
            // Else...
182 1
            if (isset($this->queue[(string)$curl])) {
183 1
                throw new \InvalidArgumentException("The cURL resource is already enqueued: $curl");
184
            }
185 1
            $this->queue[(string)$curl] = $curl;
186
        }
187 1
    }
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) {
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
    }
243
244
    /**
245
     * Unset table of dependencies.
246
     *
247
     * @access private
248
     * @param string $hash *Stack ID* or *cURL ID*
249
     */
250
    private function unsetTable($hash)
251
    {
252
        $parent_hash = $this->value_to_parent[$hash];
253
        // Clear self table.
254
        if (isset($this->queue[$hash])) {
255
            unset($this->queue[$hash]);
256
        }
257
        unset($this->values[$hash]);
258
        unset($this->value_to_parent[$hash]);
259
        unset($this->value_to_keylist[$hash]);
260
        // Clear descendants tables.
261
        // (This is required for cases that
262
        //  some cURL resources are abondoned because of Exceptions thrown)
263
        if (isset($this->value_to_children[$hash])) {
264
            foreach ($this->value_to_children[$hash] as $child => $_) {
265
                $this->unsetTable($child);
266
            }
267
            unset($this->value_to_children[$hash]);
268
        }
269
        // Clear reference from ancestor table.
270
        if (isset($this->value_to_children[$parent_hash][$hash])) {
271
            unset($this->value_to_children[$parent_hash][$hash]);
272
        }
273
    }
274
275
    /**
276
     * Run curl_multi_exec() loop.
277
     *
278
     * @access private
279
     * @see self::updateCurl(), self::enqueue()
280
     */
281
    private function run()
282
    {
283
        curl_multi_exec($this->mh, $active); // Start requests.
284
        do {
285
            curl_multi_select($this->mh, $this->options['interval']); // Wait events.
286
            curl_multi_exec($this->mh, $active); // Update resources.
287
            // NOTE: DO NOT call curl_multi_remove_handle
288
            //       or curl_multi_add_handle while looping curl_multi_info_read!
289
            $entries = array();
290
            do if ($entry = curl_multi_info_read($this->mh, $remains)) {
291
                $entries[] = $entry;
292
            } while ($remains);
293
            // Remove done and consume queue.
294
            foreach ($entries as $entry) {
295
                curl_multi_remove_handle($this->mh, $entry['handle']);
296
                --$this->count;
297
                if ($curl = array_shift($this->queue)) {
298
                    $this->enqueue($curl);
299
                }
300
            }
301
            // Update cURL and Generator stacks.
302
            foreach ($entries as $entry) {
303
                $this->updateCurl($entry['handle'], $entry['result']);
304
            }
305
        } while ($this->count > 0 || $this->queue);
306
        // All request must be done when reached here.
307
        if ($active) {
308
            throw new \LogicException('Unreachable statement.');
309
        }
310
    }
311
312
    /**
313
     * Unset table of dependencies.
314
     *
315
     * @access private
316
     * @param mixed $value
317
     * @param string $parent_hash  *Stack ID* or *cURL ID*
318
     * @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...
319
     * @return bool                Enqueued?
320
     */
321
    private function initialize($value, $parent_hash, array $keylist = array())
322
    {
323
        $value = self::normalize($value);
324
        // Array or Traversable
325
        if (self::isArrayLike($value)) {
326
            $this->setTree($value, $parent_hash, $keylist);
327
            $enqueued = false;
328
            foreach ($value as $k => $v) {
329
                // Append current key and call recursively
330
                $tmp_keylist = $keylist;
331
                $tmp_keylist[] = $k;
332
                $enqueued = $this->initialize($v, $parent_hash, $tmp_keylist) || $enqueued;
333
            }
334
            return $enqueued;
335
        }
336
        // Generator
337
        if (self::isGenerator($value)) {
338
            $hash = spl_object_hash($value);
339
            if (isset($this->values[$hash])) {
340
                throw new \InvalidArgumentException("The Genertor is already running: #$hash");
341
            }
342
            $this->setTree($value, $parent_hash, $keylist);
343
            while (self::isGeneratorRunning($value)) {
344
                $current = self::normalize($value->current());
345
                // Call recursively
346
                $enqueued = $this->initialize($current, $hash);
347
                if ($enqueued) { // If cURL resource found?
348
                    $this->setTable($value, $parent_hash, $keylist);
349
                    return true;
350
                }
351
                // Search more...
352
                $value->send($current);
353
            }
354
            $value = self::getGeneratorReturn($value);
355
            // Replace current tree with new value
356
            $this->unsetTree($hash);
357
            return $this->initialize($value, $parent_hash, $keylist);
358
        }
359
        // cURL resource
360
        if (self::isCurl($value)) {
361
            $this->enqueue($value);
362
            $this->setTree($value, $parent_hash, $keylist);
363
            $this->setTable($value, $parent_hash, $keylist);
364
            return true;
365
        }
366
        // Other
367
        $this->setTree($value, $parent_hash, $keylist);
368
        return false;
369
    }
370
371
    /**
372
     * Update tree with cURL result.
373
     *
374
     * @access private
375
     * @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...
376
     * @param int $errno
377
     * @see self::updateGenerator()
378
     */
379
    private function updateCurl($value, $errno)
380
    {
381
        $hash = (string)$value;
382
        if (!isset($this->values[$hash])) {
383
            return;
384
        }
385
        $parent_hash = $this->value_to_parent[$hash]; // *Stack ID*
386
        $parent = isset($this->values[$parent_hash]) ? $this->values[$parent_hash] : null; // Generator or null
387
        $keylist = $this->value_to_keylist[$hash];
388
        $result =
389
            $errno === CURLE_OK
390
            ? curl_multi_getcontent($value)
391
            : new CURLException(curl_error($value), $errno, $value)
392
        ;
393
        $this->setTree($result, $parent_hash, $keylist);
394
        $this->unsetTable($hash);
395
        if ($errno !== CURLE_OK && $parent && $this->canThrow($parent)) {// Error and is to be thrown into Generator?
396
            $this->unsetTree($hash); // No more needed
397
            $parent->throw($result);
398
            $this->updateGenerator($parent);
399
        } elseif ($errno !== CURLE_OK && !$parent && $this->options['throw']) { // Error and is to be thrown globally?
400
            $this->unsetTree($hash); // No more needed
401
            throw $result;
402
        } elseif ($parent_hash === 'async') { // Co::async() complete?
403
            $this->unsetTree($hash); // No more needed
404
        } 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...
405
            $this->unsetTree($hash); // No more needed
406
            $result = $this->tree[$parent_hash];
407
            $parent->send($result);
408
            $this->updateGenerator($parent);
409
        }
410
    }
411
412
    /**
413
     * Check current Generator can throw a CURLException.
414
     *
415
     * @access private
416
     * @param Generator $value
417
     * @return bool
418
     */
419
    private function canThrow(\Generator $value)
420
    {
421
        while (true) {
422
            $key = $value->key();
423
            if ($key === self::SAFE) {
424
                return false;
425
            }
426
            if ($key === self::UNSAFE) {
427
                return true;
428
            }
429
            $parent_hash = $this->value_to_parent[spl_object_hash($value)];
430
            if (!isset($this->values[$parent_hash])) {
431
                return $this->options['throw'];
432
            }
433
            $value = $this->values[$parent_hash];
434
        }
435
    }
436
437
    /**
438
     * Update tree with updateCurl() result.
439
     *
440
     * @access private
441
     * @param Generator $value
442
     */
443
    private function updateGenerator(\Generator $value)
444
    {
445
        $hash = spl_object_hash($value);
446
        if (!isset($this->values[$hash])) {
447
            return;
448
        }
449
        while (self::isGeneratorRunning($value)) {
450
            $current = self::normalize($value->current());
451
            $enqueued = $this->initialize($current, $hash);
452
            if ($enqueued) { // cURL resource found?
453
                return;
454
            }
455
            // Search more...
456
            $value->send($current);
457
        }
458
        $value = self::getGeneratorReturn($value);
459
        $parent_hash = $this->value_to_parent[$hash];
460
        $parent = isset($this->values[$parent_hash]) ? $this->values[$parent_hash] : null;
461
        $keylist = $this->value_to_keylist[$hash];
462
        $this->unsetTable($hash);
463
        $this->unsetTree($hash);
464
        $enqueued = $this->initialize($value, $parent_hash, $keylist);
465
        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...
466
            // Traverse parent stack.
467
            $next = $this->tree[$parent_hash];
468
            $this->unsetTree($parent_hash);
469
            $parent->send($next);
470
            $this->updateGenerator($parent);
471
        }
472
    }
473
474
    /**
475
     * Validate options.
476
     *
477
     * @access private
478
     * @static
479
     * @param array<string, mixed> $options
480
     * @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...
481
     */
482 1
    private static function validateOptions(array $options)
483 1
    {
484 1
        foreach ($options as $key => $value) {
485 1
            if (in_array($key, array('throw', 'pipeline', 'multiplex'), true)) {
486 1
                $value = filter_var($value, FILTER_VALIDATE_BOOLEAN, array(
487 1
                    'flags' => FILTER_NULL_ON_FAILURE,
488
                ));
489 1
                if ($value === null) {
490 1
                    throw new \InvalidArgumentException("Option[$key] must be boolean.");
491
                }
492 1
            } elseif ($key === 'interval') {
493 1
                $value = filter_var($value, FILTER_VALIDATE_FLOAT);
494 1
                if ($value === false || $value < 0.0) {
495 1
                    throw new \InvalidArgumentException("Option[interval] must be positive float or zero.");
496
                }
497 1
            } elseif ($key === 'concurrency') {
498 1
                $value = filter_var($value, FILTER_VALIDATE_INT);
499 1
                if ($value === false || $value < 0) {
500 1
                    throw new \InvalidArgumentException("Option[concurrency] must be positive integer or zero.");
501
                }
502
            } else {
503
                throw new \InvalidArgumentException("Unknown option: $key");
504
            }
505 1
            $options[$key] = $value;
506
        }
507 1
        return $options;
508
    }
509
510
    /**
511
     * Normalize value.
512
     *
513
     * @access private
514
     * @static
515
     * @param mixed $value
516
     * @return miexed
517
     */
518 1
    private static function normalize($value)
519 1
    {
520 1
        while ($value instanceof \Closure) {
521 1
            $value = $value();
522
        }
523 1
        if (self::isArrayLike($value)
524 1
            && !is_array($value)
525 1
            && !$value->valid()) {
526 1
            $value = array();
527
        }
528 1
        return $value;
529
    }
530
531
    /**
532
     * Check if a Generator is running.
533
     * This method supports psuedo return with Co::RETURN_WITH.
534
     *
535
     * @access private
536
     * @static
537
     * @param Generator $value
538
     * @return bool
539
     */
540 1
    private static function isGeneratorRunning(\Generator $value)
541 1
    {
542 1
        $value->current();
543 1
        return $value->valid() && $value->key() !== self::RETURN_WITH; // yield Co::RETURN_WITH => XX
544
    }
545
546
    /**
547
     * Get return value from a Generator.
548
     * This method supports psuedo return with Co::RETURN_WITH.
549
     *
550
     * @access private
551
     * @static
552
     * @param Generator $value
553
     * @return bool
554
     */
555 1
    private static function getGeneratorReturn(\Generator $value)
556 1
    {
557 1
        $value->current();
558 1
        if ($value->valid() && $value->key() === self::RETURN_WITH) {  // yield Co::RETURN_WITH => XX
559 1
            return $value->current();
560
        }
561 1
        if ($value->valid()) {
562 1
            throw new \LogicException('Unreachable statement.');
563
        }
564 1
        return method_exists($value, 'getReturn') ? $value->getReturn() : null;
565
    }
566
567
    /**
568
     * Check if value is a valid cURL resource.
569
     *
570
     * @access private
571
     * @static
572
     * @param mixed $value
573
     * @return bool
574
     */
575 1
    private static function isCurl($value)
576 1
    {
577 1
        return is_resource($value) && get_resource_type($value) === 'curl';
578
    }
579
580
    /**
581
     * Check if value is a valid Generator.
582
     *
583
     * @access private
584
     * @static
585
     * @param mixed $value
586
     * @return bool
587
     */
588 1
    private static function isGenerator($value)
589 1
    {
590 1
        return $value instanceof \Generator;
591
    }
592
593
    /**
594
     * Check if value is a valid array or Traversable, not a Generator.
595
     *
596
     * @access private
597
     * @static
598
     * @param mixed $value
599
     * @return bool
600
     */
601 3
    private static function isArrayLike($value)
602 3
    {
603 3
        return $value instanceof \Traversable && !$value instanceof \Generator
604 3
               || is_array($value);
605
    }
606
607
    /**
608
     * Flatten an array or a Traversable.
609
     *
610
     * @access private
611
     * @static
612
     * @param mixed $value
613
     * @param array &$carry
614
     * @return array<mixed>
615
     */
616 1
    private static function flatten($value, array &$carry = array())
617 1
    {
618 1
        if (!self::isArrayLike($value)) {
619 1
            $carry[] = $value;
620
        } else {
621 1
            foreach ($value as $v) {
622 1
                self::flatten($v, $carry);
623
            }
624
        }
625 1
        return func_num_args() <= 1 ? $carry : null;
626
    }
627
628
}
629