From 8c80353c94d27a4d38eee1443f423f4883d7fcfd Mon Sep 17 00:00:00 2001 From: Greg Bowler Date: Tue, 28 Mar 2023 15:37:51 +0100 Subject: [PATCH] Unit test coverage and tweaks (#80) * build: upgrade phpunit config * tweak: only close if already open * test: test coverage for curl class * test: test coverage for curlobjectlookup class * test: test coverage for curlobjectlookup class * test: test coverage for curlmulti class * test: test coverage for curlshare class * test: test coverage for curlmultiinfo class * docs: update examples * docs: update examples to use UploadFile * docs: update curl multi callback functions * tweak: phpmd --- examples/01-http-get.php | 16 +++++++ examples/02-http-post-file-upload.php | 47 +++++++++++++++++++ .../{01-json-usage.php => 03-json-usage.php} | 6 +-- .../{02-curl-multi.php => 04-curl-multi.php} | 3 -- ...eaming.php => 05-curl-multi-streaming.php} | 25 ++++++++++ src/Curl.php | 19 ++++++++ src/CurlInterface.php | 4 ++ src/CurlMulti.php | 36 +++++++++++--- src/CurlMultiInfo.php | 2 +- src/UploadFile.php | 6 +++ test/phpunit/CurlMultiTest.php | 30 ++++++++++-- test/phpunit/CurlTest.php | 16 +++++++ 12 files changed, 194 insertions(+), 16 deletions(-) create mode 100644 examples/01-http-get.php create mode 100644 examples/02-http-post-file-upload.php rename examples/{01-json-usage.php => 03-json-usage.php} (75%) rename examples/{02-curl-multi.php => 04-curl-multi.php} (87%) rename examples/{03-curl-multi-streaming.php => 05-curl-multi-streaming.php} (54%) create mode 100644 src/UploadFile.php diff --git a/examples/01-http-get.php b/examples/01-http-get.php new file mode 100644 index 0000000..6fcbecf --- /dev/null +++ b/examples/01-http-get.php @@ -0,0 +1,16 @@ +exec(); +echo "Your country is: "; +echo $curl->output(), PHP_EOL; + +/* Example output: +Your country is: United Kingdom +*/ diff --git a/examples/02-http-post-file-upload.php b/examples/02-http-post-file-upload.php new file mode 100644 index 0000000..dc40b42 --- /dev/null +++ b/examples/02-http-post-file-upload.php @@ -0,0 +1,47 @@ +output()); + +// Now POST a form containing the cat photo to the Postman echo test server +$upload = new UploadFile($tmpFile); +$curl = new Curl("https://postman-echo.com/post"); +$curl->setOpt(CURLOPT_POSTFIELDS, [ + "cat-photo" => $upload +]); +$curl->exec(); +echo $curl->output(); + +// Remove the temporary file before finishing +unlink($tmpFile); + +/* + * Example output: + * { + * "args": {}, + * "data": {}, + * "files": { + * "cat.jpg": "data:application/octet-stream;base64,/9j/4AAQSkZJRgABAQEIAAD" + * }, + * "form": {}, + * "headers": { + * "host": "postman-echo.com", + * "content-length": "42285", + * "accept": "*\/*", + * "content-type": "multipart/form-data; boundary=--------------d7b86ee9056" + * }, + * "json": null, + * "url": "https://postman-echo.com/post" + * } +*/ diff --git a/examples/01-json-usage.php b/examples/03-json-usage.php similarity index 75% rename from examples/01-json-usage.php rename to examples/03-json-usage.php index 9bc31a0..b7659b1 100644 --- a/examples/01-json-usage.php +++ b/examples/03-json-usage.php @@ -1,8 +1,8 @@ setOpt(CURLOPT_RETURNTRANSFER, true); $curlIp = new Curl("https://api.ipify.org/?format=json"); -$curlIp->setOpt(CURLOPT_RETURNTRANSFER, true); $curlTimeout = new Curl("https://this-domain-name-does-not-exist.example.com/nothing.json"); -$curlTimeout->setOpt(CURLOPT_RETURNTRANSFER, true); $multi = new CurlMulti(); $multi->add($curlCat); diff --git a/examples/03-curl-multi-streaming.php b/examples/05-curl-multi-streaming.php similarity index 54% rename from examples/03-curl-multi-streaming.php rename to examples/05-curl-multi-streaming.php index e7cc034..2fd61a1 100644 --- a/examples/03-curl-multi-streaming.php +++ b/examples/05-curl-multi-streaming.php @@ -47,3 +47,28 @@ while($stillRunning > 0); echo PHP_EOL; + +/** Example output: +.................................HEADER: HTTP/2 200 +HEADER: server: nginx +HEADER: date: Tue, 28 Mar 2023 11:26:46 GMT +HEADER: content-type: application/json +HEADER: vary: Accept-Encoding +HEADER: cache-control: no-cache, private +HEADER: access-control-allow-origin: * +HEADER: set-cookie: XSRF-TOKEN=eyJpdiI6I; expires=Tue, 28-Mar-2023 13:26:46 GMT; path=/; samesite=lax +HEADER: set-cookie: catfacts_session=eyJpdiI6InUzdFY; expires=Tue, 28-Mar-2023 13:26:46 GMT; path=/; httponly; samesite=lax +HEADER: x-frame-options: SAMEORIGIN +HEADER: x-xss-protection: 1; mode=block +HEADER: x-content-type-options: nosniff +HEADER: +BODY: {"fact":"Edward Lear, author of \\The Owl and the Pussycat\\\"\", is said to have had his new house in San Remo built to exactly the same specification as his previous residence, so that his much-loved tabby, Foss, would immediately feel at home.\"\"\"","length":236} +...............HEADER: HTTP/2 200 +HEADER: content-type: application/json +HEADER: date: Tue, 28 Mar 2023 11:26:46 GMT +HEADER: vary: Origin +HEADER: content-length: 20 +HEADER: +BODY: {"ip":"82.4.210.105"} +. +*/ diff --git a/src/Curl.php b/src/Curl.php index 2e0b405..5a6389c 100644 --- a/src/Curl.php +++ b/src/Curl.php @@ -9,6 +9,10 @@ class Curl implements CurlInterface { protected CurlHandle $ch; protected ?string $buffer; + /** @var callable */ + protected $headerFunction; + /** @var callable */ + protected $writeFunction; public function __construct(string $url = null) { $this->buffer = null; @@ -169,6 +173,13 @@ public function reset():void { * @see http://php.net/manual/en/function.curl-setopt.php */ public function setOpt(int $option, mixed $value):bool { + if($option === CURLOPT_HEADERFUNCTION) { + $this->headerFunction = $value; + } + elseif($option === CURLOPT_WRITEFUNCTION) { + $this->writeFunction = $value; + } + return curl_setopt($this->ch, $option, $value); } @@ -205,4 +216,12 @@ public function getAllInfo():array { $result = curl_getinfo($this->ch); return $result ?: []; } + + public function getHeaderFunction():?callable { + return $this->headerFunction ?? null; + } + + public function getWriteFunction():?callable { + return $this->writeFunction ?? null; + } } diff --git a/src/CurlInterface.php b/src/CurlInterface.php index bd5586a..57217c5 100644 --- a/src/CurlInterface.php +++ b/src/CurlInterface.php @@ -114,4 +114,8 @@ public function getHandle():CurlHandle; * @return array */ public function getAllInfo():array; + + public function getHeaderFunction():?callable; + + public function getWriteFunction():?callable; } diff --git a/src/CurlMulti.php b/src/CurlMulti.php index 7b4eafb..41f055c 100644 --- a/src/CurlMulti.php +++ b/src/CurlMulti.php @@ -1,12 +1,19 @@ */ + protected array $curlHandleArray; + /** @var array */ + protected array $responseBodyArray; public function __construct() { + $this->curlHandleArray = []; + $this->responseBodyArray = []; $this->init(); } @@ -27,9 +34,22 @@ public static function strError(int $errorNum):string { * @throws CurlException if a CURLM_XXX error is made */ public function add(CurlInterface $curl):void { -// The RETURNTRANSFER option must be set to be able to use the internal buffers. - $curl->setOpt(CURLOPT_RETURNTRANSFER, true); - curl_multi_add_handle($this->mh, $curl->getHandle()); + $handle = $curl->getHandle(); + array_push($this->curlHandleArray, $handle); + array_push($this->responseBodyArray, ""); + + $existingWrite = $curl->getWriteFunction(); + if(!$existingWrite) { + $curl->setOpt( + CURLOPT_WRITEFUNCTION, + function (CurlHandle $ch, string $rawBody):int { + $index = array_search($ch, $this->curlHandleArray); + $this->responseBodyArray[$index] .= $rawBody; + return strlen($rawBody); + } + ); + } + curl_multi_add_handle($this->mh, $handle); } /** @@ -66,12 +86,16 @@ public function getHandle():CurlMultiHandle { } /** - * Return the content of a cURL handle if CURLOPT_RETURNTRANSFER is set + * Return the content of a cURL handle if CURLOPT_RETURNTRANSFER is set, + * or retrieves the content of the cURL handle from the internal buffer. * @see http://php.net/manual/en/function.curl-multi-getcontent.php - * @return string the content of a cURL handle if CURLOPT_RETURNTRANSFER is set + * @return string the content of a cURL handle */ public function getContent(CurlInterface $curl):string { - return curl_multi_getcontent($curl->getHandle()) ?? ""; + $index = array_search($curl->getHandle(), $this->curlHandleArray); + return $this->responseBodyArray[$index] + ?? curl_multi_getcontent($curl->getHandle()) + ?? ""; } /** diff --git a/src/CurlMultiInfo.php b/src/CurlMultiInfo.php index 4128332..f29136f 100644 --- a/src/CurlMultiInfo.php +++ b/src/CurlMultiInfo.php @@ -7,7 +7,7 @@ * @see http://php.net/manual/en/function.curl-multi-info-read.php */ class CurlMultiInfo implements CurlMultiInfoInterface { - protected string $message; + protected int $message; protected int $result; protected CurlHandle $handle; diff --git a/src/UploadFile.php b/src/UploadFile.php new file mode 100644 index 0000000..4e27276 --- /dev/null +++ b/src/UploadFile.php @@ -0,0 +1,6 @@ +expects(self::once()) - ->method("setOpt") - ->with(CURLOPT_RETURNTRANSFER, true); $curlInterface->expects(self::once()) ->method("getHandle") ->willReturn(curl_init()); @@ -29,6 +26,32 @@ public function testAdd():void { $sut->add($curlInterface); } + public function testAdd_noExistingWriteFunction():void { + $curlInterface = self::createMock(CurlInterface::class); + $curlInterface->method("getHandle") + ->willReturn(curl_init()); + $curlInterface->method("getWriteFunction") + ->willReturn(null); + $curlInterface->expects(self::once()) + ->method("setOpt") + ->with(CURLOPT_WRITEFUNCTION, self::anything()); + $sut = new CurlMulti(); + $sut->add($curlInterface); + } + + public function testAdd_existingWriteFunction():void { + $curlInterface = self::createMock(CurlInterface::class); + $curlInterface->method("getHandle") + ->willReturn(curl_init()); + $curlInterface->method("getWriteFunction") + ->willReturn(function() {}); + $curlInterface->expects(self::never()) + ->method("setOpt") + ->with(CURLOPT_WRITEFUNCTION, self::anything()); + $sut = new CurlMulti(); + $sut->add($curlInterface); + } + public function testClose():void { $sut = new CurlMulti(); $exception = null; @@ -76,6 +99,7 @@ public function testGetContent():void { } } $curl = new Curl("http://localhost:$port"); + $curl->setOpt(CURLOPT_RETURNTRANSFER, true); $sut = new CurlMulti(); $sut->add($curl); $stillRunning = 0; diff --git a/test/phpunit/CurlTest.php b/test/phpunit/CurlTest.php index 497d15c..44b7003 100644 --- a/test/phpunit/CurlTest.php +++ b/test/phpunit/CurlTest.php @@ -285,4 +285,20 @@ public function testOutputJson():void { $sut->exec(); self::assertSame("Hello, PHP.Gt!", $sut->outputJson()->getString("message")); } + + public function testGetHeaderFunction():void { + $func = function() {}; + $sut = new Curl(); + self::assertNull($sut->getHeaderFunction()); + $sut->setOpt(CURLOPT_HEADERFUNCTION, $func); + self::assertSame($func, $sut->getHeaderFunction()); + } + + public function testGetWriteFunction():void { + $func = function() {}; + $sut = new Curl(); + self::assertNull($sut->getWriteFunction()); + $sut->setOpt(CURLOPT_WRITEFUNCTION, $func); + self::assertSame($func, $sut->getWriteFunction()); + } }