Skip to content

Commit

Permalink
Event store testing (#62)
Browse files Browse the repository at this point in the history
* Introduce EventStoreFake

* Extract broker interface

* Broker fake

* Switch to StoresEvents interface in more places

* Remove some convenience methods from interface

* A little reorganization

* Move `realNow` to BrokerConvenienceMethods

* Update order to match how they're called
  • Loading branch information
inxilpro authored Feb 15, 2024
1 parent 76eafe2 commit c4aabc1
Show file tree
Hide file tree
Showing 17 changed files with 594 additions and 89 deletions.
4 changes: 2 additions & 2 deletions src/Commands/ReplayCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@
namespace Thunk\Verbs\Commands;

use Illuminate\Console\Command;
use Thunk\Verbs\Contracts\BrokersEvents;
use Thunk\Verbs\Event;
use Thunk\Verbs\Lifecycle\Broker;

class ReplayCommand extends Command
{
protected $signature = 'verbs:replay';

protected $description = 'Replay all Verbs events.';

public function handle(Broker $broker): void
public function handle(BrokersEvents $broker): void
{
$broker->replay(
beforeEach: fn (Event $event) => $this->getOutput()->write(sprintf(
Expand Down
18 changes: 18 additions & 0 deletions src/Contracts/BrokersEvents.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace Thunk\Verbs\Contracts;

use Thunk\Verbs\Event;

interface BrokersEvents
{
public function fire(Event $event): ?Event;

public function commit(): bool;

public function isValid(Event $event): bool;

public function isAllowed(Event $event): bool;

public function replay(?callable $beforeEach = null, ?callable $afterEach = null);
}
23 changes: 23 additions & 0 deletions src/Contracts/StoresEvents.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

namespace Thunk\Verbs\Contracts;

use Glhd\Bits\Bits;
use Illuminate\Support\LazyCollection;
use Ramsey\Uuid\UuidInterface;
use Symfony\Component\Uid\AbstractUid;
use Thunk\Verbs\Event;
use Thunk\Verbs\State;

interface StoresEvents
{
public function read(
?State $state = null,
Bits|UuidInterface|AbstractUid|int|string|null $after_id = null,
Bits|UuidInterface|AbstractUid|int|string|null $up_to_id = null,
bool $singleton = false
): LazyCollection;

/** @param Event[] $events */
public function write(array $events): bool;
}
32 changes: 30 additions & 2 deletions src/Facades/Verbs.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
namespace Thunk\Verbs\Facades;

use Carbon\CarbonInterface;
use Closure;
use Illuminate\Support\Facades\Facade;
use Thunk\Verbs\Contracts\BrokersEvents;
use Thunk\Verbs\Event;
use Thunk\Verbs\Lifecycle\Broker;
use Thunk\Verbs\Testing\BrokerFake;
use Thunk\Verbs\Testing\EventStoreFake;

/**
* @method static bool commit()
Expand All @@ -15,12 +18,37 @@
* @method static Event fire(Event $event)
* @method static void createMetadataUsing(callable $callback)
* @method static void commitImmediately(bool $commit_immediately = true)
* @method static EventStoreFake assertCommitted(string|Closure $event, Closure|int|null $callback = null)
* @method static EventStoreFake assertNotCommitted(string|Closure $event, ?Closure $callback = null)
* @method static EventStoreFake assertNothingCommitted()
* @method static CarbonInterface realNow()
*/
class Verbs extends Facade
{
public static function fake()
{
$real_broker = static::isFake()
? static::getFacadeRoot()->broker
: static::getFacadeRoot();

$fake_broker = new BrokerFake(
static::getFacadeApplication(),
static::getFacadeApplication()->make(EventStoreFake::class),
$real_broker
);

static::swap($fake_broker);

return $fake_broker;
}

public static function getFacadeRoot(): BrokersEvents
{
return parent::getFacadeRoot();
}

protected static function getFacadeAccessor()
{
return Broker::class;
return BrokersEvents::class;
}
}
76 changes: 5 additions & 71 deletions src/Lifecycle/Broker.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,21 @@

namespace Thunk\Verbs\Lifecycle;

use Carbon\CarbonInterface;
use Glhd\Bits\Bits;
use Ramsey\Uuid\UuidInterface;
use Symfony\Component\Uid\AbstractUid;
use Throwable;
use Thunk\Verbs\CommitsImmediately;
use Thunk\Verbs\Contracts\BrokersEvents;
use Thunk\Verbs\Contracts\StoresEvents;
use Thunk\Verbs\Event;
use Thunk\Verbs\Exceptions\EventNotValidForCurrentState;
use Thunk\Verbs\Lifecycle\Queue as EventQueue;
use Thunk\Verbs\Support\Wormhole;

class Broker
class Broker implements BrokersEvents
{
public bool $is_replaying = false;
use BrokerConvenienceMethods;

public bool $commit_immediately = false;

public function __construct(
protected Dispatcher $dispatcher,
protected MetadataManager $metadata,
protected Wormhole $wormhole,
) {
}

Expand Down Expand Up @@ -73,41 +67,13 @@ public function commit(): bool
return $this->commit();
}

public function isValid(Event $event): bool
{
try {
$states = $event->states();

Guards::for($event, null)->validate();
$states->each(fn ($state) => Guards::for($event, $state)->validate());

return true;
} catch (EventNotValidForCurrentState $e) {
return false;
}
}

public function isAllowed(Event $event): bool
{
try {
$states = $event->states();

Guards::for($event, null)->authorize();
$states->each(fn ($state) => Guards::for($event, $state)->authorize());

return true;
} catch (Throwable $e) {
return false;
}
}

public function replay(?callable $beforeEach = null, ?callable $afterEach = null)
{
$this->is_replaying = true;

app(SnapshotStore::class)->reset();

app(EventStore::class)->read()
app(StoresEvents::class)->read()
->each(function (Event $event) use ($beforeEach, $afterEach) {
app(StateManager::class)->setMaxEventId($event->id);

Expand All @@ -130,40 +96,8 @@ public function replay(?callable $beforeEach = null, ?callable $afterEach = null
$this->is_replaying = false;
}

public function isReplaying(): bool
{
return $this->is_replaying;
}

public function unlessReplaying(callable $callback)
{
if (! $this->is_replaying) {
$callback();
}
}

public function createMetadataUsing(?callable $callback = null): void
{
app(MetadataManager::class)->createMetadataUsing($callback);
}

public function toId(Bits|UuidInterface|AbstractUid|int|string|null $id): int|string|null
{
return match (true) {
$id instanceof Bits => $id->id(),
$id instanceof UuidInterface => $id->toString(),
$id instanceof AbstractUid => (string) $id,
default => $id,
};
}

public function commitImmediately(bool $commit_immediately = true): void
{
$this->commit_immediately = $commit_immediately;
}

public function realNow(): CarbonInterface
{
return $this->wormhole->realNow();
}
}
77 changes: 77 additions & 0 deletions src/Lifecycle/BrokerConvenienceMethods.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

namespace Thunk\Verbs\Lifecycle;

use Carbon\CarbonInterface;
use Glhd\Bits\Bits;
use Ramsey\Uuid\UuidInterface;
use Symfony\Component\Uid\AbstractUid;
use Throwable;
use Thunk\Verbs\Event;
use Thunk\Verbs\Exceptions\EventNotValidForCurrentState;
use Thunk\Verbs\Support\Wormhole;

trait BrokerConvenienceMethods
{
public bool $is_replaying = false;

public function toId(Bits|UuidInterface|AbstractUid|int|string|null $id): int|string|null
{
return match (true) {
$id instanceof Bits => $id->id(),
$id instanceof UuidInterface => $id->toString(),
$id instanceof AbstractUid => (string) $id,
default => $id,
};
}

public function createMetadataUsing(?callable $callback = null): void
{
app(MetadataManager::class)->createMetadataUsing($callback);
}

public function isAllowed(Event $event): bool
{
try {
$states = $event->states();

Guards::for($event, null)->authorize();
$states->each(fn ($state) => Guards::for($event, $state)->authorize());

return true;
} catch (Throwable $e) {
return false;
}
}

public function isValid(Event $event): bool
{
try {
$states = $event->states();

Guards::for($event, null)->validate();
$states->each(fn ($state) => Guards::for($event, $state)->validate());

return true;
} catch (EventNotValidForCurrentState $e) {
return false;
}
}

public function isReplaying(): bool
{
return $this->is_replaying;
}

public function unlessReplaying(callable $callback)
{
if (! $this->is_replaying) {
$callback();
}
}

public function realNow(): CarbonInterface
{
return app(Wormhole::class)->realNow();
}
}
9 changes: 6 additions & 3 deletions src/Lifecycle/EventStore.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use Illuminate\Support\LazyCollection;
use Ramsey\Uuid\UuidInterface;
use Symfony\Component\Uid\AbstractUid;
use Thunk\Verbs\Contracts\StoresEvents;
use Thunk\Verbs\Event;
use Thunk\Verbs\Exceptions\ConcurrencyException;
use Thunk\Verbs\Facades\Verbs;
Expand All @@ -19,7 +20,7 @@
use Thunk\Verbs\State;
use Thunk\Verbs\Support\Serializer;

class EventStore
class EventStore implements StoresEvents
{
public function __construct(
protected MetadataManager $metadata,
Expand All @@ -37,7 +38,6 @@ public function read(
->map(fn (VerbEvent $model) => $model->event());
}

/** @param Event[] $events */
public function write(array $events): bool
{
if (empty($events)) {
Expand Down Expand Up @@ -67,7 +67,10 @@ protected function readEvents(
->map(fn (VerbStateEvent $pivot) => $pivot->event);
}

return VerbEvent::query()->lazyById();
return VerbEvent::query()
->when($after_id, fn (Builder $query) => $query->where('id', '>', Verbs::toId($after_id)))
->when($up_to_id, fn (Builder $query) => $query->where('id', '<=', Verbs::toId($up_to_id)))
->lazyById();
}

/** @param Event[] $events */
Expand Down
3 changes: 2 additions & 1 deletion src/Lifecycle/Queue.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Thunk\Verbs\Lifecycle;

use Thunk\Verbs\Contracts\StoresEvents;
use Thunk\Verbs\Event;

class Queue
Expand All @@ -19,7 +20,7 @@ public function flush(): array

// TODO: Concurrency check

if (! app(EventStore::class)->write($events)) {
if (! app(StoresEvents::class)->write($events)) {
throw new \Exception('Failed to write events to store.');
}

Expand Down
3 changes: 2 additions & 1 deletion src/Lifecycle/StateManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Illuminate\Database\Eloquent\Collection;
use Ramsey\Uuid\UuidInterface;
use Symfony\Component\Uid\AbstractUid;
use Thunk\Verbs\Contracts\StoresEvents;
use Thunk\Verbs\Event;
use Thunk\Verbs\Facades\Verbs;
use Thunk\Verbs\State;
Expand All @@ -22,7 +23,7 @@ class StateManager
public function __construct(
protected Dispatcher $dispatcher,
protected SnapshotStore $snapshots,
protected EventStore $events,
protected StoresEvents $events,
) {
$this->states = new Collection();
}
Expand Down
4 changes: 2 additions & 2 deletions src/State.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
use Glhd\Bits\Bits;
use Ramsey\Uuid\UuidInterface;
use Symfony\Component\Uid\AbstractUid;
use Thunk\Verbs\Lifecycle\EventStore;
use Thunk\Verbs\Contracts\StoresEvents;
use Thunk\Verbs\Lifecycle\StateManager;
use Thunk\Verbs\Support\Serializer;

Expand Down Expand Up @@ -62,7 +62,7 @@ public static function singleton(): static

public function storedEvents()
{
return app(EventStore::class)
return app(StoresEvents::class)
->read(state: $this)
->collect();
}
Expand Down
Loading

0 comments on commit c4aabc1

Please sign in to comment.