Skip to content

Commit 46e791f

Browse files
authored
Merge pull request #8 from utopia-php/feat-eldad-v1
Add stream support
2 parents 524dd50 + 2f870fd commit 46e791f

File tree

6 files changed

+440
-24
lines changed

6 files changed

+440
-24
lines changed

src/Chunk.php

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php
2+
3+
namespace Utopia\Fetch;
4+
5+
/**
6+
* Chunk class
7+
* Represents a chunk of data received from an HTTP response
8+
* @package Utopia\Fetch
9+
*/
10+
class Chunk
11+
{
12+
/**
13+
* @param string $data The raw chunk data
14+
* @param int $size The size of the chunk in bytes
15+
* @param float $timestamp The timestamp when the chunk was received
16+
* @param int $index The sequential index of this chunk in the response
17+
*/
18+
public function __construct(
19+
private readonly string $data,
20+
private readonly int $size,
21+
private readonly float $timestamp,
22+
private readonly int $index,
23+
) {
24+
}
25+
26+
/**
27+
* Get the raw chunk data
28+
*
29+
* @return string
30+
*/
31+
public function getData(): string
32+
{
33+
return $this->data;
34+
}
35+
36+
/**
37+
* Get the size of the chunk in bytes
38+
*
39+
* @return int
40+
*/
41+
public function getSize(): int
42+
{
43+
return $this->size;
44+
}
45+
46+
/**
47+
* Get the timestamp when the chunk was received
48+
*
49+
* @return float
50+
*/
51+
public function getTimestamp(): float
52+
{
53+
return $this->timestamp;
54+
}
55+
56+
/**
57+
* Get the sequential index of this chunk
58+
*
59+
* @return int
60+
*/
61+
public function getIndex(): int
62+
{
63+
return $this->index;
64+
}
65+
}

