Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a766032
chore: Add .idea to .gitignore
Jul 15, 2025
ca127a4
chore: rectorphp was implemented and the following adjustments
Jul 18, 2025
6c0326e
chore: remove .idea from .gitignore and clean up configuration files
Jul 18, 2025
28b7de2
chore: ractor config adjustement - skip SimplifyUselessVariableRector
Jul 18, 2025
3e9b132
Merge branch 'thenativeweb:main' into main
wundii Jul 19, 2025
1db4811
Merge branch 'thenativeweb:main' into main
wundii Jul 20, 2025
5b7d8fe
Merge branch 'thenativeweb:main' into main
wundii Jul 20, 2025
2553030
Merge branch 'thenativeweb:main' into main
wundii Jul 22, 2025
8b8739d
Merge branch 'thenativeweb:main' into main
wundii Jul 23, 2025
2e7c771
Merge branch 'thenativeweb:main' into main
wundii Jul 23, 2025
b636c85
Merge branch 'thenativeweb:main' into main
wundii Jul 23, 2025
5bfa6bd
refactor: update classes to final and readonly, enhance container sta…
Jul 23, 2025
3458d2e
Merge remote-tracking branch 'origin/main'
Jul 23, 2025
33580dd
docs: update README to include iterator_to_array usage examples for w…
Jul 25, 2025
3c4cd49
docs: update README to include iterator_to_array usage examples for w…
Jul 26, 2025
be405de
refactor: change writeEvents return type to array and update usage in…
Jul 26, 2025
2f015bb
Merge branch 'thenativeweb:main' into main
wundii Jul 27, 2025
2db57ec
Merge branch 'thenativeweb:main' into main
wundii Jul 27, 2025
7589e75
Merge branch 'thenativeweb:main' into main
wundii Jul 28, 2025
0ce4195
Merge branch 'thenativeweb:main' into main
wundii Jul 29, 2025
513a796
Merge branch 'thenativeweb:main' into main
wundii Jul 30, 2025
1897e12
chore: increased phpstan to level 8 and the resulting changes
Jul 30, 2025
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
7 changes: 6 additions & 1 deletion phpstan.neon
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
parameters:
level: 5
level: 8
checkMissingCallableSignature: true
paths:
- src
ignoreErrors:
-
identifier: missingType.generics
-
identifier: missingType.iterableValue
12 changes: 7 additions & 5 deletions src/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,14 +107,14 @@ public function start(): void

public function getHost(): string
{
$this->ensureRunning();
return $this->container->getHost();
$startedGenericContainer = $this->runningContainer();
return $startedGenericContainer->getHost();
}

public function getMappedPort(): int
{
$this->ensureRunning();
return $this->container->getMappedPort($this->internalPort);
$startedGenericContainer = $this->runningContainer();
return $startedGenericContainer->getMappedPort($this->internalPort);
}

public function getBaseUrl(): string
Expand Down Expand Up @@ -148,10 +148,12 @@ public function getClient(): Client
return new Client($baseUrl, $this->apiToken);
}

private function ensureRunning(): void
private function runningContainer(): StartedGenericContainer
{
if (!$this->container instanceof StartedGenericContainer) {
throw new RuntimeException('Container must be running');
}

return $this->container;
}
}
97 changes: 53 additions & 44 deletions src/Stream/CurlMultiHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@

