Skip to content

Commit

Permalink
add stream readonly store
Browse files Browse the repository at this point in the history
  • Loading branch information
DavidBadura committed Jan 14, 2025
1 parent b27682d commit bbd7f5e
Show file tree
Hide file tree
Showing 5 changed files with 186 additions and 2 deletions.
10 changes: 8 additions & 2 deletions docs/pages/store.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,17 +138,23 @@ $store = new InMemoryStore();

You can pass messages to the constructor to initialize the store with some events.

### ReadOnlyStore
### ReadOnlyStore & StreamReadOnlyStore

Last but not least, we offer a read-only store decorator.
Last but not least, we offer two read-only stores.
One for the `DoctrineDbalStore` and one for the `StreamDoctrineDbalStore`.
It passes all methods to the underlying store, but the save throws an `StoreIsReadOnly` exception.

```php
use Patchlevel\EventSourcing\Store\ReadOnlyStore;
use Patchlevel\EventSourcing\Store\Store;
use Patchlevel\EventSourcing\Store\StreamReadOnlyStore;
use Patchlevel\EventSourcing\Store\StreamStore;

/** @var Store $store */
$readOnlyStore = new ReadOnlyStore($store);

/** @var StreamStore $store */
$readOnlyStore = new StreamReadOnlyStore($store);
```
## Schema

Expand Down
4 changes: 4 additions & 0 deletions src/Store/ReadOnlyStore.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Patchlevel\EventSourcing\Store;

use Closure;
use InvalidArgumentException;
use Patchlevel\EventSourcing\Message\Message;
use Patchlevel\EventSourcing\Store\Criteria\Criteria;
use Psr\Log\LoggerInterface;
Expand All @@ -15,6 +16,9 @@ public function __construct(
private readonly Store $store,
private readonly LoggerInterface|null $logger = null,
) {
if ($this->store instanceof StreamStore) {
throw new InvalidArgumentException('store must not be a StreamStore. use StreamReadOnlyStore instead');
}
}

public function load(
Expand Down
63 changes: 63 additions & 0 deletions src/Store/StreamReadOnlyStore.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

declare(strict_types=1);

namespace Patchlevel\EventSourcing\Store;

use Closure;
use Patchlevel\EventSourcing\Message\Message;
use Patchlevel\EventSourcing\Store\Criteria\Criteria;
use Psr\Log\LoggerInterface;

final class StreamReadOnlyStore implements StreamStore
{
public function __construct(
private readonly StreamStore $store,
private readonly LoggerInterface|null $logger = null,
) {
}

public function load(
Criteria|null $criteria = null,
int|null $limit = null,
int|null $offset = null,
bool $backwards = false,
): Stream {
return $this->store->load($criteria, $limit, $offset, $backwards);
}

public function count(Criteria|null $criteria = null): int
{
return $this->store->count($criteria);
}

public function save(Message ...$messages): void
{
foreach ($messages as $message) {
$this->logger?->info('tried to save message in read only store', ['message' => $message]);
}

throw new StoreIsReadOnly();
}

public function transactional(Closure $function): void
{
$this->store->transactional($function);
}

/** @return list<string> */
public function streams(): array
{
return $this->store->streams();
}

public function remove(Criteria|null $criteria = null): void
{
throw new StoreIsReadOnly();
}

public function archive(Criteria|null $criteria = null): void
{
throw new StoreIsReadOnly();
}
}
11 changes: 11 additions & 0 deletions tests/Unit/Store/ReadOnlyStoreTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,29 @@

namespace Patchlevel\EventSourcing\Tests\Unit\Store;

use InvalidArgumentException;
use Patchlevel\EventSourcing\Message\Message;
use Patchlevel\EventSourcing\Store\Criteria\Criteria;
use Patchlevel\EventSourcing\Store\ReadOnlyStore;
use Patchlevel\EventSourcing\Store\Store;
use Patchlevel\EventSourcing\Store\StoreIsReadOnly;
use Patchlevel\EventSourcing\Store\StreamStore;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;

/** @covers \Patchlevel\EventSourcing\Store\ReadOnlyStore */
final class ReadOnlyStoreTest extends TestCase
{
use ProphecyTrait;

public function testUnsupportedStore(): void
{
$parentStore = $this->prophesize(StreamStore::class);

$this->expectException(InvalidArgumentException::class);
new ReadOnlyStore($parentStore->reveal());
}

public function testLoad(): void
{
$criteria = new Criteria();
Expand Down
100 changes: 100 additions & 0 deletions tests/Unit/Store/StreamReadOnlyStoreTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<?php

declare(strict_types=1);

namespace Patchlevel\EventSourcing\Tests\Unit\Store;

use Patchlevel\EventSourcing\Message\Message;
use Patchlevel\EventSourcing\Store\Criteria\Criteria;
use Patchlevel\EventSourcing\Store\StoreIsReadOnly;
use Patchlevel\EventSourcing\Store\StreamReadOnlyStore;
use Patchlevel\EventSourcing\Store\StreamStore;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;

/** @covers \Patchlevel\EventSourcing\Store\ReadOnlyStore */
final class StreamReadOnlyStoreTest extends TestCase
{
use ProphecyTrait;

public function testLoad(): void
{
$criteria = new Criteria();

$parentStore = $this->prophesize(StreamStore::class);
$parentStore->load($criteria, 8, 42, true)->shouldBeCalled();

$store = new StreamReadOnlyStore($parentStore->reveal());
$store->load($criteria, 8, 42, true);
}

public function testCount(): void
{
$criteria = new Criteria();

$parentStore = $this->prophesize(StreamStore::class);
$parentStore->count($criteria)->shouldBeCalled();

$store = new StreamReadOnlyStore($parentStore->reveal());
$store->count($criteria);
}

public function testSave(): void
{
$message = new Message(new class () {
});

$parentStore = $this->prophesize(StreamStore::class);
$parentStore->save($message)->shouldNotBeCalled();

$store = new StreamReadOnlyStore($parentStore->reveal());
$this->expectException(StoreIsReadOnly::class);
$store->save($message);
}

public function testTransactional(): void
{
$callback = static function (): void {
};

$parentStore = $this->prophesize(StreamStore::class);
$parentStore->transactional($callback)->shouldBeCalled();

$store = new StreamReadOnlyStore($parentStore->reveal());
$store->transactional($callback);
}

public function testStreams(): void
{
$parentStore = $this->prophesize(StreamStore::class);
$parentStore->streams()->willReturn(['foo', 'bar'])->shouldBeCalled();

$store = new StreamReadOnlyStore($parentStore->reveal());

self::assertEquals(['foo', 'bar'], $store->streams());
}

public function testRemove(): void
{
$criteria = new Criteria();

$parentStore = $this->prophesize(StreamStore::class);
$parentStore->remove($criteria)->shouldNotBeCalled();

$store = new StreamReadOnlyStore($parentStore->reveal());
$this->expectException(StoreIsReadOnly::class);
$store->remove($criteria);
}

public function testArchive(): void
{
$criteria = new Criteria();

$parentStore = $this->prophesize(StreamStore::class);
$parentStore->archive($criteria)->shouldNotBeCalled();

$store = new StreamReadOnlyStore($parentStore->reveal());
$this->expectException(StoreIsReadOnly::class);
$store->archive($criteria);
}
}

0 comments on commit bbd7f5e

Please sign in to comment.