├── .github └── workflows │ └── test_master.yml ├── .gitignore ├── LICENSE ├── README.md ├── codeception.yml ├── composer.json ├── src ├── MultiCurl.php └── MultiCurlRunner.php └── tests ├── _bootstrap.php ├── _support └── UnitTester.php ├── coding_standard.xml ├── unit.suite.yml └── unit └── MultiCurlTest.php /.github/workflows/test_master.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | test: 13 | name: Test 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | php: ['7.4', '8.0', '8.1', '8.2', '8.3'] 18 | 19 | steps: 20 | - name: Set up PHP 21 | uses: shivammathur/setup-php@v2 22 | with: 23 | php-version: ${{ matrix.php }} 24 | coverage: xdebug 25 | tools: composer:v2 26 | 27 | - name: Checkout code 28 | uses: actions/checkout@v2 29 | with: 30 | fetch-depth: 0 31 | 32 | - name: PHP Version Check 33 | run: php -v 34 | 35 | - name: Validate Composer JSON 36 | run: composer validate 37 | 38 | - name: Run Composer 39 | run: composer install --no-interaction 40 | 41 | - name: Unit tests 42 | run: | 43 | composer test-init 44 | composer test 45 | 46 | - name: PHP Code Sniffer 47 | run: composer codesniffer 48 | 49 | code-coverage: 50 | name: Code coverage 51 | runs-on: ubuntu-latest 52 | strategy: 53 | matrix: 54 | php: ['8.1'] 55 | 56 | steps: 57 | - name: Set up PHP 58 | uses: shivammathur/setup-php@v2 59 | with: 60 | php-version: ${{ matrix.php }} 61 | coverage: xdebug 62 | tools: composer:v2 63 | 64 | - name: Checkout code 65 | uses: actions/checkout@v2 66 | with: 67 | fetch-depth: 0 68 | 69 | - name: Run Composer 70 | run: composer install --no-interaction 71 | 72 | - name: Unit tests 73 | run: | 74 | composer test-init 75 | composer test-coverage-xml 76 | mkdir -p ./build/logs 77 | cp ./tests/_output/coverage.xml ./build/logs/clover.xml 78 | 79 | - name: PHPStan analysis 80 | run: composer stan 81 | 82 | - name: Code Coverage (Coveralls) 83 | env: 84 | COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} 85 | run: php vendor/bin/php-coveralls -v 86 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | .idea 3 | tests/_support/_generated 4 | tests/_output 5 | composer.lock 6 | biuld 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Smoren 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # multicurl 2 | 3 | Multi curl wrapper for making parallel HTTP requests 4 | 5 | ![Packagist PHP Version Support](https://img.shields.io/packagist/php-v/smoren/multicurl) 6 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/Smoren/multicurl-php/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/Smoren/multicurl-php/?branch=master) 7 | [![Coverage Status](https://coveralls.io/repos/github/Smoren/multicurl-php/badge.svg?branch=master)](https://coveralls.io/github/Smoren/multicurl-php?branch=master) 8 | ![Build and test](https://github.com/Smoren/multicurl-php/actions/workflows/test_master.yml/badge.svg) 9 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 10 | 11 | ### How to install to your project 12 | ``` 13 | composer require smoren/multicurl 14 | ``` 15 | 16 | ### Unit testing 17 | ``` 18 | composer install 19 | composer test-init 20 | composer test 21 | ``` 22 | 23 | ### Usage 24 | 25 | ```php 26 | use Smoren\MultiCurl\MultiCurl; 27 | 28 | $mc = new MultiCurl(10, [ 29 | CURLOPT_POST => true, 30 | CURLOPT_FOLLOWLOCATION => 1, 31 | ], [ 32 | 'Content-Type' => 'application/json', 33 | ]); 34 | 35 | $mc->addRequest(1, 'https://httpbin.org/anything', ['some' => 'data']); 36 | $mc->addRequest(2, 'https://httpbin.org/anything', ['some' => 'another data']); 37 | 38 | $result = $mc->makeRequests(); 39 | print_r($result); 40 | 41 | /* 42 | Array 43 | ( 44 | [1] => Array 45 | ( 46 | [code] => 200 47 | [headers] => Array 48 | ( 49 | [Date] => Sat, 14 May 2022 08:21:35 GMT 50 | [Content-Type] => application/json 51 | [Content-Length] => 459 52 | [Connection] => keep-alive 53 | [Server] => gunicorn/19.9.0 54 | [Access-Control-Allow-Origin] => * 55 | [Access-Control-Allow-Credentials] => true 56 | ) 57 | 58 | [body] => Array 59 | ( 60 | [args] => Array 61 | ( 62 | ) 63 | 64 | [data] => {"some":"data"} 65 | [files] => Array 66 | ( 67 | ) 68 | 69 | [form] => Array 70 | ( 71 | ) 72 | 73 | [headers] => Array 74 | ( 75 | [Accept] => 76 | [Accept-Encoding] => deflate, gzip 77 | [Content-Length] => 15 78 | [Content-Type] => application/json 79 | [Host] => httpbin.org 80 | [X-Amzn-Trace-Id] => Root=1-627f668f-2c004f4e5817d2b508e0cd6c 81 | ) 82 | 83 | [json] => Array 84 | ( 85 | [some] => data 86 | ) 87 | 88 | [method] => POST 89 | [origin] => 46.22.56.202 90 | [url] => https://httpbin.org/anything 91 | ) 92 | 93 | ) 94 | 95 | [2] => Array 96 | ( 97 | [code] => 200 98 | [headers] => Array 99 | ( 100 | [Date] => Sat, 14 May 2022 08:21:36 GMT 101 | [Content-Type] => application/json 102 | [Content-Length] => 475 103 | [Connection] => keep-alive 104 | [Server] => gunicorn/19.9.0 105 | [Access-Control-Allow-Origin] => * 106 | [Access-Control-Allow-Credentials] => true 107 | ) 108 | 109 | [body] => Array 110 | ( 111 | [args] => Array 112 | ( 113 | ) 114 | 115 | [data] => {"some":"another data"} 116 | [files] => Array 117 | ( 118 | ) 119 | 120 | [form] => Array 121 | ( 122 | ) 123 | 124 | [headers] => Array 125 | ( 126 | [Accept] => 127 | [Accept-Encoding] => deflate, gzip 128 | [Content-Length] => 23 129 | [Content-Type] => application/json 130 | [Host] => httpbin.org 131 | [X-Amzn-Trace-Id] => Root=1-627f668f-67767ca73cdb2bf313afa566 132 | ) 133 | 134 | [json] => Array 135 | ( 136 | [some] => another data 137 | ) 138 | 139 | [method] => POST 140 | [origin] => 46.22.56.202 141 | [url] => https://httpbin.org/anything 142 | ) 143 | 144 | ) 145 | 146 | ) 147 | */ 148 | ``` -------------------------------------------------------------------------------- /codeception.yml: -------------------------------------------------------------------------------- 1 | actor: Tester 2 | bootstrap: _bootstrap.php 3 | paths: 4 | tests: tests 5 | log: tests/_output 6 | data: tests/_data 7 | helpers: tests/_support 8 | settings: 9 | memory_limit: 1024M 10 | colors: true 11 | coverage: 12 | enabled: true 13 | show_uncovered: false 14 | include: 15 | - src/* 16 | exclude: 17 | - vendor/* 18 | - tests/* 19 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "smoren/multicurl", 3 | "description": "Multi curl wrapper for making parallel HTTP requests", 4 | "keywords": ["curl", "multicurl", "parallel", "request", "http"], 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Smoren", 9 | "email": "ofigate@gmail.com" 10 | } 11 | ], 12 | "require": { 13 | "php": ">=7.2.0", 14 | "ext-curl": "*", 15 | "ext-json": "*" 16 | }, 17 | "require-dev": { 18 | "codeception/codeception": "^4.2.1", 19 | "codeception/module-asserts": "^2.0", 20 | "php-coveralls/php-coveralls": "^2.0", 21 | "squizlabs/php_codesniffer": "3.*", 22 | "phpstan/phpstan": "^1.8" 23 | }, 24 | "autoload": { 25 | "psr-4": { 26 | "Smoren\\MultiCurl\\": "src", 27 | "Smoren\\MultiCurl\\Tests\\Unit\\": "tests/unit" 28 | } 29 | }, 30 | "config": { 31 | "fxp-asset": { 32 | "enabled": false 33 | } 34 | }, 35 | "repositories": [ 36 | { 37 | "type": "composer", 38 | "url": "https://asset-packagist.org" 39 | } 40 | ], 41 | "scripts": { 42 | "test-init": ["./vendor/bin/codecept build"], 43 | "test-all": ["composer test-coverage", "composer codesniffer", "composer stan"], 44 | "test": ["./vendor/bin/codecept run unit tests/unit"], 45 | "test-coverage": ["./vendor/bin/codecept run unit tests/unit --coverage"], 46 | "test-coverage-html": ["./vendor/bin/codecept run unit tests/unit --coverage-html"], 47 | "test-coverage-xml": ["./vendor/bin/codecept run unit tests/unit --coverage-xml"], 48 | "codesniffer": ["./vendor/bin/phpcs --ignore=vendor,tests --standard=tests/coding_standard.xml -s ."], 49 | "stan": ["./vendor/bin/phpstan analyse -l 9 src"] 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/MultiCurl.php: -------------------------------------------------------------------------------- 1 | Smoren 10 | */ 11 | class MultiCurl 12 | { 13 | /** 14 | * @var int max parallel connections count 15 | */ 16 | protected $maxConnections; 17 | /** 18 | * @var array common CURL options for every request 19 | */ 20 | protected $commonOptions = []; 21 | /** 22 | * @var array common headers for every request 23 | */ 24 | protected $commonHeaders = []; 25 | /** 26 | * @var array> map of CURL options including headers by custom request ID 27 | */ 28 | protected $requestsConfigMap = []; 29 | 30 | /** 31 | * MultiCurl constructor 32 | * @param int $maxConnections max parallel connections count 33 | * @param array $commonOptions common CURL options for every request 34 | * @param array $commonHeaders common headers for every request 35 | */ 36 | public function __construct( 37 | int $maxConnections = 100, 38 | array $commonOptions = [], 39 | array $commonHeaders = [] 40 | ) { 41 | $this->maxConnections = $maxConnections; 42 | $this->commonOptions = [ 43 | CURLOPT_URL => '', 44 | CURLOPT_RETURNTRANSFER => 1, 45 | CURLOPT_ENCODING => '' 46 | ]; 47 | 48 | foreach($commonOptions as $key => $value) { 49 | $this->commonOptions[$key] = $value; 50 | } 51 | 52 | $this->commonHeaders = $commonHeaders; 53 | } 54 | 55 | /** 56 | * Executes all the requests and returns their results map 57 | * @param bool $dataOnly if true: return only response body data, exclude status code and headers 58 | * @param bool $okOnly if true: return only responses with (200 <= status code < 300) 59 | * @return array responses mapped by custom request IDs 60 | * @throws RuntimeException 61 | */ 62 | public function makeRequests(bool $dataOnly = false, bool $okOnly = false): array 63 | { 64 | $runner = new MultiCurlRunner($this->requestsConfigMap, $this->maxConnections); 65 | $runner->run(); 66 | $this->requestsConfigMap = []; 67 | 68 | return $dataOnly ? $runner->getResultData($okOnly) : $runner->getResult($okOnly); 69 | } 70 | 71 | /** 72 | * Adds new request to execute 73 | * @param string $requestId custom request ID 74 | * @param string $url URL for request 75 | * @param mixed $body body data (will be json encoded if array) 76 | * @param array $options CURL request options 77 | * @param array $headers request headers 78 | * @return self 79 | */ 80 | public function addRequest( 81 | string $requestId, 82 | string $url, 83 | $body = null, 84 | array $options = [], 85 | array $headers = [] 86 | ): self { 87 | foreach($this->commonOptions as $key => $value) { 88 | if(!array_key_exists($key, $options)) { 89 | $options[$key] = $value; 90 | } 91 | } 92 | 93 | foreach($this->commonHeaders as $key => $value) { 94 | if(!array_key_exists($key, $headers)) { 95 | $headers[$key] = $value; 96 | } 97 | } 98 | 99 | if(is_array($body)) { 100 | $body = json_encode($body); 101 | } 102 | 103 | if($body !== null) { 104 | /** @var string|numeric $body */ 105 | $options[CURLOPT_POSTFIELDS] = strval($body); 106 | } 107 | 108 | $options[CURLOPT_HTTPHEADER] = $this->formatHeaders($headers); 109 | $options[CURLOPT_URL] = $url; 110 | 111 | $this->requestsConfigMap[$requestId] = $options; 112 | 113 | return $this; 114 | } 115 | 116 | /** 117 | * Formats headers from associative array 118 | * @param array $headers request headers associative array 119 | * @return array headers array to pass to curl_setopt_array 120 | */ 121 | protected function formatHeaders(array $headers): array 122 | { 123 | $result = []; 124 | 125 | foreach($headers as $key => $value) { 126 | $result[] = "{$key}: {$value}"; 127 | } 128 | 129 | return $result; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/MultiCurlRunner.php: -------------------------------------------------------------------------------- 1 | Smoren 12 | */ 13 | class MultiCurlRunner 14 | { 15 | /** 16 | * @var resource|CurlMultiHandle MultiCurl resource 17 | */ 18 | protected $mh; 19 | /** 20 | * @var array map [workerId => customRequestId] 21 | */ 22 | protected $workersMap; 23 | /** 24 | * @var array unemployed workers stack 25 | */ 26 | protected $unemployedWorkers; 27 | /** 28 | * @var int max parallel connections count 29 | */ 30 | protected $maxConnections; 31 | /** 32 | * @var array> map of CURL options including headers by custom request ID 33 | */ 34 | protected $requestsConfigMap; 35 | /** 36 | * @var array> responses mapped by custom request ID 37 | */ 38 | protected $result; 39 | 40 | /** 41 | * MultiCurlRunner constructor 42 | * @param array> $requestsConfigMap map of CURL options 43 | * including headers by custom request ID 44 | * @param int $maxConnections max parallel connections count 45 | */ 46 | public function __construct(array $requestsConfigMap, int $maxConnections) 47 | { 48 | $this->requestsConfigMap = $requestsConfigMap; 49 | $this->maxConnections = min($maxConnections, count($requestsConfigMap)); 50 | 51 | $mh = curl_multi_init(); 52 | 53 | $this->mh = $mh; 54 | $this->workersMap = []; 55 | $this->unemployedWorkers = []; 56 | $this->result = []; 57 | } 58 | 59 | /** 60 | * Makes requests and stores responses 61 | * @return self 62 | * @throws RuntimeException 63 | */ 64 | public function run(): self 65 | { 66 | for($i=0; $i<$this->maxConnections; ++$i) { 67 | /** @var resource|false $unemployedWorker */ 68 | $unemployedWorker = curl_init(); 69 | if(!$unemployedWorker) { 70 | throw new RuntimeException("failed creating unemployed worker #{$i}"); 71 | } 72 | $this->unemployedWorkers[] = $unemployedWorker; 73 | } 74 | unset($i, $this->unemployedWorker); 75 | 76 | foreach($this->requestsConfigMap as $id => $options) { 77 | while(!count($this->unemployedWorkers)) { 78 | $this->doWork(); 79 | } 80 | 81 | $options[CURLOPT_HEADER] = 1; 82 | 83 | $newWorker = array_pop($this->unemployedWorkers); 84 | 85 | // @phpstan-ignore-next-line 86 | if(!curl_setopt_array($newWorker, $options)) { 87 | $errNo = curl_errno($newWorker); // @phpstan-ignore-line 88 | $errMess = curl_error($newWorker); // @phpstan-ignore-line 89 | $errData = var_export($options, true); 90 | throw new RuntimeException("curl_setopt_array failed: {$errNo} {$errMess} {$errData}"); 91 | } 92 | 93 | $this->workersMap[(int)$newWorker] = $id; 94 | curl_multi_add_handle($this->mh, $newWorker); // @phpstan-ignore-line 95 | } 96 | unset($options); 97 | 98 | while(count($this->workersMap) > 0) { 99 | $this->doWork(); 100 | } 101 | 102 | foreach($this->unemployedWorkers as $unemployedWorker) { 103 | curl_close($unemployedWorker); // @phpstan-ignore-line 104 | } 105 | 106 | curl_multi_close($this->mh); // @phpstan-ignore-line 107 | 108 | return $this; 109 | } 110 | 111 | /** 112 | * Returns response: 113 | * [customRequestId => [code => statusCode, headers => [key => value, ...], body => responseBody], ...] 114 | * @param bool $okOnly if true: return only responses with (200 <= status code < 300) 115 | * @return array> responses mapped by custom request IDs 116 | */ 117 | public function getResult(bool $okOnly = false): array 118 | { 119 | $result = []; 120 | 121 | foreach($this->result as $key => $value) { 122 | if(!$okOnly || $value['code'] === 200) { 123 | $result[$key] = $value; 124 | } 125 | } 126 | 127 | return $result; 128 | } 129 | 130 | /** 131 | * Returns response bodies: 132 | * [customRequestId => responseBody, ...] 133 | * @param bool $okOnly if true: return only responses with (200 <= status code < 300) 134 | * @return array responses mapped by custom request IDs 135 | */ 136 | public function getResultData(bool $okOnly = false): array 137 | { 138 | $result = []; 139 | 140 | foreach($this->result as $key => $value) { 141 | if(!$okOnly || $value['code'] >= 200 && $value['code'] < 300) { 142 | $result[$key] = $value['body']; 143 | } 144 | } 145 | 146 | return $result; 147 | } 148 | 149 | /** 150 | * Manages workers during making the requests 151 | * @return void 152 | */ 153 | protected function doWork(): void 154 | { 155 | assert(count($this->workersMap) > 0, "work() called with 0 workers!!"); 156 | $stillRunning = null; 157 | 158 | while(true) { 159 | do { 160 | $err = curl_multi_exec($this->mh, $stillRunning); // @phpstan-ignore-line 161 | } while($err === CURLM_CALL_MULTI_PERFORM); 162 | 163 | if($err !== CURLM_OK) { 164 | $errInfo = [ 165 | "multi_exec_return" => $err, 166 | "curl_multi_errno" => curl_multi_errno($this->mh), // @phpstan-ignore-line 167 | "curl_multi_strerror" => curl_multi_strerror($err) 168 | ]; 169 | 170 | $errData = str_replace(["\r", "\n"], "", var_export($errInfo, true)); 171 | throw new RuntimeException("curl_multi_exec error: {$errData}"); 172 | } 173 | if($stillRunning < count($this->workersMap)) { 174 | // some workers has finished downloading, process them 175 | // echo "processing!"; 176 | break; 177 | } else { 178 | // no workers finished yet, sleep-wait for workers to finish downloading. 179 | curl_multi_select($this->mh, 1); // @phpstan-ignore-line 180 | // sleep(1); 181 | } 182 | } 183 | // @phpstan-ignore-next-line 184 | while(($info = curl_multi_info_read($this->mh)) !== false) { 185 | if($info['msg'] !== CURLMSG_DONE) { 186 | // no idea what this is, it's not the message we're looking for though, ignore it. 187 | continue; 188 | } 189 | 190 | if($info['result'] !== CURLM_OK) { 191 | $errInfo = [ 192 | "effective_url" => curl_getinfo($info['handle'], CURLINFO_EFFECTIVE_URL), 193 | "curl_errno" => curl_errno($info['handle']), 194 | "curl_error" => curl_error($info['handle']), 195 | "curl_multi_errno" => curl_multi_errno($this->mh), // @phpstan-ignore-line 196 | "curl_multi_strerror" => curl_multi_strerror(curl_multi_errno($this->mh)) // @phpstan-ignore-line 197 | ]; 198 | 199 | $errData = str_replace(["\r", "\n"], "", var_export($errInfo, true)); 200 | throw new RuntimeException("curl_multi worker error: {$errData}"); 201 | } 202 | 203 | $ch = $info['handle']; 204 | $chIndex = (int)$ch; 205 | 206 | // @phpstan-ignore-next-line 207 | $this->result[$this->workersMap[$chIndex]] = $this->parseResponse(curl_multi_getcontent($ch)); 208 | 209 | unset($this->workersMap[$chIndex]); 210 | curl_multi_remove_handle($this->mh, $ch); // @phpstan-ignore-line 211 | $this->unemployedWorkers[] = $ch; 212 | } 213 | } 214 | 215 | /** 216 | * Parses the response 217 | * @param string $response raw HTTP response 218 | * @return array [code => statusCode, headers => [key => value, ...], body => responseBody] 219 | */ 220 | protected function parseResponse(string $response): array 221 | { 222 | $arResponse = explode("\r\n\r\n", $response); 223 | 224 | $arHeaders = []; 225 | $statusCode = null; 226 | $body = null; 227 | 228 | while(count($arResponse)) { 229 | $respItem = array_shift($arResponse); 230 | 231 | $line = (string)strtok($respItem, "\r\n"); 232 | $statusCodeLine = trim($line); 233 | if(preg_match('|HTTP/[\d.]+\s+(\d+)|', $statusCodeLine, $matches)) { 234 | $arHeaders = []; 235 | 236 | if(isset($matches[1])) { 237 | $statusCode = (int)$matches[1]; 238 | } else { 239 | $statusCode = null; 240 | } 241 | 242 | // Parse the string, saving it into an array instead 243 | while(($line = strtok("\r\n")) !== false) { 244 | if(($matches = explode(':', $line, 2)) !== false) { 245 | $arHeaders[trim(mb_strtolower($matches[0]))] = trim(mb_strtolower($matches[1])); 246 | } 247 | } 248 | } else { 249 | $contentType = $arHeaders['content-type'] ?? null; 250 | if($contentType === 'application/json') { 251 | $body = json_decode($respItem, true); 252 | } else { 253 | $body = $respItem; 254 | } 255 | } 256 | } 257 | 258 | return [ 259 | 'code' => $statusCode, 260 | 'headers' => $arHeaders, 261 | 'body' => $body, 262 | ]; 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /tests/_bootstrap.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | The coding standard for Schemator PHP. 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/unit.suite.yml: -------------------------------------------------------------------------------- 1 | # Codeception Test Suite Configuration 2 | # 3 | # Suite for unit or integration tests. 4 | 5 | class_name: UnitTester 6 | modules: 7 | enabled: 8 | - Asserts 9 | -------------------------------------------------------------------------------- /tests/unit/MultiCurlTest.php: -------------------------------------------------------------------------------- 1 | true, 13 | CURLOPT_FOLLOWLOCATION => 1, 14 | ], [ 15 | 'Content-Type' => 'application/json', 16 | ]); 17 | 18 | $mc->addRequest(1, 'https://httpbin.org/anything', ['some' => 'data']); 19 | $mc->addRequest(2, 'https://httpbin.org/anything', ['some' => 'another data']); 20 | $result = $mc->makeRequests(false, false); 21 | $this->assertEquals($this->getExpected(false), $this->formatResult($result, false)); 22 | 23 | $mc->addRequest(1, 'https://httpbin.org/anything', ['some' => 'data']); 24 | $mc->addRequest(2, 'https://httpbin.org/anything', ['some' => 'another data']); 25 | $mc->addRequest(3, 'https://httpbin.org/status/500', ['bad' => 'request']); 26 | 27 | $result = $mc->makeRequests(false, true); 28 | $this->assertEquals($this->getExpected(false), $this->formatResult($result, false)); 29 | 30 | $mc->addRequest(1, 'https://httpbin.org/anything', ['some' => 'data']); 31 | $mc->addRequest(2, 'https://httpbin.org/anything', ['some' => 'another data']); 32 | 33 | $result = $mc->makeRequests(true, false); 34 | $this->assertEquals($this->getExpected(true), $this->formatResult($result, true)); 35 | 36 | $mc->addRequest(1, 'https://httpbin.org/anything', ['some' => 'data']); 37 | $mc->addRequest(2, 'https://httpbin.org/anything', ['some' => 'another data']); 38 | $mc->addRequest(3, 'https://httpbin.org/status/500', ['bad' => 'request']); 39 | 40 | $result = $mc->makeRequests(true, true); 41 | $this->assertEquals($this->getExpected(true), $this->formatResult($result, true)); 42 | 43 | for($i=1; $i<=5; ++$i) { 44 | $mc->addRequest($i, 'https://httpbin.org/anything', ['id' => $i]); 45 | } 46 | $result = $mc->makeRequests(true, false); 47 | $this->assertCount(5, $result); 48 | for($i=1; $i<=5; ++$i) { 49 | $this->assertEquals($i, $result[$i]['json']['id'] ?? null); 50 | } 51 | } 52 | 53 | protected function formatResult(array $result, bool $dataOnly): array 54 | { 55 | foreach($result as &$item) { 56 | if($dataOnly) { 57 | $item = $item['json']; 58 | } else { 59 | $item['headers'] = [ 60 | 'content-type' => $item['headers']['content-type'], 61 | ]; 62 | $item['body'] = $item['body']['json']; 63 | } 64 | } 65 | unset($item); 66 | 67 | return $result; 68 | } 69 | 70 | protected function getExpected(bool $dataOnly): array 71 | { 72 | $result = [ 73 | 1 => [ 74 | "code" => 200, 75 | "headers" => [ 76 | "content-type" => "application/json", 77 | ], 78 | "body" => [ 79 | "some" => "data" 80 | ] 81 | ], 82 | 2 => [ 83 | "code" => 200, 84 | "headers" => [ 85 | "content-type" => "application/json", 86 | ], 87 | "body" => [ 88 | "some" => "another data" 89 | ] 90 | ] 91 | ]; 92 | 93 | if($dataOnly) { 94 | foreach($result as &$item) { 95 | $item = $item['body']; 96 | } 97 | } 98 | 99 | return $result; 100 | } 101 | } 102 | --------------------------------------------------------------------------------