Skip to content

Commit 7c51a28

Browse files
committed
Add QueryBus integration test which was missing, also add "full cycle" bench which dispatches a command and then query the name.
Fix a deprecation notice regarding `SplObjectStorage`
1 parent 440a663 commit 7c51a28

11 files changed

Lines changed: 366 additions & 13 deletions

File tree

phpstan-baseline.neon

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,6 @@ parameters:
4242
count: 1
4343
path: src/Message/Serializer/DefaultHeadersSerializer.php
4444

45-
-
46-
message: '#^Call to an undefined method Patchlevel\\EventSourcing\\Store\\Store\:\:archive\(\)\.$#'
47-
identifier: method.notFound
48-
count: 1
49-
path: src/Repository/DefaultRepository.php
50-
5145
-
5246
message: '#^Parameter \#1 \.\.\.\$streamName of class Patchlevel\\EventSourcing\\Store\\Criteria\\StreamCriterion constructor expects string, string\|null given\.$#'
5347
identifier: argument.type
@@ -168,6 +162,24 @@ parameters:
168162
count: 3
169163
path: src/Subscription/ThrowableToErrorContextTransformer.php
170164

165+
-
166+
message: '#^Property Patchlevel\\EventSourcing\\Tests\\Benchmark\\BasicImplementation\\ProfileWithCommands\:\:\$id is never read, only written\.$#'
167+
identifier: property.onlyWritten
168+
count: 1
169+
path: tests/Benchmark/BasicImplementation/ProfileWithCommands.php
170+
171+
-
172+
message: '#^Cannot access offset ''name'' on array\<string, mixed\>\|false\.$#'
173+
identifier: offsetAccess.nonOffsetAccessible
174+
count: 1
175+
path: tests/Benchmark/BasicImplementation/Projection/ProfileProjector.php
176+
177+
-
178+
message: '#^Method Patchlevel\\EventSourcing\\Tests\\Benchmark\\BasicImplementation\\Projection\\ProfileProjector\:\:getProfileName\(\) should return string but returns mixed\.$#'
179+
identifier: return.type
180+
count: 1
181+
path: tests/Benchmark/BasicImplementation/Projection/ProfileProjector.php
182+
171183
-
172184
message: '#^Parameter \#1 \$id of static method Patchlevel\\EventSourcing\\Tests\\Benchmark\\BasicImplementation\\Profile\:\:create\(\) expects Patchlevel\\EventSourcing\\Tests\\Benchmark\\BasicImplementation\\ProfileId, Patchlevel\\EventSourcing\\Aggregate\\AggregateRootId given\.$#'
173185
identifier: argument.type
@@ -234,6 +246,18 @@ parameters:
234246
count: 1
235247
path: tests/Integration/BasicImplementation/ProfileWithCommands.php
236248

249+
-
250+
message: '#^Cannot access offset ''name'' on array\<string, mixed\>\|false\.$#'
251+
identifier: offsetAccess.nonOffsetAccessible
252+
count: 1
253+
path: tests/Integration/BasicImplementation/Projection/ProfileProjector.php
254+
255+
-
256+
message: '#^Method Patchlevel\\EventSourcing\\Tests\\Integration\\BasicImplementation\\Projection\\ProfileProjector\:\:getProfileName\(\) should return string but returns mixed\.$#'
257+
identifier: return.type
258+
count: 1
259+
path: tests/Integration/BasicImplementation/Projection/ProfileProjector.php
260+
237261
-
238262
message: '#^Call to static method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) with ''Patchlevel\\\\EventSourcing\\\\Tests\\\\Integration\\\\ChildAggregate\\\\Profile'' and Patchlevel\\EventSourcing\\Tests\\Integration\\ChildAggregate\\Profile will always evaluate to true\.$#'
239263
identifier: staticMethod.alreadyNarrowedType

src/Subscription/Engine/SubscriptionManager.php

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -77,48 +77,48 @@ public function find(SubscriptionCriteria $criteria): array
7777
public function add(Subscription ...$subscriptions): void
7878
{
7979
foreach ($subscriptions as $sub) {
80-
$this->forAdd->attach($sub);
80+
$this->forAdd->offsetSet($sub);
8181
}
8282
}
8383

