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

Abstract request handler logic to Http class #4

Merged
merged 3 commits into from
Sep 24, 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
96 changes: 17 additions & 79 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 @@ -21,20 +21,17 @@ composer require jerome/fetch-php

## **Core Functions Overview**

FetchPHP provides two main functions:
FetchPHP provides three 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`.
3. **`fetchAsync`** – An alias for `fetch_async`, but **deprecated**.

---

### **Important Consideration: Guzzle Client Instantiation**
### **Custom Guzzle Client Usage**

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.

#### **Mitigating Guzzle Client Reinstantiation**

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.
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.

### **How to Provide a Custom Guzzle Client**

Expand All @@ -55,7 +52,6 @@ $response = fetch('/todos/1', [
'client' => $client
]);

// The Guzzle client instance will now be reused across multiple fetch calls
$response2 = fetch('/todos/2', [
'client' => $client
]);
Expand All @@ -69,7 +65,7 @@ print_r($response2->json());
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 configure specific client-wide options (e.g., base URI, timeouts, headers) and reuse them across multiple requests.

---

Expand Down Expand Up @@ -106,9 +102,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,7 +113,7 @@ The `fetchAsync` function returns a `PromiseInterface` object. You can use the `

require 'vendor/autoload.php';

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

$promise->then(function ($response) {
$data = $response->json();
Expand All @@ -130,10 +126,10 @@ $promise->wait();

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

You can handle errors with the `catch` or `then` method of the promise:
You can handle errors with the `then` or `catch` methods 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 +145,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 @@ -220,66 +216,6 @@ echo $response->statusText();

---

### **Detailed Request Customization**

#### **Custom Headers**

You can specify custom headers using the `headers` option:

```php
<?php

$response = fetch('https://example.com/endpoint', [
'method' => 'POST',
'headers' => [
'Authorization' => 'Bearer YOUR_TOKEN',
'Accept' => 'application/json'
],
'json' => [
'key' => 'value'
]
]);

print_r($response->json());
```

#### **Handling Cookies**

To enable cookies, set the `cookies` option to `true` or pass a `CookieJar` instance:

```php
<?php

use GuzzleHttp\Cookie\CookieJar;

$jar = new CookieJar();

$response = fetch('https://example.com', [
'cookies' => $jar
]);

print_r($response->json());
```

#### **Timeouts and Redirects**

You can control timeouts and whether redirects are followed:

```php
<?php

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


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

echo $response->statusText();
```

---

### **Error Handling**

FetchPHP gracefully handles errors, returning a `500` status code and error message in the response when a request fails.
Expand All @@ -300,7 +236,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 @@ -345,7 +281,9 @@ 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
2 changes: 1 addition & 1 deletion 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 Down
149 changes: 149 additions & 0 deletions src/Http.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
<?php

namespace Fetch;

use GuzzleHttp\Client;
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
{
/**
* The Guzzle client instance.
*
* @var \GuzzleHttp\Client|null
*/
protected static ?Client $client = null;

/**
* Get the Guzzle client instance.
*
* @param array $options
*
* @return \GuzzleHttp\Client
*/
public static function getClient(array $options = []): Client
{
if (self::$client === null) {
self::$client = new Client($options);
}

return self::$client;
}

/**
* Set the Guzzle client instance.
*
* @param \GuzzleHttp\Client $client
*
* @return void
*/
public static function setClient(Client $client): void
{
self::$client = $client;
}

/**
* Helper function to perform HTTP requests using Guzzle.
*
* @param string $url
* @param array $options
* @param bool $async
*
* @return \GuzzleHttp\Promise\PromiseInterface|\Fetch\Response
*/
public static function makeRequest(
string $url,
array $options,
bool $async
): PromiseInterface|Response {
if (isset($options['client'])) {
self::setClient($options['client']);
}

$client = self::getClient([
'base_uri' => $options['base_uri'] ?? null,
'timeout' => $options['timeout'] ?? 0,
'allow_redirects' => $options['allow_redirects'] ?? true,
'cookies' => isset($options['cookies']) ? new CookieJar() : false,
'verify' => $options['verify'] ?? true,
'proxy' => $options['proxy'] ?? null,
]);

$method = $options['method'] ?? 'GET';
$headers = $options['headers'] ?? [];
$body = $options['body'] ?? null;
$query = $options['query'] ?? [];

if (isset($options['multipart'])) {
$body = new MultipartStream($options['multipart']);
$headers['Content-Type'] = 'multipart/form-data';
} elseif (isset($options['json'])) {
$body = json_encode($options['json']);
$headers['Content-Type'] = 'application/json';
}

$requestOptions = [
'headers' => $headers,
'body' => $body,
'query' => $query,
'auth' => $options['auth'] ?? null,
];

if ($async) {
return $client->requestAsync($method, $url, $requestOptions)->then(
fn (ResponseInterface $response) => new Response($response),
fn (RequestException $e) => self::handleRequestException($e)
);
}

try {
$response = $client->request($method, $url, $requestOptions);

return new Response($response);
} catch (RequestException $e) {
return self::handleRequestException($e);
}
}

/**
* Handles the RequestException and returns a Response.
*
* @param \GuzzleHttp\Exception\RequestException $e
*
* @return \Fetch\Response
*/
protected static function handleRequestException(RequestException $e): Response
{
$response = $e->getResponse();

if ($response) {
return new Response($response);
}

return self::createErrorResponse($e);
}

/**
* Creates a mock response for error handling.
*
* @param \GuzzleHttp\Exception\RequestException $e
*
* @return \Fetch\Response
*/
protected static function createErrorResponse(RequestException $e): Response
{
$mockResponse = new GuzzleResponse(
SymfonyResponse::HTTP_INTERNAL_SERVER_ERROR,
[],
$e->getMessage()
);

return new Response($mockResponse);
}
}
Loading