diff --git a/baseline.xml b/baseline.xml index 10aae5a50..ad895cc90 100644 --- a/baseline.xml +++ b/baseline.xml @@ -1,5 +1,5 @@ - + @@ -100,6 +100,15 @@ + + + + + + + + + diff --git a/docs/pages/getting_started.md b/docs/pages/getting_started.md index fa30276fa..66e437737 100644 --- a/docs/pages/getting_started.md +++ b/docs/pages/getting_started.md @@ -181,7 +181,7 @@ final class HotelProjector */ public function getHotels(): array { - return $this->db->fetchAllAssociative("SELECT id, name, guests FROM ${this->table()};"); + return $this->db->fetchAllAssociative("SELECT id, name, guests FROM {$this->table()};"); } #[Subscribe(HotelCreated::class)] @@ -203,7 +203,7 @@ final class HotelProjector public function handleGuestIsCheckedIn(Message $message): void { $this->db->executeStatement( - "UPDATE ${this->table()} SET guests = guests + 1 WHERE id = ?;", + "UPDATE {$this->table()} SET guests = guests + 1 WHERE id = ?;", [$message->aggregateId()] ); } @@ -212,7 +212,7 @@ final class HotelProjector public function handleGuestIsCheckedOut(Message $message): void { $this->db->executeStatement( - "UPDATE ${this->table()} SET guests = guests - 1 WHERE id = ?;", + "UPDATE {$this->table()} SET guests = guests - 1 WHERE id = ?;", [$message->aggregateId()] ); } @@ -220,13 +220,13 @@ final class HotelProjector #[Setup] public function create(): void { - $this->db->executeStatement("CREATE TABLE IF NOT EXISTS ${this->table()} (id VARCHAR PRIMARY KEY, name VARCHAR, guests INTEGER);"); + $this->db->executeStatement("CREATE TABLE IF NOT EXISTS {$this->table()} (id VARCHAR PRIMARY KEY, name VARCHAR, guests INTEGER);"); } #[Teardown] public function drop(): void { - $this->db->executeStatement("DROP TABLE IF EXISTS ${this->table()};"); + $this->db->executeStatement("DROP TABLE IF EXISTS {$this->table()};"); } private function table(): string diff --git a/docs/pages/projection.md b/docs/pages/projection.md index 42c52823b..52c3a61dd 100644 --- a/docs/pages/projection.md +++ b/docs/pages/projection.md @@ -69,7 +69,7 @@ final class ProfileProjector $profileCreated = $message->event(); $this->connection->executeStatement( - "INSERT INTO ${this->table()} (id, name) VALUES(?, ?);", + "INSERT INTO {$this->table()} (id, name) VALUES(?, ?);", [ 'id' => $profileCreated->profileId->toString(), 'name' => $profileCreated->name @@ -123,14 +123,14 @@ final class ProfileProjector public function create(): void { $this->connection->executeStatement( - "CREATE TABLE IF NOT EXISTS ${this->table()} (id VARCHAR PRIMARY KEY, name VARCHAR NOT NULL);" + "CREATE TABLE IF NOT EXISTS {$this->table()} (id VARCHAR PRIMARY KEY, name VARCHAR NOT NULL);" ); } #[Teardown] public function drop(): void { - $this->connection->executeStatement("DROP TABLE IF EXISTS ${this->table()};"); + $this->connection->executeStatement("DROP TABLE IF EXISTS {$this->table()};"); } private function table(): string @@ -175,7 +175,7 @@ final class ProfileProjector */ public function getProfiles(): array { - return $this->connection->fetchAllAssociative("SELECT id, name FROM ${this->table()};"); + return $this->connection->fetchAllAssociative("SELECT id, name FROM {$this->table()};"); } private function table(): string diff --git a/phpcs.xml.dist b/phpcs.xml.dist index b6748a932..5fc7ab501 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -14,5 +14,6 @@ + diff --git a/src/Projection/Projectionist/DefaultProjectionist.php b/src/Projection/Projectionist/DefaultProjectionist.php index 340ef084e..850539c1b 100644 --- a/src/Projection/Projectionist/DefaultProjectionist.php +++ b/src/Projection/Projectionist/DefaultProjectionist.php @@ -36,7 +36,7 @@ final class DefaultProjectionist implements Projectionist /** @param iterable $projectors */ public function __construct( - private readonly Store $streamableMessageStore, + private readonly Store $messageStore, private readonly ProjectionStore $projectionStore, private readonly iterable $projectors, private readonly RetryStrategy $retryStrategy = new ClockBasedRetryStrategy(), @@ -86,7 +86,7 @@ function ($projections) use ($limit): void { $stream = null; try { - $stream = $this->streamableMessageStore->load( + $stream = $this->messageStore->load( new Criteria(fromIndex: $startIndex), ); @@ -215,7 +215,7 @@ function (array $projections) use ($limit): void { try { $criteria = new Criteria(fromIndex: $startIndex); - $stream = $this->streamableMessageStore->load($criteria); + $stream = $this->messageStore->load($criteria); $messageCounter = 0; @@ -868,7 +868,7 @@ private function projectorMetadata(object $projector): ProjectorMetadata private function latestIndex(): int { - $stream = $this->streamableMessageStore->load(null, 1, null, true); + $stream = $this->messageStore->load(null, 1, null, true); return $stream->index() ?: 0; } diff --git a/tests/Benchmark/BasicImplementation/Processor/SendEmailProcessor.php b/tests/Benchmark/BasicImplementation/Processor/SendEmailProcessor.php index d4ec19a64..0af5b1d99 100644 --- a/tests/Benchmark/BasicImplementation/Processor/SendEmailProcessor.php +++ b/tests/Benchmark/BasicImplementation/Processor/SendEmailProcessor.php @@ -4,11 +4,13 @@ namespace Patchlevel\EventSourcing\Tests\Benchmark\BasicImplementation\Processor; +use Patchlevel\EventSourcing\Attribute\Projector; use Patchlevel\EventSourcing\Attribute\Subscribe; use Patchlevel\EventSourcing\EventBus\Message; use Patchlevel\EventSourcing\Tests\Integration\BasicImplementation\Events\ProfileCreated; use Patchlevel\EventSourcing\Tests\Integration\BasicImplementation\SendEmailMock; +#[Projector('send_email')] final class SendEmailProcessor { #[Subscribe(ProfileCreated::class)] diff --git a/tests/Benchmark/BasicImplementation/Projection/ProfileProjector.php b/tests/Benchmark/BasicImplementation/Projection/ProfileProjector.php index 229492df3..fc8528838 100644 --- a/tests/Benchmark/BasicImplementation/Projection/ProfileProjector.php +++ b/tests/Benchmark/BasicImplementation/Projection/ProfileProjector.php @@ -10,13 +10,17 @@ use Patchlevel\EventSourcing\Attribute\Subscribe; use Patchlevel\EventSourcing\Attribute\Teardown; use Patchlevel\EventSourcing\EventBus\Message; +use Patchlevel\EventSourcing\Projection\Projector\ProjectorUtil; +use Patchlevel\EventSourcing\Tests\Benchmark\BasicImplementation\Events\NameChanged; use Patchlevel\EventSourcing\Tests\Benchmark\BasicImplementation\Events\ProfileCreated; use function assert; -#[Projector('dummy_1')] +#[Projector('profile')] final class ProfileProjector { + use ProjectorUtil; + public function __construct( private Connection $connection, ) { @@ -25,28 +29,47 @@ public function __construct( #[Setup] public function create(): void { - $this->connection->executeStatement('CREATE TABLE IF NOT EXISTS projection_profile (id VARCHAR PRIMARY KEY, name VARCHAR);'); + $this->connection->executeStatement("CREATE TABLE IF NOT EXISTS {$this->table()} (id VARCHAR PRIMARY KEY, name VARCHAR);"); } #[Teardown] public function drop(): void { - $this->connection->executeStatement('DROP TABLE IF EXISTS projection_profile;'); + $this->connection->executeStatement("DROP TABLE IF EXISTS {$this->table()};"); } #[Subscribe(ProfileCreated::class)] - public function handleProfileCreated(Message $message): void + public function onProfileCreated(Message $message): void { $profileCreated = $message->event(); assert($profileCreated instanceof ProfileCreated); - $this->connection->executeStatement( - 'INSERT INTO projection_profile (`id`, `name`) VALUES(:id, :name);', + $this->connection->insert( + $this->table(), [ 'id' => $profileCreated->profileId->toString(), 'name' => $profileCreated->name, ], ); } + + #[Subscribe(NameChanged::class)] + public function onNameChanged(Message $message): void + { + $nameChanged = $message->event(); + + assert($nameChanged instanceof NameChanged); + + $this->connection->update( + $this->table(), + ['name' => $nameChanged->name], + ['id' => $message->aggregateId()], + ); + } + + public function table(): string + { + return 'projection_' . $this->projectorId(); + } } diff --git a/tests/Benchmark/ProjectionistBench.php b/tests/Benchmark/ProjectionistBench.php new file mode 100644 index 000000000..468751144 --- /dev/null +++ b/tests/Benchmark/ProjectionistBench.php @@ -0,0 +1,105 @@ + Driver::class, + 'path' => self::DB_PATH, + ]); + + $this->bus = DefaultEventBus::create(); + + $this->store = new DoctrineDbalStore( + $connection, + DefaultEventSerializer::createFromPaths([__DIR__ . '/BasicImplementation/Events']), + 'eventstore', + ); + + $this->repository = new DefaultRepository($this->store, $this->bus, Profile::metadata()); + + $projectionStore = new DoctrineStore( + $connection, + ); + + $schemaDirector = new DoctrineSchemaDirector( + $connection, + new ChainSchemaConfigurator([ + $this->store, + $projectionStore, + ]), + ); + + $schemaDirector->create(); + + $this->id = ProfileId::v7(); + + $profile = Profile::create($this->id, 'Peter'); + + for ($i = 1; $i < 10_000; $i++) { + $profile->changeName('Peter ' . $i); + } + + $this->repository->save($profile); + + $this->projectionist = new DefaultProjectionist( + $this->store, + $projectionStore, + [ + new ProfileProjector($connection), + new SendEmailProcessor(), + ], + ); + } + + #[Bench\Revs(20)] + public function benchHandle10000Events(): void + { + $this->projectionist->boot(); + $this->projectionist->remove(); + } +}