Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 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
19c70c1
feat: implemented readEventType and add getJsonData method
Jul 27, 2025
4224f65
fix: simplify RuntimeException handling in Client
Jul 27, 2025
9a244b7
fix: enhance RuntimeException handling in Client to include code and …
Jul 27, 2025
65b3b1e
Merge branch 'main' into readEventType
goloroden Jul 27, 2025
2db57ec
Merge branch 'thenativeweb:main' into main
wundii Jul 27, 2025
7725bd9
Merge branch 'refs/heads/main' into readEventType
Jul 27, 2025
7589e75
Merge branch 'thenativeweb:main' into main
wundii Jul 28, 2025
ec0377c
Merge branch 'refs/heads/main' into readEventType
Jul 28, 2025
5cc90da
fix: improve container initialization error handling
Jul 28, 2025
446b16c
chore: ecs fix
Jul 28, 2025
e4a6bd9
chore: container fix outsource to a separate branch
Jul 28, 2025
1a2ddf6
Merge branch 'main' into readEventType
goloroden Jul 29, 2025
6dcd8dd
Update README.md
goloroden Jul 29, 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
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,14 @@ foreach($eventTypes as $eventType) {
}
```

### Listing a Specific Event Type

To list a specific event type, call the `readEventType` function with the event type as an argument. The function returns the detailed event type, which includes the schema:

```php
$eventType = $client->readEventType('io.eventsourcingdb.library.book-acquired');
```

### Using Testcontainers

Import the `Container` class, call the `start` function to run a test container, get a client, run your test code, and finally call the `stop` function to stop the test container:
Expand Down
77 changes: 61 additions & 16 deletions src/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,15 @@ public function ping(): void
));
}

$body = $response->getStream()->getContents();
$data = json_decode($body, true);
try {
$data = $response->getStream()->getJsonData();
} catch (RuntimeException $runtimeException) {
throw new RuntimeException(
'Failed to ping: ' . $runtimeException->getMessage(),
$runtimeException->getCode(),
$runtimeException,
);
}

if (!isset($data['type']) || $data['type'] !== 'io.eventsourcingdb.api.ping-received') {
throw new RuntimeException('Failed to ping');
Expand All @@ -62,8 +69,15 @@ public function verifyApiToken(): void
));
}

$body = $response->getStream()->getContents();
$data = json_decode($body, true);
try {
$data = $response->getStream()->getJsonData();
} catch (RuntimeException $runtimeException) {
throw new RuntimeException(
'Failed to verify API token: ' . $runtimeException->getMessage(),
$runtimeException->getCode(),
$runtimeException,
);
}

if (!isset($data['type']) || $data['type'] !== 'io.eventsourcingdb.api.api-token-verified') {
throw new RuntimeException('Failed to verify API token');
Expand Down Expand Up @@ -93,18 +107,14 @@ public function writeEvents(array $events, array $preconditions = []): array
));
}

$body = $response->getStream()->getContents();
if ($body === '') {
return [];
}

if (!json_validate($body)) {
throw new RuntimeException('Failed to read events, after writing.');
}

$data = json_decode($body, true);
if (!is_array($data)) {
throw new RuntimeException('Failed to read events, expected an array.');
try {
$data = $response->getStream()->getJsonData();
} catch (RuntimeException $runtimeException) {
throw new RuntimeException(
'Failed to read events, after writing: ' . $runtimeException->getMessage(),
$runtimeException->getCode(),
$runtimeException,
);
}

$writtenEvents = array_map(
Expand Down Expand Up @@ -350,4 +360,39 @@ public function readEventTypes(): iterable
}
}
}

public function readEventType(string $eventType): EventType
{
$response = $this->httpClient->post(
'/api/v1/read-event-type',
$this->apiToken,
[
'eventType' => $eventType,
],
);

$status = $response->getStatusCode();
if ($status !== 200) {
throw new RuntimeException(sprintf(
"Failed to read event type, got HTTP status code '%d', expected '200'",
$status
));
}

try {
$data = $response->getStream()->getJsonData();
} catch (RuntimeException $runtimeException) {
throw new RuntimeException(
'Failed to read event type: ' . $runtimeException->getMessage(),
$runtimeException->getCode(),
$runtimeException,
);
}

return new EventType(
$data['eventType'],
$data['isPhantom'],
$data['schema'] ?? [],
);
}
}
21 changes: 21 additions & 0 deletions src/Stream/Stream.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Thenativeweb\Eventsourcingdb\Stream;

use IteratorAggregate;
use RuntimeException;
use Stringable;
use Traversable;

Expand All @@ -31,4 +32,24 @@ public function getContents(): string
{
return implode('', iterator_to_array($this));
}

public function getJsonData(): array
{
$contents = $this->getContents();
if ($contents === '') {
return [];
}

if (!json_validate($contents)) {
throw new RuntimeException('invalid json string');
}

$data = json_decode($contents, true);
if (!is_array($data)) {
$dataType = gettype($data);
throw new RuntimeException("json data is from type '{$dataType}', expected an array");
}

return $data;
}
}
63 changes: 63 additions & 0 deletions tests/ReadEventTypeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

declare(strict_types=1);

use PHPUnit\Framework\TestCase;
use Thenativeweb\Eventsourcingdb\EventCandidate;
use Thenativeweb\Eventsourcingdb\EventType;
use Thenativeweb\Eventsourcingdb\Tests\ClientTestTrait;

final class ReadEventTypeTest extends TestCase
{
use ClientTestTrait;

public function testFailsIfTheEventTypeDoesNotExist(): void
{
$this->expectExceptionMessage("Failed to read event type, got HTTP status code '404', expected '200'");

$this->client->readEventType('non.existent.eventType');
}

public function testFailsIfTheEventTypeIsMalformed(): void
{
$this->expectExceptionMessage("Failed to read event type, got HTTP status code '400', expected '200'");

$this->client->readEventType('malformed.eventType.');
}

public function testReadAnExistingEventType(): void
{
$firstEvent = new EventCandidate(
source: 'https://www.eventsourcingdb.io',
subject: '/test',
type: 'io.eventsourcingdb.test.foo',
data: [
'value' => 23,
],
);

$secondEvent = new EventCandidate(
source: 'https://www.eventsourcingdb.io',
subject: '/test',
type: 'io.eventsourcingdb.test.bar',
data: [
'value' => 42,
],
);

$this->client->writeEvents([
$firstEvent,
$secondEvent,
]);

$eventType = $this->client->readEventType('io.eventsourcingdb.test.foo');

$expected = new EventType(
eventType: 'io.eventsourcingdb.test.foo',
isPhantom: false,
schema: [],
);

$this->assertEquals($expected, $eventType);
}
}
51 changes: 51 additions & 0 deletions tests/Stream/StreamTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use ArrayIterator;
use PHPUnit\Framework\TestCase;
use RuntimeException;
use Thenativeweb\Eventsourcingdb\Stream\CurlMultiHandler;
use Thenativeweb\Eventsourcingdb\Stream\Stream;

Expand Down Expand Up @@ -44,4 +45,54 @@ public function testToStringReturnsContents(): void

$this->assertSame('foobar', (string) $stream);
}

public function testThrowsExceptionOnInvalidJson(): void
{
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('invalid json string');

$mockHandler = $this->createMock(CurlMultiHandler::class);
$mockHandler->method('contentIterator')
->willReturn(new ArrayIterator(['{invalid json']));

$stream = new Stream($mockHandler);
$stream->getJsonData();
}

public function testThrowsExceptionIfJsonIsNotArray(): void
{
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage("json data is from type 'boolean', expected an array");

$mockHandler = $this->createMock(CurlMultiHandler::class);
$mockHandler->method('contentIterator')
->willReturn(new ArrayIterator(['true']));

$stream = new Stream($mockHandler);
$stream->getJsonData();
}

public function testReturnsEmptyArrayOnEmptyContents(): void
{
$mockHandler = $this->createMock(CurlMultiHandler::class);
$mockHandler->method('contentIterator')
->willReturn(new ArrayIterator([]));

$stream = new Stream($mockHandler);

$this->assertSame([], $stream->getJsonData());
}

public function testReturnsDecodedArrayIfValidJsonArray(): void
{
$mockHandler = $this->createMock(CurlMultiHandler::class);
$mockHandler->method('contentIterator')
->willReturn(new ArrayIterator(['{"foo":"bar"}']));

$stream = new Stream($mockHandler);

$this->assertSame([
'foo' => 'bar',
], $stream->getJsonData());
}
}
12 changes: 6 additions & 6 deletions tests/WriteEventsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,14 +94,14 @@ public function testSupportsTheIsSubjectPristinePrecondition(): void

$this->expectExceptionMessage("Failed to write events, got HTTP status code '409', expected '200'");

iterator_to_array($this->client->writeEvents(
$this->client->writeEvents(
[
$secondEvent,
],
[
new IsSubjectPristine('/test'),
],
));
);
}

public function testSupportsTheIsSubjectOnEventIdPrecondition(): void
Expand Down Expand Up @@ -129,14 +129,14 @@ public function testSupportsTheIsSubjectOnEventIdPrecondition(): void
);

$this->expectExceptionMessage("Failed to write events, got HTTP status code '409', expected '200'");
iterator_to_array($this->client->writeEvents(
$this->client->writeEvents(
[
$secondEvent,
],
[
new IsSubjectOnEventId('/test', '1'),
],
));
);
}

public function testSupportsTheIsEventQlTruePrecondition(): void
Expand Down Expand Up @@ -164,13 +164,13 @@ public function testSupportsTheIsEventQlTruePrecondition(): void
);

$this->expectExceptionMessage("Failed to write events, got HTTP status code '409', expected '200'");
iterator_to_array($this->client->writeEvents(
$this->client->writeEvents(
[
$secondEvent,
],
[
new IsEventQlTrue('FROM e IN events PROJECT INTO COUNT() == 0'),
],
));
);
}
}