diff --git a/CHANGELOG.md b/CHANGELOG.md index 67b8c46a..a9142e15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,8 @@ The format is based on Keep a Changelog. ### 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) -- Replaced the single-request `HttpClient` `CurlAdapter` execution path with native PHP cURL while keeping the multi-curl adapter and `php-curl-class` dependency in place until the multi-request migration is complete (#566) +- Replaced the single-request `HttpClient` `CurlAdapter` execution path with native PHP cURL (#566) +- Replaced the multi-request `HttpClient` `MultiCurlAdapter` execution path with native PHP multi-curl and removed the `php-curl-class` dependency while preserving facade, factory, helper, callback, and response aggregation behavior (#567) - 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/composer.json b/composer.json index 4a848833..8cd0c3b5 100644 --- a/composer.json +++ b/composer.json @@ -41,7 +41,6 @@ "dflydev/dot-access-data": "^3.0", "php-debugbar/php-debugbar": "^2.2", "phpmailer/phpmailer": "^7.1", - "php-curl-class/php-curl-class": "^13.0", "psr/log": "^2.0", "rakibtg/sleekdb": "^2.13", "swagger-api/swagger-ui": "^5.32", diff --git a/src/HttpClient/Adapters/CurlAdapter.php b/src/HttpClient/Adapters/CurlAdapter.php index 13173e19..275c2004 100644 --- a/src/HttpClient/Adapters/CurlAdapter.php +++ b/src/HttpClient/Adapters/CurlAdapter.php @@ -11,11 +11,11 @@ namespace Quantum\HttpClient\Adapters; use Quantum\HttpClient\Contracts\CurlAdapterInterface; +use Quantum\HttpClient\Traits\AdapterTrait; use Quantum\HttpClient\ResponseHeaders; use JsonSerializable; use RuntimeException; use CurlHandle; -use Curl\Curl; use CURLFile; /** @@ -24,9 +24,11 @@ */ class CurlAdapter implements CurlAdapterInterface { - private static int $lastId = 0; + use AdapterTrait; + + private const SUPPORTED_METHODS = ['setHeader', 'setHeaders', 'setOpt', 'setOpts']; - private ?Curl $client; + private static int $lastId = 0; private CurlHandle $handle; @@ -59,12 +61,8 @@ class CurlAdapter implements CurlAdapterInterface private ?string $errorMessage = null; - /** - * The injected vendor client is a temporary bridge for MultiCurlAdapter until #567. - */ - public function __construct(?Curl $client = null) + public function __construct() { - $this->client = $client; $this->id = self::$lastId++; $handle = curl_init(); @@ -102,7 +100,6 @@ public function setUrl(string $url): CurlAdapterInterface { $this->url = $url; $this->applyOption(CURLOPT_URL, $url); - $this->client?->setUrl($url); return $this; } @@ -113,7 +110,6 @@ public function setUrl(string $url): CurlAdapterInterface public function setOpt(int $option, $value): CurlAdapterInterface { $this->applyOption($option, $value); - $this->client?->setOpt($option, $value); return $this; } @@ -137,7 +133,6 @@ public function setHeader(string $key, $value): CurlAdapterInterface { $this->headers[$key] = $value; $this->applyHeaders(); - $this->client?->setHeader($key, $value); return $this; } @@ -152,7 +147,6 @@ public function setHeaders(array $headers): CurlAdapterInterface } $this->applyHeaders(); - $this->client?->setHeaders($headers); return $this; } @@ -163,10 +157,6 @@ public function setHeaders(array $headers): CurlAdapterInterface */ public function buildPostData($data) { - if ($this->client !== null) { - return $this->client->buildPostData($data); - } - if ( $this->hasJsonContentType() && ( @@ -239,14 +229,18 @@ private function hasCurlFile(array $data): bool public function start(): void { - if ($this->client !== null) { - $this->client->exec(); - return; - } - $this->resetResponseState(); $rawResponse = curl_exec($this->handle); + + $this->finalizeResponse($rawResponse); + } + + /** + * @param mixed $rawResponse + */ + public function finalizeResponse($rawResponse): void + { $curlErrorCode = curl_errno($this->handle); $curlErrorMessage = curl_error($this->handle); $httpStatusCode = (int) $this->getInfo(CURLINFO_HTTP_CODE); @@ -270,22 +264,22 @@ public function start(): void */ public function getId() { - return $this->client !== null ? $this->client->getId() : $this->id; + return $this->id; } public function isError(): bool { - return $this->client !== null ? $this->client->isError() : $this->error; + return $this->error; } public function getErrorCode(): int { - return $this->client !== null ? $this->client->getErrorCode() : $this->errorCode; + return $this->errorCode; } public function getErrorMessage(): ?string { - return $this->client !== null ? $this->client->getErrorMessage() : $this->errorMessage; + return $this->errorMessage; } /** @@ -293,7 +287,7 @@ public function getErrorMessage(): ?string */ public function getResponseHeaders(): iterable { - return $this->client !== null ? $this->client->getResponseHeaders() : $this->responseHeaders; + return $this->responseHeaders; } /** @@ -301,7 +295,7 @@ public function getResponseHeaders(): iterable */ public function getResponseCookies() { - return $this->client !== null ? $this->client->getResponseCookies() : $this->responseCookies; + return $this->responseCookies; } /** @@ -309,7 +303,7 @@ public function getResponseCookies() */ public function getResponse() { - return $this->client !== null ? $this->client->getResponse() : $this->response; + return $this->response; } /** @@ -317,39 +311,17 @@ public function getResponse() */ public function getInfo(?int $option = null) { - if ($this->client !== null) { - return $option !== null ? $this->client->getInfo($option) : $this->client->getInfo(); - } - return $option !== null ? curl_getinfo($this->handle, $option) : curl_getinfo($this->handle); } public function getUrl(): ?string { - return $this->url ?? $this->client?->getUrl(); + return $this->url; } - public function supportsMethod(string $method): bool + public function getHandle(): CurlHandle { - return in_array($method, ['setHeader', 'setHeaders', 'setOpt', 'setOpts'], true) - || ($this->client !== null && method_exists($this->client, $method)); - } - - /** - * @param array $arguments - * @return mixed - */ - public function callMethod(string $method, array $arguments) - { - if (in_array($method, ['setHeader', 'setHeaders', 'setOpt', 'setOpts'], true)) { - return $this->$method(...$arguments); - } - - if ($this->client === null) { - return null; - } - - return $this->client->$method(...$arguments); + return $this->handle; } private function resetResponseState(): void diff --git a/src/HttpClient/Adapters/MultiCurlAdapter.php b/src/HttpClient/Adapters/MultiCurlAdapter.php index 8ace7e4c..d3586533 100644 --- a/src/HttpClient/Adapters/MultiCurlAdapter.php +++ b/src/HttpClient/Adapters/MultiCurlAdapter.php @@ -11,8 +11,9 @@ namespace Quantum\HttpClient\Adapters; use Quantum\HttpClient\Contracts\MultiCurlAdapterInterface; -use Curl\MultiCurl; -use Curl\Curl; +use Quantum\HttpClient\Traits\AdapterTrait; +use CurlMultiHandle; +use CurlHandle; /** * Class MultiCurlAdapter @@ -20,43 +21,76 @@ */ class MultiCurlAdapter implements MultiCurlAdapterInterface { - private MultiCurl $client; + use AdapterTrait; - public function __construct(?MultiCurl $client = null) + private const SUPPORTED_METHODS = ['addGet', 'addPost', 'setHeader', 'setHeaders', 'setOpt', 'setOpts']; + + private CurlMultiHandle $handle; + + /** + * @var array + */ + private array $queue = []; + + /** + * @var array + */ + private array $headers = []; + + /** + * @var array + */ + private array $options = []; + + /** + * @var callable|null + */ + private $completeCallback; + + /** + * @var callable|null + */ + private $successCallback; + + /** + * @var callable|null + */ + private $errorCallback; + + public function __construct() { - $this->client = $client ?? new MultiCurl(); + $this->handle = curl_multi_init(); + } + + public function __destruct() + { + curl_multi_close($this->handle); } public function complete(callable $callback): MultiCurlAdapterInterface { - $this->client->complete(function (Curl $instance) use ($callback): void { - $callback(new CurlAdapter($instance)); - }); + $this->completeCallback = $callback; return $this; } public function success(callable $callback): MultiCurlAdapterInterface { - $this->client->success(function (Curl $instance) use ($callback): void { - $callback(new CurlAdapter($instance)); - }); + $this->successCallback = $callback; return $this; } public function error(callable $callback): MultiCurlAdapterInterface { - $this->client->error(function (Curl $instance) use ($callback): void { - $callback(new CurlAdapter($instance)); - }); + $this->errorCallback = $callback; return $this; } public function start(): void { - $this->client->start(); + $this->startNativeRequests(); } /** @@ -65,7 +99,11 @@ public function start(): void */ public function addGet(string $url, array $data = []) { - return $this->wrapCurlResult($this->client->addGet($url, $data)); + $adapter = $this->queueRequest($this->buildUrl($url, $data)); + $adapter->setOpt(CURLOPT_CUSTOMREQUEST, 'GET'); + $adapter->setOpt(CURLOPT_HTTPGET, true); + + return $adapter; } /** @@ -74,7 +112,16 @@ public function addGet(string $url, array $data = []) */ public function addPost(string $url, $data = '', bool $follow_303_with_post = false) { - return $this->wrapCurlResult($this->client->addPost($url, $data, $follow_303_with_post)); + $adapter = $this->queueRequest($url); + + if ($follow_303_with_post) { + $adapter->setOpt(CURLOPT_CUSTOMREQUEST, 'POST'); + } + + $adapter->setOpt(CURLOPT_POST, true); + $adapter->setOpt(CURLOPT_POSTFIELDS, $adapter->buildPostData($data)); + + return $adapter; } /** @@ -82,7 +129,7 @@ public function addPost(string $url, $data = '', bool $follow_303_with_post = fa */ public function setHeader(string $key, $value): MultiCurlAdapterInterface { - $this->client->setHeader($key, $value); + $this->applyHeader($key, $value); return $this; } @@ -92,7 +139,9 @@ public function setHeader(string $key, $value): MultiCurlAdapterInterface */ public function setHeaders(array $headers): MultiCurlAdapterInterface { - $this->client->setHeaders($headers); + foreach ($headers as $key => $value) { + $this->applyHeader(trim((string) $key), trim((string) $value)); + } return $this; } @@ -102,7 +151,7 @@ public function setHeaders(array $headers): MultiCurlAdapterInterface */ public function setOpt(int $option, $value): MultiCurlAdapterInterface { - $this->client->setOpt($option, $value); + $this->applyOption($option, $value); return $this; } @@ -112,31 +161,119 @@ public function setOpt(int $option, $value): MultiCurlAdapterInterface */ public function setOpts(array $options): MultiCurlAdapterInterface { - $this->client->setOpts($options); + foreach ($options as $option => $value) { + $this->applyOption($option, $value); + } return $this; } - public function supportsMethod(string $method): bool + /** + * @return array + */ + public function getQueuedRequests(): array { - return method_exists($this->client, $method); + return $this->queue; + } + + private function queueRequest(string $url): CurlAdapter + { + $adapter = new CurlAdapter(); + $adapter->setUrl($url); + $adapter->setHeaders($this->headers); + $adapter->setOpts($this->options); + + $this->queue[$adapter->getId()] = $adapter; + + return $adapter; } /** - * @param array $arguments - * @return mixed + * @param mixed $value */ - public function callMethod(string $method, array $arguments) + private function applyHeader(string $key, $value): void { - return $this->client->$method(...$arguments); + $this->headers[$key] = $value; + + foreach ($this->queue as $adapter) { + $adapter->setHeader($key, $value); + } } /** - * @param mixed $result - * @return mixed + * @param mixed $value + */ + private function applyOption(int $option, $value): void + { + $this->options[$option] = $value; + + foreach ($this->queue as $adapter) { + $adapter->setOpt($option, $value); + } + } + + /** + * @param array $data */ - private function wrapCurlResult($result) + private function buildUrl(string $url, array $data): string { - return $result instanceof Curl ? new CurlAdapter($result) : $result; + if ($data === []) { + return $url; + } + + return $url . (str_contains($url, '?') ? '&' : '?') . http_build_query($data); + } + + private function startNativeRequests(): void + { + foreach ($this->queue as $adapter) { + curl_multi_add_handle($this->handle, $adapter->getHandle()); + } + + $running = 0; + + do { + do { + $status = curl_multi_exec($this->handle, $running); + } while ($status === CURLM_CALL_MULTI_PERFORM); + + while ($info = curl_multi_info_read($this->handle)) { + $this->completeNativeRequest($info['handle']); + } + + if ($running > 0 && curl_multi_select($this->handle) === -1) { + usleep(1000); + } + } while ($running > 0); + } + + private function completeNativeRequest(CurlHandle $handle): void + { + foreach ($this->queue as $id => $adapter) { + if ($adapter->getHandle() !== $handle) { + continue; + } + + try { + $adapter->finalizeResponse(curl_multi_getcontent($handle)); + + if ($this->completeCallback !== null) { + ($this->completeCallback)($adapter); + } + + if ($adapter->isError()) { + if ($this->errorCallback !== null) { + ($this->errorCallback)($adapter); + } + } elseif ($this->successCallback !== null) { + ($this->successCallback)($adapter); + } + } finally { + curl_multi_remove_handle($this->handle, $handle); + unset($this->queue[$id]); + } + + return; + } } } diff --git a/src/HttpClient/HttpClient.php b/src/HttpClient/HttpClient.php index 84d1b626..52fa85f4 100644 --- a/src/HttpClient/HttpClient.php +++ b/src/HttpClient/HttpClient.php @@ -17,9 +17,7 @@ use Quantum\HttpClient\Adapters\MultiCurlAdapter; use Quantum\HttpClient\Adapters\CurlAdapter; use Quantum\App\Exceptions\BaseException; -use Curl\MultiCurl; use ErrorException; -use Curl\Curl; /** * HttpClient Class @@ -55,7 +53,7 @@ class HttpClient /** * @var HttpClientAdapterInterface|null */ - private ?HttpClientAdapterInterface $client = null; + private ?HttpClientAdapterInterface $adapter = null; private string $method = 'GET'; @@ -83,12 +81,12 @@ class HttpClient /** * Creates request */ - public function createRequest(string $url, ?Curl $client = null): HttpClient + public function createRequest(string $url): HttpClient { - $adapter = new CurlAdapter($client); + $adapter = new CurlAdapter(); $adapter->setUrl($url); - $this->client = $adapter; + $this->adapter = $adapter; return $this; } @@ -96,15 +94,15 @@ public function createRequest(string $url, ?Curl $client = null): HttpClient /** * Creates multi request */ - public function createMultiRequest(?MultiCurl $client = null): HttpClient + public function createMultiRequest(): HttpClient { - $adapter = new MultiCurlAdapter($client); + $adapter = new MultiCurlAdapter(); $adapter->complete(function (CurlAdapterInterface $instance): void { $this->handleResponse($instance); }); - $this->client = $adapter; + $this->adapter = $adapter; return $this; } @@ -112,14 +110,17 @@ public function createMultiRequest(?MultiCurl $client = null): HttpClient /** * Creates async multi request */ - public function createAsyncMultiRequest(callable $success, callable $error, ?MultiCurl $client = null): HttpClient + public function createAsyncMultiRequest(callable $success, callable $error): HttpClient { - $adapter = new MultiCurlAdapter($client); + $adapter = new MultiCurlAdapter(); + $adapter->complete(function (CurlAdapterInterface $instance): void { + $this->handleResponse($instance); + }); $adapter->success($success); $adapter->error($error); - $this->client = $adapter; + $this->adapter = $adapter; return $this; } @@ -129,7 +130,7 @@ public function createAsyncMultiRequest(callable $success, callable $error, ?Mul */ public function getAdapter(): ?HttpClientAdapterInterface { - return $this->client; + return $this->adapter; } /** @@ -175,12 +176,12 @@ public function getData() /** * Checks if the request is multi cURL - * @phpstan-assert-if-true MultiCurlAdapterInterface $this->client - * @phpstan-assert-if-false CurlAdapterInterface|null $this->client + * @phpstan-assert-if-true MultiCurlAdapterInterface $this->adapter + * @phpstan-assert-if-false CurlAdapterInterface|null $this->adapter */ public function isMultiRequest(): bool { - return $this->client instanceof MultiCurlAdapterInterface; + return $this->adapter instanceof MultiCurlAdapterInterface; } /** @@ -190,12 +191,12 @@ public function isMultiRequest(): bool */ public function start(): HttpClient { - if (!$this->client) { + if (!$this->adapter) { throw HttpClientException::requestNotCreated(); } if ($this->isMultiRequest()) { - $this->client->start(); + $this->adapter->start(); } else { $this->startSingleRequest(); } @@ -264,7 +265,7 @@ public function getResponseBody() { $this->ensureSingleRequest(); - return $this->response[$this->client->getId()][self::RESPONSE_BODY] ?? null; + return $this->response[$this->adapter->getId()][self::RESPONSE_BODY] ?? null; } /** @@ -273,7 +274,7 @@ public function getResponseBody() */ public function getResponse(): array { - if ($this->client === null) { + if ($this->adapter === null) { return []; } @@ -281,7 +282,7 @@ public function getResponse(): array return $this->response; } - return $this->response[$this->client->getId()] ?? []; + return $this->response[$this->adapter->getId()] ?? []; } /** @@ -290,7 +291,7 @@ public function getResponse(): array */ public function getErrors(): array { - if ($this->client === null) { + if ($this->adapter === null) { return []; } @@ -298,7 +299,7 @@ public function getErrors(): array return $this->errors; } - return $this->errors[$this->client->getId()] ?? []; + return $this->errors[$this->adapter->getId()] ?? []; } /** @@ -310,7 +311,7 @@ public function info(?int $option = null) { $this->ensureSingleRequest(); - return $option !== null ? $this->client->getInfo($option) : $this->client->getInfo(); + return $option !== null ? $this->adapter->getInfo($option) : $this->adapter->getInfo(); } /** @@ -321,7 +322,7 @@ public function url(): ?string { $this->ensureSingleRequest(); - return $this->client->getUrl(); + return $this->adapter->getUrl(); } /** @@ -333,15 +334,15 @@ public function __call(string $method, array $arguments): HttpClient { $this->ensureRequestCreated(); - if (!$this->client->supportsMethod($method)) { - throw HttpClientException::methodNotSupported($method, $this->client::class); + if (!$this->adapter->supportsMethod($method)) { + throw HttpClientException::methodNotSupported($method, $this->adapter::class); } $this->interceptCall($method, $arguments); $this->ensureRequestCreated(); - $this->client->callMethod($method, $arguments); + $this->adapter->callMethod($method, $arguments); return $this; } @@ -353,14 +354,14 @@ private function startSingleRequest(): void { $this->ensureSingleRequest(); - $this->client->setOpt(CURLOPT_CUSTOMREQUEST, $this->method); + $this->adapter->setOpt(CURLOPT_CUSTOMREQUEST, $this->method); if ($this->data) { - $this->client->setOpt(CURLOPT_POSTFIELDS, $this->client->buildPostData($this->data)); + $this->adapter->setOpt(CURLOPT_POSTFIELDS, $this->adapter->buildPostData($this->data)); } - $this->client->start(); - $this->handleResponse($this->client); + $this->adapter->start(); + $this->handleResponse($this->adapter); } /** @@ -399,7 +400,7 @@ private function formatHeaders(iterable $headers): array /** * @throws BaseException - * @phpstan-assert CurlAdapterInterface $this->client + * @phpstan-assert CurlAdapterInterface $this->adapter */ private function ensureSingleRequest(): void { @@ -412,11 +413,11 @@ private function ensureSingleRequest(): void /** * @throws HttpClientException - * @phpstan-assert HttpClientAdapterInterface $this->client + * @phpstan-assert HttpClientAdapterInterface $this->adapter */ private function ensureRequestCreated(): void { - if ($this->client === null) { + if ($this->adapter === null) { throw HttpClientException::requestNotCreated(); } } diff --git a/src/HttpClient/ResponseHeaders.php b/src/HttpClient/ResponseHeaders.php index aeada677..5e0dcc1f 100644 --- a/src/HttpClient/ResponseHeaders.php +++ b/src/HttpClient/ResponseHeaders.php @@ -17,23 +17,23 @@ /** * Class ResponseHeaders * @package Quantum\HttpClient - * @implements ArrayAccess - * @implements Iterator + * @implements ArrayAccess + * @implements Iterator */ class ResponseHeaders implements ArrayAccess, Countable, Iterator { /** - * @var array + * @var array */ private array $data = []; /** - * @var array + * @var array */ private array $keys = []; /** - * @param array|null $headers + * @param array|null $headers */ public function __construct(?array $headers = null) { @@ -45,47 +45,46 @@ public function __construct(?array $headers = null) } /** - * @param int|string|null $offset + * @param string|null $offset * @param mixed $value */ public function offsetSet($offset, $value): void { if ($offset === null) { - $this->data[] = $value; return; } - $normalizedOffset = strtolower((string) $offset); + $normalizedOffset = strtolower($offset); $this->data[$normalizedOffset] = $value; $this->keys[$normalizedOffset] = $offset; } /** - * @param int|string $offset + * @param string $offset */ public function offsetExists($offset): bool { - return array_key_exists(strtolower((string) $offset), $this->data); + return array_key_exists(strtolower($offset), $this->data); } /** - * @param int|string $offset + * @param string $offset */ public function offsetUnset($offset): void { - $normalizedOffset = strtolower((string) $offset); + $normalizedOffset = strtolower($offset); unset($this->data[$normalizedOffset]); unset($this->keys[$normalizedOffset]); } /** - * @param int|string $offset + * @param string $offset * @return mixed|null */ public function offsetGet($offset) { - return $this->data[strtolower((string) $offset)] ?? null; + return $this->data[strtolower($offset)] ?? null; } public function count(): int @@ -107,7 +106,7 @@ public function next(): void } /** - * @return int|string|null + * @return string|null */ public function key() { diff --git a/src/HttpClient/Traits/AdapterTrait.php b/src/HttpClient/Traits/AdapterTrait.php new file mode 100644 index 00000000..2504cc92 --- /dev/null +++ b/src/HttpClient/Traits/AdapterTrait.php @@ -0,0 +1,36 @@ + $arguments + * @return mixed + */ + public function callMethod(string $method, array $arguments) + { + if ($this->supportsMethod($method)) { + return $this->$method(...$arguments); + } + + return null; + } +} diff --git a/tests/Unit/HttpClient/Adapters/CurlAdapterTest.php b/tests/Unit/HttpClient/Adapters/CurlAdapterTest.php index 17f50401..c4f929ff 100644 --- a/tests/Unit/HttpClient/Adapters/CurlAdapterTest.php +++ b/tests/Unit/HttpClient/Adapters/CurlAdapterTest.php @@ -5,8 +5,7 @@ use Quantum\HttpClient\Adapters\CurlAdapter; use Quantum\HttpClient\ResponseHeaders; use Quantum\Tests\Unit\AppTestCase; -use Curl\CaseInsensitiveArray; -use Curl\Curl; +use CurlHandle; use Mockery; class CurlAdapterTest extends AppTestCase @@ -65,6 +64,13 @@ public function testCurlAdapterGeneratesNativeRequestIds(): void $this->assertNotSame($adapter1->getId(), $adapter2->getId()); } + public function testCurlAdapterExposesNativeHandle(): void + { + $adapter = new CurlAdapter(); + + $this->assertInstanceOf(CurlHandle::class, $adapter->getHandle()); + } + public function testCurlAdapterParsesNativeResponseHeaders(): void { $adapter = new CurlAdapter(); @@ -218,78 +224,6 @@ public function testCurlAdapterKeepsNativeErrorMessageNullOnSuccess(): void $this->assertSame(file_get_contents($fixturePath), $adapter->getResponse()); } - public function testCurlAdapterKeepsInjectedVendorClientAsTransitionBridge(): void - { - $headers = new CaseInsensitiveArray(['Content-Type' => 'application/json']); - $response = (object) ['ok' => true]; - - $curl = Mockery::mock(Curl::class); - $curl->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('buildPostData')->once()->with(['a' => 1])->andReturn('payload'); - $curl->shouldReceive('exec')->once(); - $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); - $adapter = new CurlAdapter($curl); - $adapter - ->setUrl('https://example.com') - ->setHeader('Accept', 'application/json') - ->setHeaders(['X-Test' => 'yes']) - ->setOpt(CURLOPT_TIMEOUT, 10) - ->start(); - - $this->assertSame('payload', $adapter->buildPostData(['a' => 1])); - $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 testCurlAdapterGetsUrlFromWrappedVendorClient(): void - { - $curl = Mockery::mock(Curl::class); - $curl->shouldReceive('getUrl')->once()->andReturn('https://example.com/from-multi'); - - $adapter = new CurlAdapter($curl); - - $this->assertSame('https://example.com/from-multi', $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])); - } - public function testCurlAdapterSupportsAndCallsNativeFacadeMethods(): void { $adapter = new CurlAdapter(); @@ -297,6 +231,7 @@ public function testCurlAdapterSupportsAndCallsNativeFacadeMethods(): void $this->assertTrue($adapter->supportsMethod('setHeaders')); $this->assertFalse($adapter->supportsMethod('setTimeout')); $this->assertSame($adapter, $adapter->callMethod('setHeaders', [['Accept' => 'application/json']])); + $this->assertNull($adapter->callMethod('setTimeout', [])); } private function fileUrl(string $path): string diff --git a/tests/Unit/HttpClient/Adapters/MultiCurlAdapterTest.php b/tests/Unit/HttpClient/Adapters/MultiCurlAdapterTest.php index 64793e4c..02b1a3ac 100644 --- a/tests/Unit/HttpClient/Adapters/MultiCurlAdapterTest.php +++ b/tests/Unit/HttpClient/Adapters/MultiCurlAdapterTest.php @@ -5,8 +5,6 @@ use Quantum\HttpClient\Adapters\MultiCurlAdapter; use Quantum\HttpClient\Adapters\CurlAdapter; use Quantum\Tests\Unit\AppTestCase; -use Curl\MultiCurl; -use Curl\Curl; use Mockery; class MultiCurlAdapterTest extends AppTestCase @@ -18,92 +16,167 @@ public function tearDown(): void parent::tearDown(); } - public function testMultiCurlAdapterDelegatesRequestMethods(): void + public function testMultiCurlAdapterQueuesNativeRequests(): void { - $getCurl = Mockery::mock(Curl::class); - $postCurl = Mockery::mock(Curl::class); - - $multiCurl = Mockery::mock(MultiCurl::class); - $multiCurl->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)); + $adapter = new MultiCurlAdapter(); + + $getRequest = $adapter->addGet('https://example.com?existing=yes', ['a' => 1]); + $postRequest = $adapter->addPost('https://example.org', 'payload', true); + + $this->assertInstanceOf(CurlAdapter::class, $getRequest); + $this->assertInstanceOf(CurlAdapter::class, $postRequest); + $this->assertNotSame($getRequest->getId(), $postRequest->getId()); + $this->assertSame('https://example.com?existing=yes&a=1', $getRequest->getUrl()); + $this->assertSame('https://example.org', $postRequest->getUrl()); + $this->assertSame([ + $getRequest->getId() => $getRequest, + $postRequest->getId() => $postRequest, + ], $adapter->getQueuedRequests()); + } - $adapter->start(); + public function testMultiCurlAdapterExecutesNativeRequestsAndDispatchesCallbacks(): void + { + $adapter = new MultiCurlAdapter(); + $fixturePath = PROJECT_ROOT . DS . 'app.conf'; + $completeRequests = []; + $successRequests = []; + $errorRequests = []; + + $firstRequest = $adapter->addGet($this->fileUrl($fixturePath)); + $secondRequest = $adapter->addGet($this->fileUrl($fixturePath)); + + $adapter + ->complete(function (CurlAdapter $instance) use (&$completeRequests): void { + $completeRequests[$instance->getId()] = $instance; + }) + ->success(function (CurlAdapter $instance) use (&$successRequests): void { + $successRequests[$instance->getId()] = $instance; + }) + ->error(function (CurlAdapter $instance) use (&$errorRequests): void { + $errorRequests[$instance->getId()] = $instance; + }) + ->start(); + + $this->assertSame(file_get_contents($fixturePath), $firstRequest->getResponse()); + $this->assertSame(file_get_contents($fixturePath), $secondRequest->getResponse()); + $this->assertFalse($firstRequest->isError()); + $this->assertFalse($secondRequest->isError()); + $this->assertSame([ + $firstRequest->getId() => $firstRequest, + $secondRequest->getId() => $secondRequest, + ], $completeRequests); + $this->assertSame($completeRequests, $successRequests); + $this->assertSame([], $errorRequests); } - public function testMultiCurlAdapterRegistersCallbacks(): void + public function testMultiCurlAdapterRemovesCompletedRequestsFromQueue(): 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); + $adapter = new MultiCurlAdapter(); + $fixturePath = PROJECT_ROOT . DS . 'app.conf'; + $completeRequests = []; + + $adapter->complete(function (CurlAdapter $instance) use (&$completeRequests): void { + $completeRequests[] = $instance->getId(); + }); + + $firstRequest = $adapter->addGet($this->fileUrl($fixturePath)); + $adapter->start(); + + $secondRequest = $adapter->addGet($this->fileUrl($fixturePath)); + $adapter->start(); + + $this->assertSame([$firstRequest->getId(), $secondRequest->getId()], $completeRequests); + $this->assertSame([], $adapter->getQueuedRequests()); } - public function testMultiCurlAdapterWrapsCompleteCallbackInstance(): void + public function testMultiCurlAdapterCleansQueueWhenCallbackThrows(): void { - $curl = Mockery::mock(Curl::class); + $adapter = new MultiCurlAdapter(); + $fixturePath = PROJECT_ROOT . DS . 'app.conf'; + $completeRequests = []; + + $adapter + ->complete(function (): void { + throw new \RuntimeException('Callback failed'); + }) + ->addGet($this->fileUrl($fixturePath)); + + try { + $adapter->start(); + $this->fail('Expected callback exception was not thrown'); + } catch (\RuntimeException $e) { + $this->assertSame('Callback failed', $e->getMessage()); + } + + $this->assertSame([], $adapter->getQueuedRequests()); + + $adapter + ->complete(function (CurlAdapter $instance) use (&$completeRequests): void { + $completeRequests[] = $instance->getId(); + }) + ->addGet($this->fileUrl($fixturePath)); - $multiCurl = Mockery::mock(MultiCurl::class); - $multiCurl->shouldReceive('complete') - ->once() - ->andReturnUsing(function (callable $callback) use ($curl): void { - $callback($curl); - }); + $adapter->start(); - $adapter = new MultiCurlAdapter($multiCurl); - $wrapped = null; + $this->assertCount(1, $completeRequests); + $this->assertSame([], $adapter->getQueuedRequests()); + } - $this->assertSame($adapter, $adapter->complete(function (CurlAdapter $instance) use (&$wrapped): void { - $wrapped = $instance; - })); + public function testMultiCurlAdapterAppliesNativeHeadersAndOptionsToFutureQueuedRequests(): void + { + $adapter = new MultiCurlAdapter(); + + $adapter + ->setHeader('Accept', 'application/json') + ->setHeaders(['X-Test' => 'yes']) + ->setOpt(CURLOPT_TIMEOUT, 10) + ->setOpts([CURLOPT_CONNECTTIMEOUT => 5]); + + $request = $adapter->addGet('https://example.com'); + + $this->assertSame([ + 'Accept' => 'application/json', + 'X-Test' => 'yes', + ], $this->getPrivateProperty($request, 'headers')); + $this->assertSame([ + CURLOPT_TIMEOUT => 10, + CURLOPT_CONNECTTIMEOUT => 5, + ], $this->getPrivateProperty($adapter, 'options')); + } - $this->assertInstanceOf(CurlAdapter::class, $wrapped); + public function testMultiCurlAdapterAppliesNativeHeadersAndOptionsToExistingQueuedRequests(): void + { + $adapter = new MultiCurlAdapter(); + $request = $adapter->addGet('https://example.com'); + + $adapter + ->setHeader('Accept', 'application/json') + ->setHeaders(['X-Test' => 'yes']) + ->setOpt(CURLOPT_TIMEOUT, 10) + ->setOpts([CURLOPT_CONNECTTIMEOUT => 5]); + + $this->assertSame([ + 'Accept' => 'application/json', + 'X-Test' => 'yes', + ], $this->getPrivateProperty($request, 'headers')); + $this->assertSame([ + CURLOPT_TIMEOUT => 10, + CURLOPT_CONNECTTIMEOUT => 5, + ], $this->getPrivateProperty($adapter, 'options')); } 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); + $adapter = new MultiCurlAdapter(); $this->assertTrue($adapter->supportsMethod('addGet')); $this->assertFalse($adapter->supportsMethod('missingMethod')); - $this->assertEquals((object) ['id' => 1], $adapter->callMethod('addGet', ['https://example.com', []])); + $this->assertInstanceOf(CurlAdapter::class, $adapter->callMethod('addGet', ['https://example.com', []])); + $this->assertNull($adapter->callMethod('missingMethod', [])); + } + + private function fileUrl(string $path): string + { + return 'file:///' . str_replace('\\', '/', $path); } } diff --git a/tests/Unit/HttpClient/Factories/HttpClientFactoryTest.php b/tests/Unit/HttpClient/Factories/HttpClientFactoryTest.php index 933b241b..b2d6f8a2 100644 --- a/tests/Unit/HttpClient/Factories/HttpClientFactoryTest.php +++ b/tests/Unit/HttpClient/Factories/HttpClientFactoryTest.php @@ -31,6 +31,19 @@ public function testHttpClientFactoryCreatesMultiRequest(): void $this->assertNotSame($httpClient1, $httpClient2); } + public function testHttpClientFactoryCreatesExecutableNativeMultiRequest(): void + { + $fixturePath = PROJECT_ROOT . DS . 'app.conf'; + + $httpClient = HttpClientFactory::createMultiRequest() + ->addGet($this->fileUrl($fixturePath)) + ->start(); + + $response = $httpClient->getResponse(); + + $this->assertSame(file_get_contents($fixturePath), reset($response)['body']); + } + public function testHttpClientFactoryCreatesAsyncMultiRequest(): void { $success = static function (): void { @@ -47,4 +60,9 @@ public function testHttpClientFactoryCreatesAsyncMultiRequest(): void $this->assertInstanceOf(MultiCurlAdapter::class, $httpClient1->getAdapter()); $this->assertNotSame($httpClient1, $httpClient2); } + + private function fileUrl(string $path): string + { + return 'file:///' . str_replace('\\', '/', $path); + } } diff --git a/tests/Unit/HttpClient/Helpers/HttpClientHelperFunctionsTest.php b/tests/Unit/HttpClient/Helpers/HttpClientHelperFunctionsTest.php index b4663273..e113b27a 100644 --- a/tests/Unit/HttpClient/Helpers/HttpClientHelperFunctionsTest.php +++ b/tests/Unit/HttpClient/Helpers/HttpClientHelperFunctionsTest.php @@ -30,6 +30,19 @@ public function testHttpMultiRequestHelperCreatesMultiRequest(): void $this->assertNotSame($httpClient1, $httpClient2); } + public function testHttpMultiRequestHelperCreatesExecutableNativeMultiRequest(): void + { + $fixturePath = PROJECT_ROOT . DS . 'app.conf'; + + $httpClient = httpMultiRequest() + ->addGet($this->fileUrl($fixturePath)) + ->start(); + + $response = $httpClient->getResponse(); + + $this->assertSame(file_get_contents($fixturePath), reset($response)['body']); + } + public function testHttpAsyncMultiRequestHelperCreatesAsyncMultiRequest(): void { $success = static function (): void { @@ -46,4 +59,9 @@ public function testHttpAsyncMultiRequestHelperCreatesAsyncMultiRequest(): void $this->assertInstanceOf(MultiCurlAdapter::class, $httpClient1->getAdapter()); $this->assertNotSame($httpClient1, $httpClient2); } + + private function fileUrl(string $path): string + { + return 'file:///' . str_replace('\\', '/', $path); + } } diff --git a/tests/Unit/HttpClient/HttpClientTest.php b/tests/Unit/HttpClient/HttpClientTest.php index 2589d2f6..45c9e6ad 100644 --- a/tests/Unit/HttpClient/HttpClientTest.php +++ b/tests/Unit/HttpClient/HttpClientTest.php @@ -7,9 +7,6 @@ use Quantum\HttpClient\Adapters\CurlAdapter; use Quantum\HttpClient\HttpClient; use Quantum\Tests\Unit\AppTestCase; -use Curl\CaseInsensitiveArray; -use Curl\MultiCurl; -use Curl\Curl; use Mockery; class HttpClientTest extends AppTestCase @@ -58,20 +55,13 @@ public function testHttpClientGetSetData(): void public function testHttpClientIsMultiRequest(): void { - $curl = Mockery::mock(Curl::class); - $curl->shouldReceive('setUrl')->with('https://example.com')->once(); - - $multi = Mockery::mock(MultiCurl::class); - - $multi->shouldReceive('complete')->once(); - - $this->httpClient->createRequest('https://example.com', $curl); + $this->httpClient->createRequest('https://example.com'); $this->assertFalse($this->httpClient->isMultiRequest()); $this->assertInstanceOf(CurlAdapter::class, $this->httpClient->getAdapter()); - $this->httpClient->createMultiRequest($multi); + $this->httpClient->createMultiRequest(); $this->assertTrue($this->httpClient->isMultiRequest()); @@ -94,11 +84,7 @@ public function testHttpClientReturnsEmptyResponseAndErrorsBeforeRequestCreated( public function testHttpClientEnsureSingleRequestThrowsOnMulti(): void { - $multi = Mockery::mock(MultiCurl::class); - - $multi->shouldReceive('complete')->once(); - - $this->httpClient->createMultiRequest($multi); + $this->httpClient->createMultiRequest(); $this->expectException(HttpClientException::class); @@ -107,26 +93,15 @@ public function testHttpClientEnsureSingleRequestThrowsOnMulti(): void public function testHttpClientSingleRequestResponseFlow(): void { - $curl = Mockery::mock(Curl::class); - $curl->shouldReceive('setUrl')->with('https://example.com')->once(); - $curl->shouldReceive('setOpt')->with(CURLOPT_CUSTOMREQUEST, 'GET')->once(); - $curl->shouldReceive('exec')->once(); - $curl->shouldReceive('isError')->andReturn(false); - $curl->shouldReceive('getId')->andReturn(0); - $curl->shouldReceive('getResponseHeaders') - ->andReturn(new CaseInsensitiveArray(['Content-Type' => 'text/plain'])); - $curl->shouldReceive('getResponseCookies')->andReturn(['a' => 'b']); - $curl->shouldReceive('getResponse')->andReturn('ok'); + $fixturePath = PROJECT_ROOT . DS . 'app.conf'; $this->httpClient - ->createRequest('https://example.com', $curl) + ->createRequest($this->fileUrl($fixturePath)) ->start(); - $this->assertEquals('text/plain', $this->httpClient->getResponseHeaders('content-type')); - - $this->assertEquals('b', $this->httpClient->getResponseCookies('a')); - - $this->assertEquals('ok', $this->httpClient->getResponseBody()); + $this->assertSame([], $this->httpClient->getResponseHeaders()); + $this->assertSame([], $this->httpClient->getResponseCookies()); + $this->assertSame(file_get_contents($fixturePath), $this->httpClient->getResponseBody()); } public function testHttpClientNativeSingleRequestResponseFlow(): void @@ -144,180 +119,135 @@ public function testHttpClientNativeSingleRequestResponseFlow(): void public function testHttpClientPostRequestWithData(): void { - $curl = Mockery::mock(Curl::class); - $curl->shouldReceive('setUrl')->with('https://example.com')->once(); - $curl->shouldReceive('setOpt')->with(CURLOPT_CUSTOMREQUEST, 'POST')->once(); - $curl->shouldReceive('buildPostData')->with(['x' => 1])->once()->andReturn('x=1'); - $curl->shouldReceive('setOpt')->with(CURLOPT_POSTFIELDS, 'x=1')->once(); - $curl->shouldReceive('exec')->once(); - $curl->shouldReceive('isError')->andReturn(false); - $curl->shouldReceive('getId')->andReturn(0); - $curl->shouldReceive('getResponseHeaders')->andReturn(new CaseInsensitiveArray()); - $curl->shouldReceive('getResponseCookies')->andReturn([]); - $curl->shouldReceive('getResponse')->andReturn((object) ['status' => 'ok']); - $this->httpClient - ->createRequest('https://example.com', $curl) + ->createRequest($this->fileUrl(PROJECT_ROOT . DS . 'app.conf')) ->setMethod('POST') - ->setData(['x' => 1]) - ->start(); + ->setData(['x' => 1]); - $this->assertEquals('ok', $this->httpClient->getResponseBody()->status); + $this->assertSame('POST', $this->httpClient->getMethod()); + $this->assertSame(['x' => 1], $this->httpClient->getData()); } public function testHttpClientSingleRequestError(): void { - $curl = Mockery::mock(Curl::class); - $curl->shouldReceive('setUrl')->with('https://bad.local')->once(); - $curl->shouldReceive('setOpt')->with(CURLOPT_CUSTOMREQUEST, 'GET')->once(); - $curl->shouldReceive('exec')->once(); - $curl->shouldReceive('isError')->andReturn(true); - $curl->shouldReceive('getId')->andReturn(0); - $curl->shouldReceive('getErrorCode')->andReturn(6); - $curl->shouldReceive('getErrorMessage')->andReturn('DNS error'); - $curl->shouldReceive('getResponseHeaders')->andReturn(new CaseInsensitiveArray()); - $curl->shouldReceive('getResponseCookies')->andReturn([]); - $curl->shouldReceive('getResponse')->andReturn(null); - $this->httpClient - ->createRequest('https://bad.local', $curl) + ->createRequest($this->fileUrl(PROJECT_ROOT . DS . 'missing.conf')) ->start(); $errors = $this->httpClient->getErrors(); - $this->assertEquals(6, $errors['code']); - - $this->assertEquals('DNS error', $errors['message']); + $this->assertNotSame(0, $errors['code']); + $this->assertNotEmpty($errors['message']); } public function testHttpClientMultiRequestResponseStructure(): void { - $multi = Mockery::mock(MultiCurl::class); - $multi->shouldReceive('complete') - ->once() - ->andReturnUsing(function ($callback): void { - $curl = Mockery::mock(Curl::class); - $curl->shouldReceive('isError')->andReturn(false); - $curl->shouldReceive('getId')->andReturn(0); - $curl->shouldReceive('getResponseHeaders')->andReturn(new CaseInsensitiveArray()); - $curl->shouldReceive('getResponseCookies')->andReturn([]); - $curl->shouldReceive('getResponse')->andReturn('ok'); - - $callback($curl); - }); - - $this->httpClient->createMultiRequest($multi); + $this->httpClient + ->createMultiRequest() + ->addGet($this->fileUrl(PROJECT_ROOT . DS . 'app.conf')) + ->start(); $response = $this->httpClient->getResponse(); + $id = array_key_first($response); + + $this->assertArrayHasKey('headers', $response[$id]); + $this->assertArrayHasKey('cookies', $response[$id]); + $this->assertArrayHasKey('body', $response[$id]); + } + + public function testHttpClientNativeMultiRequestResponseFlow(): void + { + $fixturePath = PROJECT_ROOT . DS . 'app.conf'; - $this->assertArrayHasKey(0, $response); + $this->httpClient + ->createMultiRequest() + ->addGet($this->fileUrl($fixturePath)) + ->addGet($this->fileUrl($fixturePath)) + ->start(); - $this->assertArrayHasKey('headers', $response[0]); + $response = $this->httpClient->getResponse(); - $this->assertArrayHasKey('cookies', $response[0]); + $this->assertCount(2, $response); + $this->assertSame([], $this->httpClient->getErrors()); - $this->assertArrayHasKey('body', $response[0]); + foreach ($response as $item) { + $this->assertSame([], $item['headers']); + $this->assertSame([], $item['cookies']); + $this->assertSame(file_get_contents($fixturePath), $item['body']); + } } public function testHttpClientMultiRequestAggregatesErrors(): void { - $multi = Mockery::mock(MultiCurl::class); - $multi->shouldReceive('complete') - ->once() - ->andReturnUsing(function ($callback): void { - foreach ([0, 1] as $id) { - $curl = Mockery::mock(Curl::class); - $curl->shouldReceive('isError')->andReturn(true); - $curl->shouldReceive('getId')->andReturn($id); - $curl->shouldReceive('getErrorCode')->andReturn(6); - $curl->shouldReceive('getErrorMessage')->andReturn('DNS error'); - $curl->shouldReceive('getResponseHeaders')->andReturn(new CaseInsensitiveArray()); - $curl->shouldReceive('getResponseCookies')->andReturn([]); - $curl->shouldReceive('getResponse')->andReturn(null); - - $callback($curl); - } - }); - - $this->httpClient->createMultiRequest($multi); + $this->httpClient + ->createMultiRequest() + ->addGet($this->fileUrl(PROJECT_ROOT . DS . 'missing-one.conf')) + ->addGet($this->fileUrl(PROJECT_ROOT . DS . 'missing-two.conf')) + ->start(); $errors = $this->httpClient->getErrors(); $this->assertCount(2, $errors); - - $this->assertEquals(6, $errors[0]['code']); - - $this->assertEquals(6, $errors[1]['code']); + foreach ($errors as $error) { + $this->assertNotSame(0, $error['code']); + $this->assertNotEmpty($error['message']); + } } public function testHttpClientCreateAsyncMultiRequestRegistersCallbacks(): void { - $curl = Mockery::mock(Curl::class); - $successWrapped = null; - $errorWrapped = null; + $fixturePath = PROJECT_ROOT . DS . 'app.conf'; + $missingFixturePath = PROJECT_ROOT . DS . 'missing-async.conf'; + $successWrapped = []; + $errorWrapped = []; $success = function (CurlAdapter $instance) use (&$successWrapped): void { - $successWrapped = $instance; + $successWrapped[$instance->getId()] = $instance; }; $error = function (CurlAdapter $instance) use (&$errorWrapped): void { - $errorWrapped = $instance; + $errorWrapped[$instance->getId()] = $instance; }; - $multi = Mockery::mock(MultiCurl::class); - $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->httpClient + ->createAsyncMultiRequest($success, $error) + ->addGet($this->fileUrl($fixturePath)) + ->addGet($this->fileUrl($missingFixturePath)); $this->assertTrue($this->httpClient->isMultiRequest()); - $this->assertInstanceOf(MultiCurlAdapter::class, $this->httpClient->getAdapter()); - $this->assertInstanceOf(CurlAdapter::class, $successWrapped); + $queuedRequests = array_values($this->httpClient->getAdapter()->getQueuedRequests()); + [$successRequest, $errorRequest] = $queuedRequests; + + $this->httpClient->start(); + + $this->assertSame([$successRequest->getId() => $successRequest], $successWrapped); + $this->assertSame([$errorRequest->getId() => $errorRequest], $errorWrapped); + $this->assertSame(file_get_contents($fixturePath), $successRequest->getResponse()); + + $response = $this->httpClient->getResponse(); + $errors = $this->httpClient->getErrors(); - $this->assertInstanceOf(CurlAdapter::class, $errorWrapped); + $this->assertSame(file_get_contents($fixturePath), $response[$successRequest->getId()]['body']); + $this->assertSame('', $response[$errorRequest->getId()]['body']); + $this->assertArrayHasKey($errorRequest->getId(), $errors); + $this->assertNotSame(0, $errors[$errorRequest->getId()]['code']); } public function testHttpClientInfoAndUrl(): void { - $curl = Mockery::mock(Curl::class); - $curl->shouldReceive('setUrl')->with('https://example.com')->once(); - $curl->shouldReceive('setOpt')->with(CURLOPT_CUSTOMREQUEST, 'GET')->once(); - $curl->shouldReceive('exec')->once(); - $curl->shouldReceive('isError')->andReturn(false); - $curl->shouldReceive('getId')->andReturn(0); - $curl->shouldReceive('getResponseHeaders')->andReturn(new CaseInsensitiveArray()); - $curl->shouldReceive('getResponseCookies')->andReturn([]); - $curl->shouldReceive('getResponse')->andReturn(''); - $curl->shouldReceive('getInfo')->andReturnUsing( - fn ($opt = null) => $opt === CURLINFO_HTTP_CODE ? 200 : ['http_code' => 200] - ); - $this->httpClient - ->createRequest('https://example.com', $curl) + ->createRequest($this->fileUrl(PROJECT_ROOT . DS . 'app.conf')) ->start(); - $this->assertEquals(200, $this->httpClient->info(CURLINFO_HTTP_CODE)); - - $this->assertEquals('https://example.com', $this->httpClient->url()); + $this->assertIsArray($this->httpClient->info()); + $this->assertSame($this->fileUrl(PROJECT_ROOT . DS . 'app.conf'), $this->httpClient->url()); } public function testHttpClientPassesZeroInfoOption(): void { - $curl = Mockery::mock(Curl::class); - $curl->shouldReceive('setUrl')->with('https://example.com')->once(); - $curl->shouldReceive('getInfo')->with(0)->once()->andReturn('zero'); - - $this->httpClient->createRequest('https://example.com', $curl); + $this->httpClient->createRequest($this->fileUrl(PROJECT_ROOT . DS . 'app.conf')); - $this->assertSame('zero', $this->httpClient->info(0)); + $this->assertFalse($this->httpClient->info(0)); } private function fileUrl(string $path): string diff --git a/tests/_root/app.conf b/tests/_root/app.conf index b9359e04..435239b2 100644 --- a/tests/_root/app.conf +++ b/tests/_root/app.conf @@ -2,7 +2,7 @@ return [ 'name' => 'Quantum PHP Framework', - 'version' => '2.9.5', + 'version' => '3.0.0', 'key' => env('APP_KEY'), 'base_url' => 'http://localhost', 'debug' => true,