Skip to content
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
65 changes: 65 additions & 0 deletions src/Chunk.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

namespace Utopia\Fetch;

/**
* Chunk class
* Represents a chunk of data received from an HTTP response
* @package Utopia\Fetch
*/
class Chunk
{
/**
* @param string $data The raw chunk data
* @param int $size The size of the chunk in bytes
* @param float $timestamp The timestamp when the chunk was received
* @param int $index The sequential index of this chunk in the response
*/
public function __construct(
private readonly string $data,
private readonly int $size,
private readonly float $timestamp,
private readonly int $index,
) {
}

/**
* Get the raw chunk data
*
* @return string
*/
public function getData(): string
{
return $this->data;
}

/**
* Get the size of the chunk in bytes
*
* @return int
*/
public function getSize(): int
{
return $this->size;
}

/**
* Get the timestamp when the chunk was received
*
* @return float
*/
public function getTimestamp(): float
{
return $this->timestamp;
}

/**
* Get the sequential index of this chunk
*
* @return int
*/
public function getIndex(): int
{
return $this->index;
}
}
35 changes: 25 additions & 10 deletions src/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -199,16 +199,18 @@ private function withRetries(callable $callback): mixed
* @param string $method
* @param array<string>|array<string, mixed> $body
* @param array<string, mixed> $query
* @param ?callable $chunks Optional callback function that receives a Chunk object
* @return Response
*/
public function fetch(
string $url,
string $method = self::METHOD_GET,
?array $body = [],
?array $query = [],
?callable $chunks = null,
): Response {
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])) {
throw new FetchException("Unsupported HTTP method");
throw new Exception("Unsupported HTTP method");
}

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

$responseHeaders = [];
$responseBody = '';
$chunkIndex = 0;
$ch = curl_init();
$curlOptions = [
CURLOPT_URL => $url,
Expand All @@ -244,11 +248,24 @@ public function fetch(
$responseHeaders[strtolower(trim($header[0]))] = trim($header[1]);
return $len;
},
CURLOPT_WRITEFUNCTION => function ($ch, $data) use ($chunks, &$responseBody, &$chunkIndex) {
if ($chunks !== null) {
$chunk = new Chunk(
data: $data,
size: strlen($data),
timestamp: microtime(true),
index: $chunkIndex++
);
$chunks($chunk);
} else {
$responseBody .= $data;
}
return strlen($data);
},
CURLOPT_CONNECTTIMEOUT => $this->connectTimeout,
CURLOPT_TIMEOUT => $this->timeout,
CURLOPT_MAXREDIRS => $this->maxRedirects,
CURLOPT_FOLLOWLOCATION => $this->allowRedirects,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_USERAGENT => $this->userAgent
];

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

$sendRequest = function () use ($ch, &$responseHeaders) {
$sendRequest = function () use ($ch, &$responseHeaders, &$responseBody) {
$responseHeaders = [];

$responseBody = curl_exec($ch);
$responseStatusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if (curl_errno($ch)) {
$success = curl_exec($ch);
if ($success === false) {
$errorMsg = curl_error($ch);
curl_close($ch);
throw new Exception($errorMsg);
}

$responseStatusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if (isset($errorMsg)) {
throw new FetchException($errorMsg);
}

return new Response(
statusCode: $responseStatusCode,
headers: $responseHeaders,
Expand Down
2 changes: 1 addition & 1 deletion src/FetchException.php → src/Exception.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace Utopia\Fetch;

class FetchException extends \Exception
class Exception extends \Exception
{
/**
* Constructor
Expand Down
125 changes: 125 additions & 0 deletions tests/ChunkTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<?php

namespace Utopia\Fetch;

use PHPUnit\Framework\TestCase;

final class ChunkTest extends TestCase
{
/**
* Test chunk creation and getters
* @return void
*/
public function testChunkCreation(): void
{
$data = '{"message": "test data"}';
$size = strlen($data);
$timestamp = microtime(true);
$index = 0;

$chunk = new Chunk($data, $size, $timestamp, $index);

// Test getData method
$this->assertEquals($data, $chunk->getData());
$this->assertIsString($chunk->getData());

// Test getSize method
$this->assertEquals($size, $chunk->getSize());
$this->assertIsInt($chunk->getSize());
$this->assertEquals(strlen($chunk->getData()), $chunk->getSize());

// Test getTimestamp method
$this->assertEquals($timestamp, $chunk->getTimestamp());
$this->assertIsFloat($chunk->getTimestamp());

// Test getIndex method
$this->assertEquals($index, $chunk->getIndex());
$this->assertIsInt($chunk->getIndex());
}

/**
* Test chunk with empty data
* @return void
*/
public function testEmptyChunk(): void
{
$data = '';
$size = 0;
$timestamp = microtime(true);
$index = 1;

$chunk = new Chunk($data, $size, $timestamp, $index);

$this->assertEquals('', $chunk->getData());
$this->assertEquals(0, $chunk->getSize());
$this->assertEquals($timestamp, $chunk->getTimestamp());
$this->assertEquals(1, $chunk->getIndex());
}

/**
* Test chunk with binary data
* @return void
*/
public function testBinaryChunk(): void
{
$data = pack('C*', 0x48, 0x65, 0x6c, 0x6c, 0x6f); // "Hello" in binary
$size = strlen($data);
$timestamp = microtime(true);
$index = 2;

$chunk = new Chunk($data, $size, $timestamp, $index);

$this->assertEquals($data, $chunk->getData());
$this->assertEquals(5, $chunk->getSize());
$this->assertEquals($timestamp, $chunk->getTimestamp());
$this->assertEquals(2, $chunk->getIndex());
$this->assertEquals("Hello", $chunk->getData());
}

/**
* Test chunk with special characters
* @return void
*/
public function testSpecialCharactersChunk(): void
{
$data = "Special chars: ñ, é, 漢字, 🌟";
$size = strlen($data);
$timestamp = microtime(true);
$index = 3;

$chunk = new Chunk($data, $size, $timestamp, $index);

$this->assertEquals($data, $chunk->getData());
$this->assertEquals($size, $chunk->getSize());
$this->assertEquals($timestamp, $chunk->getTimestamp());
$this->assertEquals(3, $chunk->getIndex());
}

/**
* Test chunk immutability
* @return void
*/
public function testChunkImmutability(): void
{
$data = "test data";
$size = strlen($data);
$timestamp = microtime(true);
$index = 4;

$chunk = new Chunk($data, $size, $timestamp, $index);
$originalData = $chunk->getData();
$originalSize = $chunk->getSize();
$originalTimestamp = $chunk->getTimestamp();
$originalIndex = $chunk->getIndex();

// Try to modify the data (this should create a new string, not modify the chunk)
$modifiedData = $chunk->getData() . " modified";

// Verify original chunk remains unchanged
$this->assertEquals($originalData, $chunk->getData());
$this->assertEquals($originalSize, $chunk->getSize());
$this->assertEquals($originalTimestamp, $chunk->getTimestamp());
$this->assertEquals($originalIndex, $chunk->getIndex());
$this->assertNotEquals($modifiedData, $chunk->getData());
}
}
Loading