{"id":15284444,"url":"https://github.com/mpyw/co","last_synced_at":"2025-10-07T00:32:01.183Z","repository":{"id":49180341,"uuid":"50116963","full_name":"mpyw/co","owner":"mpyw","description":"Asynchronous cURL executor simply based on resource and Generator.","archived":true,"fork":false,"pushed_at":"2019-12-10T22:54:29.000Z","size":329,"stargazers_count":137,"open_issues_count":0,"forks_count":18,"subscribers_count":13,"default_branch":"master","last_synced_at":"2025-09-21T10:15:20.129Z","etag":null,"topics":["coroutine","curl","php"],"latest_commit_sha":null,"homepage":"","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mpyw.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2016-01-21T15:35:00.000Z","updated_at":"2025-05-25T05:18:54.000Z","dependencies_parsed_at":"2022-09-21T11:11:41.418Z","dependency_job_id":null,"html_url":"https://github.com/mpyw/co","commit_stats":null,"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/mpyw/co","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mpyw%2Fco","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mpyw%2Fco/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mpyw%2Fco/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mpyw%2Fco/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mpyw","download_url":"https://codeload.github.com/mpyw/co/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mpyw%2Fco/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":278703582,"owners_count":26031204,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-06T02:00:05.630Z","response_time":65,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["coroutine","curl","php"],"created_at":"2024-09-30T14:56:39.985Z","updated_at":"2025-10-07T00:32:00.851Z","avatar_url":"https://github.com/mpyw.png","language":"PHP","readme":"# Co [![Build Status](https://travis-ci.com/mpyw/co.svg?branch=master)](https://travis-ci.com/mpyw/co) [![Coverage Status](https://coveralls.io/repos/github/mpyw/co/badge.svg?branch=master)](https://coveralls.io/github/mpyw/co?branch=master) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/mpyw/co/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/mpyw/co/?branch=master)\n\nAsynchronous cURL executor simply based on resource and Generator\n\n| PHP | :question: | Feature Restriction |\n|:---:|:---:|:---:|\n| 7.0~ | :smile: | Full Support |\n| 5.5~5.6 | :anguished: | Generator is not so cool |\n| ~5.4 | :boom: | Incompatible |\n\n```php\nfunction curl_init_with(string $url, array $options = [])\n{\n    $ch = curl_init();\n    $options = array_replace([\n        CURLOPT_URL =\u003e $url,\n        CURLOPT_RETURNTRANSFER =\u003e true,\n    ], $options);\n    curl_setopt_array($ch, $options);\n    return $ch;\n}\nfunction get_xpath_async(string $url) : \\Generator\n{\n    $dom = new \\DOMDocument;\n    @$dom-\u003eloadHTML(yield curl_init_with($url));\n    return new \\DOMXPath($dom);\n}\n\nvar_dump(Co::wait([\n\n    'Delay 5 secs' =\u003e function () {\n        echo \"[Delay] I start to have a pseudo-sleep in this coroutine for about 5 secs\\n\";\n        for ($i = 0; $i \u003c 5; ++$i) {\n            yield Co::DELAY =\u003e 1;\n            if ($i \u003c 4) {\n                printf(\"[Delay] %s\\n\", str_repeat('.', $i + 1));\n            }\n        }\n        echo \"[Delay] Done!\\n\";\n    },\n\n    \"google.com HTML\" =\u003e curl_init_with(\"https://google.com\"),\n\n    \"Content-Length of github.com\" =\u003e function () {\n        echo \"[GitHub] I start to request for github.com to calculate Content-Length\\n\";\n        $content = yield curl_init_with(\"https://github.com\");\n        echo \"[GitHub] Done! Now I calculate length of contents\\n\";\n        return strlen($content);\n    },\n\n    \"Save mpyw's Gravatar Image URL to local\" =\u003e function () {\n        echo \"[Gravatar] I start to request for github.com to get Gravatar URL\\n\";\n        $src = (yield get_xpath_async('https://github.com/mpyw'))\n                 -\u003eevaluate('string(//img[contains(@class,\"avatar\")]/@src)');\n        echo \"[Gravatar] Done! Now I download its data\\n\";\n        yield curl_init_with($src, [CURLOPT_FILE =\u003e fopen('/tmp/mpyw.png', 'wb')]);\n        echo \"[Gravatar] Done! Saved as /tmp/mpyw.png\\n\";\n    }\n\n]));\n```\n\nThe requests are executed as parallelly as possible :smile:  \nNote that there is only **1 process** and **1 thread**.\n\n```Text\n[Delay] I start to have a pseudo-sleep in this coroutine for about 5 secs\n[GitHub] I start to request for github.com to calculate Content-Length\n[Gravatar] I start to request for github.com to get Gravatar URL\n[Delay] .\n[Delay] ..\n[GitHub] Done! Now I calculate length of contents\n[Gravatar] Done! Now I download its data\n[Delay] ...\n[Gravatar] Done! Saved as /tmp/mpyw.png\n[Delay] ....\n[Delay] Done!\narray(4) {\n  [\"Delay 5 secs\"]=\u003e\n  NULL\n  [\"google.com HTML\"]=\u003e\n  string(262) \"\u003cHTML\u003e\u003cHEAD\u003e\u003cmeta http-equiv=\"content-type\" content=\"text/html;charset=utf-8\"\u003e\n\u003cTITLE\u003e302 Moved\u003c/TITLE\u003e\u003c/HEAD\u003e\u003cBODY\u003e\n\u003cH1\u003e302 Moved\u003c/H1\u003e\nThe document has moved\n\u003cA HREF=\"https://www.google.co.jp/?gfe_rd=cr\u0026amp;ei=XXXXXX\"\u003ehere\u003c/A\u003e.\n\u003c/BODY\u003e\u003c/HTML\u003e\n\"\n  [\"Content-Length of github.com\"]=\u003e\n  int(25534)\n  [\"Save mpyw's Gravatar Image URL to local\"]=\u003e\n  NULL\n}\n```\n\n## Table of Contents\n\n- [Installing](#installing)\n- [API](#api)\n  - **[Co::wait()](#cowait)**\n  - **[Co::async()](#coasync)**\n  - [Co::isRunning()](#coisrunning)\n  - [Co::any() / Co::race() / Co::all()](#coanycoracecoall)\n  - [Co::setDefaultOptions() / Co::getDefaultOptions()](#cosetdefaultoptionscogetdefaultoptions)\n- [Rules](#rules)\n  - **[Conversion on Resolving](#conversion-on-resolving)**\n  - **[Exception-safe or Exception-unsafe Priority](#exception-safe-or-exception-unsafe-priority)**\n  - **[Pseudo-sleep for Each Coroutine](#pseudo-sleep-for-each-coroutine)**\n  - **[Comparison with Generators of PHP7.0+ or PHP5.5~5.6](#comparison-with-generators-of-php70-or-php5556)**\n- [Appendix](#appendix)\n  - [Timing Charts](#timing-charts)\n\n## Installing\n\nInstall via Composer.\n\n```sh\ncomposer require mpyw/co:^1.5\n```\n\nAnd require Composer autoloader in your scripts.\n\n```php\nrequire __DIR__ . '/vendor/autoload.php';\n\nuse mpyw\\Co\\Co;\nuse mpyw\\Co\\CURLException;\n```\n\n## API\n\n### Co::wait()\n\nWait for all the cURL requests to complete.  \nThe options will override static defaults.\n\n```php\nstatic Co::wait(mixed $value, array $options = []) : mixed\n```\n\n#### Arguments\n\n- **`(mixed)`** __*$value*__\u003cbr /\u003e Any values to be parallelly resolved.\n- **`(array\u003cstring, mixed\u003e)`** __*$options*__\u003cbr /\u003e Associative array of options.\n\n| Key | Default | Description |\n|:---:|:---:|:---|\n| `throw` | **`true`** | Whether to throw or capture `CURLException` or `RuntimeException` on top-level.|\n| `pipeline` | **`false`** | Whether to use HTTP/1.1 pipelining.\u003cbr /\u003e**At most 5** requests for the same destination are bundled into single TCP connection.|\n| `multiplex` | **`true`** | Whether to use HTTP/2 multiplexing.\u003cbr /\u003e**All** requests for the same destination are bundled into single TCP connection.|\n| `autoschedule` | **`false`** | Whether to use automatic scheduling by `CURLMOPT_MAX_TOTAL_CONNECTIONS`.|\n| `interval` | **`0.002`** | `curl_multi_select()` timeout seconds. `0` means real-time observation.|\n| `concurrency` | **`6`** | Limit of concurrent TCP connections. `0` means unlimited.\u003cbr /\u003eThe value should be within `10` at most. |\n\n- `Throwable` which are not extended from `RuntimeException`, such as `Error` `Exception` `LogicException` are not captured. If you need to capture them, you have to write your own try-catch blocks in your functions.\n- HTTP/1.1 pipelining can be used only if the TCP connection is already established and verified that uses keep-alive session. It means that **the first bundle of HTTP/1.1 requests CANNOT be pipelined**. You can use it from second `yield` in `Co::wait()` call.\n- To use HTTP/2 multiplexing, you have to build PHP with libcurl 7.43.0+ and `--with-nghttp2`.\n- To use `autoschedule`, PHP 7.0.7 or later is required.\n\n**When `autoschedule` Disabled:**\n\n- `curl_multi_add_handle()` call can be delayed.\n- `concurrency` controlling with `pipeline` / `multiplex` CANNOT be correctly driven. You should set higher `concurrency` in those cases.\n\n**When `autoschedule` Enabled:**\n\n- `curl_multi_add_handle()` is always immediately called.\n- `CURLINFO_TOTAL_TIME` CANNOT be correctly calculated. \"Total Time\" includes the time waiting for other requests are finished.\n\nThe details of `CURLIFNO_*_TIME` timing charts are described at the bottom of this page.\n\n#### Return Value\n\n**`(mixed)`**\u003cbr /\u003eResolved values; in exception-safe context, it may contain...\n\n- `CURLException` which has been raised internally.\n- `RuntimeException` which has been raised by user.\n\n#### Exception\n\n- Throws `CURLException` or `RuntimeException` in exception-unsafe context.\n\n### Co::async()\n\nExecute cURL requests along with `Co::wait()` call, **without waiting** resolved values.  \nThe options are inherited from `Co::wait()`.  \n\nThis method is mainly expected to be used ...\n\n- When you are not interested in responses.\n- In `CURLOPT_WRITEFUNCTION` or `CURLOPT_HEADERFUNCTION` callbacks.\n\n```php\nstatic Co::async(mixed $value, mixed $throw = null) : null\n```\n\n#### Arguments\n\n- **`(mixed)`** __*$value*__\u003cbr /\u003e Any values to be parallelly resolved.\n- **`(mixed)`** __*$throw*__\u003cbr /\u003e Overrides `throw` in `Co::wait()` options when you passed `true` or `false`.\n\n#### Return Value\n\n**`(null)`**\n\n#### Exception\n\n- `CURLException` or `RuntimeException` can be thrown in exception-unsafe context.\u003cbr /\u003eNote that you **CANNOT** capture top-level exceptions unless you catch **outside of `Co::wait()` call**.\n\n### Co::isRunning()\n\nReturn if `Co::wait()` is running().  \nWith this check, you can safely call `Co::wait()` or `Co::async()`.\n\n```php\nstatic Co::isRunning() : bool\n```\n\n### Co::any()\u003cbr /\u003eCo::race()\u003cbr /\u003eCo::all()\n\nReturn a Generator that resolves with specific value.\n\n```php\nstatic Co::any(array $value) : \\Generator\u003cmixed\u003e\nstatic Co::race(array $value) : \\Generator\u003cmixed\u003e\nstatic Co::all(array $value) : \\Generator\u003cmixed\u003e\n```\n\n| Family | Return Value | Exception |\n|:---:|:---:|:---|\n| `Co::any()` | First Success | `AllFailedException` |\n| `Co::race()` | First Success | First Failure |\n\n- **Jobs CANNOT be canceled.**\u003cbr /\u003eIncomplete jobs remain even if `Co::any()` or `Co::race()` is resolved.\n- `Co::all(...)` is just a wrapper of `(function () { return yield ...; })()`.\u003cbr /\u003eIt should be only used with `Co::race()` or `Co::any()`.\n\n```php\nCo::wait(function () {\n    $group1 = Co::all([$ch1, $ch2, $ch3]);\n    $group2 = Co::all([$ch4, $ch5, $ch6]);\n    $group1or2 = Co::any([$group1, $group2]);\n    var_dump(yield $group1or2);\n});\n```\n\n### Co::setDefaultOptions()\u003cbr /\u003eCo::getDefaultOptions()\n\nOverrides/gets static default settings.\n\n```php\nstatic Co::setDefaultOptions(array $options) : null\nstatic Co::getDefaultOptions() : array\n```\n\n## Rules\n\n### Conversion on Resolving\n\nThe all yielded/returned values are resolved by the following rules.  \nYielded values are also resent to the Generator.  \nThe rules will be applied recursively.\n\n| Before | After |\n|:---:|:----:|\n|cURL resource|`curl_multi_getconent()` result or `CURLException`|\n|Array|Array (with resolved children) or `RuntimeException`|\n|Generator Closure\u003cbr\u003eGenerator| Return value (after all yields done) or `RuntimeException`|\n\n\"Generator Closure\" means Closure that contains `yield` keywords.\n\n### Exception-safe or Exception-unsafe Priority\n\n#### Context in Generator\n\n**Exception-unsafe** context by default.  \nThe following `yield` statement specifies exception-safe context.\n\n```php\n$results = yield Co::SAFE =\u003e [$ch1, $ch2];\n```\n\nThis is equivalent to:\n\n```php\n$results = yield [\n    function () use ($ch1) {\n        try {\n            return yield $ch1;\n        } catch (\\RuntimeException $e) {\n            return $e;\n        }\n    },\n    function () use ($ch2) {\n        try {\n            return yield $ch2;\n        } catch (\\RuntimeException $e) {\n            return $e;\n        }\n    },\n];\n```\n\n#### Context on `Co::wait()`\n\n**Exception-unsafe** context by default.  \nThe following setting specifies exception-safe context.\n\n```php\n$result = Co::wait([$ch1, $ch2], ['throw' =\u003e false]);\n```\n\nThis is equivalent to:\n\n```php\n$results = Co::wait([\n    function () use ($ch1) {\n        try {\n            return yield $ch1;\n        } catch (\\RuntimeException $e) {\n            return $e;\n        }\n    },\n    function () use ($ch2) {\n        try {\n            return yield $ch2;\n        } catch (\\RuntimeException $e) {\n            return $e;\n        }\n    },\n]);\n```\n\n#### Context on `Co::async()`\n\nContexts are **inherited** from `Co::wait()`.  \nThe following setting overrides parent context as exception-safe.\n\n```php\nCo::async($value, false);\n```\n\nThe following setting overrides parent context as exception-unsafe.\n\n```php\nCo::async($value, true);\n```\n\n### Pseudo-sleep for Each Coroutine\n\nThe following `yield` statements delay the coroutine processing:\n\n```php\nyield Co::DELAY =\u003e $seconds\nyield Co::SLEEP =\u003e $seconds  # Alias\n```\n\n### Comparison with Generators of PHP7.0+ or PHP5.5~5.6\n\n#### `return` Statements\n\nPHP 7.0+:\n\n```php\nyield $foo;\nyield $bar;\nreturn $baz;\n```\n\nPHP 5.5~5.6:\n\n```php\nyield $foo;\nyield $bar;\nyield Co::RETURN_WITH =\u003e $baz;\n```\n\nAlthough experimental aliases `Co::RETURN_` `Co::RET` `Co::RTN` are provided,  \n**`Co::RETURN_WITH`** is recommended in terms of readability.\n\n#### `yield` Statements with Assignment\n\nPHP 7.0+:\n\n```php\n$a = yield $foo;\necho yield $bar;\n```\n\nPHP 5.5~5.6:\n\n```php\n$a = (yield $foo);\necho (yield $bar);\n```\n\n#### `finally` Statements\n\nBe careful that `return` triggers `finally` while `yield Co::RETURN_WITH =\u003e` does not.\n\n```php\ntry {\n    return '...';\n} finally {\n    // Reachable\n}\n```\n\n```php\ntry {\n    yield Co::RETURN_WITH =\u003e '...';\n} finally {\n    // Unreachable\n}\n```\n\n## Appendix\n\n### Timing Charts\n\nNote that S is equal to Q when `autoschedule` is disabled.\n\n#### Basic\n\n| ID | When |\n|:---:|:---|\n| Q | `curl_multi_exec()` immediately after `curl_multi_add_handle()` called |\n| S | Processing started actually |\n| DNS | DNS resolution completed |\n| TCP | TCP connection established |\n| TLS | TLS/SSL session established |\n| HS | All HTTP request headers sent |\n| BS | Whole HTTP request body sent |\n| HR | All HTTP response headers received |\n| BR | Whole HTTP response body received |\n\n| Constant | Time |\n|:---|:---|\n| CURLINFO_NAMELOOKUP_TIME | DNS - S |\n| CURLINFO_CONNECT_TIME | TCP - S |\n| CURLINFO_APPCONNECT_TIME | TLS - S |\n| CURLINFO_PRETRANSFER_TIME | HS - S |\n| CURLINFO_STARTTRANSFER_TIME | HR - S |\n| CURLINFO_TOTAL_TIME | BR - Q |\n\n#### With Redirections by `CURLOPT_FOLLOWLOCATION`\n\n| ID | When |\n|:---:|:---|\n| Q | `curl_multi_exec()` immediately after `curl_multi_add_handle()` called |\n| S | Processing started actually |\n| DNS(1) | DNS resolution completed |\n| TCP(1) | TCP connection established |\n| TLS(1) | TLS/SSL session established |\n| HS(1) | All HTTP request headers sent |\n| BS(1) | Whole HTTP request body sent |\n| HR(1) | All HTTP response headers received |\n| DNS(2) | DNS resolution completed |\n| TCP(2) | TCP connection established |\n| TLS(2) | TLS/SSL session established |\n| HS(2) | All HTTP request headers sent |\n| BS(2) | Whole HTTP request body sent |\n| HR(2) | All HTTP response headers received |\n| BR(2) | Whole HTTP response body received |\n\n| Constant | Time |\n|:---|:---|\n| CURLINFO_REDIRECT_TIME | HR(1) - Q |\n| CURLINFO_NAMELOOKUP_TIME | DNS(2) - HR(1) |\n| CURLINFO_CONNECT_TIME | TCP(2) - HR(1) |\n| CURLINFO_APPCONNECT_TIME | TLS(2) - HR(1) |\n| CURLINFO_PRETRANSFER_TIME | HS(2) - HR(1) |\n| CURLINFO_STARTTRANSFER_TIME | HR(2) - HR(1) |\n| CURLINFO_TOTAL_TIME | BR(2) - Q |\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmpyw%2Fco","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmpyw%2Fco","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmpyw%2Fco/lists"}