class CurlMultiHandler
{
private ?CurlHandle $handle = null;
private ?CurlMultiHandle $multiHandle = null;
private ?CurlHandle $curlHandle = null;
private ?CurlMultiHandle $curlMultiHandle = null;
private float $abortIn = 0.0;
private float $iteratorTime;
private ?Queue $header = null;
Expand Down Expand Up @@ -43,7 +43,7 @@ public function getWriteQueue(): Queue

public function addHandle(Request $request): void
{
$handle = curl_init();
$curlHandle = curl_init();

$this->header = new Queue(maxSize: 100);
$this->write = new Queue();
Expand All @@ -54,50 +54,41 @@ public function addHandle(Request $request): void
$this->write,
);

if (!curl_setopt_array($handle, $options)) {
throw new RuntimeException('Internal HttpClient: Failed to set cURL options: ' . curl_error($handle));
if (!curl_setopt_array($curlHandle, $options)) {
throw new RuntimeException('Internal HttpClient: Failed to set cURL options: ' . curl_error($curlHandle));
}

$this->handle = $handle;
$this->curlHandle = $curlHandle;
}

public function execute(): void
{
if (!$this->handle instanceof CurlHandle) {
throw new RuntimeException('Internal HttpClient: No handle to execute.');
}

if (!$this->header instanceof Queue) {
throw new RuntimeException('Internal HttpClient: No header queue available.');
}
$curlHandle = $this->curlHandle();
$queue = $this->getHeaderQueue();

$multiHandle = curl_multi_init();
if (curl_multi_add_handle($multiHandle, $this->handle) !== CURLM_OK) {
throw new RuntimeException('Internal HttpClient: Failed to add cURL handle to multi handle: ' . curl_multi_strerror(curl_multi_errno($multiHandle)));
$curlMultiHandle = curl_multi_init();
if (curl_multi_add_handle($curlMultiHandle, $curlHandle) !== CURLM_OK) {
throw new RuntimeException('Internal HttpClient: Failed to add cURL handle to multi handle: ' . curl_multi_strerror(curl_multi_errno($curlMultiHandle)));
}

do {
$status = curl_multi_exec($multiHandle, $isRunning);
$status = curl_multi_exec($curlMultiHandle, $isRunning);
if ($isRunning) {
curl_multi_select($multiHandle);
curl_multi_select($curlMultiHandle);
}

$this->verifyCurlHandle($multiHandle);
$this->verifyCurlHandle($curlMultiHandle);

} while ($this->header->isEmpty() && $isRunning && $status === CURLM_OK);
} while ($queue->isEmpty() && $isRunning && $status === CURLM_OK);

$this->multiHandle = $multiHandle;
$this->curlMultiHandle = $curlMultiHandle;
}

public function contentIterator(): iterable
{
if (!$this->multiHandle instanceof CurlMultiHandle) {
throw new RuntimeException('Internal HttpClient: No multi handle to execute.');
}

if (!$this->write instanceof Queue) {
throw new RuntimeException('Internal HttpClient: No write queue available.');
}
$curlHandle = $this->curlHandle();
$curlMultiHandle = $this->curlMultiHandle();
$queue = $this->getWriteQueue();

$this->iteratorTime = microtime(true);

Expand All @@ -109,31 +100,31 @@ public function contentIterator(): iterable
break;
}

$status = curl_multi_exec($this->multiHandle, $isRunning);
$status = curl_multi_exec($curlMultiHandle, $isRunning);
if ($isRunning) {
curl_multi_select($this->multiHandle);
curl_multi_select($curlMultiHandle);
}

$this->verifyCurlHandle($this->multiHandle);
$this->verifyCurlHandle($curlMultiHandle);

while (!$this->write->isEmpty()) {
yield $this->write->read();
while (!$queue->isEmpty()) {
yield $queue->read();
}
} while ($isRunning && $status === CURLM_OK);

curl_multi_remove_handle($this->multiHandle, $this->handle);
curl_multi_close($this->multiHandle);
curl_close($this->handle);
curl_multi_remove_handle($curlMultiHandle, $curlHandle);
curl_multi_close($curlMultiHandle);
curl_close($curlHandle);

unset(
$this->handle,
$this->multiHandle,
$this->curlHandle,
$this->curlMultiHandle,
$this->header,
$this->write,
);

$this->handle = null;
$this->multiHandle = null;
$this->curlHandle = null;
$this->curlMultiHandle = null;
$this->header = null;
$this->write = null;
}
Expand All @@ -145,13 +136,31 @@ private function verifyCurlHandle(CurlMultiHandle $curlMultiHandle): void
return;
}

