Skip to content

Commit ba67f9b

Browse files
authored
Merge pull request #4 from Thavarshan/feature/request-handler
Abstract request handler logic to Http class
2 parents e1bc656 + 2e44d48 commit ba67f9b

File tree

7 files changed

+360
-183
lines changed

7 files changed

+360
-183
lines changed

README.md

Lines changed: 17 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
[![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)
88
[![Total Downloads](https://img.shields.io/packagist/dt/jerome/fetch-php.svg)](https://packagist.org/packages/jerome/fetch-php)
99

10-
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.
10+
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.
1111

1212
## **Installation**
1313

@@ -21,20 +21,17 @@ composer require jerome/fetch-php
2121

2222
## **Core Functions Overview**
2323

24-
FetchPHP provides two main functions:
24+
FetchPHP provides three main functions:
2525

2626
1. **`fetch`** – Performs a **synchronous** HTTP request.
27-
2. **`fetchAsync`** – Performs an **asynchronous** HTTP request and returns a Guzzle `PromiseInterface`.
27+
2. **`fetch_async`** – Performs an **asynchronous** HTTP request and returns a Guzzle `PromiseInterface`.
28+
3. **`fetchAsync`** – An alias for `fetch_async`, but **deprecated**.
2829

2930
---
3031

31-
### **Important Consideration: Guzzle Client Instantiation**
32+
### **Custom Guzzle Client Usage**
3233

33-
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.
34-
35-
#### **Mitigating Guzzle Client Reinstantiation**
36-
37-
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.
34+
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.
3835

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

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

58-
// The Guzzle client instance will now be reused across multiple fetch calls
5955
$response2 = fetch('/todos/2', [
6056
'client' => $client
6157
]);
@@ -69,7 +65,7 @@ print_r($response2->json());
6965
Passing a singleton Guzzle client is useful when:
7066

7167
- You're making many requests and want to avoid the overhead of creating a new client each time.
72-
- You want to configure specific client-wide options (e.g., base URI, timeouts, headers) and use them across multiple requests.
68+
- You want to configure specific client-wide options (e.g., base URI, timeouts, headers) and reuse them across multiple requests.
7369

7470
---
7571

@@ -106,9 +102,9 @@ echo $response->statusText();
106102

107103
---
108104

109-
### **2. Asynchronous Requests with `fetchAsync`**
105+
### **2. Asynchronous Requests with `fetch_async`**
110106

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

113109
#### **Basic Asynchronous GET Request Example**
114110

@@ -117,7 +113,7 @@ The `fetchAsync` function returns a `PromiseInterface` object. You can use the `
117113

118114
require 'vendor/autoload.php';
119115

120-
$promise = fetchAsync('https://jsonplaceholder.typicode.com/todos/1');
116+
$promise = fetch_async('https://jsonplaceholder.typicode.com/todos/1');
121117

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

131127
#### **Error Handling in Asynchronous Requests**
132128

133-
You can handle errors with the `catch` or `then` method of the promise:
129+
You can handle errors with the `then` or `catch` methods of the promise:
134130

135131
```php
136-
$promise = fetchAsync('https://nonexistent-url.com');
132+
$promise = fetch_async('https://nonexistent-url.com');
137133

138134
$promise->then(function ($response) {
139135
// handle success
@@ -149,7 +145,7 @@ $promise->wait();
149145

150146
## **Request Options**
151147

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

154150
### **Available Request Options**
155151

@@ -220,66 +216,6 @@ echo $response->statusText();
220216

221217
---
222218

223-
### **Detailed Request Customization**
224-
225-
#### **Custom Headers**
226-
227-
You can specify custom headers using the `headers` option:
228-
229-
```php
230-
<?php
231-
232-
$response = fetch('https://example.com/endpoint', [
233-
'method' => 'POST',
234-
'headers' => [
235-
'Authorization' => 'Bearer YOUR_TOKEN',
236-
'Accept' => 'application/json'
237-
],
238-
'json' => [
239-
'key' => 'value'
240-
]
241-
]);
242-
243-
print_r($response->json());
244-
```
245-
246-
#### **Handling Cookies**
247-
248-
To enable cookies, set the `cookies` option to `true` or pass a `CookieJar` instance:
249-
250-
```php
251-
<?php
252-
253-
use GuzzleHttp\Cookie\CookieJar;
254-
255-
$jar = new CookieJar();
256-
257-
$response = fetch('https://example.com', [
258-
'cookies' => $jar
259-
]);
260-
261-
print_r($response->json());
262-
```
263-
264-
#### **Timeouts and Redirects**
265-
266-
You can control timeouts and whether redirects are followed:
267-
268-
```php
269-
<?php
270-
271-
$response = fetch('https://example.com/slow-request', [
272-
273-
274-
'timeout' => 5, // 5-second timeout
275-
'allow_redirects' => false
276-
]);
277-
278-
echo $response->statusText();
279-
```
280-
281-
---
282-
283219
### **Error Handling**
284220

285221
FetchPHP gracefully handles errors, returning a `500` status code and error message in the response when a request fails.
@@ -300,7 +236,7 @@ echo $response->text(); // Outputs error message
300236
```php
301237
<?php
302238

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

305241
$promise->then(function ($response) {
306242
echo $response->text();
@@ -345,7 +281,9 @@ echo $response->statusText();
345281

346282
---
347283

348-
### **Working with the Response Object**
284+
### **Working
285+
286+
with the Response Object**
349287

350288
The `Response` class provides convenient methods for interacting with the response body, headers, and status codes.
351289

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "jerome/fetch-php",
33
"description": "The fetch API for PHP.",
4-
"version": "1.0.0",
4+
"version": "1.1.0",
55
"type": "library",
66
"license": "MIT",
77
"authors": [

src/Http.php

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
<?php
2+
3+
namespace Fetch;
4+
5+
use GuzzleHttp\Client;
6+
use GuzzleHttp\Cookie\CookieJar;
7+
use GuzzleHttp\Exception\RequestException;
8+
use GuzzleHttp\Promise\PromiseInterface;
9+
use GuzzleHttp\Psr7\MultipartStream;
10+
use GuzzleHttp\Psr7\Response as GuzzleResponse;
11+
use Psr\Http\Message\ResponseInterface;
12+
use Symfony\Component\HttpFoundation\Response as SymfonyResponse;
13+
14+
class Http
15+
{
16+
/**
17+
* The Guzzle client instance.
18+
*
19+
* @var \GuzzleHttp\Client|null
20+
*/
21+
protected static ?Client $client = null;
22+
23+
/**
24+
* Get the Guzzle client instance.
25+
*
26+
* @param array $options
27+
*
28+
* @return \GuzzleHttp\Client
29+
*/
30+
public static function getClient(array $options = []): Client
31+
{
32+
if (self::$client === null) {
33+
self::$client = new Client($options);
34+
}
35+
36+
return self::$client;
37+
}
38+
39+
/**
40+
* Set the Guzzle client instance.
41+
*
42+
* @param \GuzzleHttp\Client $client
43+
*
44+
* @return void
45+
*/
46+
public static function setClient(Client $client): void
47+
{
48+
self::$client = $client;
49+
}
50+
51+
/**
52+
* Helper function to perform HTTP requests using Guzzle.
53+
*
54+
* @param string $url
55+
* @param array $options
56+
* @param bool $async
57+
*
58+
* @return \GuzzleHttp\Promise\PromiseInterface|\Fetch\Response
59+
*/
60+
public static function makeRequest(
61+
string $url,
62+
array $options,
63+
bool $async
64+
): PromiseInterface|Response {
65+
if (isset($options['client'])) {
66+
self::setClient($options['client']);
67+
}
68+
69+
$client = self::getClient([
70+
'base_uri' => $options['base_uri'] ?? null,
71+
'timeout' => $options['timeout'] ?? 0,
72+
'allow_redirects' => $options['allow_redirects'] ?? true,
73+
'cookies' => isset($options['cookies']) ? new CookieJar() : false,
74+
'verify' => $options['verify'] ?? true,
75+
'proxy' => $options['proxy'] ?? null,
76+
]);
77+
78+
$method = $options['method'] ?? 'GET';
79+
$headers = $options['headers'] ?? [];
80+
$body = $options['body'] ?? null;
81+
$query = $options['query'] ?? [];
82+
83+
if (isset($options['multipart'])) {
84+
$body = new MultipartStream($options['multipart']);
85+
$headers['Content-Type'] = 'multipart/form-data';
86+
} elseif (isset($options['json'])) {
87+
$body = json_encode($options['json']);
88+
$headers['Content-Type'] = 'application/json';
89+
}
90+
91+
$requestOptions = [
92+
'headers' => $headers,
93+
'body' => $body,
94+
'query' => $query,
95+
'auth' => $options['auth'] ?? null,
96+
];
97+
98+
if ($async) {
99+
return $client->requestAsync($method, $url, $requestOptions)->then(
100+
fn (ResponseInterface $response) => new Response($response),
101+
fn (RequestException $e) => self::handleRequestException($e)
102+
);
103+
}
104+
105+
try {
106+
$response = $client->request($method, $url, $requestOptions);
107+
108+
return new Response($response);
109+
} catch (RequestException $e) {
110+
return self::handleRequestException($e);
111+
}
112+
}
113+
114+
/**
115+
* Handles the RequestException and returns a Response.
116+
*
117+
* @param \GuzzleHttp\Exception\RequestException $e
118+
*
119+
* @return \Fetch\Response
120+
*/
121+
protected static function handleRequestException(RequestException $e): Response
122+
{
123+
$response = $e->getResponse();
124+
125+
if ($response) {
126+
return new Response($response);
127+
}
128+
129+
return self::createErrorResponse($e);
130+
}
131+
132+
/**
133+
* Creates a mock response for error handling.
134+
*
135+
* @param \GuzzleHttp\Exception\RequestException $e
136+
*
137+
* @return \Fetch\Response
138+
*/
139+
protected static function createErrorResponse(RequestException $e): Response
140+
{
141+
$mockResponse = new GuzzleResponse(
142+
SymfonyResponse::HTTP_INTERNAL_SERVER_ERROR,
143+
[],
144+
$e->getMessage()
145+
);
146+
147+
return new Response($mockResponse);
148+
}
149+
}

0 commit comments

Comments
 (0)