Skip to content

Commit b46c3c2

Browse files
Add logger support: Introduced optional logger in the HttpClient constructor to enable request and response logging for better debugging and monitoring.
1 parent 6ff54c7 commit b46c3c2

File tree

4 files changed

+112
-37
lines changed

4 files changed

+112
-37
lines changed

src/HttpStatusCode.php src/Http/HttpStatusCode.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?php
22

3-
namespace PhpDevCommunity\HttpClient;
3+
namespace PhpDevCommunity\HttpClient\Http;
44

55
use SplEnum;
66

src/HttpClient.php

+43-10
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use InvalidArgumentException;
66
use LogicException;
7+
use PhpDevCommunity\HttpClient\Http\HttpStatusCode;
78
use PhpDevCommunity\HttpClient\Http\Response;
89

910
final class HttpClient
@@ -21,6 +22,8 @@ final class HttpClient
2122
*/
2223
private array $options;
2324

25+
private $logger;
26+
2427
/**
2528
* HttpClient constructor.
2629
*
@@ -31,7 +34,7 @@ final class HttpClient
3134
* - array headers An associative array of HTTP headers to include in the request.
3235
* - string base_url The base URL to prepend to relative URLs in the request.
3336
*/
34-
public function __construct(array $options = [])
37+
public function __construct(array $options = [], ?callable $logger = null)
3538
{
3639
self::validateOptions($options, ['user_agent', 'timeout', 'headers', 'base_url']);
3740
$this->options = array_replace([
@@ -40,6 +43,7 @@ public function __construct(array $options = [])
4043
'headers' => [],
4144
'base_url' => null,
4245
], $options);
46+
$this->logger = $logger;
4347
}
4448

4549
/**
@@ -95,6 +99,8 @@ public function post(string $url, array $data, bool $json = false, array $header
9599
*/
96100
public function fetch(string $url, array $options = []): Response
97101
{
102+
$options['method'] = strtoupper($options['method'] ?? 'GET');
103+
$options['body'] = $options['body'] ?? '';
98104
self::validateOptions($options, ['user_agent', 'timeout', 'headers', 'body', 'method']);
99105

100106
$options = array_merge_recursive($this->options, $options);
@@ -110,12 +116,28 @@ public function fetch(string $url, array $options = []): Response
110116
throw new InvalidArgumentException(sprintf('Invalid URL: %s', $url));
111117
}
112118

119+
$info = [
120+
'url' => $url,
121+
'request' => [
122+
'user_agent' => $options['user_agent'],
123+
'method' => $options['method'],
124+
'headers' => $options['headers'],
125+
'body' => $options['body'],
126+
],
127+
'response' => [
128+
'body' => '',
129+
'headers' => [],
130+
]
131+
];
132+
113133
$response = '';
114134
$fp = fopen($url, 'rb', false, $context);
115135
$httpResponseHeaders = $http_response_header;
136+
$headers = self::parseHttpResponseHeaders($httpResponseHeaders);
137+
$info['response']['headers'] = $headers;
116138
if ($fp === false) {
117-
$detail = $httpResponseHeaders[0] ?? '';
118-
throw new LogicException(sprintf('Error opening request to %s: %s', $url, $detail));
139+
$this->log($info);
140+
throw new LogicException(sprintf('Error opening request to %s: %s', $url, $httpResponseHeaders[0] ?? ''));
119141
}
120142

121143
while (!feof($fp)) {
@@ -124,9 +146,10 @@ public function fetch(string $url, array $options = []): Response
124146

125147
fclose($fp);
126148

127-
$headers = self::parseHttpResponseHeaders($httpResponseHeaders);
149+
$info['response']['body'] = $response;
150+
$this->log($info);
128151

129-
return new Response($response, $headers['status_code'] ?? HttpStatusCode::HTTP_VERSION_NOT_SUPPORTED, $headers);
152+
return new Response($response, $headers['status_code'], $headers);
130153
}
131154

