diff --git a/README.md b/README.md index 73151d6..d1f71a7 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ FetchPHP provides three main functions: ### **Custom Guzzle Client Usage** -By default, FetchPHP uses a single instance of the Guzzle client shared across all requests. However, you can provide your own Guzzle client through the `options` parameter of both `fetch` and `fetch_async`. This gives you full control over the client configuration, including base URI, headers, timeouts, and more. +By default, FetchPHP uses a singleton instance of the Guzzle client shared across all requests. However, you can provide your own Guzzle client through the `options` parameter of both `fetch` and `fetch_async`. This gives you full control over the client configuration, including base URI, headers, timeouts, and more. ### **How to Provide a Custom Guzzle Client** @@ -92,6 +92,8 @@ echo $response->statusText(); #### **Available Response Methods** +The `Response` class, now based on Guzzle’s `Psr7\Response`, provides various methods to interact with the response data: + - **`json(bool $assoc = true)`**: Decodes the response body as JSON. If `$assoc` is `true`, it returns an associative array. If `false`, it returns an object. - **`text()`**: Returns the response body as plain text. - **`blob()`**: Returns the response body as a PHP stream resource (like a "blob"). @@ -238,7 +240,9 @@ echo $response->text(); // Outputs error message $promise = fetch_async('https://nonexistent-url.com'); -$promise->then(function ($response) { +$promise->then(function ($ + +response) { echo $response->text(); }, function ($exception) { echo "Request failed: " . $exception->getMessage(); @@ -281,9 +285,7 @@ echo $response->statusText(); --- -### **Working - - with the Response Object** +### **Working with the Response Object** The `Response` class provides convenient methods for interacting with the response body, headers, and status codes. diff --git a/composer.json b/composer.json index 86be904..64af59a 100644 --- a/composer.json +++ b/composer.json @@ -26,8 +26,8 @@ "require": { "php": "^8.2", "guzzlehttp/guzzle": "^7.8", - "psr/http-message": "^1.0 || ^2.0", - "symfony/http-foundation": "^6.0 || ^7.1" + "guzzlehttp/psr7": "^2.7", + "psr/http-message": "^1.0 || ^2.0" }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.64", diff --git a/src/Http.php b/src/Http.php index 11c21c8..fb6b371 100644 --- a/src/Http.php +++ b/src/Http.php @@ -7,9 +7,7 @@ use GuzzleHttp\Cookie\CookieJar; use GuzzleHttp\Exception\RequestException; use GuzzleHttp\Promise\PromiseInterface; use GuzzleHttp\Psr7\MultipartStream; -use GuzzleHttp\Psr7\Response as GuzzleResponse; use Psr\Http\Message\ResponseInterface; -use Symfony\Component\HttpFoundation\Response as SymfonyResponse; class Http { @@ -48,6 +46,16 @@ class Http self::$client = $client; } + /** + * Reset the Guzzle client instance. + * + * @return void + */ + public static function resetClient(): void + { + self::$client = null; + } + /** * Helper function to perform HTTP requests using Guzzle. * @@ -97,7 +105,13 @@ class Http if ($async) { return $client->requestAsync($method, $url, $requestOptions)->then( - fn (ResponseInterface $response) => new Response($response), + fn (ResponseInterface $response) => new Response( + $response->getStatusCode(), + $response->getHeaders(), + (string) $response->getBody(), + $response->getProtocolVersion(), + $response->getReasonPhrase() + ), fn (RequestException $e) => self::handleRequestException($e) ); } @@ -105,7 +119,13 @@ class Http try { $response = $client->request($method, $url, $requestOptions); - return new Response($response); + return new Response( + $response->getStatusCode(), + $response->getHeaders(), + (string) $response->getBody(), + $response->getProtocolVersion(), + $response->getReasonPhrase() + ); } catch (RequestException $e) { return self::handleRequestException($e); } @@ -123,9 +143,17 @@ class Http $response = $e->getResponse(); if ($response) { - return new Response($response); + return new Response( + $response->getStatusCode(), + $response->getHeaders(), + (string) $response->getBody(), + $response->getProtocolVersion(), + $response->getReasonPhrase() + ); } + error_log('HTTP Error: ' . $e->getMessage()); + return self::createErrorResponse($e); } @@ -138,12 +166,10 @@ class Http */ protected static function createErrorResponse(RequestException $e): Response { - $mockResponse = new GuzzleResponse( - SymfonyResponse::HTTP_INTERNAL_SERVER_ERROR, + return new Response( + 500, [], $e->getMessage() ); - - return new Response($mockResponse); } } diff --git a/src/Response.php b/src/Response.php index 98ed387..78c33ed 100644 --- a/src/Response.php +++ b/src/Response.php @@ -2,11 +2,10 @@ namespace Fetch; -use Psr\Http\Message\ResponseInterface; +use GuzzleHttp\Psr7\Response as BaseResponse; use RuntimeException; -use Symfony\Component\HttpFoundation\Response as SymfonyResponse; -class Response extends SymfonyResponse +class Response extends BaseResponse { /** * The buffered content of the body. @@ -18,20 +17,21 @@ class Response extends SymfonyResponse /** * Create new response instance. * - * @param \Psr\Http\Message\ResponseInterface $guzzleResponse - * - * @return void + * @param int $status + * @param array $headers + * @param string $body + * @param string $version + * @param string $reason */ - public function __construct(protected ResponseInterface $guzzleResponse) - { - // Buffer the body contents to allow multiple reads - $this->bodyContents = (string) $guzzleResponse->getBody(); - - parent::__construct( - $this->bodyContents, - $guzzleResponse->getStatusCode(), - $guzzleResponse->getHeaders() - ); + public function __construct( + int $status = 200, + array $headers = [], + string $body = '', + string $version = '1.1', + string $reason = null + ) { + parent::__construct($status, $headers, $body, $version, $reason); + $this->bodyContents = (string) $this->getBody(); } /** @@ -41,14 +41,20 @@ class Response extends SymfonyResponse * * @return mixed */ - public function json(bool $assoc = true) + public function json(bool $assoc = true, bool $throwOnError = true) { $decoded = json_decode($this->bodyContents, $assoc); - if (json_last_error() !== \JSON_ERROR_NONE) { + $jsonError = json_last_error(); + + if ($jsonError === \JSON_ERROR_NONE) { + return $decoded; + } + + if ($throwOnError) { throw new RuntimeException('Failed to decode JSON: ' . json_last_error_msg()); } - return $decoded; + return null; // or return an empty array/object depending on your needs. } /** @@ -95,8 +101,7 @@ class Response extends SymfonyResponse */ public function statusText(): string { - return $this->statusText - ?? SymfonyResponse::$statusTexts[$this->getStatusCode()]; + return $this->getReasonPhrase() ?: 'No reason phrase available'; } /** diff --git a/tests/ResponseTest.php b/tests/ResponseTest.php index 0788825..9c81fed 100644 --- a/tests/ResponseTest.php +++ b/tests/ResponseTest.php @@ -5,7 +5,13 @@ use GuzzleHttp\Psr7\Response as GuzzleResponse; test('Response::json() correctly decodes JSON', function () { $guzzleResponse = new GuzzleResponse(200, ['Content-Type' => 'application/json'], '{"key":"value"}'); - $response = new Response($guzzleResponse); + $response = new Response( + $guzzleResponse->getStatusCode(), + $guzzleResponse->getHeaders(), + (string) $guzzleResponse->getBody(), + $guzzleResponse->getProtocolVersion(), + $guzzleResponse->getReasonPhrase() + ); $json = $response->json(); expect($json)->toMatchArray(['key' => 'value']); @@ -13,14 +19,26 @@ test('Response::json() correctly decodes JSON', function () { test('Response::text() correctly retrieves plain text', function () { $guzzleResponse = new GuzzleResponse(200, [], 'Plain text content'); - $response = new Response($guzzleResponse); + $response = new Response( + $guzzleResponse->getStatusCode(), + $guzzleResponse->getHeaders(), + (string) $guzzleResponse->getBody(), + $guzzleResponse->getProtocolVersion(), + $guzzleResponse->getReasonPhrase() + ); expect($response->text())->toBe('Plain text content'); }); test('Response::blob() correctly retrieves blob (stream)', function () { $guzzleResponse = new GuzzleResponse(200, [], 'Binary data'); - $response = new Response($guzzleResponse); + $response = new Response( + $guzzleResponse->getStatusCode(), + $guzzleResponse->getHeaders(), + (string) $guzzleResponse->getBody(), + $guzzleResponse->getProtocolVersion(), + $guzzleResponse->getReasonPhrase() + ); $blob = $response->blob(); expect(is_resource($blob))->toBeTrue(); @@ -28,24 +46,36 @@ test('Response::blob() correctly retrieves blob (stream)', function () { test('Response::arrayBuffer() correctly retrieves binary data as string', function () { $guzzleResponse = new GuzzleResponse(200, [], 'Binary data'); - $response = new Response($guzzleResponse); + $response = new Response( + $guzzleResponse->getStatusCode(), + $guzzleResponse->getHeaders(), + (string) $guzzleResponse->getBody(), + $guzzleResponse->getProtocolVersion(), + $guzzleResponse->getReasonPhrase() + ); expect($response->arrayBuffer())->toBe('Binary data'); }); test('Response::statusText() correctly retrieves status text', function () { $guzzleResponse = new GuzzleResponse(200); - $response = new Response($guzzleResponse); + $response = new Response( + $guzzleResponse->getStatusCode(), + $guzzleResponse->getHeaders(), + (string) $guzzleResponse->getBody(), + $guzzleResponse->getProtocolVersion(), + $guzzleResponse->getReasonPhrase() + ); expect($response->statusText())->toBe('OK'); }); test('Response status helper methods work correctly', function () { - $informationalResponse = new Response(new GuzzleResponse(100)); - $successfulResponse = new Response(new GuzzleResponse(200)); - $redirectionResponse = new Response(new GuzzleResponse(301)); - $clientErrorResponse = new Response(new GuzzleResponse(404)); - $serverErrorResponse = new Response(new GuzzleResponse(500)); + $informationalResponse = new Response(100); + $successfulResponse = new Response(200); + $redirectionResponse = new Response(301); + $clientErrorResponse = new Response(404); + $serverErrorResponse = new Response(500); expect($informationalResponse->isInformational())->toBeTrue(); expect($successfulResponse->ok())->toBeTrue(); @@ -57,7 +87,13 @@ test('Response status helper methods work correctly', function () { test('Response handles error gracefully', function () { $errorMessage = 'Something went wrong'; $guzzleResponse = new GuzzleResponse(500, [], $errorMessage); - $response = new Response($guzzleResponse); + $response = new Response( + $guzzleResponse->getStatusCode(), + $guzzleResponse->getHeaders(), + (string) $guzzleResponse->getBody(), + $guzzleResponse->getProtocolVersion(), + $guzzleResponse->getReasonPhrase() + ); expect($response->getStatusCode())->toBe(500); expect($response->text())->toBe($errorMessage);