diff --git a/CHANGELOG.md b/CHANGELOG.md index 51258dfe..05211c09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ The format is based on Keep a Changelog. - Added adapter-based Lang provider support with `DeepL` and `Google Translate` adapters plus shared remote request/caching infrastructure (#533) ### Changed +- Refactored `HttpClient` internals behind explicit `CurlAdapter` and `MultiCurlAdapter` wrappers while preserving the existing facade methods and keeping `php-curl-class` as the underlying transport for this phase (#534) - Refactored the Lang package to resolve adapter instances through `LangFactory` configuration and load file translations lazily on first use instead of preloading them during web boot (#533) - **BREAKING:** Reshaped Lang configuration so `lang.default` now selects the adapter, locale fallback moved to `lang.default_locale`, and the unused `lang.enabled` toggle was removed (#533) - **BREAKING:** Removed `Lang::isEnabled()` from the public Lang API because it no longer affected runtime behavior (#533) diff --git a/src/HttpClient/Adapters/CurlAdapter.php b/src/HttpClient/Adapters/CurlAdapter.php new file mode 100644 index 00000000..0efa7f8e --- /dev/null +++ b/src/HttpClient/Adapters/CurlAdapter.php @@ -0,0 +1,166 @@ +client = $client ?? new Curl(); + } + + public function setUrl(string $url): CurlAdapterInterface + { + $this->client->setUrl($url); + + return $this; + } + + /** + * @param mixed $value + */ + public function setOpt(int $option, $value): CurlAdapterInterface + { + $this->client->setOpt($option, $value); + + return $this; + } + + /** + * @param array $options + */ + public function setOpts(array $options): CurlAdapterInterface + { + $this->client->setOpts($options); + + return $this; + } + + /** + * @param mixed $value + */ + public function setHeader(string $key, $value): CurlAdapterInterface + { + $this->client->setHeader($key, $value); + + return $this; + } + + /** + * @param array $headers + */ + public function setHeaders(array $headers): CurlAdapterInterface + { + $this->client->setHeaders($headers); + + return $this; + } + + /** + * @param mixed $data + * @return mixed + */ + public function buildPostData($data) + { + return $this->client->buildPostData($data); + } + + /** + * @return mixed + */ + public function start() + { + return $this->client->exec(); + } + + /** + * @return int|string + */ + public function getId() + { + return $this->client->getId(); + } + + public function isError(): bool + { + return $this->client->isError(); + } + + public function getErrorCode(): int + { + return $this->client->getErrorCode(); + } + + public function getErrorMessage(): ?string + { + return $this->client->getErrorMessage(); + } + + /** + * @return iterable + */ + public function getResponseHeaders(): iterable + { + return $this->client->getResponseHeaders(); + } + + /** + * @return mixed + */ + public function getResponseCookies() + { + return $this->client->getResponseCookies(); + } + + /** + * @return mixed + */ + public function getResponse() + { + return $this->client->getResponse(); + } + + /** + * @return mixed + */ + public function getInfo(?int $option = null) + { + return $option !== null ? $this->client->getInfo($option) : $this->client->getInfo(); + } + + public function getUrl(): ?string + { + return $this->client->getUrl(); + } + + public function supportsMethod(string $method): bool + { + return method_exists($this->client, $method); + } + + /** + * @param array $arguments + * @return mixed + */ + public function callMethod(string $method, array $arguments) + { + return $this->client->$method(...$arguments); + } +} diff --git a/src/HttpClient/Adapters/MultiCurlAdapter.php b/src/HttpClient/Adapters/MultiCurlAdapter.php new file mode 100644 index 00000000..825b8512 --- /dev/null +++ b/src/HttpClient/Adapters/MultiCurlAdapter.php @@ -0,0 +1,145 @@ +client = $client ?? new MultiCurl(); + } + + public function complete(callable $callback): MultiCurlAdapterInterface + { + $this->client->complete(function (Curl $instance) use ($callback): void { + $callback(new CurlAdapter($instance)); + }); + + return $this; + } + + public function success(callable $callback): MultiCurlAdapterInterface + { + $this->client->success(function (Curl $instance) use ($callback): void { + $callback(new CurlAdapter($instance)); + }); + + return $this; + } + + public function error(callable $callback): MultiCurlAdapterInterface + { + $this->client->error(function (Curl $instance) use ($callback): void { + $callback(new CurlAdapter($instance)); + }); + + return $this; + } + + /** + * @return mixed + */ + public function start() + { + return $this->client->start(); + } + + /** + * @param array $data + * @return mixed + */ + public function addGet(string $url, array $data = []) + { + return $this->wrapCurlResult($this->client->addGet($url, $data)); + } + + /** + * @param mixed $data + * @return mixed + */ + public function addPost(string $url, $data = '', bool $follow_303_with_post = false) + { + return $this->wrapCurlResult($this->client->addPost($url, $data, $follow_303_with_post)); + } + + /** + * @param mixed $value + */ + public function setHeader(string $key, $value): MultiCurlAdapterInterface + { + $this->client->setHeader($key, $value); + + return $this; + } + + /** + * @param array $headers + */ + public function setHeaders(array $headers): MultiCurlAdapterInterface + { + $this->client->setHeaders($headers); + + return $this; + } + + /** + * @param mixed $value + */ + public function setOpt(int $option, $value): MultiCurlAdapterInterface + { + $this->client->setOpt($option, $value); + + return $this; + } + + /** + * @param array $options + */ + public function setOpts(array $options): MultiCurlAdapterInterface + { + $this->client->setOpts($options); + + return $this; + } + + public function supportsMethod(string $method): bool + { + return method_exists($this->client, $method); + } + + /** + * @param array $arguments + * @return mixed + */ + public function callMethod(string $method, array $arguments) + { + return $this->client->$method(...$arguments); + } + + /** + * @param mixed $result + * @return mixed + */ + private function wrapCurlResult($result) + { + return $result instanceof Curl ? new CurlAdapter($result) : $result; + } +} diff --git a/src/HttpClient/Contracts/CurlAdapterInterface.php b/src/HttpClient/Contracts/CurlAdapterInterface.php new file mode 100644 index 00000000..4582b123 --- /dev/null +++ b/src/HttpClient/Contracts/CurlAdapterInterface.php @@ -0,0 +1,80 @@ + + */ + public function getResponseHeaders(): iterable; + + /** + * Gets response cookies + * @return mixed + */ + public function getResponseCookies(); + + /** + * Gets response + * @return mixed + */ + public function getResponse(); + + /** + * Gets curl info + * @return mixed + */ + public function getInfo(?int $option = null); + + /** + * Gets request URL + */ + public function getUrl(): ?string; +} diff --git a/src/HttpClient/Contracts/HttpClientAdapterInterface.php b/src/HttpClient/Contracts/HttpClientAdapterInterface.php new file mode 100644 index 00000000..be48c272 --- /dev/null +++ b/src/HttpClient/Contracts/HttpClientAdapterInterface.php @@ -0,0 +1,60 @@ + $headers + */ + public function setHeaders(array $headers): self; + + /** + * Sets request option + * @param mixed $value + */ + public function setOpt(int $option, $value): self; + + /** + * Sets multiple request options + * @param array $options + */ + public function setOpts(array $options): self; + + /** + * Checks if adapter supports method passthrough + */ + public function supportsMethod(string $method): bool; + + /** + * Calls supported adapter method + * @param array $arguments + * @return mixed + */ + public function callMethod(string $method, array $arguments); +} diff --git a/src/HttpClient/Contracts/MultiCurlAdapterInterface.php b/src/HttpClient/Contracts/MultiCurlAdapterInterface.php new file mode 100644 index 00000000..f04ed09e --- /dev/null +++ b/src/HttpClient/Contracts/MultiCurlAdapterInterface.php @@ -0,0 +1,48 @@ + $data + * @return mixed + */ + public function addGet(string $url, array $data = []); + + /** + * Adds POST request + * @param mixed $data + * @return mixed + */ + public function addPost(string $url, $data = '', bool $follow_303_with_post = false); + +} diff --git a/src/HttpClient/Enums/HttpClientType.php b/src/HttpClient/Enums/HttpClientType.php new file mode 100644 index 00000000..a57c3916 --- /dev/null +++ b/src/HttpClient/Enums/HttpClientType.php @@ -0,0 +1,27 @@ +resolve(); + } + + public function resolve(): HttpClient + { + if (!$this->instance) { + $this->instance = $this->createInstance(); + } + + return $this->instance; + } + + private function createInstance(): HttpClient + { + return new HttpClient(); + } +} diff --git a/src/HttpClient/HttpClient.php b/src/HttpClient/HttpClient.php index b864b3d2..84d1b626 100644 --- a/src/HttpClient/HttpClient.php +++ b/src/HttpClient/HttpClient.php @@ -10,9 +10,13 @@ namespace Quantum\HttpClient; +use Quantum\HttpClient\Contracts\HttpClientAdapterInterface; +use Quantum\HttpClient\Contracts\MultiCurlAdapterInterface; +use Quantum\HttpClient\Contracts\CurlAdapterInterface; use Quantum\HttpClient\Exceptions\HttpClientException; +use Quantum\HttpClient\Adapters\MultiCurlAdapter; +use Quantum\HttpClient\Adapters\CurlAdapter; use Quantum\App\Exceptions\BaseException; -use Curl\CaseInsensitiveArray; use Curl\MultiCurl; use ErrorException; use Curl\Curl; @@ -20,7 +24,6 @@ /** * HttpClient Class * @package Quantum\HttpClient - * @uses php-curl-class/php-curl-class * @method object addGet(string $url, array $data = []) * @method object addPost(string $url, string $data = '', bool $follow_303_with_post = false) * @method setHeader($key, $value) @@ -50,9 +53,9 @@ class HttpClient public const RESPONSE_BODY = 'body'; /** - * @var MultiCurl|Curl + * @var HttpClientAdapterInterface|null */ - private Curl|MultiCurl|null $client = null; + private ?HttpClientAdapterInterface $client = null; private string $method = 'GET'; @@ -68,12 +71,12 @@ class HttpClient private array $requestHeaders = []; /** - * @var array + * @var array */ private array $response = []; /** - * @var array + * @var array */ private array $errors = []; @@ -82,8 +85,11 @@ class HttpClient */ public function createRequest(string $url, ?Curl $client = null): HttpClient { - $this->client = $client ?: new Curl(); - $this->client->setUrl($url); + $adapter = new CurlAdapter($client); + $adapter->setUrl($url); + + $this->client = $adapter; + return $this; } @@ -92,12 +98,14 @@ public function createRequest(string $url, ?Curl $client = null): HttpClient */ public function createMultiRequest(?MultiCurl $client = null): HttpClient { - $this->client = $client ?: new MultiCurl(); + $adapter = new MultiCurlAdapter($client); - $this->client->complete(function (Curl $instance): void { + $adapter->complete(function (CurlAdapterInterface $instance): void { $this->handleResponse($instance); }); + $this->client = $adapter; + return $this; } @@ -106,14 +114,24 @@ public function createMultiRequest(?MultiCurl $client = null): HttpClient */ public function createAsyncMultiRequest(callable $success, callable $error, ?MultiCurl $client = null): HttpClient { - $this->client = $client ?: new MultiCurl(); + $adapter = new MultiCurlAdapter($client); - $this->client->success($success); - $this->client->error($error); + $adapter->success($success); + $adapter->error($error); + + $this->client = $adapter; return $this; } + /** + * Gets the current adapter + */ + public function getAdapter(): ?HttpClientAdapterInterface + { + return $this->client; + } + /** * Sets http method * @throws BaseException @@ -157,12 +175,12 @@ public function getData() /** * Checks if the request is multi cURL - * @phpstan-assert-if-true MultiCurl $this->client - * @phpstan-assert-if-false Curl $this->client + * @phpstan-assert-if-true MultiCurlAdapterInterface $this->client + * @phpstan-assert-if-false CurlAdapterInterface|null $this->client */ public function isMultiRequest(): bool { - return $this->client instanceof MultiCurl; + return $this->client instanceof MultiCurlAdapterInterface; } /** @@ -251,20 +269,36 @@ public function getResponseBody() /** * Gets the entire response - * @return array + * @return array */ public function getResponse(): array { - return $this->isMultiRequest() ? $this->response : ($this->response[$this->client->getId()] ?? []); + if ($this->client === null) { + return []; + } + + if ($this->isMultiRequest()) { + return $this->response; + } + + return $this->response[$this->client->getId()] ?? []; } /** * Returns the errors - * @return array + * @return array */ public function getErrors(): array { - return $this->isMultiRequest() ? $this->errors : ($this->errors[$this->client->getId()] ?? []); + if ($this->client === null) { + return []; + } + + if ($this->isMultiRequest()) { + return $this->errors; + } + + return $this->errors[$this->client->getId()] ?? []; } /** @@ -276,7 +310,7 @@ public function info(?int $option = null) { $this->ensureSingleRequest(); - return $option ? $this->client->getInfo($option) : $this->client->getInfo(); + return $option !== null ? $this->client->getInfo($option) : $this->client->getInfo(); } /** @@ -297,17 +331,17 @@ public function url(): ?string */ public function __call(string $method, array $arguments): HttpClient { - if (is_null($this->client)) { - throw HttpClientException::requestNotCreated(); - } + $this->ensureRequestCreated(); - if (!method_exists($this->client, $method)) { + if (!$this->client->supportsMethod($method)) { throw HttpClientException::methodNotSupported($method, $this->client::class); } $this->interceptCall($method, $arguments); - $this->client->$method(...$arguments); + $this->ensureRequestCreated(); + + $this->client->callMethod($method, $arguments); return $this; } @@ -325,14 +359,14 @@ private function startSingleRequest(): void $this->client->setOpt(CURLOPT_POSTFIELDS, $this->client->buildPostData($this->data)); } - $this->client->exec(); + $this->client->start(); $this->handleResponse($this->client); } /** * Handles the response */ - private function handleResponse(Curl $instance): void + private function handleResponse(CurlAdapterInterface $instance): void { if ($instance->isError()) { $this->errors[$instance->getId()] = [ @@ -349,9 +383,10 @@ private function handleResponse(Curl $instance): void } /** + * @param iterable $headers * @return array */ - private function formatHeaders(CaseInsensitiveArray $headers): array + private function formatHeaders(iterable $headers): array { $formatted = []; @@ -364,12 +399,25 @@ private function formatHeaders(CaseInsensitiveArray $headers): array /** * @throws BaseException - * @phpstan-assert Curl $this->client + * @phpstan-assert CurlAdapterInterface $this->client */ private function ensureSingleRequest(): void { + $this->ensureRequestCreated(); + if ($this->isMultiRequest()) { - throw HttpClientException::methodNotSupported(__METHOD__, MultiCurl::class); + throw HttpClientException::methodNotSupported(__METHOD__, MultiCurlAdapter::class); + } + } + + /** + * @throws HttpClientException + * @phpstan-assert HttpClientAdapterInterface $this->client + */ + private function ensureRequestCreated(): void + { + if ($this->client === null) { + throw HttpClientException::requestNotCreated(); } } diff --git a/tests/Unit/HttpClient/Adapters/CurlAdapterTest.php b/tests/Unit/HttpClient/Adapters/CurlAdapterTest.php new file mode 100644 index 00000000..af0cac78 --- /dev/null +++ b/tests/Unit/HttpClient/Adapters/CurlAdapterTest.php @@ -0,0 +1,92 @@ +shouldReceive('setUrl')->once()->with('https://example.com'); + $curl->shouldReceive('setHeader')->once()->with('Accept', 'application/json'); + $curl->shouldReceive('setHeaders')->once()->with(['X-Test' => 'yes']); + $curl->shouldReceive('setOpt')->once()->with(CURLOPT_TIMEOUT, 10); + $curl->shouldReceive('setOpts')->once()->with([CURLOPT_CONNECTTIMEOUT => 5]); + $curl->shouldReceive('buildPostData')->once()->with(['a' => 1])->andReturn('a=1'); + $curl->shouldReceive('exec')->once()->andReturn('ok'); + + $adapter = new CurlAdapter($curl); + + $this->assertSame($adapter, $adapter->setUrl('https://example.com')); + $this->assertSame($adapter, $adapter->setHeader('Accept', 'application/json')); + $this->assertSame($adapter, $adapter->setHeaders(['X-Test' => 'yes'])); + $this->assertSame($adapter, $adapter->setOpt(CURLOPT_TIMEOUT, 10)); + $this->assertSame($adapter, $adapter->setOpts([CURLOPT_CONNECTTIMEOUT => 5])); + $this->assertSame('a=1', $adapter->buildPostData(['a' => 1])); + $this->assertSame('ok', $adapter->start()); + } + + public function testCurlAdapterDelegatesResponseMethods(): void + { + $headers = new CaseInsensitiveArray(['Content-Type' => 'application/json']); + $response = (object) ['ok' => true]; + + $curl = Mockery::mock(Curl::class); + $curl->shouldReceive('getId')->once()->andReturn(7); + $curl->shouldReceive('isError')->once()->andReturn(false); + $curl->shouldReceive('getErrorCode')->once()->andReturn(0); + $curl->shouldReceive('getErrorMessage')->once()->andReturn(null); + $curl->shouldReceive('getResponseHeaders')->once()->andReturn($headers); + $curl->shouldReceive('getResponseCookies')->once()->andReturn(['sid' => 'abc']); + $curl->shouldReceive('getResponse')->once()->andReturn($response); + $curl->shouldReceive('getInfo')->with(CURLINFO_HTTP_CODE)->once()->andReturn(200); + $curl->shouldReceive('getUrl')->once()->andReturn('https://example.com'); + + $adapter = new CurlAdapter($curl); + + $this->assertSame(7, $adapter->getId()); + $this->assertFalse($adapter->isError()); + $this->assertSame(0, $adapter->getErrorCode()); + $this->assertNull($adapter->getErrorMessage()); + $this->assertSame($headers, $adapter->getResponseHeaders()); + $this->assertSame(['sid' => 'abc'], $adapter->getResponseCookies()); + $this->assertSame($response, $adapter->getResponse()); + $this->assertSame(200, $adapter->getInfo(CURLINFO_HTTP_CODE)); + $this->assertSame('https://example.com', $adapter->getUrl()); + } + + public function testCurlAdapterPassesZeroInfoOption(): void + { + $curl = Mockery::mock(Curl::class); + $curl->shouldReceive('getInfo')->with(0)->once()->andReturn('zero'); + + $adapter = new CurlAdapter($curl); + + $this->assertSame('zero', $adapter->getInfo(0)); + } + + public function testCurlAdapterSupportsAndCallsVendorMethods(): void + { + $curl = Mockery::mock(Curl::class); + $curl->shouldReceive('setTimeout')->once()->with(15)->andReturnNull(); + + $adapter = new CurlAdapter($curl); + + $this->assertTrue($adapter->supportsMethod('setTimeout')); + $this->assertFalse($adapter->supportsMethod('missingMethod')); + $this->assertNull($adapter->callMethod('setTimeout', [15])); + } +} diff --git a/tests/Unit/HttpClient/Adapters/MultiCurlAdapterTest.php b/tests/Unit/HttpClient/Adapters/MultiCurlAdapterTest.php new file mode 100644 index 00000000..f950bb45 --- /dev/null +++ b/tests/Unit/HttpClient/Adapters/MultiCurlAdapterTest.php @@ -0,0 +1,108 @@ +shouldReceive('setHeader')->once()->with('Accept', 'application/json'); + $multiCurl->shouldReceive('setHeaders')->once()->with(['X-Test' => 'yes']); + $multiCurl->shouldReceive('setOpt')->once()->with(CURLOPT_TIMEOUT, 10); + $multiCurl->shouldReceive('setOpts')->once()->with([CURLOPT_CONNECTTIMEOUT => 5]); + $multiCurl->shouldReceive('addGet')->once()->with('https://example.com', ['a' => 1])->andReturn($getCurl); + $multiCurl->shouldReceive('addPost')->once()->with('https://example.com', 'payload', true)->andReturn($postCurl); + $multiCurl->shouldReceive('start')->once()->andReturnNull(); + + $adapter = new MultiCurlAdapter($multiCurl); + + $this->assertSame($adapter, $adapter->setHeader('Accept', 'application/json')); + $this->assertSame($adapter, $adapter->setHeaders(['X-Test' => 'yes'])); + $this->assertSame($adapter, $adapter->setOpt(CURLOPT_TIMEOUT, 10)); + $this->assertSame($adapter, $adapter->setOpts([CURLOPT_CONNECTTIMEOUT => 5])); + $this->assertInstanceOf(CurlAdapter::class, $adapter->addGet('https://example.com', ['a' => 1])); + $this->assertInstanceOf(CurlAdapter::class, $adapter->addPost('https://example.com', 'payload', true)); + $this->assertNull($adapter->start()); + } + + public function testMultiCurlAdapterRegistersCallbacks(): void + { + $curl = Mockery::mock(Curl::class); + + $multiCurl = Mockery::mock(MultiCurl::class); + $multiCurl->shouldReceive('success') + ->once() + ->andReturnUsing(function (callable $callback) use ($curl): void { + $callback($curl); + }); + $multiCurl->shouldReceive('error') + ->once() + ->andReturnUsing(function (callable $callback) use ($curl): void { + $callback($curl); + }); + + $adapter = new MultiCurlAdapter($multiCurl); + $successWrapped = null; + $errorWrapped = null; + + $this->assertSame($adapter, $adapter->success(function (CurlAdapter $instance) use (&$successWrapped): void { + $successWrapped = $instance; + })); + $this->assertSame($adapter, $adapter->error(function (CurlAdapter $instance) use (&$errorWrapped): void { + $errorWrapped = $instance; + })); + $this->assertInstanceOf(CurlAdapter::class, $successWrapped); + $this->assertInstanceOf(CurlAdapter::class, $errorWrapped); + } + + public function testMultiCurlAdapterWrapsCompleteCallbackInstance(): void + { + $curl = Mockery::mock(Curl::class); + + $multiCurl = Mockery::mock(MultiCurl::class); + $multiCurl->shouldReceive('complete') + ->once() + ->andReturnUsing(function (callable $callback) use ($curl): void { + $callback($curl); + }); + + $adapter = new MultiCurlAdapter($multiCurl); + $wrapped = null; + + $this->assertSame($adapter, $adapter->complete(function (CurlAdapter $instance) use (&$wrapped): void { + $wrapped = $instance; + })); + + $this->assertInstanceOf(CurlAdapter::class, $wrapped); + } + + public function testMultiCurlAdapterSupportsDocumentedMethods(): void + { + $multiCurl = Mockery::mock(MultiCurl::class); + $multiCurl->shouldReceive('addGet')->once()->with('https://example.com', [])->andReturn((object) ['id' => 1]); + + $adapter = new MultiCurlAdapter($multiCurl); + + $this->assertTrue($adapter->supportsMethod('addGet')); + $this->assertFalse($adapter->supportsMethod('missingMethod')); + $this->assertEquals((object) ['id' => 1], $adapter->callMethod('addGet', ['https://example.com', []])); + } +} diff --git a/tests/Unit/HttpClient/Factories/HttpClientFactoryTest.php b/tests/Unit/HttpClient/Factories/HttpClientFactoryTest.php new file mode 100644 index 00000000..771f6cfa --- /dev/null +++ b/tests/Unit/HttpClient/Factories/HttpClientFactoryTest.php @@ -0,0 +1,51 @@ +resetHttpClientFactory(); + } + + public function testHttpClientFactoryInstance(): void + { + $this->assertInstanceOf(HttpClient::class, HttpClientFactory::get()); + } + + public function testHttpClientFactoryReturnsSameInstance(): void + { + $httpClient1 = HttpClientFactory::get(); + $httpClient2 = HttpClientFactory::get(); + + $this->assertSame($httpClient1, $httpClient2); + } + + public function testHttpClientFactoryResolveReturnsSameInstance(): void + { + $factory = Di::get(HttpClientFactory::class); + + $httpClient1 = $factory->resolve(); + $httpClient2 = $factory->resolve(); + + $this->assertSame($httpClient1, $httpClient2); + } + + private function resetHttpClientFactory(): void + { + if (!Di::isRegistered(HttpClientFactory::class)) { + Di::register(HttpClientFactory::class); + } + + $factory = Di::get(HttpClientFactory::class); + $this->setPrivateProperty($factory, 'instance', null); + } +} diff --git a/tests/Unit/HttpClient/HttpClientTest.php b/tests/Unit/HttpClient/HttpClientTest.php index c0ed19cd..abe3e22c 100644 --- a/tests/Unit/HttpClient/HttpClientTest.php +++ b/tests/Unit/HttpClient/HttpClientTest.php @@ -3,6 +3,8 @@ namespace Quantum\Tests\Unit\HttpClient; use Quantum\HttpClient\Exceptions\HttpClientException; +use Quantum\HttpClient\Adapters\MultiCurlAdapter; +use Quantum\HttpClient\Adapters\CurlAdapter; use Quantum\HttpClient\HttpClient; use Quantum\Tests\Unit\AppTestCase; use Curl\CaseInsensitiveArray; @@ -30,6 +32,8 @@ public function tearDown(): void public function testHttpClientGetSetMethod(): void { + $this->assertNull($this->httpClient->getAdapter()); + $this->assertEquals('GET', $this->httpClient->getMethod()); $this->httpClient->setMethod('POST'); @@ -66,9 +70,13 @@ public function testHttpClientIsMultiRequest(): void $this->assertFalse($this->httpClient->isMultiRequest()); + $this->assertInstanceOf(CurlAdapter::class, $this->httpClient->getAdapter()); + $this->httpClient->createMultiRequest($multi); $this->assertTrue($this->httpClient->isMultiRequest()); + + $this->assertInstanceOf(MultiCurlAdapter::class, $this->httpClient->getAdapter()); } public function testHttpClientRequestNotCreated(): void @@ -78,6 +86,13 @@ public function testHttpClientRequestNotCreated(): void $this->httpClient->start(); } + public function testHttpClientReturnsEmptyResponseAndErrorsBeforeRequestCreated(): void + { + $this->assertSame([], $this->httpClient->getResponse()); + + $this->assertSame([], $this->httpClient->getErrors()); + } + public function testHttpClientEnsureSingleRequestThrowsOnMulti(): void { $multi = Mockery::mock(MultiCurl::class); @@ -225,16 +240,37 @@ public function testHttpClientMultiRequestAggregatesErrors(): void public function testHttpClientCreateAsyncMultiRequestRegistersCallbacks(): void { - $success = fn () => null; - $error = fn () => null; + $curl = Mockery::mock(Curl::class); + $successWrapped = null; + $errorWrapped = null; + $success = function (CurlAdapter $instance) use (&$successWrapped): void { + $successWrapped = $instance; + }; + $error = function (CurlAdapter $instance) use (&$errorWrapped): void { + $errorWrapped = $instance; + }; $multi = Mockery::mock(MultiCurl::class); - $multi->shouldReceive('success')->once()->with($success)->andReturnSelf(); - $multi->shouldReceive('error')->once()->with($error)->andReturnSelf(); + $multi->shouldReceive('success') + ->once() + ->andReturnUsing(function (callable $callback) use ($curl): void { + $callback($curl); + }); + $multi->shouldReceive('error') + ->once() + ->andReturnUsing(function (callable $callback) use ($curl): void { + $callback($curl); + }); $this->httpClient->createAsyncMultiRequest($success, $error, $multi); $this->assertTrue($this->httpClient->isMultiRequest()); + + $this->assertInstanceOf(MultiCurlAdapter::class, $this->httpClient->getAdapter()); + + $this->assertInstanceOf(CurlAdapter::class, $successWrapped); + + $this->assertInstanceOf(CurlAdapter::class, $errorWrapped); } public function testHttpClientInfoAndUrl(): void @@ -261,4 +297,15 @@ public function testHttpClientInfoAndUrl(): void $this->assertEquals('https://example.com', $this->httpClient->url()); } + + public function testHttpClientPassesZeroInfoOption(): void + { + $curl = Mockery::mock(Curl::class); + $curl->shouldReceive('setUrl')->once(); + $curl->shouldReceive('getInfo')->with(0)->once()->andReturn('zero'); + + $this->httpClient->createRequest('https://example.com', $curl); + + $this->assertSame('zero', $this->httpClient->info(0)); + } }