src/Client.php

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -199,16 +199,18 @@ private function withRetries(callable $callback): mixed
199199
* @param string $method
200200
* @param array<string>|array<string, mixed> $body
201201
* @param array<string, mixed> $query
202+
* @param ?callable $chunks Optional callback function that receives a Chunk object
202203
* @return Response
203204
*/
204205
public function fetch(
205206
string $url,
206207
string $method = self::METHOD_GET,
207208
?array $body = [],
208209
?array $query = [],
210+
?callable $chunks = null,
209211
): Response {
210212
if (!in_array($method, [self::METHOD_PATCH, self::METHOD_GET, self::METHOD_CONNECT, self::METHOD_DELETE, self::METHOD_POST, self::METHOD_HEAD, self::METHOD_OPTIONS, self::METHOD_PUT, self::METHOD_TRACE])) {
211-
throw new FetchException("Unsupported HTTP method");
213+
throw new Exception("Unsupported HTTP method");
212214
}
213215

214216
if (isset($this->headers['content-type']) && $body !== null) {
@@ -229,6 +231,8 @@ public function fetch(
229231
}
230232

231233
$responseHeaders = [];
234+
$responseBody = '';
235+
$chunkIndex = 0;
232236
$ch = curl_init();
233237
$curlOptions = [
234238
CURLOPT_URL => $url,
@@ -244,11 +248,24 @@ public function fetch(
244248
$responseHeaders[strtolower(trim($header[0]))] = trim($header[1]);
245249
return $len;
246250
},
251+
CURLOPT_WRITEFUNCTION => function ($ch, $data) use ($chunks, &$responseBody, &$chunkIndex) {
252+
if ($chunks !== null) {
253+
$chunk = new Chunk(
254+
data: $data,
255+
size: strlen($data),
256+
timestamp: microtime(true),
257+
index: $chunkIndex++
258+
);
259+
$chunks($chunk);
260+
} else {
261+
$responseBody .= $data;
262+
}
263+
return strlen($data);
264+
},
247265
CURLOPT_CONNECTTIMEOUT => $this->connectTimeout,
248266
CURLOPT_TIMEOUT => $this->timeout,
249267
CURLOPT_MAXREDIRS => $this->maxRedirects,
250268
CURLOPT_FOLLOWLOCATION => $this->allowRedirects,
251-
CURLOPT_RETURNTRANSFER => true,
252269
CURLOPT_USERAGENT => $this->userAgent
253270
];
254271

@@ -257,21 +274,19 @@ public function fetch(
257274
curl_setopt($ch, $option, $value);
258275
}
259276

260-
$sendRequest = function () use ($ch, &$responseHeaders) {
277+
$sendRequest = function () use ($ch, &$responseHeaders, &$responseBody) {
261278
$responseHeaders = [];
262279

263-
$responseBody = curl_exec($ch);
264-
$responseStatusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
265-
if (curl_errno($ch)) {
280+
$success = curl_exec($ch);
281+
if ($success === false) {
266282
$errorMsg = curl_error($ch);
283+
curl_close($ch);
284+
throw new Exception($errorMsg);
267285
}
268286

287+
$responseStatusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
269288
curl_close($ch);
270289

271-
if (isset($errorMsg)) {
272-
throw new FetchException($errorMsg);
273-
}
274-
275290
return new Response(
276291
statusCode: $responseStatusCode,
277292
headers: $responseHeaders,
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
namespace Utopia\Fetch;
44

5-
class FetchException extends \Exception
5+
class Exception extends \Exception
66
{
77
/**
88
* Constructor

tests/ChunkTest.php

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
<?php
2+
3+
namespace Utopia\Fetch;
4+
5+
use PHPUnit\Framework\TestCase;
6+
7+
final class ChunkTest extends TestCase
8+
{
9+
/**
10+
* Test chunk creation and getters
11+
* @return void
12+
*/
13+
public function testChunkCreation(): void
14+
{
15+
$data = '{"message": "test data"}';
16+
$size = strlen($data);
17+
$timestamp = microtime(true);
18+
$index = 0;
19+
20+
$chunk = new Chunk($data, $size, $timestamp, $index);
21+
22+
// Test getData method
23+
$this->assertEquals($data, $chunk->getData());
24+
$this->assertIsString($chunk->getData());
25+
26+
// Test getSize method
27+
$this->assertEquals($size, $chunk->getSize());
28+
$this->assertIsInt($chunk->getSize());
29+
$this->assertEquals(strlen($chunk->getData()), $chunk->getSize());
30+
31+
// Test getTimestamp method
32+
$this->assertEquals($timestamp, $chunk->getTimestamp());
33+
$this->assertIsFloat($chunk->getTimestamp());
34+
35+
// Test getIndex method
36+
$this->assertEquals($index, $chunk->getIndex());
37+
$this->assertIsInt($chunk->getIndex());
38+
}
39+
40+
/**
41+
* Test chunk with empty data
42+
* @return void
43+
*/
44+
public function testEmptyChunk(): void
45+
{
46+
$data = '';
47+
$size = 0;
48+
$timestamp = microtime(true);
49+
$index = 1;
50+
51+
$chunk = new Chunk($data, $size, $timestamp, $index);
52+
53+
$this->assertEquals('', $chunk->getData());
54+
$this->assertEquals(0, $chunk->getSize());
55+
$this->assertEquals($timestamp, $chunk->getTimestamp());
56+
$this->assertEquals(1, $chunk->getIndex());
57+
}
58+
59+
/**
60+
* Test chunk with binary data
61+
* @return void
62+
*/
63+
public function testBinaryChunk(): void
64+
{
65+
$data = pack('C*', 0x48, 0x65, 0x6c, 0x6c, 0x6f); // "Hello" in binary
66+
$size = strlen($data);
67+
$timestamp = microtime(true);
68+
$index = 2;
69+
70+
$chunk = new Chunk($data, $size, $timestamp, $index);
71+
72+
$this->assertEquals($data, $chunk->getData());
73+
$this->assertEquals(5, $chunk->getSize());
74+
$this->assertEquals($timestamp, $chunk->getTimestamp());
75+
$this->assertEquals(2, $chunk->getIndex());
76+
$this->assertEquals("Hello", $chunk->getData());
77+
}
78+
79+
/**
80+
* Test chunk with special characters
81+
* @return void
82+
*/
83+
public function testSpecialCharactersChunk(): void
84+
{
85+
$data = "Special chars: ñ, é, 漢字, 🌟";
86+
$size = strlen($data);
87+
$timestamp = microtime(true);
88+
$index = 3;
89+
90+
$chunk = new Chunk($data, $size, $timestamp, $index);
91+
92+
$this->assertEquals($data, $chunk->getData());
93+
$this->assertEquals($size, $chunk->getSize());
94+
$this->assertEquals($timestamp, $chunk->getTimestamp());
95+
$this->assertEquals(3, $chunk->getIndex());
96+
}
97+
98+
/**
99+
* Test chunk immutability
100+
* @return void
101+
*/
102+
public function testChunkImmutability(): void
103+
{
104+
$data = "test data";
105+
$size = strlen($data);
106+
$timestamp = microtime(true);
107+
$index = 4;
108+
109+
$chunk = new Chunk($data, $size, $timestamp, $index);
110+
$originalData = $chunk->getData();
111+
$originalSize = $chunk->getSize();
112+
$originalTimestamp = $chunk->getTimestamp();
113+
$originalIndex = $chunk->getIndex();
114+
115+
// Try to modify the data (this should create a new string, not modify the chunk)
116+
$modifiedData = $chunk->getData() . " modified";
117+
118+
// Verify original chunk remains unchanged
119+
$this->assertEquals($originalData, $chunk->getData());
120+
$this->assertEquals($originalSize, $chunk->getSize());
121+
$this->assertEquals($originalTimestamp, $chunk->getTimestamp());
122+
$this->assertEquals($originalIndex, $chunk->getIndex());
123+
$this->assertNotEquals($modifiedData, $chunk->getData());
124+
}
125+
}

0 commit comments

Comments
 (0)