Skip to content

Commit

Permalink
Merge pull request #6 from Thavarshan/refactor/use-guzzle-psr7
Browse files Browse the repository at this point in the history
Use guzzlehttp/psr7 instead of symfony/http-foundation
  • Loading branch information
Thavarshan authored Sep 27, 2024
2 parents e95b135 + f8bf5d6 commit 59216aa
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 49 deletions.
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**

Expand Down Expand Up @@ -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").
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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.

Expand Down
6 changes: 3 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "jerome/fetch-php",
"description": "The JavaScript fetch API for PHP.",
"version": "1.1.1",
"version": "1.2.0",
"type": "library",
"license": "MIT",
"authors": [
Expand All @@ -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",
Expand Down
44 changes: 35 additions & 9 deletions src/Http.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@
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
{
Expand Down Expand Up @@ -48,6 +46,16 @@ public static function setClient(Client $client): void
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.
*
Expand Down Expand Up @@ -97,15 +105,27 @@ public static function makeRequest(

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)
);
}

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);
}
Expand All @@ -123,9 +143,17 @@ protected static function handleRequestException(RequestException $e): Response
$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);
}

Expand All @@ -138,12 +166,10 @@ protected static function handleRequestException(RequestException $e): Response
*/
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);
}
}
47 changes: 26 additions & 21 deletions src/Response.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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();
}

/**
Expand All @@ -41,14 +41,20 @@ public function __construct(protected ResponseInterface $guzzleResponse)
*
* @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.
}

/**
Expand Down Expand Up @@ -95,8 +101,7 @@ public function arrayBuffer(): string
*/
public function statusText(): string
{
return $this->statusText
?? SymfonyResponse::$statusTexts[$this->getStatusCode()];
return $this->getReasonPhrase() ?: 'No reason phrase available';
}

/**
Expand Down
58 changes: 47 additions & 11 deletions tests/ResponseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,47 +5,77 @@

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']);
});

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();
});

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();
Expand All @@ -57,7 +87,13 @@
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);
Expand Down

0 comments on commit 59216aa

Please sign in to comment.