$handle = $info['handle'] ?? null;
if (!$handle instanceof CurlHandle) {
$curlHandle = $info['handle'] ?? null;
if (!$curlHandle instanceof CurlHandle) {
throw new RuntimeException('Internal HttpClient: cURL handle info read returned an invalid handle.');
}

if (curl_errno($handle) !== 0) {
throw new RuntimeException('Internal HttpClient: cURL handle execution failed with error: ' . curl_error($handle));
if (curl_errno($curlHandle) !== 0) {
throw new RuntimeException('Internal HttpClient: cURL handle execution failed with error: ' . curl_error($curlHandle));
}
}

private function curlHandle(): CurlHandle
{
if (!$this->curlHandle instanceof CurlHandle) {
throw new RuntimeException('Internal HttpClient: No handle available.');
}

return $this->curlHandle;
}

private function curlMultiHandle(): CurlMultiHandle
{
if (!$this->curlMultiHandle instanceof CurlMultiHandle) {
throw new RuntimeException('Internal HttpClient: No multi handle available.');
}

return $this->curlMultiHandle;
}
}
7 changes: 6 additions & 1 deletion src/Stream/HttpClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,12 @@ public function buildBody(null|array|object $body): string
return '';
}

return json_encode($body);
$json = json_encode($body);
if ($json === false) {
throw new InvalidArgumentException('Internal HttpClient: Failed to encode body to JSON: ' . json_last_error_msg());
}

return $json;
}

public function get(string $uri, ?string $apiToken = null): Response
Expand Down
4 changes: 4 additions & 0 deletions src/Stream/Response.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ public function __construct(

public function getStream(): Stream
{
if (!$this->stream instanceof Stream) {
throw new InvalidArgumentException('Internal HttpClient: No stream available in response.');
}

return $this->stream;
}

Expand Down
32 changes: 25 additions & 7 deletions tests/Stream/CurlMultiHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ public function getPropertyValue(object $object, string $propertyName): mixed
return $reflectionProperty->getValue($object);
}

public function setPropertyValue(object $object, string $propertyName, mixed $propertyValue): void
{
$reflectionClass = new ReflectionClass($object);
$reflectionProperty = $reflectionClass->getProperty($propertyName);
$reflectionProperty->setAccessible(true);
$reflectionProperty->setValue($object, $propertyValue);
}

public function removeLineBrakes(string $line): string
{
return preg_replace('/\r\n|\r|\n/', '', $line);
Expand Down Expand Up @@ -74,13 +82,13 @@ public function testAddHandleSetsQueuesAndHandle(): void

$this->assertInstanceOf(Queue::class, $this->getPropertyValue($curlMultiHandler, 'header'));
$this->assertInstanceOf(Queue::class, $this->getPropertyValue($curlMultiHandler, 'write'));
$this->assertNotNull($this->getPropertyValue($curlMultiHandler, 'handle'));
$this->assertNotNull($this->getPropertyValue($curlMultiHandler, 'curlHandle'));
}

public function testExecuteThrowsIfHandleMissing(): void
{
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Internal HttpClient: No handle to execute.');
$this->expectExceptionMessage('Internal HttpClient: No handle available.');

$curlMultiHandler = new CurlMultiHandler();
$curlMultiHandler->execute();
Expand Down Expand Up @@ -121,12 +129,24 @@ public function testExecuteSendsRequestAndParsesHttpHeadersCorrectly(): void
$this->assertSame('Content-Type: application/json', $this->removeLineBrakes($headerQueue->read()));
}

public function testContentIteratorThrowsIfHandleMissing(): void
{
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Internal HttpClient: No handle available.');

$curlMultiHandler = new CurlMultiHandler();
iterator_count($curlMultiHandler->contentIterator());
}

public function testContentIteratorThrowsIfMultiHandleMissing(): void
{
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Internal HttpClient: No multi handle to execute.');
$this->expectExceptionMessage('Internal HttpClient: No multi handle available.');

$curlMultiHandler = new CurlMultiHandler();

$this->setPropertyValue($curlMultiHandler, 'curlHandle', curl_init());

iterator_count($curlMultiHandler->contentIterator());
}

Expand All @@ -137,10 +157,8 @@ public function testContentIteratorThrowsIfWriteQueueMissing(): void

$curlMultiHandler = new CurlMultiHandler();

$reflectionClass = new ReflectionClass($curlMultiHandler);
$reflectionProperty = $reflectionClass->getProperty('multiHandle');
$reflectionProperty->setAccessible(true);
$reflectionProperty->setValue($curlMultiHandler, curl_multi_init());
$this->setPropertyValue($curlMultiHandler, 'curlHandle', curl_init());
$this->setPropertyValue($curlMultiHandler, 'curlMultiHandle', curl_multi_init());

iterator_count($curlMultiHandler->contentIterator());
}
Expand Down