132155
/**
@@ -137,16 +160,14 @@ public function fetch(string $url, array $options = []): Response
137160
*/
138161
private function createContext(array $options)
139162
{
140-
$method = strtoupper($options['method'] ?? 'GET');
141-
$body = $options['body'] ?? '';
142-
143-
if (in_array($method, ['POST', 'PUT']) && is_array($body)) {
163+
$body = $options['body'];
164+
if (in_array($options['method'], ['POST', 'PUT']) && is_array($body)) {
144165
$body = self::prepareRequestBody($body, $options['headers']);
145166
}
146167

147168
$opts = [
148169
'http' => [
149-
'method' => $method,
170+
'method' => $options['method'],
150171
'header' => self::formatHttpRequestHeaders($options['headers']),
151172
'content' => $body,
152173
'user_agent' => $options['user_agent'],
@@ -158,6 +179,14 @@ private function createContext(array $options)
158179
return stream_context_create($opts);
159180
}
160181

182+
private function log(array $info): void
183+
{
184+
$logger = $this->logger;
185+
if (is_callable($logger)) {
186+
$logger($info);
187+
}
188+
}
189+
161190
/**
162191
* Format HTTP headers from the provided associative array.
163192
*
@@ -212,6 +241,10 @@ private static function parseHttpResponseHeaders(array $responseHeaders): array
212241
}
213242
}
214243
}
244+
245+
if (!isset($headers['status_code'])) {
246+
$headers['status_code'] = HttpStatusCode::HTTP_VERSION_NOT_SUPPORTED;
247+
}
215248
return $headers;
216249
}
217250

src/helpers.php

+4-4
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@
1111
* @param array $options The options to configure the HttpClient
1212
* @return HttpClient The newly created HttpClient instance
1313
*/
14-
function http_client(array $options = []): HttpClient
14+
function http_client(array $options = [], callable $logger = null): HttpClient
1515
{
16-
return new HttpClient($options);
16+
return new HttpClient($options, $logger);
1717
}
1818
}
1919

@@ -28,7 +28,7 @@ function http_client(array $options = []): HttpClient
2828
*/
2929
function http_post(string $url, array $data = [], array $headers = []): Response
3030
{
31-
return http_client()->post($url, $data, false,$headers);
31+
return http_client()->post($url, $data, false, $headers);
3232
}
3333
}
3434

@@ -43,7 +43,7 @@ function http_post(string $url, array $data = [], array $headers = []): Response
4343
*/
4444
function http_post_json(string $url, array $data = [], array $headers = []): Response
4545
{
46-
return http_client()->post($url, $data, true ,$headers);
46+
return http_client()->post($url, $data, true, $headers);
4747
}
4848
}
4949

tests/HttpClientTest.php

+64-22
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace PhpDevCommunity\tests;
44

5+
use Exception;
6+
use LogicException;
57
use PhpDevCommunity\HttpClient\HttpClient;
68
use PHPUnit\Framework\TestCase;
79

@@ -10,38 +12,46 @@ class HttpClientTest extends TestCase
1012
const URL = 'http://localhost:4245';
1113

1214
protected static ?string $serverProcess = null;
15+
1316
public static function setUpBeforeClass(): void
1417
{
15-
$fileToRun = __DIR__.DIRECTORY_SEPARATOR.'test_server.php';
16-
$command = sprintf('php -S %s %s > /dev/null 2>&1 & echo $!;',str_replace('http://', '', self::URL), $fileToRun);
18+
$fileToRun = __DIR__ . DIRECTORY_SEPARATOR . 'test_server.php';
19+
$command = sprintf('php -S %s %s > /dev/null 2>&1 & echo $!;', str_replace('http://', '', self::URL), $fileToRun);
1720
self::$serverProcess = exec($command);
1821
if (empty(self::$serverProcess) || !is_numeric(self::$serverProcess)) {
19-
throw new \Exception('Could not start test server');
22+
throw new Exception('Could not start test server');
2023
}
2124
sleep(1);
2225
}
2326

2427
public function testGetRequest()
2528
{
26-
$response = http_client(['base_url' => self::URL, 'headers' => ['Authorization' => 'Bearer secret_token']])->get('/api/data');
27-
$this->assertEquals( 200, $response->getStatusCode() );
29+
$response = http_client(
30+
['base_url' => self::URL, 'headers' => ['Authorization' => 'Bearer secret_token']],
31+
function ($info) {
32+
$this->assertEquals( 'GET', $info['request']['method']);
33+
$this->assertEquals( 'Bearer secret_token', $info['request']['headers']['Authorization']);
34+
$this->assertEquals( '{"message":"GET request received"}', $info['response']['body']);
35+
}
36+
)->get('/api/data');
37+
$this->assertEquals(200, $response->getStatusCode());
2838
$this->assertNotEmpty($response->getBody());
2939
}
3040

