Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use guzzlehttp/psr7 instead of symfony/http-foundation #6

Merged
merged 2 commits into from
Sep 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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