Skip to content

Commit

Permalink
Update fetch functions and guzzle client instanceissue
Browse files Browse the repository at this point in the history
  • Loading branch information
Thavarshan committed Sep 23, 2024
1 parent e1bc656 commit dcd05ea
Show file tree
Hide file tree
Showing 6 changed files with 294 additions and 70 deletions.
88 changes: 67 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
[![Check & fix styling](https://github.com/Thavarshan/fetch-php/actions/workflows/php-cs-fixer.yml/badge.svg?label=code%20style&branch=main)](https://github.com/Thavarshan/fetch-php/actions/workflows/php-cs-fixer.yml)
[![Total Downloads](https://img.shields.io/packagist/dt/jerome/fetch-php.svg)](https://packagist.org/packages/jerome/fetch-php)

FetchPHP is a PHP library that mimics the behavior of JavaScript’s `fetch` API using the powerful Guzzle HTTP client. FetchPHP supports both synchronous and asynchronous requests, and provides an easy-to-use, flexible API for making HTTP requests in PHP.
FetchPHP is a PHP library that mimics the behavior of JavaScript’s `fetch` API using the powerful Guzzle HTTP client. FetchPHP supports both synchronous and asynchronous requests and provides an easy-to-use, flexible API for making HTTP requests in PHP.

## **Installation**

Expand All @@ -24,27 +24,69 @@ composer require jerome/fetch-php
FetchPHP provides two main functions:

1. **`fetch`** – Performs a **synchronous** HTTP request.
2. **`fetchAsync`** – Performs an **asynchronous** HTTP request and returns a Guzzle `PromiseInterface`.
2. **`fetch_async`** – Performs an **asynchronous** HTTP request and returns a Guzzle `PromiseInterface`.

> **Deprecation Warning:** The function `fetchAsync` has been deprecated in version `1.1.0`. Please use `fetch_async` instead. If you continue using `fetchAsync`, you will see a deprecation warning in your code.
---

### **Important Consideration: Guzzle Client Instantiation**
### **Guzzle Client Instantiation and Static Variables**

By default, FetchPHP uses a **singleton** pattern to create and reuse the Guzzle HTTP client across multiple `fetch` or `fetch_async` function calls. This ensures that a new client is not created for every request, reducing overhead. If you do not pass a custom client, the first time you call `fetch`, a new Guzzle client will be created and stored as a **static variable**. All subsequent requests will reuse this client.

#### **Static Variables and Potential Risks**

While using a static variable for the Guzzle client improves performance by avoiding repeated client instantiation, it introduces some potential issues that you should be aware of:

1. **Shared State and Configuration**:
- Any changes to the static client configuration (e.g., headers, base URI, timeouts) will persist across all subsequent requests. This could lead to unexpected behavior if different configurations are required for different requests.
- **Mitigation**: Always pass explicit configurations (like `headers`, `auth`, or `timeout`) in the options parameter for every request. This ensures that each request uses the intended configuration.

Example:

```php
$response1 = fetch('/endpoint1', ['headers' => ['Authorization' => 'Bearer token1']]);
$response2 = fetch('/endpoint2', ['headers' => ['Authorization' => 'Bearer token2']]); // Independent headers
```

2. **Memory Leaks in Long-Running Processes**:
- If the application is a long-running process (e.g., a worker or daemon), the static Guzzle client will remain in memory. Over time, it could accumulate state (such as cookies or connection pools) and potentially cause memory issues.
- **Mitigation**: If your application is long-running, consider resetting or replacing the static Guzzle client at regular intervals, especially if there is a risk of accumulated state affecting performance.

By default, the Guzzle HTTP client is instantiated every time the `fetch` or `fetchAsync` function is called. While this is fine for most cases, it can introduce some inefficiency if you're making frequent HTTP requests in your application.
3. **Concurrency and Thread Safety**:
- In environments where concurrent requests are made (e.g., using Swoole or ReactPHP), the static client could introduce thread-safety issues. Since PHP is not inherently multi-threaded, this is not a concern for standard PHP web applications. However, in concurrent environments, race conditions could occur.
- **Mitigation**: In environments with concurrent requests, avoid using static variables for the Guzzle client. Instead, instantiate separate Guzzle clients or use dependency injection for better control over individual request contexts.

#### **Mitigating Guzzle Client Reinstantiation**
4. **Global Impact**:
- Modifying the static Guzzle client affects all subsequent requests globally. If different services or APIs are accessed with different configurations, this could cause unexpected side effects.
- **Mitigation**: Always pass client-specific configurations in the options, or use separate Guzzle client instances for different services.

You can mitigate the overhead of creating a new Guzzle client each time by passing a custom Guzzle client through the `options` parameter. This allows you to use a **singleton instance** of the client across multiple `fetch` requests.
In applications where more granular control over the client lifecycle is required, or in environments with dependency injection support, consider passing the Guzzle client explicitly via service containers or dependency injection frameworks.

### **How to Provide a Custom Guzzle Client**
#### **Using a Custom Guzzle Client with Middleware Support**

If you want to provide a custom Guzzle client (with custom configurations or middleware), you can pass it through the `options` parameter. This allows you to add middleware for things like logging, retries, caching, etc.

```php
<?php

use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use Psr\Http\Message\RequestInterface;

// Create a Guzzle handler stack with custom middleware
$stack = HandlerStack::create();

// Create a singleton instance of Guzzle client
// Add custom middleware to the handler stack
$stack->push(Middleware::mapRequest(function (RequestInterface $request) {
// Example middleware to modify the request
return $request->withHeader('X-Custom-Header', 'Value');
}));

// Create a singleton instance of Guzzle client with middleware
$client = new Client([
'handler' => $stack,
'base_uri' => 'https://jsonplaceholder.typicode.com',
'timeout' => 10.0,
// Other default options
Expand All @@ -64,12 +106,14 @@ print_r($response->json());
print_r($response2->json());
```

### **Why use a Singleton?**
### **Why is Singleton Guzzle Client Useful?**

Passing a singleton Guzzle client is useful when:

- You're making many requests and want to avoid the overhead of creating a new client each time.
- You want to configure specific client-wide options (e.g., base URI, timeouts, headers) and use them across multiple requests.
- You want to apply custom middleware to every request made by the client (e.g., logging, retries, etc.).
- If different configurations are needed for different requests, you can pass a new instance of the Guzzle client in the `options` parameter to bypass the singleton behavior.

---

Expand All @@ -96,7 +140,7 @@ echo $response->statusText();

#### **Available Response Methods**

- **`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.
- **`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. If the JSON decoding fails, a `RuntimeException` will be thrown. Ensure you handle this exception when working with potentially malformed JSON responses.
- **`text()`**: Returns the response body as plain text.
- **`blob()`**: Returns the response body as a PHP stream resource (like a "blob").
- **`arrayBuffer()`**: Returns the response body as a binary string.
Expand All @@ -106,9 +150,9 @@ echo $response->statusText();

---

### **2. Asynchronous Requests with `fetchAsync`**
### **2. Asynchronous Requests with `fetch_async`**

The `fetchAsync` function returns a `PromiseInterface` object. You can use the `.then()` and `.wait()` methods to manage the asynchronous flow.
The `fetch_async` function returns a `PromiseInterface` object. You can use the `.then()` and `.wait()` methods to manage the asynchronous flow.

#### **Basic Asynchronous GET Request Example**

Expand All @@ -117,23 +161,27 @@ The `fetchAsync` function returns a `PromiseInterface` object. You can use the `

require 'vendor/autoload.php';

$promise = fetchAsync('https://jsonplaceholder.typicode.com/todos/1');
$data = [];

$promise->then(function ($response) {
$promise = fetch_async('https://jsonplaceholder.typicode.com/todos/1');

$promise->then(function ($response) use (&$data) {
$data = $response->json();
print_r($data);
});

// Wait for the promise to resolve
$promise->wait();

echo "Data received: " . $data['title'];
```

#### **Error Handling in Asynchronous Requests**

You can handle errors with the `catch` or `then` method of the promise:

```php
$promise = fetchAsync('https://nonexistent-url.com');
$promise = fetch_async('https://nonexistent-url.com');

$promise->then(function ($response) {
// handle success
Expand All @@ -149,7 +197,7 @@ $promise->wait();

## **Request Options**

FetchPHP accepts an array of options as the second argument in both `fetch` and `fetchAsync`. These options configure how the request is handled.
FetchPHP accepts an array of options as the second argument in both `fetch` and `fetch_async`. These options configure how the request is handled.

### **Available Request Options**

Expand Down Expand Up @@ -269,9 +317,7 @@ You can control timeouts and whether redirects are followed:
<?php

$response = fetch('https://example.com/slow-request', [


'timeout' => 5, // 5-second timeout
'timeout' => 5, // 5-second timeout
'allow_redirects' => false
]);

Expand Down Expand Up @@ -300,7 +346,7 @@ echo $response->text(); // Outputs error message
```php
<?php

$promise = fetchAsync('https://nonexistent-url.com');
$promise = fetch_async('https://nonexistent-url.com');

$promise->then(function ($response) {
echo $response->text();
Expand Down Expand Up @@ -351,7 +397,7 @@ The `Response` class provides convenient methods for interacting with the respon

#### **Response Methods Overview**

- **`json()`**: Decodes the response body as JSON.
- **`json()`**: Decodes the response body as JSON. If the JSON decoding fails, a `RuntimeException` is thrown.
- **`text()`**: Returns the raw response body as plain text.
- **`blob()`**: Returns the body as a PHP stream (useful for file handling).
- **`arrayBuffer()`**: Returns the body as a binary string.
Expand Down
5 changes: 3 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "jerome/fetch-php",
"description": "The fetch API for PHP.",
"version": "1.0.0",
"version": "1.1.0",
"type": "library",
"license": "MIT",
"authors": [
Expand All @@ -20,7 +20,8 @@
"Fetch\\": "src/"
},
"files": [
"src/fetch.php"
"src/fetch.php",
"src/helpers.php"
]
},
"require": {
Expand Down
10 changes: 10 additions & 0 deletions src/Response.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public function __construct(protected ResponseInterface $guzzleResponse)
// Buffer the body contents to allow multiple reads
$this->bodyContents = (string) $guzzleResponse->getBody();

// Pass the string body to the Symfony Response
parent::__construct(
$this->bodyContents,
$guzzleResponse->getStatusCode(),
Expand All @@ -43,14 +44,21 @@ public function __construct(protected ResponseInterface $guzzleResponse)
*/
public function json(bool $assoc = true)
{
// Handle empty body by returning null or an empty array/object
if (trim($this->bodyContents) === '') {
return $assoc ? [] : null;
}

$decoded = json_decode($this->bodyContents, $assoc);

if (json_last_error() !== \JSON_ERROR_NONE) {
throw new RuntimeException('Failed to decode JSON: ' . json_last_error_msg());
}

return $decoded;
}


/**
* Get the body as plain text.
*
Expand All @@ -69,9 +77,11 @@ public function text(): string
public function blob()
{
$stream = fopen('php://memory', 'r+');

if ($stream === false) {
return false;
}

fwrite($stream, $this->bodyContents);
rewind($stream);

Expand Down
Loading

0 comments on commit dcd05ea

Please sign in to comment.