8484
public function update(Subscription ...$subscriptions): void
8585
{
8686
foreach ($subscriptions as $sub) {
87-
$this->forUpdate->attach($sub);
87+
$this->forUpdate->offsetSet($sub);
8888
}
8989
}
9090

9191
public function remove(Subscription ...$subscriptions): void
9292
{
9393
foreach ($subscriptions as $sub) {
94-
$this->forRemove->attach($sub);
94+
$this->forRemove->offsetSet($sub);
9595
}
9696
}
9797

9898
public function flush(): void
9999
{
100100
foreach ($this->forAdd as $subscription) {
101-
if ($this->forRemove->contains($subscription)) {
101+
if ($this->forRemove->offsetExists($subscription)) {
102102
continue;
103103
}
104104

105105
$this->subscriptionStore->add($subscription);
106106
}
107107

108108
foreach ($this->forUpdate as $subscription) {
109-
if ($this->forAdd->contains($subscription)) {
109+
if ($this->forAdd->offsetExists($subscription)) {
110110
continue;
111111
}
112112

113-
if ($this->forRemove->contains($subscription)) {
113+
if ($this->forRemove->offsetExists($subscription)) {
114114
continue;
115115
}
116116

117117
$this->subscriptionStore->update($subscription);
118118
}
119119

120120
foreach ($this->forRemove as $subscription) {
121-
if ($this->forAdd->contains($subscription)) {
121+
if ($this->forAdd->offsetExists($subscription)) {
122122
continue;
123123
}
124124

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\EventSourcing\Tests\Benchmark\BasicImplementation\Command;
6+
7+
use Patchlevel\EventSourcing\Attribute\Id;
8+
use Patchlevel\EventSourcing\Tests\Benchmark\BasicImplementation\ProfileId;
9+
10+
final class ChangeProfileName
11+
{
12+
public function __construct(
13+
#[Id]
14+
public readonly ProfileId $id,
15+
public readonly string $name,
16+
) {
17+
}
18+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\EventSourcing\Tests\Benchmark\BasicImplementation\Command;
6+
7+
use Patchlevel\EventSourcing\Tests\Benchmark\BasicImplementation\ProfileId;
8+
9+
final class CreateProfile
10+
{
11+
public function __construct(
12+
public readonly ProfileId $id,
13+
public readonly string $name,
14+
) {
15+
}
16+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\EventSourcing\Tests\Benchmark\BasicImplementation;
6+
7+
use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot;
8+
use Patchlevel\EventSourcing\Attribute\Aggregate;
9+
use Patchlevel\EventSourcing\Attribute\Apply;
10+
use Patchlevel\EventSourcing\Attribute\Handle;
11+
use Patchlevel\EventSourcing\Attribute\Id;
12+
use Patchlevel\EventSourcing\Attribute\Inject;
13+
use Patchlevel\EventSourcing\Attribute\Snapshot;
14+
use Patchlevel\EventSourcing\Tests\Benchmark\BasicImplementation\Command\ChangeProfileName;
15+
use Patchlevel\EventSourcing\Tests\Benchmark\BasicImplementation\Command\CreateProfile;
16+
use Patchlevel\EventSourcing\Tests\Benchmark\BasicImplementation\Events\NameChanged;
17+
use Patchlevel\EventSourcing\Tests\Benchmark\BasicImplementation\Events\ProfileCreated;
18+
use Psr\Clock\ClockInterface;
19+
20+
#[Aggregate('profile_with_commands')]
21+
#[Snapshot('default', 100)]
22+
final class ProfileWithCommands extends BasicAggregateRoot
23+
{
24+
#[Id]
25+
private ProfileId $id;
26+
private string $name;
27+
28+
#[Handle]
29+
public static function create(
30+
CreateProfile $command,
31+
ClockInterface $clock,
32+
#[Inject('env')]
33+
string $env,
34+
): self {
35+
$self = new self();
36+
$self->recordThat(new ProfileCreated($command->id, $command->name, null));
37+
38+
return $self;
39+
}
40+
41+
#[Handle]
42+
public function changeName(
43+
ChangeProfileName $command,
44+
ClockInterface $clock,
45+
#[Inject('env')]
46+
string $env,
47+
): void {
48+
$this->recordThat(new NameChanged($command->name));
49+
}
50+
51+
#[Apply]
52+
protected function applyProfileCreated(ProfileCreated $event): void
53+
{
54+
$this->id = $event->profileId;
55+
$this->name = $event->name;
56+
}
57+
58+
#[Apply]
59+
protected function applyNameChanged(NameChanged $event): void
60+
{
61+
$this->name = $event->name;
62+
}
63+
64+
public function name(): string
65+
{
66+
return $this->name;
67+
}
68+
}

tests/Benchmark/BasicImplementation/Projection/ProfileProjector.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Patchlevel\EventSourcing\Tests\Benchmark\BasicImplementation\Projection;
66

77
use Doctrine\DBAL\Connection;
8+
use Patchlevel\EventSourcing\Attribute\Answer;
89
use Patchlevel\EventSourcing\Attribute\Projector;
910
use Patchlevel\EventSourcing\Attribute\Setup;
1011
use Patchlevel\EventSourcing\Attribute\Subscribe;
@@ -13,6 +14,7 @@
1314
use Patchlevel\EventSourcing\Tests\Benchmark\BasicImplementation\Events\NameChanged;
1415
use Patchlevel\EventSourcing\Tests\Benchmark\BasicImplementation\Events\ProfileCreated;
1516
use Patchlevel\EventSourcing\Tests\Benchmark\BasicImplementation\ProfileId;
17+
use Patchlevel\EventSourcing\Tests\Benchmark\BasicImplementation\Query\QueryProfileName;
1618

1719
#[Projector('profile')]
1820
final class ProfileProjector
@@ -58,6 +60,15 @@ public function onNameChanged(NameChanged $nameChanged, ProfileId $profileId): v
5860
);
5961
}
6062

63+
#[Answer]
64+
public function getProfileName(QueryProfileName $queryProfileName): string
65+
{
66+
return $this->connection->fetchAssociative(
67+
"SELECT name FROM {$this->table()} WHERE id = :id",
68+
['id' => $queryProfileName->id->toString()],
69+
)['name'];
70+
}
71+
6172
public function table(): string
6273
{
6374
return 'projection_' . $this->subscriberId();
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\EventSourcing\Tests\Benchmark\BasicImplementation\Query;
6+
7+
use Patchlevel\EventSourcing\Tests\Benchmark\BasicImplementation\ProfileId;
8+
9+
final readonly class QueryProfileName
10+
{
11+
public function __construct(public ProfileId $id)
12+
{
13+
}
14+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Benchmark;
6+
7+
use Patchlevel\EventSourcing\Clock\SystemClock;
8+
use Patchlevel\EventSourcing\CommandBus\CommandBus;
9+
use Patchlevel\EventSourcing\CommandBus\ServiceLocator;
10+
use Patchlevel\EventSourcing\CommandBus\SyncCommandBus;
11+
use Patchlevel\EventSourcing\Metadata\AggregateRoot\AggregateRootRegistry;
12+
use Patchlevel\EventSourcing\QueryBus\QueryBus;
13+
use Patchlevel\EventSourcing\QueryBus\ServiceHandlerProvider;
14+
use Patchlevel\EventSourcing\QueryBus\SyncQueryBus;
15+
use Patchlevel\EventSourcing\Repository\DefaultRepositoryManager;
16+
use Patchlevel\EventSourcing\Schema\DoctrineSchemaDirector;
17+
use Patchlevel\EventSourcing\Serializer\DefaultEventSerializer;
18+
use Patchlevel\EventSourcing\Snapshot\Adapter\InMemorySnapshotAdapter;
19+
use Patchlevel\EventSourcing\Snapshot\DefaultSnapshotStore;
20+
use Patchlevel\EventSourcing\Store\StreamDoctrineDbalStore;
21+
use Patchlevel\EventSourcing\Subscription\Engine\DefaultSubscriptionEngine;
22+
use Patchlevel\EventSourcing\Subscription\Repository\RunSubscriptionEngineRepositoryManager;
23+
use Patchlevel\EventSourcing\Subscription\Store\InMemorySubscriptionStore;
24+
use Patchlevel\EventSourcing\Subscription\Subscriber\MetadataSubscriberAccessorRepository;
25+
use Patchlevel\EventSourcing\Tests\Benchmark\BasicImplementation\Command\ChangeProfileName;
26+
use Patchlevel\EventSourcing\Tests\Benchmark\BasicImplementation\Command\CreateProfile;
27+
use Patchlevel\EventSourcing\Tests\Benchmark\BasicImplementation\Processor\SendEmailProcessor;
28+
use Patchlevel\EventSourcing\Tests\Benchmark\BasicImplementation\ProfileId;
29+
use Patchlevel\EventSourcing\Tests\Benchmark\BasicImplementation\ProfileWithCommands;
30+
use Patchlevel\EventSourcing\Tests\Benchmark\BasicImplementation\Projection\ProfileProjector;
31+
use Patchlevel\EventSourcing\Tests\Benchmark\BasicImplementation\Query\QueryProfileName;
32+
use Patchlevel\EventSourcing\Tests\DbalManager;
33+
use PhpBench\Attributes as Bench;
34+
use Psr\Clock\ClockInterface;
35+
36+
#[Bench\BeforeMethods('setUp')]
37+
final class CommandToQueryBench
38+
{
39+
private CommandBus $commandBus;
40+
private QueryBus $queryBus;
41+
42+
private ProfileId $updateId;
43+
44+
public function setUp(): void
45+
{
46+
$connection = DbalManager::createConnection();
47+
$store = new StreamDoctrineDbalStore(
48+
$connection,
49+
DefaultEventSerializer::createFromPaths([__DIR__ . '/BasicImplementation/Events']),
50+
);
51+
52+
$aggregateRootRegistry = new AggregateRootRegistry(['profile_with_commands' => ProfileWithCommands::class]);
53+
54+
$manager = new DefaultRepositoryManager(
55+
$aggregateRootRegistry,
56+
$store,
57+
null,
58+
new DefaultSnapshotStore(['default' => new InMemorySnapshotAdapter()]),
59+
);
60+
61+
$projectionConnection = DbalManager::createConnection();
62+
$profileProjection = new ProfileProjector($projectionConnection);
63+
64+
$engine = new DefaultSubscriptionEngine(
65+
$store,
66+
new InMemorySubscriptionStore(),
67+
new MetadataSubscriberAccessorRepository([
68+
$profileProjection,
69+
new SendEmailProcessor(),
70+
]),
71+
);
72+
73+
$manager = new RunSubscriptionEngineRepositoryManager($manager, $engine);
74+
75+
$this->commandBus = SyncCommandBus::createForAggregateHandlers(
76+
$aggregateRootRegistry,
77+
$manager,
78+
new ServiceLocator([
79+
ClockInterface::class => new SystemClock(),
80+
'env' => 'test',
81+
]),
82+
);
83+
84+
$this->queryBus = new SyncQueryBus(new ServiceHandlerProvider([$profileProjection]));
85+
86+
$schemaDirector = new DoctrineSchemaDirector($connection, $store);
87+
88+
$schemaDirector->create();
89+
$engine->setup(skipBooting: true);
90+
91+
$this->updateId = ProfileId::generate();
92+
$this->commandBus->dispatch(new CreateProfile($this->updateId, 'James'));
93+
}
94+
95+
#[Bench\Revs(10)]
96+
public function benchCreate(): void
97+
{
98+
$id = ProfileId::generate();
99+
$this->commandBus->dispatch(new CreateProfile($id, 'James'));
100+
$this->queryBus->dispatch(new QueryProfileName($id));
101+
}
102+
103+
#[Bench\Revs(10)]
104+
public function benchUpdate(): void
105+
{
106+
$this->commandBus->dispatch(new ChangeProfileName($this->updateId, 'James Doe'));
107+
$this->queryBus->dispatch(new QueryProfileName($this->updateId));
108+
}
109+
}

0 commit comments

Comments
 (0)