Replace symfony/http-foundation with guzzlehttp/psr7

This commit is contained in:
Jerome Thayananthajothy 2024-09-27 23:56:28 +01:00
parent e95b135011
commit fabf4cfc98
5 changed files with 117 additions and 48 deletions

View File

@ -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 Guzzles `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.

View File

@ -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",

View File

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

View File

@ -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';
}
/**

View File

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