3141
public function testGetWithQueryRequest()
3242
{
33-
$client = new HttpClient(['base_url' => self::URL,'headers' => ['Authorization' => 'Bearer secret_token']]);
43+
$client = new HttpClient(['base_url' => self::URL, 'headers' => ['Authorization' => 'Bearer secret_token']]);
3444
$response = $client->get('/api/search', [
3545
'name' => 'foo',
3646
]);
3747

38-
$this->assertEquals( 200, $response->getStatusCode() );
48+
$this->assertEquals(200, $response->getStatusCode());
3949
$this->assertNotEmpty($response->getBody());
4050

4151
$data = $response->bodyToArray();
42-
$this->assertEquals( 'foo', $data['name'] );
43-
$this->assertEquals( 1, $data['page'] );
44-
$this->assertEquals( 10, $data['limit'] );
52+
$this->assertEquals('foo', $data['name']);
53+
$this->assertEquals(1, $data['page']);
54+
$this->assertEquals(10, $data['limit']);
4555

4656

4757
$response = $client->get('/api/search', [
@@ -50,13 +60,13 @@ public function testGetWithQueryRequest()
5060
'limit' => 100
5161
]);
5262

53-
$this->assertEquals( 200, $response->getStatusCode() );
63+
$this->assertEquals(200, $response->getStatusCode());
5464
$this->assertNotEmpty($response->getBody());
5565

5666
$data = $response->bodyToArray();
57-
$this->assertEquals( 'foo', $data['name'] );
58-
$this->assertEquals( 10, $data['page'] );
59-
$this->assertEquals( 100, $data['limit'] );
67+
$this->assertEquals('foo', $data['name']);
68+
$this->assertEquals(10, $data['page']);
69+
$this->assertEquals(100, $data['limit']);
6070
}
6171

6272
public function testPostJsonRequest()
@@ -67,14 +77,14 @@ public function testPostJsonRequest()
6777
'userId' => 1
6878
];
6979
$client = new HttpClient(['headers' => ['Authorization' => 'Bearer secret_token']]);
70-
$response = $client->post(self::URL.'/api/post/data', [
80+
$response = $client->post(self::URL . '/api/post/data', [
7181
'title' => 'foo',
7282
'body' => 'bar',
7383
'userId' => 1
7484
], true);
7585

76-
$this->assertEquals( 200, $response->getStatusCode());
77-
$this->assertEquals( $dataToPost, $response->bodyToArray());
86+
$this->assertEquals(200, $response->getStatusCode());
87+
$this->assertEquals($dataToPost, $response->bodyToArray());
7888
}
7989

8090
public function testPostFormRequest()
@@ -85,18 +95,50 @@ public function testPostFormRequest()
8595
'userId' => 1
8696
];
8797
$client = new HttpClient(['headers' => ['Authorization' => 'Bearer secret_token']]);
88-
$response = $client->post(self::URL.'/api/post/data/form', $dataToPost);
98+
$response = $client->post(self::URL . '/api/post/data/form', $dataToPost);
8999

90-
$this->assertEquals( 200, $response->getStatusCode());
91-
$this->assertEquals( $dataToPost, $response->bodyToArray());
100+
$this->assertEquals(200, $response->getStatusCode());
101+
$this->assertEquals($dataToPost, $response->bodyToArray());
92102
}
93103

94104
public function testPostEmptyFormRequest()
95105
{
96106
$client = new HttpClient(['headers' => ['Authorization' => 'Bearer secret_token']]);
97-
$response = $client->post(self::URL.'/api/post/data/form', []);
107+
$response = $client->post(self::URL . '/api/post/data/form', []);
108+
109+
$this->assertEquals(400, $response->getStatusCode());
110+
}
111+
112+
public function testWrongOptions()
113+
{
114+
$this->expectException(LogicException::class);
115+
new HttpClient(['headers' => 'string']);
116+
}
117+
118+
public function testWrongOptions2()
119+
{
120+
$this->expectException(LogicException::class);
121+
new HttpClient(['options_not_supported' => 'value']);
122+
}
123+
124+
public function testWrongOptions3()
125+
{
126+
$this->expectException(LogicException::class);
127+
new HttpClient(['timeout' => 'string']);
128+
}
129+
130+
public function testWrongMethod()
131+
{
132+
$client = new HttpClient(['headers' => ['Authorization' => 'Bearer secret_token']]);
133+
$this->expectException(LogicException::class);
134+
$client->fetch(self::URL . '/api/data', ['method' => 'WRONG']);
135+
}
98136

99-
$this->assertEquals( 400, $response->getStatusCode() );
137+
public function testWrongUrl()
138+
{
139+
$client = new HttpClient(['headers' => ['Authorization' => 'Bearer secret_token']]);
140+
$this->expectException(LogicException::class);
141+
$client->fetch('WRONG_URL', ['method' => 'GET']);
100142
}
101143

102144

0 commit comments

Comments
 (0)