From 9c338be83829b5332a4637a5a19508a478eba862 Mon Sep 17 00:00:00 2001 From: David Badura Date: Fri, 8 Mar 2024 11:50:06 +0100 Subject: [PATCH] rename projection into subscription --- docs/pages/cli.md | 16 +- docs/pages/getting_started.md | 25 +- docs/pages/projection.md | 74 +- .../{Projector.php => Subscriber.php} | 8 +- .../Command/ProjectionRebuildCommand.php | 33 - ...ommand.php => SubscriptionBootCommand.php} | 10 +- ...ionCommand.php => SubscriptionCommand.php} | 16 +- ...mmand.php => SubscriptionPauseCommand.php} | 10 +- ....php => SubscriptionReactivateCommand.php} | 10 +- ...mand.php => SubscriptionRemoveCommand.php} | 12 +- ...Command.php => SubscriptionRunCommand.php} | 16 +- ...mand.php => SubscriptionStatusCommand.php} | 54 +- ...nd.php => SubscriptionTeardownCommand.php} | 10 +- .../Projector/DuplicateSetupMethod.php | 25 - .../Projector/DuplicateTeardownMethod.php | 25 - .../Projector/ProjectorMetadataFactory.php | 11 - .../Psr16ProjectorMetadataFactory.php | 33 - .../Psr6ProjectorMetadataFactory.php | 38 - .../AttributeSubscriberMetadataFactory.php} | 40 +- .../ClassIsNotASubscriber.php} | 6 +- .../Subscriber/DuplicateSetupMethod.php | 25 + .../Subscriber/DuplicateTeardownMethod.php | 25 + .../Psr16SubscriberMetadataFactory.php | 33 + .../Psr6SubscriberMetadataFactory.php | 38 + .../SubscriberMetadata.php} | 10 +- .../Subscriber/SubscriberMetadataFactory.php | 11 + .../Projection/ProjectionAlreadyExists.php | 17 - .../Projection/ProjectionNotFound.php | 17 - .../Projection/Store/InMemoryStore.php | 98 - .../Store/LockableProjectionStore.php | 12 - .../Projection/Store/ProjectionStore.php | 28 - .../Projectionist/DefaultProjectionist.php | 868 -------- .../Projectionist/Projectionist.php | 41 - .../Projectionist/ProjectorNotFound.php | 22 - .../MetadataProjectorAccessorRepository.php | 51 - .../Projector/ProjectorAccessorRepository.php | 13 - src/Projection/Projector/ProjectorHelper.php | 27 - src/Projection/Projector/ProjectorUtil.php | 37 - .../TraceableProjectorAccessorRepository.php | 52 - .../RetryStrategy/NoRetryStrategy.php | 15 - .../RetryStrategy/RetryStrategy.php | 12 - .../Engine/DefaultSubscriptionEngine.php | 868 ++++++++ .../Engine/SubscriberNotFound.php | 22 + .../Engine/SubscriptionEngine.php | 41 + .../Engine/SubscriptionEngineCriteria.php} | 4 +- .../Engine}/UnexpectedError.php | 2 +- .../RetryStrategy/ClockBasedRetryStrategy.php | 14 +- .../RetryStrategy/NoRetryStrategy.php | 15 + .../RetryStrategy/RetryStrategy.php | 12 + .../RetryStrategy/UnexpectedError.php | 2 +- .../Store/DoctrineSubscriptionStore.php} | 110 +- .../Store/InMemorySubscriptionStore.php | 95 + .../Store/LockableSubscriptionStore.php | 12 + .../Store/SubscriptionAlreadyExists.php | 17 + .../Store/SubscriptionCriteria.php} | 12 +- .../Store/SubscriptionNotFound.php | 17 + src/Subscription/Store/SubscriptionStore.php | 25 + .../Store/TransactionCommitNotPossible.php | 2 +- .../MetadataSubscriberAccessor.php} | 18 +- .../MetadataSubscriberAccessorRepository.php | 51 + .../Subscriber/SubscriberAccessor.php} | 6 +- .../SubscriberAccessorRepository.php | 13 + .../Subscriber/SubscriberHelper.php | 27 + .../Subscriber/SubscriberUtil.php | 37 + .../TraceableSubscriberAccessor.php} | 10 +- .../TraceableSubscriberAccessorRepository.php | 52 + .../Subscription}/NoErrorToRetry.php | 4 +- .../Subscription}/RunMode.php | 2 +- .../Subscription/Status.php} | 4 +- .../Subscription/Subscription.php} | 44 +- .../Subscription/SubscriptionError.php} | 10 +- .../ThrowableToErrorContextTransformer.php | 6 +- .../Processor/SendEmailProcessor.php | 4 +- .../Projection/ProfileProjector.php | 8 +- tests/Benchmark/ProjectionistBench.php | 16 +- .../IntegrationTest.php | 16 +- .../Projection/BankAccountProjector.php | 4 +- .../BasicIntegrationTest.php | 24 +- .../Projection/ProfileProjector.php | 4 +- .../Projection/ErrorProducerProjector.php | 4 +- .../Projection/ProfileProcessor.php | 4 +- .../Projection/ProfileProjector.php | 8 +- .../Projectionist/ProjectionistTest.php | 144 +- .../{ProjectorTest.php => SubscriberTest.php} | 8 +- ...ummyProjector.php => Dummy2Subscriber.php} | 6 +- ...ummy2Projector.php => DummySubscriber.php} | 6 +- ...ttributeSubscriberMetadataFactoryTest.php} | 66 +- tests/Unit/Projection/DummyStore.php | 59 - .../ProjectionAlreadyExistsTest.php | 24 - .../Projection/ProjectionCriteriaTest.php | 20 - .../Projection/ProjectionErrorTest.php | 26 - .../Projection/ProjectionNotFoundTest.php | 23 - .../Projection/Projection/ProjectionTest.php | 149 -- .../Projection/Store/InMemoryStoreTest.php | 154 -- .../DefaultProjectionistTest.php | 1873 ----------------- ...etadataProjectorAccessorRepositoryTest.php | 45 - .../MetadataProjectorAccessorTest.php | 206 -- .../Projector/ProjectorHelperTest.php | 24 - .../RetryStrategy/NoRetryStrategyTest.php | 22 - .../Subscription/DummySubscriptionStore.php | 59 + .../Engine/DefaultSubscriptionEngineTest.php | 1873 +++++++++++++++++ .../ClockBasedRetryStrategyTest.php | 18 +- .../RetryStrategy/NoRetryStrategyTest.php | 22 + .../Store/ErrorContextTest.php | 6 +- .../Store/InMemorySubscriptionStoreTest.php | 154 ++ .../Store/SubscriptionAlreadyExistsTest.php | 24 + .../Store/SubscriptionCriteriaTest.php | 20 + .../Store/SubscriptionNotFoundTest.php | 23 + ...tadataSubscriberAccessorRepositoryTest.php | 46 + .../MetadataSubscriberAccessorTest.php | 206 ++ .../Subscriber/SubscriberHelperTest.php | 24 + .../Subscription}/ErrorContextTest.php | 4 +- .../Subscription/SubscriptionErrorTest.php | 26 + .../Subscription/SubscriptionTest.php | 149 ++ 114 files changed, 4517 insertions(+), 4562 deletions(-) rename src/Attribute/{Projector.php => Subscriber.php} (56%) delete mode 100644 src/Console/Command/ProjectionRebuildCommand.php rename src/Console/Command/{ProjectionBootCommand.php => SubscriptionBootCommand.php} (75%) rename src/Console/Command/{ProjectionCommand.php => SubscriptionCommand.php} (65%) rename src/Console/Command/{ProjectionPauseCommand.php => SubscriptionPauseCommand.php} (60%) rename src/Console/Command/{ProjectionReactivateCommand.php => SubscriptionReactivateCommand.php} (59%) rename src/Console/Command/{ProjectionRemoveCommand.php => SubscriptionRemoveCommand.php} (63%) rename src/Console/Command/{ProjectionRunCommand.php => SubscriptionRunCommand.php} (87%) rename src/Console/Command/{ProjectionStatusCommand.php => SubscriptionStatusCommand.php} (56%) rename src/Console/Command/{ProjectionTeardownCommand.php => SubscriptionTeardownCommand.php} (58%) delete mode 100644 src/Metadata/Projector/DuplicateSetupMethod.php delete mode 100644 src/Metadata/Projector/DuplicateTeardownMethod.php delete mode 100644 src/Metadata/Projector/ProjectorMetadataFactory.php delete mode 100644 src/Metadata/Projector/Psr16ProjectorMetadataFactory.php delete mode 100644 src/Metadata/Projector/Psr6ProjectorMetadataFactory.php rename src/Metadata/{Projector/AttributeProjectorMetadataFactory.php => Subscriber/AttributeSubscriberMetadataFactory.php} (60%) rename src/Metadata/{Projector/ClassIsNotAProjector.php => Subscriber/ClassIsNotASubscriber.php} (65%) create mode 100644 src/Metadata/Subscriber/DuplicateSetupMethod.php create mode 100644 src/Metadata/Subscriber/DuplicateTeardownMethod.php create mode 100644 src/Metadata/Subscriber/Psr16SubscriberMetadataFactory.php create mode 100644 src/Metadata/Subscriber/Psr6SubscriberMetadataFactory.php rename src/Metadata/{Projector/ProjectorMetadata.php => Subscriber/SubscriberMetadata.php} (59%) create mode 100644 src/Metadata/Subscriber/SubscriberMetadataFactory.php delete mode 100644 src/Projection/Projection/ProjectionAlreadyExists.php delete mode 100644 src/Projection/Projection/ProjectionNotFound.php delete mode 100644 src/Projection/Projection/Store/InMemoryStore.php delete mode 100644 src/Projection/Projection/Store/LockableProjectionStore.php delete mode 100644 src/Projection/Projection/Store/ProjectionStore.php delete mode 100644 src/Projection/Projectionist/DefaultProjectionist.php delete mode 100644 src/Projection/Projectionist/Projectionist.php delete mode 100644 src/Projection/Projectionist/ProjectorNotFound.php delete mode 100644 src/Projection/Projector/MetadataProjectorAccessorRepository.php delete mode 100644 src/Projection/Projector/ProjectorAccessorRepository.php delete mode 100644 src/Projection/Projector/ProjectorHelper.php delete mode 100644 src/Projection/Projector/ProjectorUtil.php delete mode 100644 src/Projection/Projector/TraceableProjectorAccessorRepository.php delete mode 100644 src/Projection/RetryStrategy/NoRetryStrategy.php delete mode 100644 src/Projection/RetryStrategy/RetryStrategy.php create mode 100644 src/Subscription/Engine/DefaultSubscriptionEngine.php create mode 100644 src/Subscription/Engine/SubscriberNotFound.php create mode 100644 src/Subscription/Engine/SubscriptionEngine.php rename src/{Projection/Projectionist/ProjectionistCriteria.php => Subscription/Engine/SubscriptionEngineCriteria.php} (76%) rename src/{Projection/Projectionist => Subscription/Engine}/UnexpectedError.php (65%) rename src/{Projection => Subscription}/RetryStrategy/ClockBasedRetryStrategy.php (81%) create mode 100644 src/Subscription/RetryStrategy/NoRetryStrategy.php create mode 100644 src/Subscription/RetryStrategy/RetryStrategy.php rename src/{Projection => Subscription}/RetryStrategy/UnexpectedError.php (64%) rename src/{Projection/Projection/Store/DoctrineStore.php => Subscription/Store/DoctrineSubscriptionStore.php} (61%) create mode 100644 src/Subscription/Store/InMemorySubscriptionStore.php create mode 100644 src/Subscription/Store/LockableSubscriptionStore.php create mode 100644 src/Subscription/Store/SubscriptionAlreadyExists.php rename src/{Projection/Projection/ProjectionCriteria.php => Subscription/Store/SubscriptionCriteria.php} (50%) create mode 100644 src/Subscription/Store/SubscriptionNotFound.php create mode 100644 src/Subscription/Store/SubscriptionStore.php rename src/{Projection/Projection => Subscription}/Store/TransactionCommitNotPossible.php (86%) rename src/{Projection/Projector/MetadataProjectorAccessor.php => Subscription/Subscriber/MetadataSubscriberAccessor.php} (76%) create mode 100644 src/Subscription/Subscriber/MetadataSubscriberAccessorRepository.php rename src/{Projection/Projector/ProjectorAccessor.php => Subscription/Subscriber/SubscriberAccessor.php} (76%) create mode 100644 src/Subscription/Subscriber/SubscriberAccessorRepository.php create mode 100644 src/Subscription/Subscriber/SubscriberHelper.php create mode 100644 src/Subscription/Subscriber/SubscriberUtil.php rename src/{Projection/Projector/TraceableProjectorAccessor.php => Subscription/Subscriber/TraceableSubscriberAccessor.php} (84%) create mode 100644 src/Subscription/Subscriber/TraceableSubscriberAccessorRepository.php rename src/{Projection/Projection => Subscription/Subscription}/NoErrorToRetry.php (58%) rename src/{Projection/Projection => Subscription/Subscription}/RunMode.php (71%) rename src/{Projection/Projection/ProjectionStatus.php => Subscription/Subscription/Status.php} (72%) rename src/{Projection/Projection/Projection.php => Subscription/Subscription/Subscription.php} (67%) rename src/{Projection/Projection/ProjectionError.php => Subscription/Subscription/SubscriptionError.php} (59%) rename src/{Projection/Projection => Subscription/Subscription}/ThrowableToErrorContextTransformer.php (91%) rename tests/Unit/Attribute/{ProjectorTest.php => SubscriberTest.php} (53%) rename tests/Unit/Fixture/{DummyProjector.php => Dummy2Subscriber.php} (88%) rename tests/Unit/Fixture/{Dummy2Projector.php => DummySubscriber.php} (88%) rename tests/Unit/Metadata/{Projector/AttributeProjectorMetadataFactoryTest.php => Subscriber/AttributeSubscriberMetadataFactoryTest.php} (57%) delete mode 100644 tests/Unit/Projection/DummyStore.php delete mode 100644 tests/Unit/Projection/Projection/ProjectionAlreadyExistsTest.php delete mode 100644 tests/Unit/Projection/Projection/ProjectionCriteriaTest.php delete mode 100644 tests/Unit/Projection/Projection/ProjectionErrorTest.php delete mode 100644 tests/Unit/Projection/Projection/ProjectionNotFoundTest.php delete mode 100644 tests/Unit/Projection/Projection/ProjectionTest.php delete mode 100644 tests/Unit/Projection/Projection/Store/InMemoryStoreTest.php delete mode 100644 tests/Unit/Projection/Projectionist/DefaultProjectionistTest.php delete mode 100644 tests/Unit/Projection/Projector/MetadataProjectorAccessorRepositoryTest.php delete mode 100644 tests/Unit/Projection/Projector/MetadataProjectorAccessorTest.php delete mode 100644 tests/Unit/Projection/Projector/ProjectorHelperTest.php delete mode 100644 tests/Unit/Projection/RetryStrategy/NoRetryStrategyTest.php create mode 100644 tests/Unit/Subscription/DummySubscriptionStore.php create mode 100644 tests/Unit/Subscription/Engine/DefaultSubscriptionEngineTest.php rename tests/Unit/{Projection => Subscription}/RetryStrategy/ClockBasedRetryStrategyTest.php (71%) create mode 100644 tests/Unit/Subscription/RetryStrategy/NoRetryStrategyTest.php rename tests/Unit/{Projection/Projection => Subscription}/Store/ErrorContextTest.php (80%) create mode 100644 tests/Unit/Subscription/Store/InMemorySubscriptionStoreTest.php create mode 100644 tests/Unit/Subscription/Store/SubscriptionAlreadyExistsTest.php create mode 100644 tests/Unit/Subscription/Store/SubscriptionCriteriaTest.php create mode 100644 tests/Unit/Subscription/Store/SubscriptionNotFoundTest.php create mode 100644 tests/Unit/Subscription/Subscriber/MetadataSubscriberAccessorRepositoryTest.php create mode 100644 tests/Unit/Subscription/Subscriber/MetadataSubscriberAccessorTest.php create mode 100644 tests/Unit/Subscription/Subscriber/SubscriberHelperTest.php rename tests/Unit/{Projection/Projection => Subscription/Subscription}/ErrorContextTest.php (93%) create mode 100644 tests/Unit/Subscription/Subscription/SubscriptionErrorTest.php create mode 100644 tests/Unit/Subscription/Subscription/SubscriptionTest.php diff --git a/docs/pages/cli.md b/docs/pages/cli.md index 6f017744e..b447a06e0 100644 --- a/docs/pages/cli.md +++ b/docs/pages/cli.md @@ -74,14 +74,14 @@ $schemaManager = new DoctrineSchemaManager(); $cli->addCommands(array( new Command\DatabaseCreateCommand($store, $doctrineHelper), new Command\DatabaseDropCommand($store, $doctrineHelper), - new Command\ProjectionBootCommand($projectionist), - new Command\ProjectionPauseCommand($projectionist), - new Command\ProjectionRunCommand($projectionist), - new Command\ProjectionTeardownCommand($projectionist), - new Command\ProjectionRemoveCommand($projectionist), - new Command\ProjectionReactivateCommand($projectionist), - new Command\ProjectionRebuildCommand($projectionist), - new Command\ProjectionStatusCommand($projectionist), + new Command\SubscriptionBootCommand($projectionist), + new Command\SubscriptionPauseCommand($projectionist), + new Command\SubscriptionRunCommand($projectionist), + new Command\SubscriptionTeardownCommand($projectionist), + new Command\SubscriptionRemoveCommand($projectionist), + new Command\SubscriptionReactivateCommand($projectionist), + new Command\SubscriptionRebuildCommand($projectionist), + new Command\SubscriptionStatusCommand($projectionist), new Command\SchemaCreateCommand($store, $schemaManager), new Command\SchemaDropCommand($store, $schemaManager), new Command\SchemaUpdateCommand($store, $schemaManager), diff --git a/docs/pages/getting_started.md b/docs/pages/getting_started.md index 9b8c53ea1..80e9d8479 100644 --- a/docs/pages/getting_started.md +++ b/docs/pages/getting_started.md @@ -161,15 +161,15 @@ use Doctrine\DBAL\Connection; use Patchlevel\EventSourcing\Attribute\Setup; use Patchlevel\EventSourcing\Attribute\Teardown; use Patchlevel\EventSourcing\Attribute\Subscribe; -use Patchlevel\EventSourcing\Attribute\Projector; +use Patchlevel\EventSourcing\Attribute\Subscriber; use Patchlevel\EventSourcing\EventBus\Message; -use Patchlevel\EventSourcing\Projection\Projection\ProjectionId; -use Patchlevel\EventSourcing\Projection\Projector\ProjectorUtil; +use Patchlevel\EventSourcing\Projection\Subscription\ProjectionId; +use Patchlevel\EventSourcing\Projection\Subscriber\SubscriberUtil; -#[Projector('hotel')] +#[Subscriber('hotel')] final class HotelProjector { - use ProjectorUtil; + use SubscriberUtil; public function __construct( private readonly Connection $db @@ -277,14 +277,7 @@ final class SendCheckInEmailProcessor After we have defined everything, we still have to plug the whole thing together: ```php -use Doctrine\DBAL\DriverManager; -use Patchlevel\EventSourcing\EventBus\DefaultEventBus; -use Patchlevel\EventSourcing\Projection\Projection\Store\DoctrineStore; -use Patchlevel\EventSourcing\Projection\Projectionist\DefaultProjectionist; -use Patchlevel\EventSourcing\Projection\Projector\MetadataProjectorAccessorRepository; -use Patchlevel\EventSourcing\Repository\DefaultRepositoryManager; -use Patchlevel\EventSourcing\Serializer\DefaultEventSerializer; -use Patchlevel\EventSourcing\Store\DoctrineDbalStore; +use Doctrine\DBAL\DriverManager;use Patchlevel\EventSourcing\EventBus\DefaultEventBus;use Patchlevel\EventSourcing\Projection\Engine\DefaultSubscriptionEngine;use Patchlevel\EventSourcing\Projection\Subscriber\MetadataSubscriberAccessorRepository;use Patchlevel\EventSourcing\Projection\Store\DoctrineSubscriptionStore;use Patchlevel\EventSourcing\Repository\DefaultRepositoryManager;use Patchlevel\EventSourcing\Serializer\DefaultEventSerializer;use Patchlevel\EventSourcing\Store\DoctrineDbalStore; $connection = DriverManager::getConnection([ 'url' => 'mysql://user:secret@localhost/app' @@ -307,13 +300,13 @@ $eventStore = new DoctrineDbalStore( $hotelProjector = new HotelProjector($projectionConnection); -$projectorRepository = new MetadataProjectorAccessorRepository([ +$projectorRepository = new MetadataSubscriberAccessorRepository([ $hotelProjector, ]); -$projectionStore = new DoctrineStore($connection); +$projectionStore = new DoctrineSubscriptionStore($connection); -$projectionist = new DefaultProjectionist( +$projectionist = new DefaultSubscriptionEngine( $eventStore, $projectionStore, $projectorRepository, diff --git a/docs/pages/projection.md b/docs/pages/projection.md index a57b3246d..8f5aa0a02 100644 --- a/docs/pages/projection.md +++ b/docs/pages/projection.md @@ -16,13 +16,13 @@ To do this, you can use the `Projector` attribute. ```php use Doctrine\DBAL\Connection; -use Patchlevel\EventSourcing\Attribute\Projector; -use Patchlevel\EventSourcing\Projection\Projector\ProjectorUtil; +use Patchlevel\EventSourcing\Attribute\Subscriber; +use Patchlevel\EventSourcing\Projection\Subscriber\SubscriberUtil; -#[Projector('profile_1')] +#[Subscriber('profile_1')] final class ProfileProjector { - use ProjectorUtil; + use SubscriberUtil; public function __construct( private readonly Connection $connection @@ -52,14 +52,14 @@ The method name itself doesn't matter. ```php use Patchlevel\EventSourcing\Attribute\Subscribe; -use Patchlevel\EventSourcing\Attribute\Projector; +use Patchlevel\EventSourcing\Attribute\Subscriber; use Patchlevel\EventSourcing\EventBus\Message; -use Patchlevel\EventSourcing\Projection\Projector\ProjectorUtil; +use Patchlevel\EventSourcing\Projection\Subscriber\SubscriberUtil; -#[Projector('profile_1')] +#[Subscriber('profile_1')] final class ProfileProjector { - use ProjectorUtil; + use SubscriberUtil; // ... @@ -109,13 +109,13 @@ as the target does it automatically, so you can skip this. ```php use Patchlevel\EventSourcing\Attribute\Setup; use Patchlevel\EventSourcing\Attribute\Teardown; -use Patchlevel\EventSourcing\Attribute\Projector; -use Patchlevel\EventSourcing\Projection\Projector\ProjectorUtil; +use Patchlevel\EventSourcing\Attribute\Subscriber; +use Patchlevel\EventSourcing\Projection\Subscriber\SubscriberUtil; -#[Projector('profile_1')] +#[Subscriber('profile_1')] final class ProfileProjector { - use ProjectorUtil; + use SubscriberUtil; // ... @@ -160,13 +160,13 @@ You can also implement your read model here. You can offer methods that then read the data and put it into a specific format. ```php -use Patchlevel\EventSourcing\Attribute\Projector; -use Patchlevel\EventSourcing\Projection\Projector\ProjectorUtil; +use Patchlevel\EventSourcing\Attribute\Subscriber; +use Patchlevel\EventSourcing\Projection\Subscriber\SubscriberUtil; -#[Projector('profile_1')] +#[Subscriber('profile_1')] final class ProfileProjector { - use ProjectorUtil; + use SubscriberUtil; // ... @@ -198,9 +198,9 @@ Otherwise, the projectionist will not recognize that the projection has changed To do this, you can add a version to the `projectorId`: ```php -use Patchlevel\EventSourcing\Attribute\Projector; +use Patchlevel\EventSourcing\Attribute\Subscriber; -#[Projector('profile_2')] +#[Subscriber('profile_2')] final class ProfileProjector { // ... @@ -218,9 +218,9 @@ You can also group projectors and address these to the projectionist. This is useful if you want to run projectors in different processes or on different servers. ```php -use Patchlevel\EventSourcing\Attribute\Projector; +use Patchlevel\EventSourcing\Attribute\Subscriber; -#[Projector('profile_1', group: 'a')] +#[Subscriber('profile_1', group: 'a')] final class ProfileProjector { // ... @@ -242,10 +242,10 @@ This is the default mode. The projector will start from the beginning of the event stream and process all events. ```php -use Patchlevel\EventSourcing\Attribute\Projector; -use Patchlevel\EventSourcing\Projection\Projection\RunMode; +use Patchlevel\EventSourcing\Attribute\Subscriber; +use Patchlevel\EventSourcing\Projection\Subscription\RunMode; -#[Projector('welcome_email', runMode: RunMode::FromBeginning)] +#[Subscriber('welcome_email', runMode: RunMode::FromBeginning)] final class WelcomeEmailProjector { // ... @@ -259,10 +259,10 @@ This is useful for projectors that are only interested in events that occur afte As example, a welcome email projector that only wants to send emails to new users. ```php -use Patchlevel\EventSourcing\Attribute\Projector; -use Patchlevel\EventSourcing\Projection\Projection\RunMode; +use Patchlevel\EventSourcing\Attribute\Subscriber; +use Patchlevel\EventSourcing\Projection\Subscription\RunMode; -#[Projector('welcome_email', runMode: RunMode::FromNow)] +#[Subscriber('welcome_email', runMode: RunMode::FromNow)] final class WelcomeEmailProjector { // ... @@ -275,10 +275,10 @@ This mode is useful for projectors that only need to run once. This is useful for projectors to create reports or to migrate data. ```php -use Patchlevel\EventSourcing\Attribute\Projector; -use Patchlevel\EventSourcing\Projection\Projection\RunMode; +use Patchlevel\EventSourcing\Attribute\Subscriber; +use Patchlevel\EventSourcing\Projection\Subscription\RunMode; -#[Projector('migration', runMode: RunMode::Once)] +#[Subscriber('migration', runMode: RunMode::Once)] final class MigrationProjector { // ... @@ -419,9 +419,9 @@ The Projectionist uses a projection store to store the status of each projection We provide a Doctrine implementation of this by default. ```php -use Patchlevel\EventSourcing\Projection\Projection\Store\DoctrineStore; +use Patchlevel\EventSourcing\Projection\Store\DoctrineSubscriptionStore; -$projectionStore = new DoctrineStore($connection); +$projectionStore = new DoctrineSubscriptionStore($connection); ``` So that the schema for the projection store can also be created, @@ -475,9 +475,9 @@ The projector accessor is responsible for providing the projectors to the projec We provide a metadata projector accessor repository by default. ```php -use Patchlevel\EventSourcing\Projection\Projector\MetadataProjectorAccessorRepository; +use Patchlevel\EventSourcing\Projection\Subscriber\MetadataSubscriberAccessorRepository; -$projectorAccessorRepository = new MetadataProjectorAccessorRepository([$projector1, $projector2, $projector3]); +$projectorAccessorRepository = new MetadataSubscriberAccessorRepository([$projector1, $projector2, $projector3]); ``` ### Projectionist @@ -487,9 +487,9 @@ The event store is needed to load the events, the Projection Store to store the and the respective projectors. Optionally, we can also pass a retry strategy. ```php -use Patchlevel\EventSourcing\Projection\Projectionist\DefaultProjectionist; +use Patchlevel\EventSourcing\Projection\Engine\DefaultSubscriptionEngine; -$projectionist = new DefaultProjectionist( +$projectionist = new DefaultSubscriptionEngine( $eventStore, $projectionStore, $projectorAccessorRepository, @@ -503,9 +503,9 @@ The Projectionist has a few methods needed to use it effectively. A `ProjectionistCriteria` can be passed to all of these methods to filter the respective projectors. ```php -use Patchlevel\EventSourcing\Projection\Projectionist\ProjectionistCriteria; +use Patchlevel\EventSourcing\Projection\Engine\SubscriptionEngineCriteria; -$criteria = new ProjectionistCriteria( +$criteria = new SubscriptionEngineCriteria( ids: ['profile_1', 'welcome_email'], groups: ['default'] ); diff --git a/src/Attribute/Projector.php b/src/Attribute/Subscriber.php similarity index 56% rename from src/Attribute/Projector.php rename to src/Attribute/Subscriber.php index a45a7586a..abf798e8e 100644 --- a/src/Attribute/Projector.php +++ b/src/Attribute/Subscriber.php @@ -5,15 +5,15 @@ namespace Patchlevel\EventSourcing\Attribute; use Attribute; -use Patchlevel\EventSourcing\Projection\Projection\Projection; -use Patchlevel\EventSourcing\Projection\Projection\RunMode; +use Patchlevel\EventSourcing\Subscription\Subscription\RunMode; +use Patchlevel\EventSourcing\Subscription\Subscription\Subscription; #[Attribute(Attribute::TARGET_CLASS)] -final class Projector +final class Subscriber { public function __construct( public readonly string $id, - public readonly string $group = Projection::DEFAULT_GROUP, + public readonly string $group = Subscription::DEFAULT_GROUP, public readonly RunMode $runMode = RunMode::FromBeginning, ) { } diff --git a/src/Console/Command/ProjectionRebuildCommand.php b/src/Console/Command/ProjectionRebuildCommand.php deleted file mode 100644 index 137333d31..000000000 --- a/src/Console/Command/ProjectionRebuildCommand.php +++ /dev/null @@ -1,33 +0,0 @@ -projectionCriteria($input); - - if (!$io->confirm('do you want to rebuild all projections?', false)) { - return 1; - } - - $this->projectionist->remove($criteria); - $this->projectionist->boot($criteria, null); - - return 0; - } -} diff --git a/src/Console/Command/ProjectionBootCommand.php b/src/Console/Command/SubscriptionBootCommand.php similarity index 75% rename from src/Console/Command/ProjectionBootCommand.php rename to src/Console/Command/SubscriptionBootCommand.php index 387224dba..e63f864eb 100644 --- a/src/Console/Command/ProjectionBootCommand.php +++ b/src/Console/Command/SubscriptionBootCommand.php @@ -11,10 +11,10 @@ use Symfony\Component\Console\Output\OutputInterface; #[AsCommand( - 'event-sourcing:projection:boot', - 'Prepare new projections and catch up with the event store', + 'event-sourcing:subscription:boot', + 'Prepare new subscriptions and catch up with the event store', )] -final class ProjectionBootCommand extends ProjectionCommand +final class SubscriptionBootCommand extends SubscriptionCommand { public function configure(): void { @@ -33,8 +33,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int { $limit = InputHelper::nullablePositiveInt($input->getOption('limit')); - $criteria = $this->projectionCriteria($input); - $this->projectionist->boot($criteria, $limit); + $criteria = $this->subscriptionEngineCriteria($input); + $this->engine->boot($criteria, $limit); return 0; } diff --git a/src/Console/Command/ProjectionCommand.php b/src/Console/Command/SubscriptionCommand.php similarity index 65% rename from src/Console/Command/ProjectionCommand.php rename to src/Console/Command/SubscriptionCommand.php index 2314882d1..8c91b0db1 100644 --- a/src/Console/Command/ProjectionCommand.php +++ b/src/Console/Command/SubscriptionCommand.php @@ -5,17 +5,17 @@ namespace Patchlevel\EventSourcing\Console\Command; use Patchlevel\EventSourcing\Console\InputHelper; -use Patchlevel\EventSourcing\Projection\Projectionist\Projectionist; -use Patchlevel\EventSourcing\Projection\Projectionist\ProjectionistCriteria; +use Patchlevel\EventSourcing\Subscription\Engine\SubscriptionEngine; +use Patchlevel\EventSourcing\Subscription\Engine\SubscriptionEngineCriteria; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; /** @interal */ -abstract class ProjectionCommand extends Command +abstract class SubscriptionCommand extends Command { public function __construct( - protected readonly Projectionist $projectionist, + protected readonly SubscriptionEngine $engine, ) { parent::__construct(); } @@ -27,19 +27,19 @@ protected function configure(): void 'id', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, - 'filter by projection id', + 'filter by subscription id', ) ->addOption( 'group', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, - 'filter by projection group', + 'filter by subscription group', ); } - protected function projectionCriteria(InputInterface $input): ProjectionistCriteria + protected function subscriptionEngineCriteria(InputInterface $input): SubscriptionEngineCriteria { - return new ProjectionistCriteria( + return new SubscriptionEngineCriteria( InputHelper::nullableStringList($input->getOption('id')), InputHelper::nullableStringList($input->getOption('group')), ); diff --git a/src/Console/Command/ProjectionPauseCommand.php b/src/Console/Command/SubscriptionPauseCommand.php similarity index 60% rename from src/Console/Command/ProjectionPauseCommand.php rename to src/Console/Command/SubscriptionPauseCommand.php index 7ab4626ba..50aa8a8f8 100644 --- a/src/Console/Command/ProjectionPauseCommand.php +++ b/src/Console/Command/SubscriptionPauseCommand.php @@ -9,15 +9,15 @@ use Symfony\Component\Console\Output\OutputInterface; #[AsCommand( - 'event-sourcing:projection:pause', - 'Set projection to pause', + 'event-sourcing:subscription:pause', + 'Set subscription to pause', )] -final class ProjectionPauseCommand extends ProjectionCommand +final class SubscriptionPauseCommand extends SubscriptionCommand { protected function execute(InputInterface $input, OutputInterface $output): int { - $criteria = $this->projectionCriteria($input); - $this->projectionist->pause($criteria); + $criteria = $this->subscriptionEngineCriteria($input); + $this->engine->pause($criteria); return 0; } diff --git a/src/Console/Command/ProjectionReactivateCommand.php b/src/Console/Command/SubscriptionReactivateCommand.php similarity index 59% rename from src/Console/Command/ProjectionReactivateCommand.php rename to src/Console/Command/SubscriptionReactivateCommand.php index da1aaa7ce..81ec2e1f0 100644 --- a/src/Console/Command/ProjectionReactivateCommand.php +++ b/src/Console/Command/SubscriptionReactivateCommand.php @@ -9,15 +9,15 @@ use Symfony\Component\Console\Output\OutputInterface; #[AsCommand( - 'event-sourcing:projection:reactivate', - 'Reactivate failed projections', + 'event-sourcing:subscription:reactivate', + 'Reactivate subscriptions', )] -final class ProjectionReactivateCommand extends ProjectionCommand +final class SubscriptionReactivateCommand extends SubscriptionCommand { protected function execute(InputInterface $input, OutputInterface $output): int { - $criteria = $this->projectionCriteria($input); - $this->projectionist->reactivate($criteria); + $criteria = $this->subscriptionEngineCriteria($input); + $this->engine->reactivate($criteria); return 0; } diff --git a/src/Console/Command/ProjectionRemoveCommand.php b/src/Console/Command/SubscriptionRemoveCommand.php similarity index 63% rename from src/Console/Command/ProjectionRemoveCommand.php rename to src/Console/Command/SubscriptionRemoveCommand.php index f08613283..dbff5177b 100644 --- a/src/Console/Command/ProjectionRemoveCommand.php +++ b/src/Console/Command/SubscriptionRemoveCommand.php @@ -10,24 +10,24 @@ use Symfony\Component\Console\Output\OutputInterface; #[AsCommand( - 'event-sourcing:projection:remove', - 'Delete all projection and metadata', + 'event-sourcing:subscription:remove', + 'Delete all subscriptions', )] -final class ProjectionRemoveCommand extends ProjectionCommand +final class SubscriptionRemoveCommand extends SubscriptionCommand { protected function execute(InputInterface $input, OutputInterface $output): int { $io = new OutputStyle($input, $output); - $criteria = $this->projectionCriteria($input); + $criteria = $this->subscriptionEngineCriteria($input); if ($criteria->ids === null) { - if (!$io->confirm('do you want to remove all projections?', false)) { + if (!$io->confirm('do you want to remove all subscriptions?', false)) { return 1; } } - $this->projectionist->remove($criteria); + $this->engine->remove($criteria); return 0; } diff --git a/src/Console/Command/ProjectionRunCommand.php b/src/Console/Command/SubscriptionRunCommand.php similarity index 87% rename from src/Console/Command/ProjectionRunCommand.php rename to src/Console/Command/SubscriptionRunCommand.php index 1a6d79381..0ef42ec35 100644 --- a/src/Console/Command/ProjectionRunCommand.php +++ b/src/Console/Command/SubscriptionRunCommand.php @@ -13,10 +13,10 @@ use Symfony\Component\Console\Output\OutputInterface; #[AsCommand( - 'event-sourcing:projection:run', - 'Run the active projections', + 'event-sourcing:subscription:run', + 'Run the active subscriptions', )] -final class ProjectionRunCommand extends ProjectionCommand +final class SubscriptionRunCommand extends SubscriptionCommand { protected function configure(): void { @@ -59,7 +59,7 @@ protected function configure(): void 'rebuild', null, InputOption::VALUE_NONE, - 'rebuild (remove & boot) projections before run', + 'rebuild (remove & boot) subscriptions before run', ); } @@ -72,13 +72,13 @@ protected function execute(InputInterface $input, OutputInterface $output): int $sleep = InputHelper::positiveIntOrZero($input->getOption('sleep')); $rebuild = InputHelper::bool($input->getOption('rebuild')); - $criteria = $this->projectionCriteria($input); + $criteria = $this->subscriptionEngineCriteria($input); $logger = new ConsoleLogger($output); $worker = DefaultWorker::create( function () use ($criteria, $messageLimit): void { - $this->projectionist->run($criteria, $messageLimit); + $this->engine->run($criteria, $messageLimit); }, [ 'runLimit' => $runLimit, @@ -89,8 +89,8 @@ function () use ($criteria, $messageLimit): void { ); if ($rebuild) { - $this->projectionist->remove($criteria); - $this->projectionist->boot($criteria); + $this->engine->remove($criteria); + $this->engine->boot($criteria); } $worker->run($sleep); diff --git a/src/Console/Command/ProjectionStatusCommand.php b/src/Console/Command/SubscriptionStatusCommand.php similarity index 56% rename from src/Console/Command/ProjectionStatusCommand.php rename to src/Console/Command/SubscriptionStatusCommand.php index 52224ef7a..896186658 100644 --- a/src/Console/Command/ProjectionStatusCommand.php +++ b/src/Console/Command/SubscriptionStatusCommand.php @@ -6,9 +6,9 @@ use Patchlevel\EventSourcing\Console\InputHelper; use Patchlevel\EventSourcing\Console\OutputStyle; -use Patchlevel\EventSourcing\Projection\Projection\Projection; -use Patchlevel\EventSourcing\Projection\Projection\ProjectionError; -use Patchlevel\EventSourcing\Projection\Projection\ProjectionNotFound; +use Patchlevel\EventSourcing\Subscription\Store\SubscriptionNotFound; +use Patchlevel\EventSourcing\Subscription\Subscription\Subscription; +use Patchlevel\EventSourcing\Subscription\Subscription\SubscriptionError; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -18,12 +18,12 @@ use function is_array; use function sprintf; -/** @psalm-import-type Context from ProjectionError */ +/** @psalm-import-type Context from SubscriptionError */ #[AsCommand( - 'event-sourcing:projection:status', - 'View the current status of the projections', + 'event-sourcing:subscription:status', + 'View the current status of the subscriptions', )] -final class ProjectionStatusCommand extends ProjectionCommand +final class SubscriptionStatusCommand extends SubscriptionCommand { protected function configure(): void { @@ -32,7 +32,7 @@ protected function configure(): void $this->addArgument( 'id', InputArgument::OPTIONAL, - 'The projection to display more information about', + 'The subscription to display more information about', ); } @@ -41,7 +41,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $io = new OutputStyle($input, $output); $id = InputHelper::nullableString($input->getArgument('id')); - $projections = $this->projectionist->projections(); + $subscriptions = $this->engine->subscriptions(); if ($id === null) { $io->table( @@ -52,20 +52,20 @@ protected function execute(InputInterface $input, OutputInterface $output): int 'error message', ], array_map( - static fn (Projection $projection) => [ - $projection->id(), - $projection->position(), - $projection->status()->value, - $projection->projectionError()?->errorMessage, + static fn (Subscription $subscription) => [ + $subscription->id(), + $subscription->position(), + $subscription->status()->value, + $subscription->subscriptionError()?->errorMessage, ], - $projections, + $subscriptions, ), ); return 0; } - $projection = $this->findProjection($projections, $id); + $subscription = $this->findSubscription($subscriptions, $id); $io->horizontalTable( [ @@ -76,15 +76,15 @@ protected function execute(InputInterface $input, OutputInterface $output): int ], [ [ - $projection->id(), - $projection->position(), - $projection->status()->value, - $projection->projectionError()?->errorMessage, + $subscription->id(), + $subscription->position(), + $subscription->status()->value, + $subscription->subscriptionError()?->errorMessage, ], ], ); - $contexts = $projection->projectionError()?->errorContext; + $contexts = $subscription->subscriptionError()?->errorContext; if (is_array($contexts)) { foreach ($contexts as $context) { @@ -105,15 +105,15 @@ private function displayError(OutputStyle $io, array $context): void } } - /** @param list $projections */ - private function findProjection(array $projections, string $id): Projection + /** @param list $subscriptions */ + private function findSubscription(array $subscriptions, string $id): Subscription { - foreach ($projections as $projection) { - if ($projection->id() === $id) { - return $projection; + foreach ($subscriptions as $subscription) { + if ($subscription->id() === $id) { + return $subscription; } } - throw new ProjectionNotFound($id); + throw new SubscriptionNotFound($id); } } diff --git a/src/Console/Command/ProjectionTeardownCommand.php b/src/Console/Command/SubscriptionTeardownCommand.php similarity index 58% rename from src/Console/Command/ProjectionTeardownCommand.php rename to src/Console/Command/SubscriptionTeardownCommand.php index 34b990e43..448429a0b 100644 --- a/src/Console/Command/ProjectionTeardownCommand.php +++ b/src/Console/Command/SubscriptionTeardownCommand.php @@ -9,15 +9,15 @@ use Symfony\Component\Console\Output\OutputInterface; #[AsCommand( - 'event-sourcing:projection:teardown', - 'Shut down and delete the outdated projections', + 'event-sourcing:subscription:teardown', + 'Shut down and delete the outdated subscriptions', )] -final class ProjectionTeardownCommand extends ProjectionCommand +final class SubscriptionTeardownCommand extends SubscriptionCommand { protected function execute(InputInterface $input, OutputInterface $output): int { - $criteria = $this->projectionCriteria($input); - $this->projectionist->teardown($criteria); + $criteria = $this->subscriptionEngineCriteria($input); + $this->engine->teardown($criteria); return 0; } diff --git a/src/Metadata/Projector/DuplicateSetupMethod.php b/src/Metadata/Projector/DuplicateSetupMethod.php deleted file mode 100644 index c279145cb..000000000 --- a/src/Metadata/Projector/DuplicateSetupMethod.php +++ /dev/null @@ -1,25 +0,0 @@ -cache->get($projector); - - if ($metadata !== null) { - return $metadata; - } - - $metadata = $this->projectorMetadataFactory->metadata($projector); - - $this->cache->set($projector, $metadata); - - return $metadata; - } -} diff --git a/src/Metadata/Projector/Psr6ProjectorMetadataFactory.php b/src/Metadata/Projector/Psr6ProjectorMetadataFactory.php deleted file mode 100644 index 0146cd24f..000000000 --- a/src/Metadata/Projector/Psr6ProjectorMetadataFactory.php +++ /dev/null @@ -1,38 +0,0 @@ -cache->getItem($projector); - - if ($item->isHit()) { - $data = $item->get(); - assert($data instanceof ProjectorMetadata); - - return $data; - } - - $metadata = $this->projectorMetadataFactory->metadata($projector); - - $item->set($metadata); - $this->cache->save($item); - - return $metadata; - } -} diff --git a/src/Metadata/Projector/AttributeProjectorMetadataFactory.php b/src/Metadata/Subscriber/AttributeSubscriberMetadataFactory.php similarity index 60% rename from src/Metadata/Projector/AttributeProjectorMetadataFactory.php rename to src/Metadata/Subscriber/AttributeSubscriberMetadataFactory.php index 0a3bcd116..dc8822687 100644 --- a/src/Metadata/Projector/AttributeProjectorMetadataFactory.php +++ b/src/Metadata/Subscriber/AttributeSubscriberMetadataFactory.php @@ -2,37 +2,37 @@ declare(strict_types=1); -namespace Patchlevel\EventSourcing\Metadata\Projector; +namespace Patchlevel\EventSourcing\Metadata\Subscriber; -use Patchlevel\EventSourcing\Attribute\Projector; use Patchlevel\EventSourcing\Attribute\Setup; use Patchlevel\EventSourcing\Attribute\Subscribe; +use Patchlevel\EventSourcing\Attribute\Subscriber; use Patchlevel\EventSourcing\Attribute\Teardown; use ReflectionClass; use function array_key_exists; -final class AttributeProjectorMetadataFactory implements ProjectorMetadataFactory +final class AttributeSubscriberMetadataFactory implements SubscriberMetadataFactory { - /** @var array */ - private array $projectorMetadata = []; + /** @var array */ + private array $subscriberMetadata = []; - /** @param class-string $projector */ - public function metadata(string $projector): ProjectorMetadata + /** @param class-string $subscriber */ + public function metadata(string $subscriber): SubscriberMetadata { - if (array_key_exists($projector, $this->projectorMetadata)) { - return $this->projectorMetadata[$projector]; + if (array_key_exists($subscriber, $this->subscriberMetadata)) { + return $this->subscriberMetadata[$subscriber]; } - $reflector = new ReflectionClass($projector); + $reflector = new ReflectionClass($subscriber); - $attributes = $reflector->getAttributes(Projector::class); + $attributes = $reflector->getAttributes(Subscriber::class); if ($attributes === []) { - throw new ClassIsNotAProjector($projector); + throw new ClassIsNotASubscriber($subscriber); } - $projectorInfo = $attributes[0]->newInstance(); + $subscriberInfo = $attributes[0]->newInstance(); $methods = $reflector->getMethods(); @@ -53,7 +53,7 @@ public function metadata(string $projector): ProjectorMetadata if ($method->getAttributes(Setup::class)) { if ($createMethod !== null) { throw new DuplicateSetupMethod( - $projector, + $subscriber, $createMethod, $method->getName(), ); @@ -68,7 +68,7 @@ public function metadata(string $projector): ProjectorMetadata if ($dropMethod !== null) { throw new DuplicateTeardownMethod( - $projector, + $subscriber, $dropMethod, $method->getName(), ); @@ -77,16 +77,16 @@ public function metadata(string $projector): ProjectorMetadata $dropMethod = $method->getName(); } - $metadata = new ProjectorMetadata( - $projectorInfo->id, - $projectorInfo->group, - $projectorInfo->runMode, + $metadata = new SubscriberMetadata( + $subscriberInfo->id, + $subscriberInfo->group, + $subscriberInfo->runMode, $subscribeMethods, $createMethod, $dropMethod, ); - $this->projectorMetadata[$projector] = $metadata; + $this->subscriberMetadata[$subscriber] = $metadata; return $metadata; } diff --git a/src/Metadata/Projector/ClassIsNotAProjector.php b/src/Metadata/Subscriber/ClassIsNotASubscriber.php similarity index 65% rename from src/Metadata/Projector/ClassIsNotAProjector.php rename to src/Metadata/Subscriber/ClassIsNotASubscriber.php index feb9276ff..3078241a8 100644 --- a/src/Metadata/Projector/ClassIsNotAProjector.php +++ b/src/Metadata/Subscriber/ClassIsNotASubscriber.php @@ -2,20 +2,20 @@ declare(strict_types=1); -namespace Patchlevel\EventSourcing\Metadata\Projector; +namespace Patchlevel\EventSourcing\Metadata\Subscriber; use Patchlevel\EventSourcing\Metadata\MetadataException; use function sprintf; -final class ClassIsNotAProjector extends MetadataException +final class ClassIsNotASubscriber extends MetadataException { /** @param class-string $class */ public function __construct(string $class) { parent::__construct( sprintf( - 'Class "%s" is not a projector', + 'Class "%s" is not a subscriber', $class, ), ); diff --git a/src/Metadata/Subscriber/DuplicateSetupMethod.php b/src/Metadata/Subscriber/DuplicateSetupMethod.php new file mode 100644 index 000000000..c39f2005f --- /dev/null +++ b/src/Metadata/Subscriber/DuplicateSetupMethod.php @@ -0,0 +1,25 @@ +cache->get($subscriber); + + if ($metadata !== null) { + return $metadata; + } + + $metadata = $this->subscriberMetadataFactory->metadata($subscriber); + + $this->cache->set($subscriber, $metadata); + + return $metadata; + } +} diff --git a/src/Metadata/Subscriber/Psr6SubscriberMetadataFactory.php b/src/Metadata/Subscriber/Psr6SubscriberMetadataFactory.php new file mode 100644 index 000000000..305e689ec --- /dev/null +++ b/src/Metadata/Subscriber/Psr6SubscriberMetadataFactory.php @@ -0,0 +1,38 @@ +cache->getItem($subscriber); + + if ($item->isHit()) { + $data = $item->get(); + assert($data instanceof SubscriberMetadata); + + return $data; + } + + $metadata = $this->subscriberMetadataFactory->metadata($subscriber); + + $item->set($metadata); + $this->cache->save($item); + + return $metadata; + } +} diff --git a/src/Metadata/Projector/ProjectorMetadata.php b/src/Metadata/Subscriber/SubscriberMetadata.php similarity index 59% rename from src/Metadata/Projector/ProjectorMetadata.php rename to src/Metadata/Subscriber/SubscriberMetadata.php index 5d776f4cc..cd3b93b95 100644 --- a/src/Metadata/Projector/ProjectorMetadata.php +++ b/src/Metadata/Subscriber/SubscriberMetadata.php @@ -2,16 +2,16 @@ declare(strict_types=1); -namespace Patchlevel\EventSourcing\Metadata\Projector; +namespace Patchlevel\EventSourcing\Metadata\Subscriber; -use Patchlevel\EventSourcing\Projection\Projection\Projection; -use Patchlevel\EventSourcing\Projection\Projection\RunMode; +use Patchlevel\EventSourcing\Subscription\Subscription\RunMode; +use Patchlevel\EventSourcing\Subscription\Subscription\Subscription; -final class ProjectorMetadata +final class SubscriberMetadata { public function __construct( public readonly string $id, - public readonly string $group = Projection::DEFAULT_GROUP, + public readonly string $group = Subscription::DEFAULT_GROUP, public readonly RunMode $runMode = RunMode::FromBeginning, /** @var array> */ public readonly array $subscribeMethods = [], diff --git a/src/Metadata/Subscriber/SubscriberMetadataFactory.php b/src/Metadata/Subscriber/SubscriberMetadataFactory.php new file mode 100644 index 000000000..db174d35e --- /dev/null +++ b/src/Metadata/Subscriber/SubscriberMetadataFactory.php @@ -0,0 +1,11 @@ + */ - private array $projections = []; - - /** @param list $projections */ - public function __construct(array $projections = []) - { - foreach ($projections as $projection) { - $this->projections[$projection->id()] = $projection; - } - } - - public function get(string $projectionId): Projection - { - if (array_key_exists($projectionId, $this->projections)) { - return $this->projections[$projectionId]; - } - - throw new ProjectionNotFound($projectionId); - } - - /** @return list */ - public function find(ProjectionCriteria|null $criteria = null): array - { - $projections = array_values($this->projections); - - if ($criteria === null) { - return $projections; - } - - return array_values( - array_filter( - $projections, - static function (Projection $projection) use ($criteria): bool { - if ($criteria->ids !== null) { - if (!in_array($projection->id(), $criteria->ids, true)) { - return false; - } - } - - if ($criteria->groups !== null) { - if (!in_array($projection->group(), $criteria->groups, true)) { - return false; - } - } - - if ($criteria->status !== null) { - if (!in_array($projection->status(), $criteria->status, true)) { - return false; - } - } - - return true; - }, - ), - ); - } - - public function add(Projection $projection): void - { - if (array_key_exists($projection->id(), $this->projections)) { - throw new ProjectionAlreadyExists($projection->id()); - } - - $this->projections[$projection->id()] = $projection; - } - - public function update(Projection $projection): void - { - if (!array_key_exists($projection->id(), $this->projections)) { - throw new ProjectionNotFound($projection->id()); - } - - $this->projections[$projection->id()] = $projection; - } - - public function remove(Projection $projection): void - { - unset($this->projections[$projection->id()]); - } -} diff --git a/src/Projection/Projection/Store/LockableProjectionStore.php b/src/Projection/Projection/Store/LockableProjectionStore.php deleted file mode 100644 index a8e3d23d0..000000000 --- a/src/Projection/Projection/Store/LockableProjectionStore.php +++ /dev/null @@ -1,12 +0,0 @@ - */ - public function find(ProjectionCriteria|null $criteria = null): array; - - /** @throws ProjectionAlreadyExists */ - public function add(Projection $projection): void; - - /** @throws ProjectionNotFound */ - public function update(Projection $projection): void; - - /** @throws ProjectionNotFound */ - public function remove(Projection $projection): void; -} diff --git a/src/Projection/Projectionist/DefaultProjectionist.php b/src/Projection/Projectionist/DefaultProjectionist.php deleted file mode 100644 index 0f3e905c5..000000000 --- a/src/Projection/Projectionist/DefaultProjectionist.php +++ /dev/null @@ -1,868 +0,0 @@ -logger?->info( - 'Projectionist: Start booting.', - ); - - $this->discoverNewProjections(); - $this->handleRetryProjections($criteria); - $this->handleNewProjections($criteria); - - $this->findForUpdate( - new ProjectionCriteria( - ids: $criteria->ids, - groups: $criteria->groups, - status: [ProjectionStatus::Booting], - ), - function ($projections) use ($limit): void { - $projections = $this->fastForwardFromNowProjections($projections); - - if (count($projections) === 0) { - $this->logger?->info('Projectionist: No projections in booting status, finish booting.'); - - return; - } - - $startIndex = $this->lowestProjectionPosition($projections); - - $this->logger?->debug( - sprintf( - 'Projectionist: Event stream is processed for booting from position %s.', - $startIndex, - ), - ); - - $stream = null; - $messageCounter = 0; - - try { - $stream = $this->messageStore->load( - new Criteria(fromIndex: $startIndex), - ); - - foreach ($stream as $message) { - $index = $stream->index(); - - if ($index === null) { - throw new UnexpectedError('Stream index is null, this should not happen.'); - } - - foreach ($projections as $projection) { - if (!$projection->isBooting()) { - continue; - } - - if ($projection->position() >= $index) { - $this->logger?->debug( - sprintf( - 'Projectionist: Projection "%s" is farther than the current position (%d > %d), continue booting.', - $projection->id(), - $projection->position(), - $index, - ), - ); - - continue; - } - - $this->handleMessage($index, $message, $projection); - } - - $messageCounter++; - - $this->logger?->debug( - sprintf( - 'Projectionist: Current event stream position for booting: %s', - $index, - ), - ); - - if ($limit !== null && $messageCounter >= $limit) { - $this->logger?->info( - sprintf( - 'Projectionist: Message limit (%d) reached, finish booting.', - $limit, - ), - ); - - return; - } - } - } finally { - if ($messageCounter > 0) { - foreach ($projections as $projection) { - if (!$projection->isBooting()) { - continue; - } - - $this->projectionStore->update($projection); - } - } - - $stream?->close(); - } - - $this->logger?->debug('Projectionist: End of stream for booting has been reached.'); - - foreach ($projections as $projection) { - if (!$projection->isBooting()) { - continue; - } - - if ($projection->runMode() === RunMode::Once) { - $projection->finished(); - $this->projectionStore->update($projection); - - $this->logger?->info(sprintf( - 'Projectionist: Projection "%s" run only once and has been set to finished.', - $projection->id(), - )); - - continue; - } - - $projection->active(); - $this->projectionStore->update($projection); - - $this->logger?->info(sprintf( - 'Projectionist: Projection "%s" has been set to active after booting.', - $projection->id(), - )); - } - - $this->logger?->info('Projectionist: Finish booting.'); - }, - ); - } - - public function run( - ProjectionistCriteria|null $criteria = null, - int|null $limit = null, - ): void { - $criteria ??= new ProjectionistCriteria(); - - $this->logger?->info('Projectionist: Start processing.'); - - $this->discoverNewProjections(); - $this->handleOutdatedProjections($criteria); - $this->handleRetryProjections($criteria); - - $this->findForUpdate( - new ProjectionCriteria( - ids: $criteria->ids, - groups: $criteria->groups, - status: [ProjectionStatus::Active], - ), - function (array $projections) use ($limit): void { - if (count($projections) === 0) { - $this->logger?->info('Projectionist: No projections to process, finish processing.'); - - return; - } - - $startIndex = $this->lowestProjectionPosition($projections); - - $this->logger?->debug( - sprintf( - 'Projectionist: Event stream is processed from position %d.', - $startIndex, - ), - ); - - $stream = null; - $messageCounter = 0; - - try { - $criteria = new Criteria(fromIndex: $startIndex); - $stream = $this->messageStore->load($criteria); - - foreach ($stream as $message) { - $index = $stream->index(); - - if ($index === null) { - throw new UnexpectedError('Stream index is null, this should not happen.'); - } - - foreach ($projections as $projection) { - if (!$projection->isActive()) { - continue; - } - - if ($projection->position() >= $index) { - $this->logger?->debug( - sprintf( - 'Projectionist: Projection "%s" is farther than the current position (%d > %d), continue processing.', - $projection->id(), - $projection->position(), - $index, - ), - ); - - continue; - } - - $this->handleMessage($index, $message, $projection); - } - - $messageCounter++; - - $this->logger?->debug(sprintf('Projectionist: Current event stream position: %s', $index)); - - if ($limit !== null && $messageCounter >= $limit) { - $this->logger?->info( - sprintf( - 'Projectionist: Message limit (%d) reached, finish processing.', - $limit, - ), - ); - - return; - } - } - } finally { - if ($messageCounter > 0) { - foreach ($projections as $projection) { - if (!$projection->isActive()) { - continue; - } - - $this->projectionStore->update($projection); - } - } - - $stream?->close(); - } - - $this->logger?->info( - sprintf( - 'Projectionist: End of stream on position "%d" has been reached, finish processing.', - $stream->index() ?: 'unknown', - ), - ); - }, - ); - } - - public function teardown(ProjectionistCriteria|null $criteria = null): void - { - $criteria ??= new ProjectionistCriteria(); - - $this->discoverNewProjections(); - - $this->logger?->info('Projectionist: Start teardown outdated projections.'); - - $this->findForUpdate( - new ProjectionCriteria( - ids: $criteria->ids, - groups: $criteria->groups, - status: [ProjectionStatus::Outdated], - ), - function (array $projections): void { - foreach ($projections as $projection) { - $projector = $this->projector($projection->id()); - - if (!$projector) { - $this->logger?->warning( - sprintf( - 'Projectionist: Projector for "%s" to teardown not found, skipped.', - $projection->id(), - ), - ); - - continue; - } - - $teardownMethod = $projector->teardownMethod(); - - if (!$teardownMethod) { - $this->projectionStore->remove($projection); - - $this->logger?->info( - sprintf( - 'Projectionist: Projector "%s" for "%s" has no teardown method and was immediately removed.', - $projector::class, - $projection->id(), - ), - ); - - continue; - } - - try { - $teardownMethod(); - - $this->logger?->debug(sprintf( - 'Projectionist: For Projector "%s" for "%s" the teardown method has been executed and is now prepared to be removed.', - $projector::class, - $projection->id(), - )); - } catch (Throwable $e) { - $this->logger?->error( - sprintf( - 'Projectionist: Projection "%s" for "%s" has an error in the teardown method, skipped: %s', - $projector::class, - $projection->id(), - $e->getMessage(), - ), - ); - continue; - } - - $this->projectionStore->remove($projection); - - $this->logger?->info( - sprintf( - 'Projectionist: Projection "%s" removed.', - $projection->id(), - ), - ); - } - - $this->logger?->info('Projectionist: Finish teardown.'); - }, - ); - } - - public function remove(ProjectionistCriteria|null $criteria = null): void - { - $criteria ??= new ProjectionistCriteria(); - - $this->discoverNewProjections(); - - $this->findForUpdate( - new ProjectionCriteria( - ids: $criteria->ids, - groups: $criteria->groups, - ), - function (array $projections): void { - foreach ($projections as $projection) { - $projector = $this->projector($projection->id()); - - if (!$projector) { - $this->projectionStore->remove($projection); - - $this->logger?->info( - sprintf( - 'Projectionist: Projection "%s" removed without a suitable projector.', - $projection->id(), - ), - ); - - continue; - } - - $teardownMethod = $projector->teardownMethod(); - - if (!$teardownMethod) { - $this->projectionStore->remove($projection); - - $this->logger?->info( - sprintf('Projectionist: Projection "%s" removed.', $projection->id()), - ); - - continue; - } - - try { - $teardownMethod(); - } catch (Throwable $e) { - $this->logger?->error( - sprintf( - 'Projectionist: Projector "%s" teardown method could not be executed: %s', - $projector::class, - $e->getMessage(), - ), - ); - } - - $this->projectionStore->remove($projection); - - $this->logger?->info( - sprintf('Projectionist: Projection "%s" removed.', $projection->id()), - ); - } - }, - ); - } - - public function reactivate(ProjectionistCriteria|null $criteria = null): void - { - $criteria ??= new ProjectionistCriteria(); - - $this->discoverNewProjections(); - - $this->findForUpdate( - new ProjectionCriteria( - ids: $criteria->ids, - groups: $criteria->groups, - status: [ - ProjectionStatus::Error, - ProjectionStatus::Outdated, - ProjectionStatus::Paused, - ProjectionStatus::Finished, - ], - ), - function (array $projections): void { - /** @var Projection $projection */ - foreach ($projections as $projection) { - $projector = $this->projector($projection->id()); - - if (!$projector) { - $this->logger?->debug( - sprintf('Projectionist: Projector for "%s" not found, skipped.', $projection->id()), - ); - - continue; - } - - $error = $projection->projectionError(); - - if ($error) { - $projection->doRetry(); - $projection->resetRetry(); - - $this->projectionStore->update($projection); - - $this->logger?->info(sprintf( - 'Projectionist: Projector "%s" for "%s" is reactivated.', - $projector::class, - $projection->id(), - )); - - continue; - } - - $projection->active(); - $this->projectionStore->update($projection); - - $this->logger?->info(sprintf( - 'Projectionist: Projector "%s" for "%s" is reactivated.', - $projector::class, - $projection->id(), - )); - } - }, - ); - } - - public function pause(ProjectionistCriteria|null $criteria = null): void - { - $criteria ??= new ProjectionistCriteria(); - - $this->discoverNewProjections(); - - $this->findForUpdate( - new ProjectionCriteria( - ids: $criteria->ids, - groups: $criteria->groups, - status: [ - ProjectionStatus::Active, - ProjectionStatus::Booting, - ProjectionStatus::Error, - ], - ), - function (array $projections): void { - /** @var Projection $projection */ - foreach ($projections as $projection) { - $projector = $this->projector($projection->id()); - - if (!$projector) { - $this->logger?->debug( - sprintf('Projectionist: Projector for "%s" not found, skipped.', $projection->id()), - ); - - continue; - } - - $projection->pause(); - $this->projectionStore->update($projection); - - $this->logger?->info(sprintf( - 'Projectionist: Projector "%s" for "%s" is paused.', - $projector::class, - $projection->id(), - )); - } - }, - ); - } - - /** @return list */ - public function projections(ProjectionistCriteria|null $criteria = null): array - { - $criteria ??= new ProjectionistCriteria(); - - $this->discoverNewProjections(); - - return $this->projectionStore->find( - new ProjectionCriteria( - ids: $criteria->ids, - groups: $criteria->groups, - ), - ); - } - - private function handleMessage(int $index, Message $message, Projection $projection): void - { - $projector = $this->projector($projection->id()); - - if (!$projector) { - throw ProjectorNotFound::forProjectionId($projection->id()); - } - - $subscribeMethods = $projector->subscribeMethods($message->event()::class); - - if ($subscribeMethods === []) { - $projection->changePosition($index); - - $this->logger?->debug( - sprintf( - 'Projectionist: Projector "%s" for "%s" has no subscribe methods for "%s", continue.', - $projector::class, - $projection->id(), - $message->event()::class, - ), - ); - - return; - } - - try { - foreach ($subscribeMethods as $subscribeMethod) { - $subscribeMethod($message); - } - } catch (Throwable $e) { - $this->logger?->error( - sprintf( - 'Projectionist: Projector "%s" for "%s" could not process the event "%s": %s', - $projector::class, - $projection->id(), - $message->event()::class, - $e->getMessage(), - ), - ); - - $this->handleError($projection, $e); - - return; - } - - $projection->changePosition($index); - $projection->resetRetry(); - - $this->logger?->debug( - sprintf( - 'Projectionist: Projector "%s" for "%s" processed the event "%s".', - $projector::class, - $projection->id(), - $message->event()::class, - ), - ); - } - - private function projector(string $projectionId): ProjectorAccessor|null - { - return $this->projectorRepository->get($projectionId); - } - - private function handleOutdatedProjections(ProjectionistCriteria $criteria): void - { - $this->findForUpdate( - new ProjectionCriteria( - ids: $criteria->ids, - groups: $criteria->groups, - status: [ProjectionStatus::Active, ProjectionStatus::Paused, ProjectionStatus::Finished], - ), - function (array $projections): void { - foreach ($projections as $projection) { - $projector = $this->projector($projection->id()); - - if ($projector) { - continue; - } - - $projection->outdated(); - $this->projectionStore->update($projection); - - $this->logger?->info( - sprintf( - 'Projectionist: Projector for "%s" not found and has been marked as outdated.', - $projection->id(), - ), - ); - } - }, - ); - } - - private function handleRetryProjections(ProjectionistCriteria $criteria): void - { - $this->findForUpdate( - new ProjectionCriteria( - ids: $criteria->ids, - groups: $criteria->groups, - status: [ProjectionStatus::Error], - ), - function (array $projections): void { - /** @var Projection $projection */ - foreach ($projections as $projection) { - $error = $projection->projectionError(); - - if ($error === null) { - continue; - } - - $retryable = in_array( - $error->previousStatus, - [ProjectionStatus::New, ProjectionStatus::Booting, ProjectionStatus::Active], - true, - ); - - if (!$retryable) { - continue; - } - - if (!$this->retryStrategy->shouldRetry($projection)) { - continue; - } - - $projection->doRetry(); - $this->projectionStore->update($projection); - - $this->logger?->info( - sprintf( - 'Projectionist: Retry projection "%s" (%d) and set back to %s.', - $projection->id(), - $projection->retryAttempt(), - $projection->status()->value, - ), - ); - } - }, - ); - } - - /** - * @param list $projections - * - * @return list - */ - private function fastForwardFromNowProjections(array $projections): array - { - $latestIndex = null; - $forwardedProjections = []; - - foreach ($projections as $projection) { - $projector = $this->projector($projection->id()); - - if (!$projector) { - $forwardedProjections[] = $projection; - - continue; - } - - if ($projection->runMode() === RunMode::FromBeginning || $projection->runMode() === RunMode::Once) { - $forwardedProjections[] = $projection; - - continue; - } - - if ($latestIndex === null) { - $latestIndex = $this->latestIndex(); - } - - $projection->changePosition($latestIndex); - $projection->active(); - $this->projectionStore->update($projection); - - $this->logger?->info( - sprintf( - 'Projectionist: Projector "%s" for "%s" is in "from now" mode: skip past messages and set to active.', - $projector::class, - $projection->id(), - ), - ); - } - - return $forwardedProjections; - } - - private function handleNewProjections(ProjectionistCriteria $criteria): void - { - $this->findForUpdate( - new ProjectionCriteria( - ids: $criteria->ids, - groups: $criteria->groups, - status: [ProjectionStatus::New], - ), - function (array $projections): void { - foreach ($projections as $projection) { - $projector = $this->projector($projection->id()); - - if (!$projector) { - throw ProjectorNotFound::forProjectionId($projection->id()); - } - - $setupMethod = $projector->setupMethod(); - - if (!$setupMethod) { - $projection->booting(); - $this->projectionStore->update($projection); - - $this->logger?->debug(sprintf( - 'Projectionist: Projector "%s" for "%s" has no setup method, continue.', - $projector::class, - $projection->id(), - )); - - continue; - } - - try { - $setupMethod(); - - $projection->booting(); - $this->projectionStore->update($projection); - - $this->logger?->debug(sprintf( - 'Projectionist: For Projector "%s" for "%s" the setup method has been executed and is now prepared for data.', - $projector::class, - $projection->id(), - )); - } catch (Throwable $e) { - $this->logger?->error(sprintf( - 'Projectionist: Projector "%s" for "%s" has an error in the setup method: %s', - $projector::class, - $projection->id(), - $e->getMessage(), - )); - - $this->handleError($projection, $e); - } - } - }, - ); - } - - private function discoverNewProjections(): void - { - $this->findForUpdate( - new ProjectionCriteria(), - function (array $projections): void { - foreach ($this->projectorRepository->all() as $projector) { - foreach ($projections as $projection) { - if ($projection->id() === $projector->id()) { - continue 2; - } - } - - $this->projectionStore->add( - new Projection( - $projector->id(), - $projector->group(), - $projector->runMode(), - ), - ); - - $this->logger?->info( - sprintf( - 'Projectionist: New Projector "%s" was found and added to the projection store.', - $projector->id(), - ), - ); - } - }, - ); - } - - private function latestIndex(): int - { - $stream = $this->messageStore->load(null, 1, null, true); - - return $stream->index() ?: 0; - } - - /** @param list $projections */ - private function lowestProjectionPosition(array $projections): int - { - $min = null; - - foreach ($projections as $projection) { - if ($min !== null && $projection->position() >= $min) { - continue; - } - - $min = $projection->position(); - } - - if ($min === null) { - return 0; - } - - return $min; - } - - /** @param Closure(list):void $closure */ - private function findForUpdate(ProjectionCriteria $criteria, Closure $closure): void - { - if (!$this->projectionStore instanceof LockableProjectionStore) { - $closure($this->projectionStore->find($criteria)); - - return; - } - - $this->projectionStore->inLock(function () use ($closure, $criteria): void { - $projections = $this->projectionStore->find($criteria); - - $closure($projections); - }); - } - - private function handleError(Projection $projection, Throwable $throwable): void - { - $projection->error($throwable); - $this->projectionStore->update($projection); - } -} diff --git a/src/Projection/Projectionist/Projectionist.php b/src/Projection/Projectionist/Projectionist.php deleted file mode 100644 index 7ad6e75bb..000000000 --- a/src/Projection/Projectionist/Projectionist.php +++ /dev/null @@ -1,41 +0,0 @@ - */ - public function projections(ProjectionistCriteria|null $criteria = null): array; -} diff --git a/src/Projection/Projectionist/ProjectorNotFound.php b/src/Projection/Projectionist/ProjectorNotFound.php deleted file mode 100644 index f2af3622d..000000000 --- a/src/Projection/Projectionist/ProjectorNotFound.php +++ /dev/null @@ -1,22 +0,0 @@ - */ - private array $projectorsMap = []; - - /** @param iterable $projectors */ - public function __construct( - private readonly iterable $projectors, - private readonly ProjectorMetadataFactory $metadataFactory = new AttributeProjectorMetadataFactory(), - ) { - } - - /** @return iterable */ - public function all(): iterable - { - return array_values($this->projectorAccessorMap()); - } - - public function get(string $id): ProjectorAccessor|null - { - $map = $this->projectorAccessorMap(); - - return $map[$id] ?? null; - } - - /** @return array */ - private function projectorAccessorMap(): array - { - if ($this->projectorsMap !== []) { - return $this->projectorsMap; - } - - foreach ($this->projectors as $projector) { - $metadata = $this->metadataFactory->metadata($projector::class); - $this->projectorsMap[$metadata->id] = new MetadataProjectorAccessor($projector, $metadata); - } - - return $this->projectorsMap; - } -} diff --git a/src/Projection/Projector/ProjectorAccessorRepository.php b/src/Projection/Projector/ProjectorAccessorRepository.php deleted file mode 100644 index 267a97e51..000000000 --- a/src/Projection/Projector/ProjectorAccessorRepository.php +++ /dev/null @@ -1,13 +0,0 @@ - */ - public function all(): iterable; - - public function get(string $id): ProjectorAccessor|null; -} diff --git a/src/Projection/Projector/ProjectorHelper.php b/src/Projection/Projector/ProjectorHelper.php deleted file mode 100644 index 5ca501d51..000000000 --- a/src/Projection/Projector/ProjectorHelper.php +++ /dev/null @@ -1,27 +0,0 @@ -getProjectorMetadata($projector)->id; - } - - private function getProjectorMetadata(object $projector): ProjectorMetadata - { - return $this->metadataFactory->metadata($projector::class); - } -} diff --git a/src/Projection/Projector/ProjectorUtil.php b/src/Projection/Projector/ProjectorUtil.php deleted file mode 100644 index ea27f821f..000000000 --- a/src/Projection/Projector/ProjectorUtil.php +++ /dev/null @@ -1,37 +0,0 @@ -getProjectorHelper()->projectorId($this); - } -} diff --git a/src/Projection/Projector/TraceableProjectorAccessorRepository.php b/src/Projection/Projector/TraceableProjectorAccessorRepository.php deleted file mode 100644 index cb5beb789..000000000 --- a/src/Projection/Projector/TraceableProjectorAccessorRepository.php +++ /dev/null @@ -1,52 +0,0 @@ - */ - private array $projectorsMap = []; - - public function __construct( - private readonly ProjectorAccessorRepository $parent, - private readonly TraceStack $traceStack, - ) { - } - - /** @return iterable */ - public function all(): iterable - { - return array_values($this->projectorAccessorMap()); - } - - public function get(string $id): TraceableProjectorAccessor|null - { - $map = $this->projectorAccessorMap(); - - return $map[$id] ?? null; - } - - /** @return array */ - private function projectorAccessorMap(): array - { - if ($this->projectorsMap !== []) { - return $this->projectorsMap; - } - - foreach ($this->parent->all() as $projectorAccessor) { - $this->projectorsMap[$projectorAccessor->id()] = new TraceableProjectorAccessor( - $projectorAccessor, - $this->traceStack, - ); - } - - return $this->projectorsMap; - } -} diff --git a/src/Projection/RetryStrategy/NoRetryStrategy.php b/src/Projection/RetryStrategy/NoRetryStrategy.php deleted file mode 100644 index a9baea3ac..000000000 --- a/src/Projection/RetryStrategy/NoRetryStrategy.php +++ /dev/null @@ -1,15 +0,0 @@ -logger?->info( + 'Subscription Engine: Start booting.', + ); + + $this->discoverNewSubscriptions(); + $this->retrySubscriptions($criteria); + $this->setupNewSubscriptions($criteria); + + $this->findForUpdate( + new SubscriptionCriteria( + ids: $criteria->ids, + groups: $criteria->groups, + status: [Status::Booting], + ), + function ($subscriptions) use ($limit): void { + $subscriptions = $this->fastForwardFromNowSubscriptions($subscriptions); + + if (count($subscriptions) === 0) { + $this->logger?->info('Subscription Engine: No subscriptions in booting status, finish booting.'); + + return; + } + + $startIndex = $this->lowestSubscriptionPosition($subscriptions); + + $this->logger?->debug( + sprintf( + 'Subscription Engine: Event stream is processed for booting from position %s.', + $startIndex, + ), + ); + + $stream = null; + $messageCounter = 0; + + try { + $stream = $this->messageStore->load( + new Criteria(fromIndex: $startIndex), + ); + + foreach ($stream as $message) { + $index = $stream->index(); + + if ($index === null) { + throw new UnexpectedError('Stream index is null, this should not happen.'); + } + + foreach ($subscriptions as $subscription) { + if (!$subscription->isBooting()) { + continue; + } + + if ($subscription->position() >= $index) { + $this->logger?->debug( + sprintf( + 'Subscription Engine: Subscription "%s" is farther than the current position (%d > %d), continue booting.', + $subscription->id(), + $subscription->position(), + $index, + ), + ); + + continue; + } + + $this->handleMessage($index, $message, $subscription); + } + + $messageCounter++; + + $this->logger?->debug( + sprintf( + 'Subscription Engine: Current event stream position for booting: %s', + $index, + ), + ); + + if ($limit !== null && $messageCounter >= $limit) { + $this->logger?->info( + sprintf( + 'Subscription Engine: Message limit (%d) reached, finish booting.', + $limit, + ), + ); + + return; + } + } + } finally { + if ($messageCounter > 0) { + foreach ($subscriptions as $subscription) { + if (!$subscription->isBooting()) { + continue; + } + + $this->subscriptionStore->update($subscription); + } + } + + $stream?->close(); + } + + $this->logger?->debug('Subscription Engine: End of stream for booting has been reached.'); + + foreach ($subscriptions as $subscription) { + if (!$subscription->isBooting()) { + continue; + } + + if ($subscription->runMode() === RunMode::Once) { + $subscription->finished(); + $this->subscriptionStore->update($subscription); + + $this->logger?->info(sprintf( + 'Subscription Engine: Subscription "%s" run only once and has been set to finished.', + $subscription->id(), + )); + + continue; + } + + $subscription->active(); + $this->subscriptionStore->update($subscription); + + $this->logger?->info(sprintf( + 'Subscription Engine: Subscription "%s" has been set to active after booting.', + $subscription->id(), + )); + } + + $this->logger?->info('Subscription Engine: Finish booting.'); + }, + ); + } + + public function run( + SubscriptionEngineCriteria|null $criteria = null, + int|null $limit = null, + ): void { + $criteria ??= new SubscriptionEngineCriteria(); + + $this->logger?->info('Subscription Engine: Start processing.'); + + $this->discoverNewSubscriptions(); + $this->markOutdatedSubscriptions($criteria); + $this->retrySubscriptions($criteria); + + $this->findForUpdate( + new SubscriptionCriteria( + ids: $criteria->ids, + groups: $criteria->groups, + status: [Status::Active], + ), + function (array $subscriptions) use ($limit): void { + if (count($subscriptions) === 0) { + $this->logger?->info('Subscription Engine: No subscriptions to process, finish processing.'); + + return; + } + + $startIndex = $this->lowestSubscriptionPosition($subscriptions); + + $this->logger?->debug( + sprintf( + 'Subscription Engine: Event stream is processed from position %d.', + $startIndex, + ), + ); + + $stream = null; + $messageCounter = 0; + + try { + $criteria = new Criteria(fromIndex: $startIndex); + $stream = $this->messageStore->load($criteria); + + foreach ($stream as $message) { + $index = $stream->index(); + + if ($index === null) { + throw new UnexpectedError('Stream index is null, this should not happen.'); + } + + foreach ($subscriptions as $subscription) { + if (!$subscription->isActive()) { + continue; + } + + if ($subscription->position() >= $index) { + $this->logger?->debug( + sprintf( + 'Subscription Engine: Subscription "%s" is farther than the current position (%d > %d), continue processing.', + $subscription->id(), + $subscription->position(), + $index, + ), + ); + + continue; + } + + $this->handleMessage($index, $message, $subscription); + } + + $messageCounter++; + + $this->logger?->debug(sprintf('Subscription Engine: Current event stream position: %s', $index)); + + if ($limit !== null && $messageCounter >= $limit) { + $this->logger?->info( + sprintf( + 'Subscription Engine: Message limit (%d) reached, finish processing.', + $limit, + ), + ); + + return; + } + } + } finally { + if ($messageCounter > 0) { + foreach ($subscriptions as $subscription) { + if (!$subscription->isActive()) { + continue; + } + + $this->subscriptionStore->update($subscription); + } + } + + $stream?->close(); + } + + $this->logger?->info( + sprintf( + 'Subscription Engine: End of stream on position "%d" has been reached, finish processing.', + $stream->index() ?: 'unknown', + ), + ); + }, + ); + } + + public function teardown(SubscriptionEngineCriteria|null $criteria = null): void + { + $criteria ??= new SubscriptionEngineCriteria(); + + $this->discoverNewSubscriptions(); + + $this->logger?->info('Subscription Engine: Start teardown outdated subscriptions.'); + + $this->findForUpdate( + new SubscriptionCriteria( + ids: $criteria->ids, + groups: $criteria->groups, + status: [Status::Outdated], + ), + function (array $subscriptions): void { + foreach ($subscriptions as $subscription) { + $subscriber = $this->subscriber($subscription->id()); + + if (!$subscriber) { + $this->logger?->warning( + sprintf( + 'Subscription Engine: Subscriber for "%s" to teardown not found, skipped.', + $subscription->id(), + ), + ); + + continue; + } + + $teardownMethod = $subscriber->teardownMethod(); + + if (!$teardownMethod) { + $this->subscriptionStore->remove($subscription); + + $this->logger?->info( + sprintf( + 'Subscription Engine: Subscriber "%s" for "%s" has no teardown method and was immediately removed.', + $subscriber::class, + $subscription->id(), + ), + ); + + continue; + } + + try { + $teardownMethod(); + + $this->logger?->debug(sprintf( + 'Subscription Engine: For Subscriber "%s" for "%s" the teardown method has been executed and is now prepared to be removed.', + $subscriber::class, + $subscription->id(), + )); + } catch (Throwable $e) { + $this->logger?->error( + sprintf( + 'Subscription Engine: Subscription "%s" for "%s" has an error in the teardown method, skipped: %s', + $subscriber::class, + $subscription->id(), + $e->getMessage(), + ), + ); + continue; + } + + $this->subscriptionStore->remove($subscription); + + $this->logger?->info( + sprintf( + 'Subscription Engine: Subscription "%s" removed.', + $subscription->id(), + ), + ); + } + + $this->logger?->info('Subscription Engine: Finish teardown.'); + }, + ); + } + + public function remove(SubscriptionEngineCriteria|null $criteria = null): void + { + $criteria ??= new SubscriptionEngineCriteria(); + + $this->discoverNewSubscriptions(); + + $this->findForUpdate( + new SubscriptionCriteria( + ids: $criteria->ids, + groups: $criteria->groups, + ), + function (array $subscriptions): void { + foreach ($subscriptions as $subscription) { + $subscriber = $this->subscriber($subscription->id()); + + if (!$subscriber) { + $this->subscriptionStore->remove($subscription); + + $this->logger?->info( + sprintf( + 'Subscription Engine: Subscription "%s" removed without a suitable subscriber.', + $subscription->id(), + ), + ); + + continue; + } + + $teardownMethod = $subscriber->teardownMethod(); + + if (!$teardownMethod) { + $this->subscriptionStore->remove($subscription); + + $this->logger?->info( + sprintf('Subscription Engine: Subscription "%s" removed.', $subscription->id()), + ); + + continue; + } + + try { + $teardownMethod(); + } catch (Throwable $e) { + $this->logger?->error( + sprintf( + 'Subscription Engine: Subscriber "%s" teardown method could not be executed: %s', + $subscriber::class, + $e->getMessage(), + ), + ); + } + + $this->subscriptionStore->remove($subscription); + + $this->logger?->info( + sprintf('Subscription Engine: Subscription "%s" removed.', $subscription->id()), + ); + } + }, + ); + } + + public function reactivate(SubscriptionEngineCriteria|null $criteria = null): void + { + $criteria ??= new SubscriptionEngineCriteria(); + + $this->discoverNewSubscriptions(); + + $this->findForUpdate( + new SubscriptionCriteria( + ids: $criteria->ids, + groups: $criteria->groups, + status: [ + Status::Error, + Status::Outdated, + Status::Paused, + Status::Finished, + ], + ), + function (array $subscriptions): void { + /** @var Subscription $subscription */ + foreach ($subscriptions as $subscription) { + $subscriber = $this->subscriber($subscription->id()); + + if (!$subscriber) { + $this->logger?->debug( + sprintf('Subscription Engine: Subscriber for "%s" not found, skipped.', $subscription->id()), + ); + + continue; + } + + $error = $subscription->subscriptionError(); + + if ($error) { + $subscription->doRetry(); + $subscription->resetRetry(); + + $this->subscriptionStore->update($subscription); + + $this->logger?->info(sprintf( + 'Subscription Engine: Subscriber "%s" for "%s" is reactivated.', + $subscriber::class, + $subscription->id(), + )); + + continue; + } + + $subscription->active(); + $this->subscriptionStore->update($subscription); + + $this->logger?->info(sprintf( + 'Subscription Engine: Subscriber "%s" for "%s" is reactivated.', + $subscriber::class, + $subscription->id(), + )); + } + }, + ); + } + + public function pause(SubscriptionEngineCriteria|null $criteria = null): void + { + $criteria ??= new SubscriptionEngineCriteria(); + + $this->discoverNewSubscriptions(); + + $this->findForUpdate( + new SubscriptionCriteria( + ids: $criteria->ids, + groups: $criteria->groups, + status: [ + Status::Active, + Status::Booting, + Status::Error, + ], + ), + function (array $subscriptions): void { + /** @var Subscription $subscription */ + foreach ($subscriptions as $subscription) { + $subscriber = $this->subscriber($subscription->id()); + + if (!$subscriber) { + $this->logger?->debug( + sprintf('Subscription Engine: Subscriber for "%s" not found, skipped.', $subscription->id()), + ); + + continue; + } + + $subscription->pause(); + $this->subscriptionStore->update($subscription); + + $this->logger?->info(sprintf( + 'Subscription Engine: Subscriber "%s" for "%s" is paused.', + $subscriber::class, + $subscription->id(), + )); + } + }, + ); + } + + /** @return list */ + public function subscriptions(SubscriptionEngineCriteria|null $criteria = null): array + { + $criteria ??= new SubscriptionEngineCriteria(); + + $this->discoverNewSubscriptions(); + + return $this->subscriptionStore->find( + new SubscriptionCriteria( + ids: $criteria->ids, + groups: $criteria->groups, + ), + ); + } + + private function handleMessage(int $index, Message $message, Subscription $subscription): void + { + $subscriber = $this->subscriber($subscription->id()); + + if (!$subscriber) { + throw SubscriberNotFound::forSubscriptionId($subscription->id()); + } + + $subscribeMethods = $subscriber->subscribeMethods($message->event()::class); + + if ($subscribeMethods === []) { + $subscription->changePosition($index); + + $this->logger?->debug( + sprintf( + 'Subscription Engine: Subscriber "%s" for "%s" has no subscribe methods for "%s", continue.', + $subscriber::class, + $subscription->id(), + $message->event()::class, + ), + ); + + return; + } + + try { + foreach ($subscribeMethods as $subscribeMethod) { + $subscribeMethod($message); + } + } catch (Throwable $e) { + $this->logger?->error( + sprintf( + 'Subscription Engine: Subscriber "%s" for "%s" could not process the event "%s": %s', + $subscriber::class, + $subscription->id(), + $message->event()::class, + $e->getMessage(), + ), + ); + + $this->handleError($subscription, $e); + + return; + } + + $subscription->changePosition($index); + $subscription->resetRetry(); + + $this->logger?->debug( + sprintf( + 'Subscription Engine: Subscriber "%s" for "%s" processed the event "%s".', + $subscriber::class, + $subscription->id(), + $message->event()::class, + ), + ); + } + + private function subscriber(string $subscriberId): SubscriberAccessor|null + { + return $this->subscriberRepository->get($subscriberId); + } + + private function markOutdatedSubscriptions(SubscriptionEngineCriteria $criteria): void + { + $this->findForUpdate( + new SubscriptionCriteria( + ids: $criteria->ids, + groups: $criteria->groups, + status: [Status::Active, Status::Paused, Status::Finished], + ), + function (array $subscriptions): void { + foreach ($subscriptions as $subscription) { + $subscriber = $this->subscriber($subscription->id()); + + if ($subscriber) { + continue; + } + + $subscription->outdated(); + $this->subscriptionStore->update($subscription); + + $this->logger?->info( + sprintf( + 'Subscription Engine: Subscriber for "%s" not found and has been marked as outdated.', + $subscription->id(), + ), + ); + } + }, + ); + } + + private function retrySubscriptions(SubscriptionEngineCriteria $criteria): void + { + $this->findForUpdate( + new SubscriptionCriteria( + ids: $criteria->ids, + groups: $criteria->groups, + status: [Status::Error], + ), + function (array $subscriptions): void { + /** @var Subscription $subscription */ + foreach ($subscriptions as $subscription) { + $error = $subscription->subscriptionError(); + + if ($error === null) { + continue; + } + + $retryable = in_array( + $error->previousStatus, + [Status::New, Status::Booting, Status::Active], + true, + ); + + if (!$retryable) { + continue; + } + + if (!$this->retryStrategy->shouldRetry($subscription)) { + continue; + } + + $subscription->doRetry(); + $this->subscriptionStore->update($subscription); + + $this->logger?->info( + sprintf( + 'Subscription Engine: Retry subscription "%s" (%d) and set back to %s.', + $subscription->id(), + $subscription->retryAttempt(), + $subscription->status()->value, + ), + ); + } + }, + ); + } + + /** + * @param list $subscriptions + * + * @return list + */ + private function fastForwardFromNowSubscriptions(array $subscriptions): array + { + $latestIndex = null; + $forwardedSubscriptions = []; + + foreach ($subscriptions as $subscription) { + $subscriber = $this->subscriber($subscription->id()); + + if (!$subscriber) { + $forwardedSubscriptions[] = $subscription; + + continue; + } + + if ($subscription->runMode() === RunMode::FromBeginning || $subscription->runMode() === RunMode::Once) { + $forwardedSubscriptions[] = $subscription; + + continue; + } + + if ($latestIndex === null) { + $latestIndex = $this->latestIndex(); + } + + $subscription->changePosition($latestIndex); + $subscription->active(); + $this->subscriptionStore->update($subscription); + + $this->logger?->info( + sprintf( + 'Subscription Engine: Subscriber "%s" for "%s" is in "from now" mode: skip past messages and set to active.', + $subscriber::class, + $subscription->id(), + ), + ); + } + + return $forwardedSubscriptions; + } + + private function setupNewSubscriptions(SubscriptionEngineCriteria $criteria): void + { + $this->findForUpdate( + new SubscriptionCriteria( + ids: $criteria->ids, + groups: $criteria->groups, + status: [Status::New], + ), + function (array $subscriptions): void { + foreach ($subscriptions as $subscription) { + $subscriber = $this->subscriber($subscription->id()); + + if (!$subscriber) { + throw SubscriberNotFound::forSubscriptionId($subscription->id()); + } + + $setupMethod = $subscriber->setupMethod(); + + if (!$setupMethod) { + $subscription->booting(); + $this->subscriptionStore->update($subscription); + + $this->logger?->debug(sprintf( + 'Subscription Engine: Subscriber "%s" for "%s" has no setup method, continue.', + $subscriber::class, + $subscription->id(), + )); + + continue; + } + + try { + $setupMethod(); + + $subscription->booting(); + $this->subscriptionStore->update($subscription); + + $this->logger?->debug(sprintf( + 'Subscription Engine: For Subscriber "%s" for "%s" the setup method has been executed and is now prepared for data.', + $subscriber::class, + $subscription->id(), + )); + } catch (Throwable $e) { + $this->logger?->error(sprintf( + 'Subscription Engine: Subscriber "%s" for "%s" has an error in the setup method: %s', + $subscriber::class, + $subscription->id(), + $e->getMessage(), + )); + + $this->handleError($subscription, $e); + } + } + }, + ); + } + + private function discoverNewSubscriptions(): void + { + $this->findForUpdate( + new SubscriptionCriteria(), + function (array $subscriptions): void { + foreach ($this->subscriberRepository->all() as $subscriber) { + foreach ($subscriptions as $subscription) { + if ($subscription->id() === $subscriber->id()) { + continue 2; + } + } + + $this->subscriptionStore->add( + new Subscription( + $subscriber->id(), + $subscriber->group(), + $subscriber->runMode(), + ), + ); + + $this->logger?->info( + sprintf( + 'Subscription Engine: New Subscriber "%s" was found and added to the subscription store.', + $subscriber->id(), + ), + ); + } + }, + ); + } + + private function latestIndex(): int + { + $stream = $this->messageStore->load(null, 1, null, true); + + return $stream->index() ?: 0; + } + + /** @param list $subscriptions */ + private function lowestSubscriptionPosition(array $subscriptions): int + { + $min = null; + + foreach ($subscriptions as $subscription) { + if ($min !== null && $subscription->position() >= $min) { + continue; + } + + $min = $subscription->position(); + } + + if ($min === null) { + return 0; + } + + return $min; + } + + /** @param Closure(list):void $closure */ + private function findForUpdate(SubscriptionCriteria $criteria, Closure $closure): void + { + if (!$this->subscriptionStore instanceof LockableSubscriptionStore) { + $closure($this->subscriptionStore->find($criteria)); + + return; + } + + $this->subscriptionStore->inLock(function () use ($closure, $criteria): void { + $subscriptions = $this->subscriptionStore->find($criteria); + + $closure($subscriptions); + }); + } + + private function handleError(Subscription $subscription, Throwable $throwable): void + { + $subscription->error($throwable); + $this->subscriptionStore->update($subscription); + } +} diff --git a/src/Subscription/Engine/SubscriberNotFound.php b/src/Subscription/Engine/SubscriberNotFound.php new file mode 100644 index 000000000..7e9958a99 --- /dev/null +++ b/src/Subscription/Engine/SubscriberNotFound.php @@ -0,0 +1,22 @@ + */ + public function subscriptions(SubscriptionEngineCriteria|null $criteria = null): array; +} diff --git a/src/Projection/Projectionist/ProjectionistCriteria.php b/src/Subscription/Engine/SubscriptionEngineCriteria.php similarity index 76% rename from src/Projection/Projectionist/ProjectionistCriteria.php rename to src/Subscription/Engine/SubscriptionEngineCriteria.php index 538acb120..264624470 100644 --- a/src/Projection/Projectionist/ProjectionistCriteria.php +++ b/src/Subscription/Engine/SubscriptionEngineCriteria.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace Patchlevel\EventSourcing\Projection\Projectionist; +namespace Patchlevel\EventSourcing\Subscription\Engine; /** @psalm-immutable */ -final class ProjectionistCriteria +final class SubscriptionEngineCriteria { /** * @param list|null $ids diff --git a/src/Projection/Projectionist/UnexpectedError.php b/src/Subscription/Engine/UnexpectedError.php similarity index 65% rename from src/Projection/Projectionist/UnexpectedError.php rename to src/Subscription/Engine/UnexpectedError.php index 5a650802e..514efbb39 100644 --- a/src/Projection/Projectionist/UnexpectedError.php +++ b/src/Subscription/Engine/UnexpectedError.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Patchlevel\EventSourcing\Projection\Projectionist; +namespace Patchlevel\EventSourcing\Subscription\Engine; use RuntimeException; diff --git a/src/Projection/RetryStrategy/ClockBasedRetryStrategy.php b/src/Subscription/RetryStrategy/ClockBasedRetryStrategy.php similarity index 81% rename from src/Projection/RetryStrategy/ClockBasedRetryStrategy.php rename to src/Subscription/RetryStrategy/ClockBasedRetryStrategy.php index cd4ea1ac1..e29c557c9 100644 --- a/src/Projection/RetryStrategy/ClockBasedRetryStrategy.php +++ b/src/Subscription/RetryStrategy/ClockBasedRetryStrategy.php @@ -2,11 +2,11 @@ declare(strict_types=1); -namespace Patchlevel\EventSourcing\Projection\RetryStrategy; +namespace Patchlevel\EventSourcing\Subscription\RetryStrategy; use DateTimeImmutable; use Patchlevel\EventSourcing\Clock\SystemClock; -use Patchlevel\EventSourcing\Projection\Projection\Projection; +use Patchlevel\EventSourcing\Subscription\Subscription\Subscription; use Psr\Clock\ClockInterface; use function round; @@ -30,19 +30,19 @@ public function __construct( ) { } - public function shouldRetry(Projection $projection): bool + public function shouldRetry(Subscription $subscription): bool { - if ($projection->retryAttempt() >= $this->maxAttempts) { + if ($subscription->retryAttempt() >= $this->maxAttempts) { return false; } - $lastSavedAt = $projection->lastSavedAt(); + $lastSavedAt = $subscription->lastSavedAt(); if ($lastSavedAt === null) { return false; } - $nextRetryDate = $this->calculateNextRetryDate($lastSavedAt, $projection->retryAttempt()); + $nextRetryDate = $this->calculateNextRetryDate($lastSavedAt, $subscription->retryAttempt()); return $nextRetryDate <= $this->clock->now(); } @@ -52,7 +52,7 @@ private function calculateNextRetryDate(DateTimeImmutable $lastDate, int $attemp $nextDate = $lastDate->modify(sprintf('+%d seconds', $this->calculateDelay($attempt))); if ($nextDate === false) { - throw new UnexpectedError('Could not calculate next date'); + throw new UnexpectedError('Could not calculate next date.'); } return $nextDate; diff --git a/src/Subscription/RetryStrategy/NoRetryStrategy.php b/src/Subscription/RetryStrategy/NoRetryStrategy.php new file mode 100644 index 000000000..fbf8459ce --- /dev/null +++ b/src/Subscription/RetryStrategy/NoRetryStrategy.php @@ -0,0 +1,15 @@ +connection->createQueryBuilder() ->select('*') - ->from($this->projectionTable) + ->from($this->tableName) ->where('id = :id') ->getSQL(); /** @var Data|false $result */ - $result = $this->connection->fetchAssociative($sql, ['id' => $projectionId]); + $result = $this->connection->fetchAssociative($sql, ['id' => $subscriptionId]); if ($result === false) { - throw new ProjectionNotFound($projectionId); + throw new SubscriptionNotFound($subscriptionId); } - return $this->createProjection($result); + return $this->createSubscription($result); } - /** @return list */ - public function find(ProjectionCriteria|null $criteria = null): array + /** @return list */ + public function find(SubscriptionCriteria|null $criteria = null): array { $qb = $this->connection->createQueryBuilder() ->select('*') - ->from($this->projectionTable); + ->from($this->tableName); if (!$this->connection->getDatabasePlatform() instanceof SQLitePlatform) { $qb->forUpdate(); @@ -105,7 +103,7 @@ public function find(ProjectionCriteria|null $criteria = null): array $qb->andWhere('status IN (:status)') ->setParameter( 'status', - array_map(static fn (ProjectionStatus $status) => $status->value, $criteria->status), + array_map(static fn (Status $status) => $status->value, $criteria->status), ArrayParameterType::STRING, ); } @@ -115,30 +113,30 @@ public function find(ProjectionCriteria|null $criteria = null): array $result = $qb->fetchAllAssociative(); return array_map( - fn (array $data) => $this->createProjection($data), + fn (array $data) => $this->createSubscription($data), $result, ); } - public function add(Projection $projection): void + public function add(Subscription $subscription): void { - $projectionError = $projection->projectionError(); + $subscriptionError = $subscription->subscriptionError(); - $projection->updateLastSavedAt($this->clock->now()); + $subscription->updateLastSavedAt($this->clock->now()); $this->connection->insert( - $this->projectionTable, + $this->tableName, [ - 'id' => $projection->id(), - 'group_name' => $projection->group(), - 'run_mode' => $projection->runMode()->value, - 'status' => $projection->status()->value, - 'position' => $projection->position(), - 'error_message' => $projectionError?->errorMessage, - 'error_previous_status' => $projectionError?->previousStatus?->value, - 'error_context' => $projectionError?->errorContext !== null ? json_encode($projectionError->errorContext, JSON_THROW_ON_ERROR) : null, - 'retry_attempt' => $projection->retryAttempt(), - 'last_saved_at' => $projection->lastSavedAt(), + 'id' => $subscription->id(), + 'group_name' => $subscription->group(), + 'run_mode' => $subscription->runMode()->value, + 'status' => $subscription->status()->value, + 'position' => $subscription->position(), + 'error_message' => $subscriptionError?->errorMessage, + 'error_previous_status' => $subscriptionError?->previousStatus?->value, + 'error_context' => $subscriptionError?->errorContext !== null ? json_encode($subscriptionError->errorContext, JSON_THROW_ON_ERROR) : null, + 'retry_attempt' => $subscription->retryAttempt(), + 'last_saved_at' => $subscription->lastSavedAt(), ], [ 'last_saved_at' => Types::DATETIME_IMMUTABLE, @@ -146,27 +144,27 @@ public function add(Projection $projection): void ); } - public function update(Projection $projection): void + public function update(Subscription $subscription): void { - $projectionError = $projection->projectionError(); + $subscriptionError = $subscription->subscriptionError(); - $projection->updateLastSavedAt($this->clock->now()); + $subscription->updateLastSavedAt($this->clock->now()); $effectedRows = $this->connection->update( - $this->projectionTable, + $this->tableName, [ - 'group_name' => $projection->group(), - 'run_mode' => $projection->runMode()->value, - 'status' => $projection->status()->value, - 'position' => $projection->position(), - 'error_message' => $projectionError?->errorMessage, - 'error_previous_status' => $projectionError?->previousStatus?->value, - 'error_context' => $projectionError?->errorContext !== null ? json_encode($projectionError->errorContext, JSON_THROW_ON_ERROR) : null, - 'retry_attempt' => $projection->retryAttempt(), - 'last_saved_at' => $projection->lastSavedAt(), + 'group_name' => $subscription->group(), + 'run_mode' => $subscription->runMode()->value, + 'status' => $subscription->status()->value, + 'position' => $subscription->position(), + 'error_message' => $subscriptionError?->errorMessage, + 'error_previous_status' => $subscriptionError?->previousStatus?->value, + 'error_context' => $subscriptionError?->errorContext !== null ? json_encode($subscriptionError->errorContext, JSON_THROW_ON_ERROR) : null, + 'retry_attempt' => $subscription->retryAttempt(), + 'last_saved_at' => $subscription->lastSavedAt(), ], [ - 'id' => $projection->id(), + 'id' => $subscription->id(), ], [ 'last_saved_at' => Types::DATETIME_IMMUTABLE, @@ -174,13 +172,13 @@ public function update(Projection $projection): void ); if ($effectedRows === 0) { - throw new ProjectionNotFound($projection->id()); + throw new SubscriptionNotFound($subscription->id()); } } - public function remove(Projection $projection): void + public function remove(Subscription $subscription): void { - $this->connection->delete($this->projectionTable, ['id' => $projection->id()]); + $this->connection->delete($this->tableName, ['id' => $subscription->id()]); } public function inLock(Closure $closure): void @@ -200,7 +198,7 @@ public function inLock(Closure $closure): void public function configureSchema(Schema $schema, Connection $connection): void { - $table = $schema->createTable($this->projectionTable); + $table = $schema->createTable($this->tableName); $table->addColumn('id', Types::STRING) ->setLength(255) @@ -235,20 +233,20 @@ public function configureSchema(Schema $schema, Connection $connection): void } /** @param Data $row */ - private function createProjection(array $row): Projection + private function createSubscription(array $row): Subscription { $context = $row['error_context'] !== null ? json_decode($row['error_context'], true, 512, JSON_THROW_ON_ERROR) : null; - return new Projection( + return new Subscription( $row['id'], $row['group_name'], RunMode::from($row['run_mode']), - ProjectionStatus::from($row['status']), + Status::from($row['status']), $row['position'], - $row['error_message'] !== null ? new ProjectionError( + $row['error_message'] !== null ? new SubscriptionError( $row['error_message'], - $row['error_previous_status'] !== null ? ProjectionStatus::from($row['error_previous_status']) : ProjectionStatus::New, + $row['error_previous_status'] !== null ? Status::from($row['error_previous_status']) : Status::New, $context, ) : null, $row['retry_attempt'], diff --git a/src/Subscription/Store/InMemorySubscriptionStore.php b/src/Subscription/Store/InMemorySubscriptionStore.php new file mode 100644 index 000000000..70aca1d92 --- /dev/null +++ b/src/Subscription/Store/InMemorySubscriptionStore.php @@ -0,0 +1,95 @@ + */ + private array $subscriptions = []; + + /** @param list $subscriptions */ + public function __construct(array $subscriptions = []) + { + foreach ($subscriptions as $subscription) { + $this->subscriptions[$subscription->id()] = $subscription; + } + } + + public function get(string $subscriptionId): Subscription + { + if (array_key_exists($subscriptionId, $this->subscriptions)) { + return $this->subscriptions[$subscriptionId]; + } + + throw new SubscriptionNotFound($subscriptionId); + } + + /** @return list */ + public function find(SubscriptionCriteria|null $criteria = null): array + { + $subscriptions = array_values($this->subscriptions); + + if ($criteria === null) { + return $subscriptions; + } + + return array_values( + array_filter( + $subscriptions, + static function (Subscription $subscription) use ($criteria): bool { + if ($criteria->ids !== null) { + if (!in_array($subscription->id(), $criteria->ids, true)) { + return false; + } + } + + if ($criteria->groups !== null) { + if (!in_array($subscription->group(), $criteria->groups, true)) { + return false; + } + } + + if ($criteria->status !== null) { + if (!in_array($subscription->status(), $criteria->status, true)) { + return false; + } + } + + return true; + }, + ), + ); + } + + public function add(Subscription $subscription): void + { + if (array_key_exists($subscription->id(), $this->subscriptions)) { + throw new SubscriptionAlreadyExists($subscription->id()); + } + + $this->subscriptions[$subscription->id()] = $subscription; + } + + public function update(Subscription $subscription): void + { + if (!array_key_exists($subscription->id(), $this->subscriptions)) { + throw new SubscriptionNotFound($subscription->id()); + } + + $this->subscriptions[$subscription->id()] = $subscription; + } + + public function remove(Subscription $subscription): void + { + unset($this->subscriptions[$subscription->id()]); + } +} diff --git a/src/Subscription/Store/LockableSubscriptionStore.php b/src/Subscription/Store/LockableSubscriptionStore.php new file mode 100644 index 000000000..48d62366e --- /dev/null +++ b/src/Subscription/Store/LockableSubscriptionStore.php @@ -0,0 +1,12 @@ +|null $ids - * @param list|null $groups - * @param list|null $status + * @param list|null $ids + * @param list|null $groups + * @param list|null $status */ public function __construct( public readonly array|null $ids = null, diff --git a/src/Subscription/Store/SubscriptionNotFound.php b/src/Subscription/Store/SubscriptionNotFound.php new file mode 100644 index 000000000..acfcea142 --- /dev/null +++ b/src/Subscription/Store/SubscriptionNotFound.php @@ -0,0 +1,17 @@ + */ + public function find(SubscriptionCriteria|null $criteria = null): array; + + /** @throws SubscriptionAlreadyExists */ + public function add(Subscription $subscription): void; + + /** @throws SubscriptionNotFound */ + public function update(Subscription $subscription): void; + + /** @throws SubscriptionNotFound */ + public function remove(Subscription $subscription): void; +} diff --git a/src/Projection/Projection/Store/TransactionCommitNotPossible.php b/src/Subscription/Store/TransactionCommitNotPossible.php similarity index 86% rename from src/Projection/Projection/Store/TransactionCommitNotPossible.php rename to src/Subscription/Store/TransactionCommitNotPossible.php index b9db0165a..6e137c3e6 100644 --- a/src/Projection/Projection/Store/TransactionCommitNotPossible.php +++ b/src/Subscription/Store/TransactionCommitNotPossible.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Patchlevel\EventSourcing\Projection\Projection\Store; +namespace Patchlevel\EventSourcing\Subscription\Store; use RuntimeException; use Throwable; diff --git a/src/Projection/Projector/MetadataProjectorAccessor.php b/src/Subscription/Subscriber/MetadataSubscriberAccessor.php similarity index 76% rename from src/Projection/Projector/MetadataProjectorAccessor.php rename to src/Subscription/Subscriber/MetadataSubscriberAccessor.php index 543c406fb..75ead5e51 100644 --- a/src/Projection/Projector/MetadataProjectorAccessor.php +++ b/src/Subscription/Subscriber/MetadataSubscriberAccessor.php @@ -2,26 +2,26 @@ declare(strict_types=1); -namespace Patchlevel\EventSourcing\Projection\Projector; +namespace Patchlevel\EventSourcing\Subscription\Subscriber; use Closure; use Patchlevel\EventSourcing\Attribute\Subscribe; use Patchlevel\EventSourcing\EventBus\Message; -use Patchlevel\EventSourcing\Metadata\Projector\ProjectorMetadata; -use Patchlevel\EventSourcing\Projection\Projection\RunMode; +use Patchlevel\EventSourcing\Metadata\Subscriber\SubscriberMetadata; +use Patchlevel\EventSourcing\Subscription\Subscription\RunMode; use function array_key_exists; use function array_map; use function array_merge; -final class MetadataProjectorAccessor implements ProjectorAccessor +final class MetadataSubscriberAccessor implements SubscriberAccessor { /** @var array> */ private array $subscribeCache = []; public function __construct( - private readonly object $projector, - private readonly ProjectorMetadata $metadata, + private readonly object $subscriber, + private readonly SubscriberMetadata $metadata, ) { } @@ -48,7 +48,7 @@ public function setupMethod(): Closure|null return null; } - return $this->projector->$method(...); + return $this->subscriber->$method(...); } public function teardownMethod(): Closure|null @@ -59,7 +59,7 @@ public function teardownMethod(): Closure|null return null; } - return $this->projector->$method(...); + return $this->subscriber->$method(...); } /** @@ -80,7 +80,7 @@ public function subscribeMethods(string $eventClass): array $this->subscribeCache[$eventClass] = array_map( /** @return Closure(Message):void */ - fn (string $method) => $this->projector->$method(...), + fn (string $method) => $this->subscriber->$method(...), $methods, ); diff --git a/src/Subscription/Subscriber/MetadataSubscriberAccessorRepository.php b/src/Subscription/Subscriber/MetadataSubscriberAccessorRepository.php new file mode 100644 index 000000000..324c1eada --- /dev/null +++ b/src/Subscription/Subscriber/MetadataSubscriberAccessorRepository.php @@ -0,0 +1,51 @@ + */ + private array $subscribersMap = []; + + /** @param iterable $subscribers */ + public function __construct( + private readonly iterable $subscribers, + private readonly SubscriberMetadataFactory $metadataFactory = new AttributeSubscriberMetadataFactory(), + ) { + } + + /** @return iterable */ + public function all(): iterable + { + return array_values($this->subscriberAccessorMap()); + } + + public function get(string $id): SubscriberAccessor|null + { + $map = $this->subscriberAccessorMap(); + + return $map[$id] ?? null; + } + + /** @return array */ + private function subscriberAccessorMap(): array + { + if ($this->subscribersMap !== []) { + return $this->subscribersMap; + } + + foreach ($this->subscribers as $subscriber) { + $metadata = $this->metadataFactory->metadata($subscriber::class); + $this->subscribersMap[$metadata->id] = new MetadataSubscriberAccessor($subscriber, $metadata); + } + + return $this->subscribersMap; + } +} diff --git a/src/Projection/Projector/ProjectorAccessor.php b/src/Subscription/Subscriber/SubscriberAccessor.php similarity index 76% rename from src/Projection/Projector/ProjectorAccessor.php rename to src/Subscription/Subscriber/SubscriberAccessor.php index 00e378da5..8498cbd08 100644 --- a/src/Projection/Projector/ProjectorAccessor.php +++ b/src/Subscription/Subscriber/SubscriberAccessor.php @@ -2,13 +2,13 @@ declare(strict_types=1); -namespace Patchlevel\EventSourcing\Projection\Projector; +namespace Patchlevel\EventSourcing\Subscription\Subscriber; use Closure; use Patchlevel\EventSourcing\EventBus\Message; -use Patchlevel\EventSourcing\Projection\Projection\RunMode; +use Patchlevel\EventSourcing\Subscription\Subscription\RunMode; -interface ProjectorAccessor +interface SubscriberAccessor { public function id(): string; diff --git a/src/Subscription/Subscriber/SubscriberAccessorRepository.php b/src/Subscription/Subscriber/SubscriberAccessorRepository.php new file mode 100644 index 000000000..0e299a239 --- /dev/null +++ b/src/Subscription/Subscriber/SubscriberAccessorRepository.php @@ -0,0 +1,13 @@ + */ + public function all(): iterable; + + public function get(string $id): SubscriberAccessor|null; +} diff --git a/src/Subscription/Subscriber/SubscriberHelper.php b/src/Subscription/Subscriber/SubscriberHelper.php new file mode 100644 index 000000000..3243ac8fb --- /dev/null +++ b/src/Subscription/Subscriber/SubscriberHelper.php @@ -0,0 +1,27 @@ +metadata($subscriber)->id; + } + + private function metadata(object $subscriber): SubscriberMetadata + { + return $this->metadataFactory->metadata($subscriber::class); + } +} diff --git a/src/Subscription/Subscriber/SubscriberUtil.php b/src/Subscription/Subscriber/SubscriberUtil.php new file mode 100644 index 000000000..b73ad0a88 --- /dev/null +++ b/src/Subscription/Subscriber/SubscriberUtil.php @@ -0,0 +1,37 @@ +subscriberHelper()->subscriberId($this); + } +} diff --git a/src/Projection/Projector/TraceableProjectorAccessor.php b/src/Subscription/Subscriber/TraceableSubscriberAccessor.php similarity index 84% rename from src/Projection/Projector/TraceableProjectorAccessor.php rename to src/Subscription/Subscriber/TraceableSubscriberAccessor.php index 1ba70b22d..1717b63b8 100644 --- a/src/Projection/Projector/TraceableProjectorAccessor.php +++ b/src/Subscription/Subscriber/TraceableSubscriberAccessor.php @@ -2,21 +2,21 @@ declare(strict_types=1); -namespace Patchlevel\EventSourcing\Projection\Projector; +namespace Patchlevel\EventSourcing\Subscription\Subscriber; use Closure; use Patchlevel\EventSourcing\EventBus\Message; -use Patchlevel\EventSourcing\Projection\Projection\RunMode; use Patchlevel\EventSourcing\Repository\MessageDecorator\Trace; use Patchlevel\EventSourcing\Repository\MessageDecorator\TraceStack; +use Patchlevel\EventSourcing\Subscription\Subscription\RunMode; use function array_map; /** @experimental */ -final class TraceableProjectorAccessor implements ProjectorAccessor +final class TraceableSubscriberAccessor implements SubscriberAccessor { public function __construct( - private readonly ProjectorAccessor $parent, + private readonly SubscriberAccessor $parent, private readonly TraceStack $traceStack, ) { } @@ -62,7 +62,7 @@ public function subscribeMethods(string $eventClass): array fn (Closure $closure) => function (Message $message) use ($closure): void { $trace = new Trace( $this->id(), - 'event_sourcing/projector/' . $this->group(), + 'event_sourcing/subscriber/' . $this->group(), ); $this->traceStack->add($trace); diff --git a/src/Subscription/Subscriber/TraceableSubscriberAccessorRepository.php b/src/Subscription/Subscriber/TraceableSubscriberAccessorRepository.php new file mode 100644 index 000000000..344073070 --- /dev/null +++ b/src/Subscription/Subscriber/TraceableSubscriberAccessorRepository.php @@ -0,0 +1,52 @@ + */ + private array $subscribersMap = []; + + public function __construct( + private readonly SubscriberAccessorRepository $parent, + private readonly TraceStack $traceStack, + ) { + } + + /** @return iterable */ + public function all(): iterable + { + return array_values($this->subscriberAccessorMap()); + } + + public function get(string $id): TraceableSubscriberAccessor|null + { + $map = $this->subscriberAccessorMap(); + + return $map[$id] ?? null; + } + + /** @return array */ + private function subscriberAccessorMap(): array + { + if ($this->subscribersMap !== []) { + return $this->subscribersMap; + } + + foreach ($this->parent->all() as $subscriberAccessor) { + $this->subscribersMap[$subscriberAccessor->id()] = new TraceableSubscriberAccessor( + $subscriberAccessor, + $this->traceStack, + ); + } + + return $this->subscribersMap; + } +} diff --git a/src/Projection/Projection/NoErrorToRetry.php b/src/Subscription/Subscription/NoErrorToRetry.php similarity index 58% rename from src/Projection/Projection/NoErrorToRetry.php rename to src/Subscription/Subscription/NoErrorToRetry.php index 2ff10b192..9a161fe83 100644 --- a/src/Projection/Projection/NoErrorToRetry.php +++ b/src/Subscription/Subscription/NoErrorToRetry.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Patchlevel\EventSourcing\Projection\Projection; +namespace Patchlevel\EventSourcing\Subscription\Subscription; use RuntimeException; @@ -10,6 +10,6 @@ final class NoErrorToRetry extends RuntimeException { public function __construct() { - parent::__construct('No error to retry'); + parent::__construct('No error to retry.'); } } diff --git a/src/Projection/Projection/RunMode.php b/src/Subscription/Subscription/RunMode.php similarity index 71% rename from src/Projection/Projection/RunMode.php rename to src/Subscription/Subscription/RunMode.php index bfb0f6a14..ffd6975a6 100644 --- a/src/Projection/Projection/RunMode.php +++ b/src/Subscription/Subscription/RunMode.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Patchlevel\EventSourcing\Projection\Projection; +namespace Patchlevel\EventSourcing\Subscription\Subscription; enum RunMode: string { diff --git a/src/Projection/Projection/ProjectionStatus.php b/src/Subscription/Subscription/Status.php similarity index 72% rename from src/Projection/Projection/ProjectionStatus.php rename to src/Subscription/Subscription/Status.php index 42619ba3e..e35309946 100644 --- a/src/Projection/Projection/ProjectionStatus.php +++ b/src/Subscription/Subscription/Status.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace Patchlevel\EventSourcing\Projection\Projection; +namespace Patchlevel\EventSourcing\Subscription\Subscription; -enum ProjectionStatus: string +enum Status: string { case New = 'new'; case Booting = 'booting'; diff --git a/src/Projection/Projection/Projection.php b/src/Subscription/Subscription/Subscription.php similarity index 67% rename from src/Projection/Projection/Projection.php rename to src/Subscription/Subscription/Subscription.php index d51b06da8..61447b069 100644 --- a/src/Projection/Projection/Projection.php +++ b/src/Subscription/Subscription/Subscription.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace Patchlevel\EventSourcing\Projection\Projection; +namespace Patchlevel\EventSourcing\Subscription\Subscription; use DateTimeImmutable; use Throwable; -final class Projection +final class Subscription { public const DEFAULT_GROUP = 'default'; @@ -15,9 +15,9 @@ public function __construct( private readonly string $id, private readonly string $group = self::DEFAULT_GROUP, private readonly RunMode $runMode = RunMode::FromBeginning, - private ProjectionStatus $status = ProjectionStatus::New, + private Status $status = Status::New, private int $position = 0, - private ProjectionError|null $error = null, + private SubscriptionError|null $error = null, private int $retryAttempt = 0, private DateTimeImmutable|null $lastSavedAt = null, ) { @@ -38,7 +38,7 @@ public function runMode(): RunMode return $this->runMode; } - public function status(): ProjectionStatus + public function status(): Status { return $this->status; } @@ -48,7 +48,7 @@ public function position(): int return $this->position; } - public function projectionError(): ProjectionError|null + public function subscriptionError(): SubscriptionError|null { return $this->error; } @@ -60,85 +60,85 @@ public function changePosition(int $position): void public function new(): void { - $this->status = ProjectionStatus::New; + $this->status = Status::New; $this->error = null; } public function isNew(): bool { - return $this->status === ProjectionStatus::New; + return $this->status === Status::New; } public function booting(): void { - $this->status = ProjectionStatus::Booting; + $this->status = Status::Booting; $this->error = null; } public function isBooting(): bool { - return $this->status === ProjectionStatus::Booting; + return $this->status === Status::Booting; } public function active(): void { - $this->status = ProjectionStatus::Active; + $this->status = Status::Active; $this->error = null; } public function isActive(): bool { - return $this->status === ProjectionStatus::Active; + return $this->status === Status::Active; } public function pause(): void { - $this->status = ProjectionStatus::Paused; + $this->status = Status::Paused; } public function isPaused(): bool { - return $this->status === ProjectionStatus::Paused; + return $this->status === Status::Paused; } public function finished(): void { - $this->status = ProjectionStatus::Finished; + $this->status = Status::Finished; $this->error = null; } public function isFinished(): bool { - return $this->status === ProjectionStatus::Finished; + return $this->status === Status::Finished; } public function outdated(): void { - $this->status = ProjectionStatus::Outdated; + $this->status = Status::Outdated; } public function isOutdated(): bool { - return $this->status === ProjectionStatus::Outdated; + return $this->status === Status::Outdated; } public function error(Throwable|string $error): void { $previousStatus = $this->status; - $this->status = ProjectionStatus::Error; + $this->status = Status::Error; if ($error instanceof Throwable) { - $this->error = ProjectionError::fromThrowable($previousStatus, $error); + $this->error = SubscriptionError::fromThrowable($previousStatus, $error); return; } - $this->error = new ProjectionError($error, $previousStatus); + $this->error = new SubscriptionError($error, $previousStatus); } public function isError(): bool { - return $this->status === ProjectionStatus::Error; + return $this->status === Status::Error; } public function retryAttempt(): int diff --git a/src/Projection/Projection/ProjectionError.php b/src/Subscription/Subscription/SubscriptionError.php similarity index 59% rename from src/Projection/Projection/ProjectionError.php rename to src/Subscription/Subscription/SubscriptionError.php index dc221bb37..61e316e11 100644 --- a/src/Projection/Projection/ProjectionError.php +++ b/src/Subscription/Subscription/SubscriptionError.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Patchlevel\EventSourcing\Projection\Projection; +namespace Patchlevel\EventSourcing\Subscription\Subscription; use Throwable; @@ -10,18 +10,18 @@ * @psalm-type Trace = array{file?: string, line?: int, function?: string, class?: string, type?: string, args?: array} * @psalm-type Context = array{class: class-string, message: string, code: int|string, file: string, line: int, trace: list} */ -final class ProjectionError +final class SubscriptionError { /** @param list|null $errorContext */ public function __construct( public readonly string $errorMessage, - public readonly ProjectionStatus $previousStatus, + public readonly Status $previousStatus, public readonly array|null $errorContext = null, ) { } - public static function fromThrowable(ProjectionStatus $projectionStatus, Throwable $error): self + public static function fromThrowable(Status $status, Throwable $error): self { - return new self($error->getMessage(), $projectionStatus, ThrowableToErrorContextTransformer::transform($error)); + return new self($error->getMessage(), $status, ThrowableToErrorContextTransformer::transform($error)); } } diff --git a/src/Projection/Projection/ThrowableToErrorContextTransformer.php b/src/Subscription/Subscription/ThrowableToErrorContextTransformer.php similarity index 91% rename from src/Projection/Projection/ThrowableToErrorContextTransformer.php rename to src/Subscription/Subscription/ThrowableToErrorContextTransformer.php index c2b2fd84d..13f08b8c6 100644 --- a/src/Projection/Projection/ThrowableToErrorContextTransformer.php +++ b/src/Subscription/Subscription/ThrowableToErrorContextTransformer.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Patchlevel\EventSourcing\Projection\Projection; +namespace Patchlevel\EventSourcing\Subscription\Subscription; use Throwable; @@ -15,8 +15,8 @@ use function sprintf; /** - * @psalm-import-type Context from ProjectionError - * @psalm-import-type Trace from ProjectionError + * @psalm-import-type Context from SubscriptionError + * @psalm-import-type Trace from SubscriptionError */ final class ThrowableToErrorContextTransformer { diff --git a/tests/Benchmark/BasicImplementation/Processor/SendEmailProcessor.php b/tests/Benchmark/BasicImplementation/Processor/SendEmailProcessor.php index 0af5b1d99..7c583f203 100644 --- a/tests/Benchmark/BasicImplementation/Processor/SendEmailProcessor.php +++ b/tests/Benchmark/BasicImplementation/Processor/SendEmailProcessor.php @@ -4,13 +4,13 @@ namespace Patchlevel\EventSourcing\Tests\Benchmark\BasicImplementation\Processor; -use Patchlevel\EventSourcing\Attribute\Projector; use Patchlevel\EventSourcing\Attribute\Subscribe; +use Patchlevel\EventSourcing\Attribute\Subscriber; use Patchlevel\EventSourcing\EventBus\Message; use Patchlevel\EventSourcing\Tests\Integration\BasicImplementation\Events\ProfileCreated; use Patchlevel\EventSourcing\Tests\Integration\BasicImplementation\SendEmailMock; -#[Projector('send_email')] +#[Subscriber('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 64b3ada6f..9285471d9 100644 --- a/tests/Benchmark/BasicImplementation/Projection/ProfileProjector.php +++ b/tests/Benchmark/BasicImplementation/Projection/ProfileProjector.php @@ -6,21 +6,21 @@ use Doctrine\DBAL\Connection; use Patchlevel\EventSourcing\Aggregate\AggregateHeader; -use Patchlevel\EventSourcing\Attribute\Projector; use Patchlevel\EventSourcing\Attribute\Setup; use Patchlevel\EventSourcing\Attribute\Subscribe; +use Patchlevel\EventSourcing\Attribute\Subscriber; use Patchlevel\EventSourcing\Attribute\Teardown; use Patchlevel\EventSourcing\EventBus\Message; -use Patchlevel\EventSourcing\Projection\Projector\ProjectorUtil; +use Patchlevel\EventSourcing\Projection\Subscriber\SubscriberUtil; use Patchlevel\EventSourcing\Tests\Benchmark\BasicImplementation\Events\NameChanged; use Patchlevel\EventSourcing\Tests\Benchmark\BasicImplementation\Events\ProfileCreated; use function assert; -#[Projector('profile')] +#[Subscriber('profile')] final class ProfileProjector { - use ProjectorUtil; + use SubscriberUtil; public function __construct( private Connection $connection, diff --git a/tests/Benchmark/ProjectionistBench.php b/tests/Benchmark/ProjectionistBench.php index fa8ff6045..1aa5cf49f 100644 --- a/tests/Benchmark/ProjectionistBench.php +++ b/tests/Benchmark/ProjectionistBench.php @@ -8,10 +8,10 @@ use Patchlevel\EventSourcing\EventBus\DefaultEventBus; use Patchlevel\EventSourcing\EventBus\EventBus; use Patchlevel\EventSourcing\EventBus\Serializer\DefaultHeadersSerializer; -use Patchlevel\EventSourcing\Projection\Projection\Store\DoctrineStore; -use Patchlevel\EventSourcing\Projection\Projectionist\DefaultProjectionist; -use Patchlevel\EventSourcing\Projection\Projectionist\Projectionist; -use Patchlevel\EventSourcing\Projection\Projector\MetadataProjectorAccessorRepository; +use Patchlevel\EventSourcing\Projection\Engine\DefaultSubscriptionEngine; +use Patchlevel\EventSourcing\Projection\Engine\SubscriptionEngine; +use Patchlevel\EventSourcing\Projection\Store\DoctrineSubscriptionStore; +use Patchlevel\EventSourcing\Projection\Subscriber\MetadataSubscriberAccessorRepository; use Patchlevel\EventSourcing\Repository\DefaultRepository; use Patchlevel\EventSourcing\Repository\Repository; use Patchlevel\EventSourcing\Schema\ChainDoctrineSchemaConfigurator; @@ -33,7 +33,7 @@ final class ProjectionistBench private EventBus $bus; private Repository $repository; - private Projectionist $projectionist; + private SubscriptionEngine $projectionist; private AggregateRootId $id; @@ -57,7 +57,7 @@ public function setUp(): void $this->repository = new DefaultRepository($this->store, $this->bus, Profile::metadata()); - $projectionStore = new DoctrineStore( + $projectionStore = new DoctrineSubscriptionStore( $connection, ); @@ -81,10 +81,10 @@ public function setUp(): void $this->repository->save($profile); - $this->projectionist = new DefaultProjectionist( + $this->projectionist = new DefaultSubscriptionEngine( $this->store, $projectionStore, - new MetadataProjectorAccessorRepository( + new MetadataSubscriberAccessorRepository( [ new ProfileProjector($connection), new SendEmailProcessor(), diff --git a/tests/Integration/BankAccountSplitStream/IntegrationTest.php b/tests/Integration/BankAccountSplitStream/IntegrationTest.php index 9df661241..3c4404e64 100644 --- a/tests/Integration/BankAccountSplitStream/IntegrationTest.php +++ b/tests/Integration/BankAccountSplitStream/IntegrationTest.php @@ -9,10 +9,10 @@ use Patchlevel\EventSourcing\EventBus\Serializer\DefaultHeadersSerializer; use Patchlevel\EventSourcing\Metadata\AggregateRoot\AggregateRootRegistry; use Patchlevel\EventSourcing\Metadata\Event\AttributeEventMetadataFactory; -use Patchlevel\EventSourcing\Projection\Projection\Store\InMemoryStore; -use Patchlevel\EventSourcing\Projection\Projectionist\DefaultProjectionist; -use Patchlevel\EventSourcing\Projection\Projectionist\ProjectionistCriteria; -use Patchlevel\EventSourcing\Projection\Projector\MetadataProjectorAccessorRepository; +use Patchlevel\EventSourcing\Projection\Engine\DefaultSubscriptionEngine; +use Patchlevel\EventSourcing\Projection\Engine\SubscriptionEngineCriteria; +use Patchlevel\EventSourcing\Projection\Store\InMemorySubscriptionStore; +use Patchlevel\EventSourcing\Projection\Subscriber\MetadataSubscriberAccessorRepository; use Patchlevel\EventSourcing\Repository\DefaultRepositoryManager; use Patchlevel\EventSourcing\Repository\MessageDecorator\ChainMessageDecorator; use Patchlevel\EventSourcing\Repository\MessageDecorator\SplitStreamDecorator; @@ -58,10 +58,10 @@ public function testSuccessful(): void $bankAccountProjector = new BankAccountProjector($this->connection); - $projectionist = new DefaultProjectionist( + $projectionist = new DefaultSubscriptionEngine( $store, - new InMemoryStore(), - new MetadataProjectorAccessorRepository([$bankAccountProjector]), + new InMemorySubscriptionStore(), + new MetadataSubscriberAccessorRepository([$bankAccountProjector]), ); $eventBus = DefaultEventBus::create([$bankAccountProjector]); @@ -83,7 +83,7 @@ public function testSuccessful(): void ); $schemaDirector->create(); - $projectionist->boot(new ProjectionistCriteria()); + $projectionist->boot(new SubscriptionEngineCriteria()); $bankAccountId = AccountId::fromString('1'); $bankAccount = BankAccount::create($bankAccountId, 'John'); diff --git a/tests/Integration/BankAccountSplitStream/Projection/BankAccountProjector.php b/tests/Integration/BankAccountSplitStream/Projection/BankAccountProjector.php index 310610db2..64f7da905 100644 --- a/tests/Integration/BankAccountSplitStream/Projection/BankAccountProjector.php +++ b/tests/Integration/BankAccountSplitStream/Projection/BankAccountProjector.php @@ -6,9 +6,9 @@ use Doctrine\DBAL\Connection; use Doctrine\DBAL\Schema\Table; -use Patchlevel\EventSourcing\Attribute\Projector; use Patchlevel\EventSourcing\Attribute\Setup; use Patchlevel\EventSourcing\Attribute\Subscribe; +use Patchlevel\EventSourcing\Attribute\Subscriber; use Patchlevel\EventSourcing\Attribute\Teardown; use Patchlevel\EventSourcing\EventBus\Message; use Patchlevel\EventSourcing\Tests\Integration\BankAccountSplitStream\Events\BalanceAdded; @@ -16,7 +16,7 @@ use function assert; -#[Projector('dummy-1')] +#[Subscriber('dummy-1')] final class BankAccountProjector { public function __construct( diff --git a/tests/Integration/BasicImplementation/BasicIntegrationTest.php b/tests/Integration/BasicImplementation/BasicIntegrationTest.php index f8f3fe476..cf9abd5de 100644 --- a/tests/Integration/BasicImplementation/BasicIntegrationTest.php +++ b/tests/Integration/BasicImplementation/BasicIntegrationTest.php @@ -8,10 +8,10 @@ use Patchlevel\EventSourcing\EventBus\DefaultEventBus; use Patchlevel\EventSourcing\EventBus\Serializer\DefaultHeadersSerializer; use Patchlevel\EventSourcing\Metadata\AggregateRoot\AggregateRootRegistry; -use Patchlevel\EventSourcing\Projection\Projection\Store\InMemoryStore; -use Patchlevel\EventSourcing\Projection\Projectionist\DefaultProjectionist; -use Patchlevel\EventSourcing\Projection\Projectionist\ProjectionistCriteria; -use Patchlevel\EventSourcing\Projection\Projector\MetadataProjectorAccessorRepository; +use Patchlevel\EventSourcing\Projection\Engine\DefaultSubscriptionEngine; +use Patchlevel\EventSourcing\Projection\Engine\SubscriptionEngineCriteria; +use Patchlevel\EventSourcing\Projection\Store\InMemorySubscriptionStore; +use Patchlevel\EventSourcing\Projection\Subscriber\MetadataSubscriberAccessorRepository; use Patchlevel\EventSourcing\Repository\DefaultRepositoryManager; use Patchlevel\EventSourcing\Schema\DoctrineSchemaDirector; use Patchlevel\EventSourcing\Serializer\DefaultEventSerializer; @@ -55,10 +55,10 @@ public function testSuccessful(): void $profileProjector = new ProfileProjector($this->connection); - $projectionist = new DefaultProjectionist( + $projectionist = new DefaultSubscriptionEngine( $store, - new InMemoryStore(), - new MetadataProjectorAccessorRepository([$profileProjector]), + new InMemorySubscriptionStore(), + new MetadataSubscriberAccessorRepository([$profileProjector]), ); $eventBus = DefaultEventBus::create([ @@ -81,7 +81,7 @@ public function testSuccessful(): void ); $schemaDirector->create(); - $projectionist->boot(new ProjectionistCriteria()); + $projectionist->boot(new SubscriptionEngineCriteria()); $profileId = ProfileId::fromString('1'); $profile = Profile::create($profileId, 'John'); @@ -123,10 +123,10 @@ public function testSnapshot(): void $profileProjection = new ProfileProjector($this->connection); - $projectionist = new DefaultProjectionist( + $projectionist = new DefaultSubscriptionEngine( $store, - new InMemoryStore(), - new MetadataProjectorAccessorRepository([$profileProjection]), + new InMemorySubscriptionStore(), + new MetadataSubscriberAccessorRepository([$profileProjection]), ); $eventBus = DefaultEventBus::create([ @@ -149,7 +149,7 @@ public function testSnapshot(): void ); $schemaDirector->create(); - $projectionist->boot(new ProjectionistCriteria()); + $projectionist->boot(new SubscriptionEngineCriteria()); $profileId = ProfileId::fromString('1'); $profile = Profile::create($profileId, 'John'); diff --git a/tests/Integration/BasicImplementation/Projection/ProfileProjector.php b/tests/Integration/BasicImplementation/Projection/ProfileProjector.php index 7913679cf..5203d4b52 100644 --- a/tests/Integration/BasicImplementation/Projection/ProfileProjector.php +++ b/tests/Integration/BasicImplementation/Projection/ProfileProjector.php @@ -6,16 +6,16 @@ use Doctrine\DBAL\Connection; use Doctrine\DBAL\Schema\Table; -use Patchlevel\EventSourcing\Attribute\Projector; use Patchlevel\EventSourcing\Attribute\Setup; use Patchlevel\EventSourcing\Attribute\Subscribe; +use Patchlevel\EventSourcing\Attribute\Subscriber; use Patchlevel\EventSourcing\Attribute\Teardown; use Patchlevel\EventSourcing\EventBus\Message; use Patchlevel\EventSourcing\Tests\Integration\BasicImplementation\Events\ProfileCreated; use function assert; -#[Projector('profile-1')] +#[Subscriber('profile-1')] final class ProfileProjector { public function __construct( diff --git a/tests/Integration/Projectionist/Projection/ErrorProducerProjector.php b/tests/Integration/Projectionist/Projection/ErrorProducerProjector.php index 78fd766a0..d9a74892e 100644 --- a/tests/Integration/Projectionist/Projection/ErrorProducerProjector.php +++ b/tests/Integration/Projectionist/Projection/ErrorProducerProjector.php @@ -4,14 +4,14 @@ namespace Patchlevel\EventSourcing\Tests\Integration\Projectionist\Projection; -use Patchlevel\EventSourcing\Attribute\Projector; use Patchlevel\EventSourcing\Attribute\Setup; use Patchlevel\EventSourcing\Attribute\Subscribe; +use Patchlevel\EventSourcing\Attribute\Subscriber; use Patchlevel\EventSourcing\Attribute\Teardown; use Patchlevel\EventSourcing\EventBus\Message; use RuntimeException; -#[Projector('error_producer')] +#[Subscriber('error_producer')] final class ErrorProducerProjector { public bool $setupError = false; diff --git a/tests/Integration/Projectionist/Projection/ProfileProcessor.php b/tests/Integration/Projectionist/Projection/ProfileProcessor.php index 7982cdcff..c313dcfd9 100644 --- a/tests/Integration/Projectionist/Projection/ProfileProcessor.php +++ b/tests/Integration/Projectionist/Projection/ProfileProcessor.php @@ -4,8 +4,8 @@ namespace Patchlevel\EventSourcing\Tests\Integration\Projectionist\Projection; -use Patchlevel\EventSourcing\Attribute\Projector; use Patchlevel\EventSourcing\Attribute\Subscribe; +use Patchlevel\EventSourcing\Attribute\Subscriber; use Patchlevel\EventSourcing\EventBus\Message; use Patchlevel\EventSourcing\Repository\RepositoryManager; use Patchlevel\EventSourcing\Tests\Integration\Projectionist\Aggregate\Profile; @@ -13,7 +13,7 @@ use function assert; -#[Projector('profile')] +#[Subscriber('profile')] final class ProfileProcessor { public function __construct( diff --git a/tests/Integration/Projectionist/Projection/ProfileProjector.php b/tests/Integration/Projectionist/Projection/ProfileProjector.php index efd4969ed..f372f195b 100644 --- a/tests/Integration/Projectionist/Projection/ProfileProjector.php +++ b/tests/Integration/Projectionist/Projection/ProfileProjector.php @@ -6,20 +6,20 @@ use Doctrine\DBAL\Connection; use Doctrine\DBAL\Schema\Table; -use Patchlevel\EventSourcing\Attribute\Projector; use Patchlevel\EventSourcing\Attribute\Setup; use Patchlevel\EventSourcing\Attribute\Subscribe; +use Patchlevel\EventSourcing\Attribute\Subscriber; use Patchlevel\EventSourcing\Attribute\Teardown; use Patchlevel\EventSourcing\EventBus\Message; -use Patchlevel\EventSourcing\Projection\Projector\ProjectorUtil; +use Patchlevel\EventSourcing\Projection\Subscriber\SubscriberUtil; use Patchlevel\EventSourcing\Tests\Integration\Projectionist\Events\ProfileCreated; use function assert; -#[Projector('profile_1')] +#[Subscriber('profile_1')] final class ProfileProjector { - use ProjectorUtil; + use SubscriberUtil; public function __construct( private Connection $connection, diff --git a/tests/Integration/Projectionist/ProjectionistTest.php b/tests/Integration/Projectionist/ProjectionistTest.php index 0b5ca0c50..b805181a8 100644 --- a/tests/Integration/Projectionist/ProjectionistTest.php +++ b/tests/Integration/Projectionist/ProjectionistTest.php @@ -11,15 +11,15 @@ use Patchlevel\EventSourcing\EventBus\Message; use Patchlevel\EventSourcing\EventBus\Serializer\DefaultHeadersSerializer; use Patchlevel\EventSourcing\Metadata\AggregateRoot\AggregateRootRegistry; -use Patchlevel\EventSourcing\Projection\Projection\Projection; -use Patchlevel\EventSourcing\Projection\Projection\ProjectionStatus; -use Patchlevel\EventSourcing\Projection\Projection\RunMode; -use Patchlevel\EventSourcing\Projection\Projection\Store\DoctrineStore; -use Patchlevel\EventSourcing\Projection\Projectionist\DefaultProjectionist; -use Patchlevel\EventSourcing\Projection\Projectionist\ProjectionistCriteria; -use Patchlevel\EventSourcing\Projection\Projector\MetadataProjectorAccessorRepository; -use Patchlevel\EventSourcing\Projection\Projector\TraceableProjectorAccessorRepository; +use Patchlevel\EventSourcing\Projection\Engine\DefaultSubscriptionEngine; +use Patchlevel\EventSourcing\Projection\Engine\SubscriptionEngineCriteria; use Patchlevel\EventSourcing\Projection\RetryStrategy\ClockBasedRetryStrategy; +use Patchlevel\EventSourcing\Projection\Store\DoctrineSubscriptionStore; +use Patchlevel\EventSourcing\Projection\Subscriber\MetadataSubscriberAccessorRepository; +use Patchlevel\EventSourcing\Projection\Subscriber\TraceableSubscriberAccessorRepository; +use Patchlevel\EventSourcing\Projection\Subscription\RunMode; +use Patchlevel\EventSourcing\Projection\Subscription\Status; +use Patchlevel\EventSourcing\Projection\Subscription\Subscription; use Patchlevel\EventSourcing\Repository\DefaultRepositoryManager; use Patchlevel\EventSourcing\Repository\MessageDecorator\TraceDecorator; use Patchlevel\EventSourcing\Repository\MessageDecorator\TraceHeader; @@ -69,7 +69,7 @@ public function testHappyPath(): void $clock = new FrozenClock(new DateTimeImmutable('2021-01-01T00:00:00')); - $projectionStore = new DoctrineStore( + $projectionStore = new DoctrineSubscriptionStore( $this->connection, $clock, ); @@ -92,30 +92,30 @@ public function testHappyPath(): void $schemaDirector->create(); - $projectionist = new DefaultProjectionist( + $projectionist = new DefaultSubscriptionEngine( $store, $projectionStore, - new MetadataProjectorAccessorRepository([new ProfileProjector($this->projectionConnection)]), + new MetadataSubscriberAccessorRepository([new ProfileProjector($this->projectionConnection)]), ); self::assertEquals( - [new Projection('profile_1', lastSavedAt: new DateTimeImmutable('2021-01-01T00:00:00'))], - $projectionist->projections(), + [new Subscription('profile_1', lastSavedAt: new DateTimeImmutable('2021-01-01T00:00:00'))], + $projectionist->subscriptions(), ); $projectionist->boot(); self::assertEquals( [ - new Projection( + new Subscription( 'profile_1', - Projection::DEFAULT_GROUP, + Subscription::DEFAULT_GROUP, RunMode::FromBeginning, - ProjectionStatus::Active, + Status::Active, lastSavedAt: new DateTimeImmutable('2021-01-01T00:00:00'), ), ], - $projectionist->projections(), + $projectionist->subscriptions(), ); $profile = Profile::create(ProfileId::fromString('1'), 'John'); @@ -125,16 +125,16 @@ public function testHappyPath(): void self::assertEquals( [ - new Projection( + new Subscription( 'profile_1', - Projection::DEFAULT_GROUP, + Subscription::DEFAULT_GROUP, RunMode::FromBeginning, - ProjectionStatus::Active, + Status::Active, 1, lastSavedAt: new DateTimeImmutable('2021-01-01T00:00:00'), ), ], - $projectionist->projections(), + $projectionist->subscriptions(), ); $result = $this->projectionConnection->fetchAssociative( @@ -151,15 +151,15 @@ public function testHappyPath(): void self::assertEquals( [ - new Projection( + new Subscription( 'profile_1', - Projection::DEFAULT_GROUP, + Subscription::DEFAULT_GROUP, RunMode::FromBeginning, - ProjectionStatus::New, + Status::New, lastSavedAt: new DateTimeImmutable('2021-01-01T00:00:00'), ), ], - $projectionist->projections(), + $projectionist->subscriptions(), ); self::assertFalse( @@ -181,7 +181,7 @@ public function testErrorHandling(): void 'eventstore', ); - $projectionStore = new DoctrineStore( + $projectionStore = new DoctrineSubscriptionStore( $this->connection, $clock, ); @@ -204,10 +204,10 @@ public function testErrorHandling(): void $projector = new ErrorProducerProjector(); - $projectionist = new DefaultProjectionist( + $projectionist = new DefaultSubscriptionEngine( $store, $projectionStore, - new MetadataProjectorAccessorRepository([$projector]), + new MetadataSubscriberAccessorRepository([$projector]), new ClockBasedRetryStrategy( $clock, ClockBasedRetryStrategy::DEFAULT_BASE_DELAY, @@ -218,10 +218,10 @@ public function testErrorHandling(): void $projectionist->boot(); - $projection = self::findProjection($projectionist->projections(), 'error_producer'); + $projection = self::findProjection($projectionist->subscriptions(), 'error_producer'); - self::assertEquals(ProjectionStatus::Active, $projection->status()); - self::assertEquals(null, $projection->projectionError()); + self::assertEquals(Status::Active, $projection->status()); + self::assertEquals(null, $projection->subscriptionError()); self::assertEquals(0, $projection->retryAttempt()); $repository = $manager->get(Profile::class); @@ -232,61 +232,61 @@ public function testErrorHandling(): void $projector->subscribeError = true; $projectionist->run(); - $projection = self::findProjection($projectionist->projections(), 'error_producer'); + $projection = self::findProjection($projectionist->subscriptions(), 'error_producer'); - self::assertEquals(ProjectionStatus::Error, $projection->status()); - self::assertEquals('subscribe error', $projection->projectionError()?->errorMessage); - self::assertEquals(ProjectionStatus::Active, $projection->projectionError()?->previousStatus); + self::assertEquals(Status::Error, $projection->status()); + self::assertEquals('subscribe error', $projection->subscriptionError()?->errorMessage); + self::assertEquals(Status::Active, $projection->subscriptionError()?->previousStatus); self::assertEquals(0, $projection->retryAttempt()); $projectionist->run(); - $projection = self::findProjection($projectionist->projections(), 'error_producer'); + $projection = self::findProjection($projectionist->subscriptions(), 'error_producer'); - self::assertEquals(ProjectionStatus::Error, $projection->status()); - self::assertEquals('subscribe error', $projection->projectionError()?->errorMessage); - self::assertEquals(ProjectionStatus::Active, $projection->projectionError()?->previousStatus); + self::assertEquals(Status::Error, $projection->status()); + self::assertEquals('subscribe error', $projection->subscriptionError()?->errorMessage); + self::assertEquals(Status::Active, $projection->subscriptionError()?->previousStatus); self::assertEquals(0, $projection->retryAttempt()); $clock->sleep(5); $projectionist->run(); - $projection = self::findProjection($projectionist->projections(), 'error_producer'); + $projection = self::findProjection($projectionist->subscriptions(), 'error_producer'); - self::assertEquals(ProjectionStatus::Error, $projection->status()); - self::assertEquals('subscribe error', $projection->projectionError()?->errorMessage); - self::assertEquals(ProjectionStatus::Active, $projection->projectionError()?->previousStatus); + self::assertEquals(Status::Error, $projection->status()); + self::assertEquals('subscribe error', $projection->subscriptionError()?->errorMessage); + self::assertEquals(Status::Active, $projection->subscriptionError()?->previousStatus); self::assertEquals(1, $projection->retryAttempt()); $clock->sleep(10); $projectionist->run(); - $projection = self::findProjection($projectionist->projections(), 'error_producer'); + $projection = self::findProjection($projectionist->subscriptions(), 'error_producer'); - self::assertEquals(ProjectionStatus::Error, $projection->status()); - self::assertEquals('subscribe error', $projection->projectionError()?->errorMessage); - self::assertEquals(ProjectionStatus::Active, $projection->projectionError()?->previousStatus); + self::assertEquals(Status::Error, $projection->status()); + self::assertEquals('subscribe error', $projection->subscriptionError()?->errorMessage); + self::assertEquals(Status::Active, $projection->subscriptionError()?->previousStatus); self::assertEquals(2, $projection->retryAttempt()); - $projectionist->reactivate(new ProjectionistCriteria( + $projectionist->reactivate(new SubscriptionEngineCriteria( ids: ['error_producer'], )); - $projection = self::findProjection($projectionist->projections(), 'error_producer'); + $projection = self::findProjection($projectionist->subscriptions(), 'error_producer'); - self::assertEquals(ProjectionStatus::Active, $projection->status()); - self::assertEquals(null, $projection->projectionError()); + self::assertEquals(Status::Active, $projection->status()); + self::assertEquals(null, $projection->subscriptionError()); self::assertEquals(0, $projection->retryAttempt()); $projectionist->run(); - $projection = self::findProjection($projectionist->projections(), 'error_producer'); + $projection = self::findProjection($projectionist->subscriptions(), 'error_producer'); - self::assertEquals(ProjectionStatus::Error, $projection->status()); - self::assertEquals('subscribe error', $projection->projectionError()?->errorMessage); - self::assertEquals(ProjectionStatus::Active, $projection->projectionError()?->previousStatus); + self::assertEquals(Status::Error, $projection->status()); + self::assertEquals('subscribe error', $projection->subscriptionError()?->errorMessage); + self::assertEquals(Status::Active, $projection->subscriptionError()?->previousStatus); self::assertEquals(0, $projection->retryAttempt()); $clock->sleep(5); @@ -294,10 +294,10 @@ public function testErrorHandling(): void $projectionist->run(); - $projection = self::findProjection($projectionist->projections(), 'error_producer'); + $projection = self::findProjection($projectionist->subscriptions(), 'error_producer'); - self::assertEquals(ProjectionStatus::Active, $projection->status()); - self::assertEquals(null, $projection->projectionError()); + self::assertEquals(Status::Active, $projection->status()); + self::assertEquals(null, $projection->subscriptionError()); self::assertEquals(0, $projection->retryAttempt()); } @@ -315,7 +315,7 @@ public function testProcessor(): void $clock = new FrozenClock(new DateTimeImmutable('2021-01-01T00:00:00')); - $projectionStore = new DoctrineStore( + $projectionStore = new DoctrineSubscriptionStore( $this->connection, $clock, ); @@ -330,8 +330,8 @@ public function testProcessor(): void new TraceDecorator($traceStack), ); - $projectorAccessorRepository = new TraceableProjectorAccessorRepository( - new MetadataProjectorAccessorRepository([new ProfileProcessor($manager)]), + $projectorAccessorRepository = new TraceableSubscriberAccessorRepository( + new MetadataSubscriberAccessorRepository([new ProfileProcessor($manager)]), $traceStack, ); @@ -347,30 +347,30 @@ public function testProcessor(): void $schemaDirector->create(); - $projectionist = new DefaultProjectionist( + $projectionist = new DefaultSubscriptionEngine( $store, $projectionStore, $projectorAccessorRepository, ); self::assertEquals( - [new Projection('profile', lastSavedAt: new DateTimeImmutable('2021-01-01T00:00:00'))], - $projectionist->projections(), + [new Subscription('profile', lastSavedAt: new DateTimeImmutable('2021-01-01T00:00:00'))], + $projectionist->subscriptions(), ); $projectionist->boot(); self::assertEquals( [ - new Projection( + new Subscription( 'profile', - Projection::DEFAULT_GROUP, + Subscription::DEFAULT_GROUP, RunMode::FromBeginning, - ProjectionStatus::Active, + Status::Active, lastSavedAt: new DateTimeImmutable('2021-01-01T00:00:00'), ), ], - $projectionist->projections(), + $projectionist->subscriptions(), ); $profile = Profile::create(ProfileId::fromString('1'), 'John'); @@ -378,7 +378,7 @@ public function testProcessor(): void $projectionist->run(); - $projections = $projectionist->projections(); + $projections = $projectionist->subscriptions(); self::assertCount(1, $projections); self::assertArrayHasKey(0, $projections); @@ -387,7 +387,7 @@ public function testProcessor(): void self::assertEquals('profile', $projection->id()); - self::assertEquals(ProjectionStatus::Active, $projection->status()); + self::assertEquals(Status::Active, $projection->status()); /** @var list $messages */ $messages = iterator_to_array($store->load()); @@ -406,8 +406,8 @@ public function testProcessor(): void ); } - /** @param list $projections */ - private static function findProjection(array $projections, string $id): Projection + /** @param list $projections */ + private static function findProjection(array $projections, string $id): Subscription { foreach ($projections as $projection) { if ($projection->id() === $id) { diff --git a/tests/Unit/Attribute/ProjectorTest.php b/tests/Unit/Attribute/SubscriberTest.php similarity index 53% rename from tests/Unit/Attribute/ProjectorTest.php rename to tests/Unit/Attribute/SubscriberTest.php index 8ad51c26e..fb14ed72a 100644 --- a/tests/Unit/Attribute/ProjectorTest.php +++ b/tests/Unit/Attribute/SubscriberTest.php @@ -4,15 +4,15 @@ namespace Patchlevel\EventSourcing\Tests\Unit\Attribute; -use Patchlevel\EventSourcing\Attribute\Projector; +use Patchlevel\EventSourcing\Attribute\Subscriber; use PHPUnit\Framework\TestCase; -/** @covers \Patchlevel\EventSourcing\Attribute\Projector */ -final class ProjectorTest extends TestCase +/** @covers \Patchlevel\EventSourcing\Attribute\Subscriber */ +final class SubscriberTest extends TestCase { public function testInstantiate(): void { - $attribute = new Projector('foo'); + $attribute = new Subscriber('foo'); self::assertSame('foo', $attribute->id); } diff --git a/tests/Unit/Fixture/DummyProjector.php b/tests/Unit/Fixture/Dummy2Subscriber.php similarity index 88% rename from tests/Unit/Fixture/DummyProjector.php rename to tests/Unit/Fixture/Dummy2Subscriber.php index 59c1197b0..032e96b36 100644 --- a/tests/Unit/Fixture/DummyProjector.php +++ b/tests/Unit/Fixture/Dummy2Subscriber.php @@ -4,14 +4,14 @@ namespace Patchlevel\EventSourcing\Tests\Unit\Fixture; -use Patchlevel\EventSourcing\Attribute\Projector; use Patchlevel\EventSourcing\Attribute\Setup; use Patchlevel\EventSourcing\Attribute\Subscribe; +use Patchlevel\EventSourcing\Attribute\Subscriber; use Patchlevel\EventSourcing\Attribute\Teardown; use Patchlevel\EventSourcing\EventBus\Message as EventMessage; -#[Projector('dummy')] -final class DummyProjector +#[Subscriber('dummy2')] +final class Dummy2Subscriber { public EventMessage|null $handledMessage = null; public bool $createCalled = false; diff --git a/tests/Unit/Fixture/Dummy2Projector.php b/tests/Unit/Fixture/DummySubscriber.php similarity index 88% rename from tests/Unit/Fixture/Dummy2Projector.php rename to tests/Unit/Fixture/DummySubscriber.php index b64bd5337..da5fa71c3 100644 --- a/tests/Unit/Fixture/Dummy2Projector.php +++ b/tests/Unit/Fixture/DummySubscriber.php @@ -4,14 +4,14 @@ namespace Patchlevel\EventSourcing\Tests\Unit\Fixture; -use Patchlevel\EventSourcing\Attribute\Projector; use Patchlevel\EventSourcing\Attribute\Setup; use Patchlevel\EventSourcing\Attribute\Subscribe; +use Patchlevel\EventSourcing\Attribute\Subscriber; use Patchlevel\EventSourcing\Attribute\Teardown; use Patchlevel\EventSourcing\EventBus\Message as EventMessage; -#[Projector('dummy2')] -final class Dummy2Projector +#[Subscriber('dummy')] +final class DummySubscriber { public EventMessage|null $handledMessage = null; public bool $createCalled = false; diff --git a/tests/Unit/Metadata/Projector/AttributeProjectorMetadataFactoryTest.php b/tests/Unit/Metadata/Subscriber/AttributeSubscriberMetadataFactoryTest.php similarity index 57% rename from tests/Unit/Metadata/Projector/AttributeProjectorMetadataFactoryTest.php rename to tests/Unit/Metadata/Subscriber/AttributeSubscriberMetadataFactoryTest.php index ab4064c13..379509630 100644 --- a/tests/Unit/Metadata/Projector/AttributeProjectorMetadataFactoryTest.php +++ b/tests/Unit/Metadata/Subscriber/AttributeSubscriberMetadataFactoryTest.php @@ -2,42 +2,42 @@ declare(strict_types=1); -namespace Patchlevel\EventSourcing\Tests\Unit\Metadata\Projector; +namespace Patchlevel\EventSourcing\Tests\Unit\Metadata\Subscriber; -use Patchlevel\EventSourcing\Attribute\Projector; use Patchlevel\EventSourcing\Attribute\Setup; use Patchlevel\EventSourcing\Attribute\Subscribe; +use Patchlevel\EventSourcing\Attribute\Subscriber; use Patchlevel\EventSourcing\Attribute\Teardown; -use Patchlevel\EventSourcing\Metadata\Projector\AttributeProjectorMetadataFactory; -use Patchlevel\EventSourcing\Metadata\Projector\ClassIsNotAProjector; -use Patchlevel\EventSourcing\Metadata\Projector\DuplicateSetupMethod; -use Patchlevel\EventSourcing\Metadata\Projector\DuplicateTeardownMethod; +use Patchlevel\EventSourcing\Metadata\Subscriber\AttributeSubscriberMetadataFactory; +use Patchlevel\EventSourcing\Metadata\Subscriber\ClassIsNotASubscriber; +use Patchlevel\EventSourcing\Metadata\Subscriber\DuplicateSetupMethod; +use Patchlevel\EventSourcing\Metadata\Subscriber\DuplicateTeardownMethod; use Patchlevel\EventSourcing\Tests\Unit\Fixture\ProfileCreated; use Patchlevel\EventSourcing\Tests\Unit\Fixture\ProfileVisited; use PHPUnit\Framework\TestCase; -/** @covers \Patchlevel\EventSourcing\Metadata\Projector\AttributeProjectorMetadataFactory */ -final class AttributeProjectorMetadataFactoryTest extends TestCase +/** @covers \Patchlevel\EventSourcing\Metadata\Subscriber\AttributeSubscriberMetadataFactory */ +final class AttributeSubscriberMetadataFactoryTest extends TestCase { - public function testNotAProjection(): void + public function testNotASubscriber(): void { - $this->expectException(ClassIsNotAProjector::class); + $this->expectException(ClassIsNotASubscriber::class); - $projection = new class { + $subscriber = new class { }; - $metadataFactory = new AttributeProjectorMetadataFactory(); - $metadataFactory->metadata($projection::class); + $metadataFactory = new AttributeSubscriberMetadataFactory(); + $metadataFactory->metadata($subscriber::class); } - public function testEmptyProjection(): void + public function testEmptySubscriber(): void { - $projection = new #[Projector('foo')] + $subscriber = new #[Subscriber('foo')] class { }; - $metadataFactory = new AttributeProjectorMetadataFactory(); - $metadata = $metadataFactory->metadata($projection::class); + $metadataFactory = new AttributeSubscriberMetadataFactory(); + $metadata = $metadataFactory->metadata($subscriber::class); self::assertSame([], $metadata->subscribeMethods); self::assertNull($metadata->setupMethod); @@ -45,9 +45,9 @@ class { self::assertSame('foo', $metadata->id); } - public function testStandardProjection(): void + public function testStandardSubscriber(): void { - $projection = new #[Projector('foo')] + $subscriber = new #[Subscriber('foo')] class { #[Subscribe(ProfileVisited::class)] public function handle(): void @@ -65,8 +65,8 @@ public function drop(): void } }; - $metadataFactory = new AttributeProjectorMetadataFactory(); - $metadata = $metadataFactory->metadata($projection::class); + $metadataFactory = new AttributeSubscriberMetadataFactory(); + $metadata = $metadataFactory->metadata($subscriber::class); self::assertEquals( [ProfileVisited::class => ['handle']], @@ -79,7 +79,7 @@ public function drop(): void public function testMultipleHandlerOnOneMethod(): void { - $projection = new #[Projector('foo')] + $subscriber = new #[Subscriber('foo')] class { #[Subscribe(ProfileVisited::class)] #[Subscribe(ProfileCreated::class)] @@ -88,8 +88,8 @@ public function handle(): void } }; - $metadataFactory = new AttributeProjectorMetadataFactory(); - $metadata = $metadataFactory->metadata($projection::class); + $metadataFactory = new AttributeSubscriberMetadataFactory(); + $metadata = $metadataFactory->metadata($subscriber::class); self::assertEquals( [ @@ -102,7 +102,7 @@ public function handle(): void public function testSubscribeAll(): void { - $projection = new #[Projector('foo')] + $subscriber = new #[Subscriber('foo')] class { #[Subscribe(Subscribe::ALL)] public function handle(): void @@ -110,8 +110,8 @@ public function handle(): void } }; - $metadataFactory = new AttributeProjectorMetadataFactory(); - $metadata = $metadataFactory->metadata($projection::class); + $metadataFactory = new AttributeSubscriberMetadataFactory(); + $metadata = $metadataFactory->metadata($subscriber::class); self::assertEquals( [ @@ -125,7 +125,7 @@ public function testDuplicateSetupAttributeException(): void { $this->expectException(DuplicateSetupMethod::class); - $projection = new #[Projector('foo')] + $subscriber = new #[Subscriber('foo')] class { #[Setup] public function create1(): void @@ -138,15 +138,15 @@ public function create2(): void } }; - $metadataFactory = new AttributeProjectorMetadataFactory(); - $metadataFactory->metadata($projection::class); + $metadataFactory = new AttributeSubscriberMetadataFactory(); + $metadataFactory->metadata($subscriber::class); } public function testDuplicateTeardownAttributeException(): void { $this->expectException(DuplicateTeardownMethod::class); - $projection = new #[Projector('foo')] + $subscriber = new #[Subscriber('foo')] class { #[Teardown] public function drop1(): void @@ -159,7 +159,7 @@ public function drop2(): void } }; - $metadataFactory = new AttributeProjectorMetadataFactory(); - $metadataFactory->metadata($projection::class); + $metadataFactory = new AttributeSubscriberMetadataFactory(); + $metadataFactory->metadata($subscriber::class); } } diff --git a/tests/Unit/Projection/DummyStore.php b/tests/Unit/Projection/DummyStore.php deleted file mode 100644 index 0bad434e8..000000000 --- a/tests/Unit/Projection/DummyStore.php +++ /dev/null @@ -1,59 +0,0 @@ - */ - public array $addedProjections = []; - - /** @var list */ - public array $updatedProjections = []; - - /** @var list */ - public array $removedProjections = []; - - /** @param list $projections */ - public function __construct(array $projections = []) - { - $this->parentStore = new InMemoryStore($projections); - } - - public function get(string $projectionId): Projection - { - return $this->parentStore->get($projectionId); - } - - /** @return list */ - public function find(ProjectionCriteria|null $criteria = null): array - { - return $this->parentStore->find($criteria); - } - - public function add(Projection $projection): void - { - $this->parentStore->add($projection); - $this->addedProjections[] = clone $projection; - } - - public function update(Projection $projection): void - { - $this->parentStore->update($projection); - $this->updatedProjections[] = clone $projection; - } - - public function remove(Projection $projection): void - { - $this->parentStore->remove($projection); - $this->removedProjections[] = clone $projection; - } -} diff --git a/tests/Unit/Projection/Projection/ProjectionAlreadyExistsTest.php b/tests/Unit/Projection/Projection/ProjectionAlreadyExistsTest.php deleted file mode 100644 index d50fcba01..000000000 --- a/tests/Unit/Projection/Projection/ProjectionAlreadyExistsTest.php +++ /dev/null @@ -1,24 +0,0 @@ -getMessage(), - ); - - self::assertSame(0, $exception->getCode()); - } -} diff --git a/tests/Unit/Projection/Projection/ProjectionCriteriaTest.php b/tests/Unit/Projection/Projection/ProjectionCriteriaTest.php deleted file mode 100644 index 4a7d2b7dc..000000000 --- a/tests/Unit/Projection/Projection/ProjectionCriteriaTest.php +++ /dev/null @@ -1,20 +0,0 @@ -ids); - } -} diff --git a/tests/Unit/Projection/Projection/ProjectionErrorTest.php b/tests/Unit/Projection/Projection/ProjectionErrorTest.php deleted file mode 100644 index 633e0b204..000000000 --- a/tests/Unit/Projection/Projection/ProjectionErrorTest.php +++ /dev/null @@ -1,26 +0,0 @@ -errorMessage); - self::assertSame(ProjectionStatus::Active, $error->previousStatus); - self::assertIsArray($error->errorContext); - } -} diff --git a/tests/Unit/Projection/Projection/ProjectionNotFoundTest.php b/tests/Unit/Projection/Projection/ProjectionNotFoundTest.php deleted file mode 100644 index c8e22e3e8..000000000 --- a/tests/Unit/Projection/Projection/ProjectionNotFoundTest.php +++ /dev/null @@ -1,23 +0,0 @@ -getMessage(), - ); - self::assertSame(0, $exception->getCode()); - } -} diff --git a/tests/Unit/Projection/Projection/ProjectionTest.php b/tests/Unit/Projection/Projection/ProjectionTest.php deleted file mode 100644 index 6c23af7b6..000000000 --- a/tests/Unit/Projection/Projection/ProjectionTest.php +++ /dev/null @@ -1,149 +0,0 @@ -id()); - self::assertEquals(ProjectionStatus::New, $projection->status()); - self::assertEquals(0, $projection->position()); - self::assertTrue($projection->isNew()); - self::assertFalse($projection->isBooting()); - self::assertFalse($projection->isActive()); - self::assertFalse($projection->isError()); - self::assertFalse($projection->isOutdated()); - } - - public function testBooting(): void - { - $projection = new Projection( - 'test', - ); - - $projection->booting(); - - self::assertEquals(ProjectionStatus::Booting, $projection->status()); - self::assertFalse($projection->isNew()); - self::assertTrue($projection->isBooting()); - self::assertFalse($projection->isActive()); - self::assertFalse($projection->isError()); - self::assertFalse($projection->isOutdated()); - } - - public function testActive(): void - { - $projection = new Projection( - 'test', - ); - - $projection->active(); - - self::assertEquals(ProjectionStatus::Active, $projection->status()); - self::assertFalse($projection->isNew()); - self::assertFalse($projection->isBooting()); - self::assertTrue($projection->isActive()); - self::assertFalse($projection->isError()); - self::assertFalse($projection->isOutdated()); - } - - public function testError(): void - { - $projection = new Projection( - 'test', - ); - - $exception = new RuntimeException('test'); - - $projection->error($exception); - - self::assertEquals(ProjectionStatus::Error, $projection->status()); - self::assertFalse($projection->isNew()); - self::assertFalse($projection->isBooting()); - self::assertFalse($projection->isActive()); - self::assertTrue($projection->isError()); - self::assertFalse($projection->isOutdated()); - self::assertEquals( - new ProjectionError( - 'test', - ProjectionStatus::New, - ThrowableToErrorContextTransformer::transform($exception), - ), - $projection->projectionError(), - ); - } - - public function testOutdated(): void - { - $projection = new Projection( - 'test', - ); - - $projection->outdated(); - - self::assertEquals(ProjectionStatus::Outdated, $projection->status()); - self::assertFalse($projection->isNew()); - self::assertFalse($projection->isBooting()); - self::assertFalse($projection->isActive()); - self::assertFalse($projection->isError()); - self::assertTrue($projection->isOutdated()); - } - - public function testChangePosition(): void - { - $projection = new Projection( - 'test', - ); - - $projection->changePosition(10); - - self::assertEquals(10, $projection->position()); - } - - public function testCanNotRetry(): void - { - $this->expectException(NoErrorToRetry::class); - - $projection = new Projection( - 'test', - ); - - $projection->doRetry(); - } - - public function testDoRetry(): void - { - $projection = new Projection( - 'test', - 'default', - RunMode::FromBeginning, - ProjectionStatus::Error, - 0, - new ProjectionError('test', ProjectionStatus::New, []), - ); - - self::assertEquals(null, $projection->retryAttempt()); - $projection->doRetry(); - - self::assertEquals(1, $projection->retryAttempt()); - $projection->resetRetry(); - - self::assertEquals(null, $projection->retryAttempt()); - } -} diff --git a/tests/Unit/Projection/Projection/Store/InMemoryStoreTest.php b/tests/Unit/Projection/Projection/Store/InMemoryStoreTest.php deleted file mode 100644 index cbc7eec72..000000000 --- a/tests/Unit/Projection/Projection/Store/InMemoryStoreTest.php +++ /dev/null @@ -1,154 +0,0 @@ -add($projection); - - self::assertEquals($projection, $store->get($id)); - self::assertEquals([$projection], $store->find()); - } - - public function testAddDuplicated(): void - { - $this->expectException(ProjectionAlreadyExists::class); - - $id = 'test'; - $projection = new Projection($id); - - $store = new InMemoryStore([$projection]); - $store->add($projection); - } - - public function testUpdate(): void - { - $id = 'test'; - $projection = new Projection($id); - - $store = new InMemoryStore([$projection]); - - $store->update($projection); - - self::assertEquals($projection, $store->get($id)); - self::assertEquals([$projection], $store->find()); - } - - public function testUpdateNotFound(): void - { - $this->expectException(ProjectionNotFound::class); - - $id = 'test'; - $projection = new Projection($id); - - $store = new InMemoryStore(); - - $store->update($projection); - } - - public function testNotFound(): void - { - $this->expectException(ProjectionNotFound::class); - - $store = new InMemoryStore(); - $store->get('test'); - } - - public function testRemove(): void - { - $id = 'test'; - $projection = new Projection($id); - - $store = new InMemoryStore([$projection]); - - $store->remove($projection); - - self::assertEquals([], $store->find()); - } - - public function testFind(): void - { - $projection1 = new Projection('1'); - $projection2 = new Projection('2'); - - $store = new InMemoryStore([$projection1, $projection2]); - - self::assertEquals([$projection1, $projection2], $store->find()); - } - - public function testFindById(): void - { - $projection1 = new Projection('1'); - $projection2 = new Projection('2'); - - $store = new InMemoryStore([$projection1, $projection2]); - - $criteria = new ProjectionCriteria( - ids: ['1'], - ); - - self::assertEquals([$projection1], $store->find($criteria)); - } - - public function testFindByGroup(): void - { - $projection1 = new Projection('1', group: 'group1'); - $projection2 = new Projection('2', group: 'group2'); - - $store = new InMemoryStore([$projection1, $projection2]); - - $criteria = new ProjectionCriteria( - groups: ['group1'], - ); - - self::assertEquals([$projection1], $store->find($criteria)); - } - - public function testFindByStatus(): void - { - $projection1 = new Projection('1', status: ProjectionStatus::New); - $projection2 = new Projection('2', status: ProjectionStatus::Booting); - - $store = new InMemoryStore([$projection1, $projection2]); - - $criteria = new ProjectionCriteria( - status: [ProjectionStatus::New], - ); - - self::assertEquals([$projection1], $store->find($criteria)); - } - - public function testFindByAll(): void - { - $projection1 = new Projection('1', group: 'group1', status: ProjectionStatus::New); - $projection2 = new Projection('2', group: 'group2', status: ProjectionStatus::Booting); - - $store = new InMemoryStore([$projection1, $projection2]); - - $criteria = new ProjectionCriteria( - ids: ['1'], - groups: ['group1'], - status: [ProjectionStatus::New], - ); - - self::assertEquals([$projection1], $store->find($criteria)); - } -} diff --git a/tests/Unit/Projection/Projectionist/DefaultProjectionistTest.php b/tests/Unit/Projection/Projectionist/DefaultProjectionistTest.php deleted file mode 100644 index b87f83e0b..000000000 --- a/tests/Unit/Projection/Projectionist/DefaultProjectionistTest.php +++ /dev/null @@ -1,1873 +0,0 @@ -prophesize(Store::class); - $streamableStore->load($this->criteria())->shouldNotBeCalled(); - - $store = new DummyStore(); - - $projectionist = new DefaultProjectionist( - $streamableStore->reveal(), - $store, - new MetadataProjectorAccessorRepository([]), - ); - - $projectionist->boot(); - - self::assertEquals([], $store->addedProjections); - self::assertEquals([], $store->updatedProjections); - } - - public function testBootDiscoverNewProjectors(): void - { - $projectionId = 'test'; - $projector = new #[ProjectionAttribute('test')] - class { - }; - - $streamableStore = $this->prophesize(Store::class); - $streamableStore->load($this->criteria())->willReturn(new ArrayStream([]))->shouldBeCalledOnce(); - - $projectionStore = new DummyStore(); - - $projectionist = new DefaultProjectionist( - $streamableStore->reveal(), - $projectionStore, - new MetadataProjectorAccessorRepository([$projector]), - ); - - $projectionist->boot(); - - self::assertEquals([ - new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::New, - ), - ], $projectionStore->addedProjections); - - self::assertEquals([ - new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Booting, - ), - new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Active, - ), - ], $projectionStore->updatedProjections); - } - - public function testBootWithoutCreateMethod(): void - { - $projectionId = 'test'; - $projector = new #[ProjectionAttribute('test')] - class { - }; - - $projectionStore = new DummyStore([ - new Projection($projectionId), - ]); - - $message = new Message(new ProfileVisited(ProfileId::fromString('test'))); - - $streamableStore = $this->prophesize(Store::class); - $streamableStore->load($this->criteria())->willReturn(new ArrayStream([$message]))->shouldBeCalledOnce(); - - $projectionist = new DefaultProjectionist( - $streamableStore->reveal(), - $projectionStore, - new MetadataProjectorAccessorRepository([$projector]), - ); - - $projectionist->boot(); - - self::assertEquals([ - new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Booting, - ), - new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Booting, - 1, - ), - new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Active, - 1, - ), - ], $projectionStore->updatedProjections); - } - - public function testBootWithMethods(): void - { - $projectionId = 'test'; - $projector = new #[ProjectionAttribute('test')] - class { - public Message|null $message = null; - public bool $created = false; - - #[Setup] - public function create(): void - { - $this->created = true; - } - - #[Subscribe(ProfileVisited::class)] - public function handle(Message $message): void - { - $this->message = $message; - } - }; - - $projectionStore = new DummyStore(); - - $message = new Message(new ProfileVisited(ProfileId::fromString('test'))); - - $streamableStore = $this->prophesize(Store::class); - $streamableStore->load($this->criteria())->willReturn(new ArrayStream([$message]))->shouldBeCalledOnce(); - - $projectionist = new DefaultProjectionist( - $streamableStore->reveal(), - $projectionStore, - new MetadataProjectorAccessorRepository([$projector]), - ); - - $projectionist->boot(); - - self::assertEquals([ - new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::New, - ), - ], $projectionStore->addedProjections); - - self::assertEquals([ - new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Booting, - ), - new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Booting, - 1, - ), - new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Active, - 1, - ), - ], $projectionStore->updatedProjections); - - self::assertTrue($projector->created); - self::assertSame($message, $projector->message); - } - - public function testBootWithLimit(): void - { - $projectionId = 'test'; - $projector = new #[ProjectionAttribute('test')] - class { - public Message|null $message = null; - public bool $created = false; - - #[Setup] - public function create(): void - { - $this->created = true; - } - - #[Subscribe(ProfileVisited::class)] - public function handle(Message $message): void - { - $this->message = $message; - } - }; - - $projectionStore = new DummyStore(); - - $message = new Message(new ProfileVisited(ProfileId::fromString('test'))); - - $streamableStore = $this->prophesize(Store::class); - $streamableStore->load($this->criteria())->willReturn(new ArrayStream([$message]))->shouldBeCalledOnce(); - - $projectionist = new DefaultProjectionist( - $streamableStore->reveal(), - $projectionStore, - new MetadataProjectorAccessorRepository([$projector]), - ); - - $projectionist->boot(new ProjectionistCriteria(), 1); - - self::assertEquals([ - new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::New, - ), - ], $projectionStore->addedProjections); - - self::assertEquals([ - new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Booting, - ), - new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Booting, - 1, - ), - ], $projectionStore->updatedProjections); - - self::assertTrue($projector->created); - self::assertSame($message, $projector->message); - } - - public function testBootingWithSkip(): void - { - $projectionId1 = 'test1'; - $projector1 = new #[ProjectionAttribute('test1')] - class { - public Message|null $message = null; - - #[Subscribe(ProfileVisited::class)] - public function handle(Message $message): void - { - $this->message = $message; - } - }; - - $projectionId2 = 'test2'; - $projector2 = new #[ProjectionAttribute('test2')] - class { - public Message|null $message = null; - - #[Subscribe(ProfileVisited::class)] - public function handle(Message $message): void - { - $this->message = $message; - } - }; - - $projectionStore = new DummyStore([ - new Projection( - $projectionId1, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Booting, - ), - new Projection( - $projectionId2, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Booting, - 1, - ), - ]); - - $message = new Message(new ProfileVisited(ProfileId::fromString('test'))); - - $streamableStore = $this->prophesize(Store::class); - $streamableStore->load($this->criteria())->willReturn(new ArrayStream([$message]))->shouldBeCalledOnce(); - - $projectionist = new DefaultProjectionist( - $streamableStore->reveal(), - $projectionStore, - new MetadataProjectorAccessorRepository([$projector1, $projector2]), - ); - - $projectionist->boot(); - - self::assertEquals([ - new Projection( - $projectionId1, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Booting, - 1, - ), - new Projection( - $projectionId2, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Booting, - 1, - ), - new Projection( - $projectionId1, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Active, - 1, - ), - new Projection( - $projectionId2, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Active, - 1, - ), - ], $projectionStore->updatedProjections); - - self::assertSame($message, $projector1->message); - self::assertNull($projector2->message); - } - - public function testBootWithCreateError(): void - { - $projectionId = 'test'; - $projector = new #[ProjectionAttribute('test')] - class { - public function __construct( - public readonly RuntimeException $exception = new RuntimeException('ERROR'), - ) { - } - - #[Setup] - public function create(): void - { - throw $this->exception; - } - }; - - $projectionStore = new DummyStore([ - new Projection($projectionId), - ]); - - $streamableStore = $this->prophesize(Store::class); - $streamableStore->load($this->criteria())->shouldNotBeCalled(); - - $projectionist = new DefaultProjectionist( - $streamableStore->reveal(), - $projectionStore, - new MetadataProjectorAccessorRepository([$projector]), - ); - - $projectionist->boot(); - - self::assertEquals( - [ - new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Error, - 0, - new ProjectionError( - 'ERROR', - ProjectionStatus::New, - ThrowableToErrorContextTransformer::transform($projector->exception), - ), - ), - ], - $projectionStore->updatedProjections, - ); - } - - public function testBootingWithGabInIndex(): void - { - $projectionId = 'test'; - $projector = new #[ProjectionAttribute('test')] - class { - /** @var list */ - public array $messages = []; - - #[Subscribe(ProfileVisited::class)] - public function handle(Message $message): void - { - $this->messages[] = $message; - } - }; - - $projectionStore = new DummyStore([ - new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Booting, - ), - ]); - - $message1 = new Message(new ProfileVisited(ProfileId::fromString('test'))); - $message2 = new Message(new ProfileVisited(ProfileId::fromString('test'))); - - $streamableStore = $this->prophesize(Store::class); - $streamableStore->load($this->criteria())->willReturn(new ArrayStream([1 => $message1, 3 => $message2]))->shouldBeCalledOnce(); - - $projectionist = new DefaultProjectionist( - $streamableStore->reveal(), - $projectionStore, - new MetadataProjectorAccessorRepository([$projector]), - ); - - $projectionist->boot(); - - self::assertEquals([ - new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Booting, - 3, - ), - new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Active, - 3, - ), - ], $projectionStore->updatedProjections); - - self::assertSame([$message1, $message2], $projector->messages); - } - - public function testBootingWithFromNow(): void - { - $projectionId = 'test'; - $projector = new #[ProjectionAttribute('test', runMode: RunMode::FromNow)] - class { - public Message|null $message = null; - - #[Subscribe(ProfileVisited::class)] - public function handle(Message $message): void - { - $this->message = $message; - } - }; - - $projectionStore = new DummyStore([ - new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromNow, - ProjectionStatus::Booting, - ), - ]); - - $message1 = new Message(new ProfileVisited(ProfileId::fromString('test'))); - - $streamableStore = $this->prophesize(Store::class); - $streamableStore->load(null, 1, null, true)->willReturn(new ArrayStream([$message1]))->shouldBeCalledOnce(); - - $projectionist = new DefaultProjectionist( - $streamableStore->reveal(), - $projectionStore, - new MetadataProjectorAccessorRepository([$projector]), - ); - - $projectionist->boot(); - - self::assertEquals([ - new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromNow, - ProjectionStatus::Active, - 1, - ), - ], $projectionStore->updatedProjections); - - self::assertNull($projector->message); - } - - public function testBootingWithFromNowWithEmtpyStream(): void - { - $projectionId = 'test'; - $projector = new #[ProjectionAttribute('test', runMode: RunMode::FromNow)] - class { - public Message|null $message = null; - - #[Subscribe(ProfileVisited::class)] - public function handle(Message $message): void - { - $this->message = $message; - } - }; - - $projectionStore = new DummyStore([ - new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromNow, - ProjectionStatus::Booting, - ), - ]); - - $message1 = new Message(new ProfileVisited(ProfileId::fromString('test'))); - - $streamableStore = $this->prophesize(Store::class); - $streamableStore->load(null, 1, null, true)->willReturn(new ArrayStream([]))->shouldBeCalledOnce(); - - $projectionist = new DefaultProjectionist( - $streamableStore->reveal(), - $projectionStore, - new MetadataProjectorAccessorRepository([$projector]), - ); - - $projectionist->boot(); - - self::assertEquals([ - new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromNow, - ProjectionStatus::Active, - 0, - ), - ], $projectionStore->updatedProjections); - - self::assertNull($projector->message); - } - - public function testBootingWithOnlyOnce(): void - { - $projectionId = 'test'; - $projector = new #[ProjectionAttribute('test', runMode: RunMode::Once)] - class { - public Message|null $message = null; - - #[Subscribe(ProfileVisited::class)] - public function handle(Message $message): void - { - $this->message = $message; - } - }; - - $projectionStore = new DummyStore([ - new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::Once, - ProjectionStatus::Booting, - ), - ]); - - $message1 = new Message(new ProfileVisited(ProfileId::fromString('test'))); - - $streamableStore = $this->prophesize(Store::class); - $streamableStore->load($this->criteria())->willReturn(new ArrayStream([$message1]))->shouldBeCalledOnce(); - - $projectionist = new DefaultProjectionist( - $streamableStore->reveal(), - $projectionStore, - new MetadataProjectorAccessorRepository([$projector]), - ); - - $projectionist->boot(); - - self::assertEquals([ - new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::Once, - ProjectionStatus::Booting, - 1, - ), - new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::Once, - ProjectionStatus::Finished, - 1, - ), - ], $projectionStore->updatedProjections); - - self::assertEquals($message1, $projector->message); - } - - public function testRunDiscoverNewProjectors(): void - { - $projectionId = 'test'; - $projector = new #[ProjectionAttribute('test')] - class { - }; - - $streamableStore = $this->prophesize(Store::class); - $projectionStore = new DummyStore(); - - $projectionist = new DefaultProjectionist( - $streamableStore->reveal(), - $projectionStore, - new MetadataProjectorAccessorRepository([$projector]), - ); - - $projectionist->run(); - - self::assertEquals([ - new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::New, - ), - ], $projectionStore->addedProjections); - } - - public function testRunning(): void - { - $projectionId = 'test'; - $projector = new #[ProjectionAttribute('test')] - class { - public Message|null $message = null; - - #[Subscribe(ProfileVisited::class)] - public function handle(Message $message): void - { - $this->message = $message; - } - }; - - $projectionStore = new DummyStore([ - new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Active, - ), - ]); - - $message = new Message(new ProfileVisited(ProfileId::fromString('test'))); - - $streamableStore = $this->prophesize(Store::class); - $streamableStore->load($this->criteria())->willReturn(new ArrayStream([$message]))->shouldBeCalledOnce(); - - $projectionist = new DefaultProjectionist( - $streamableStore->reveal(), - $projectionStore, - new MetadataProjectorAccessorRepository([$projector]), - ); - - $projectionist->run(); - - self::assertEquals([ - new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Active, - 1, - ), - ], $projectionStore->updatedProjections); - - self::assertSame($message, $projector->message); - } - - public function testRunningWithLimit(): void - { - $projectionId = 'test'; - $projector = new #[ProjectionAttribute('test')] - class { - public Message|null $message = null; - - #[Subscribe(ProfileVisited::class)] - public function handle(Message $message): void - { - $this->message = $message; - } - }; - - $projectionStore = new DummyStore([ - new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Active, - ), - ]); - - $message1 = new Message(new ProfileVisited(ProfileId::fromString('test'))); - $message2 = new Message(new ProfileVisited(ProfileId::fromString('test'))); - - $streamableStore = $this->prophesize(Store::class); - $streamableStore - ->load($this->criteria()) - ->willReturn(new ArrayStream([$message1, $message2])) - ->shouldBeCalledOnce(); - - $projectionist = new DefaultProjectionist( - $streamableStore->reveal(), - $projectionStore, - new MetadataProjectorAccessorRepository([$projector]), - ); - - $projectionist->run(new ProjectionistCriteria(), 1); - - self::assertEquals([ - new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Active, - 1, - ), - ], $projectionStore->updatedProjections); - - self::assertSame($message1, $projector->message); - } - - public function testRunningWithSkip(): void - { - $projectionId1 = 'test1'; - $projector1 = new #[ProjectionAttribute('test1')] - class { - public Message|null $message = null; - - #[Subscribe(ProfileVisited::class)] - public function handle(Message $message): void - { - $this->message = $message; - } - }; - - $projectionId2 = 'test2'; - $projector2 = new #[ProjectionAttribute('test2')] - class { - public Message|null $message = null; - - #[Subscribe(ProfileVisited::class)] - public function handle(Message $message): void - { - $this->message = $message; - } - }; - - $projectionStore = new DummyStore([ - new Projection( - $projectionId1, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Active, - ), - new Projection( - $projectionId2, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Active, - 1, - ), - ]); - - $message = new Message(new ProfileVisited(ProfileId::fromString('test'))); - - $streamableStore = $this->prophesize(Store::class); - $streamableStore->load($this->criteria())->willReturn(new ArrayStream([$message]))->shouldBeCalledOnce(); - - $projectionist = new DefaultProjectionist( - $streamableStore->reveal(), - $projectionStore, - new MetadataProjectorAccessorRepository([$projector1, $projector2]), - ); - - $projectionist->run(); - - self::assertEquals([ - new Projection( - $projectionId1, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Active, - 1, - ), - new Projection( - $projectionId2, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Active, - 1, - ), - ], $projectionStore->updatedProjections); - - self::assertSame($message, $projector1->message); - self::assertNull($projector2->message); - } - - public function testRunningWithError(): void - { - $projectionId = 'test'; - $projector = new #[ProjectionAttribute('test')] - class { - public function __construct( - public readonly RuntimeException $exception = new RuntimeException('ERROR'), - ) { - } - - #[Subscribe(ProfileVisited::class)] - public function handle(Message $message): void - { - throw $this->exception; - } - }; - - $projectionStore = new DummyStore([ - new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Active, - ), - ]); - - $message = new Message(new ProfileVisited(ProfileId::fromString('test'))); - - $streamableStore = $this->prophesize(Store::class); - $streamableStore->load($this->criteria())->willReturn(new ArrayStream([$message]))->shouldBeCalledOnce(); - - $projectionist = new DefaultProjectionist( - $streamableStore->reveal(), - $projectionStore, - new MetadataProjectorAccessorRepository([$projector]), - ); - - $projectionist->run(); - - self::assertEquals( - [ - new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Error, - 0, - new ProjectionError( - 'ERROR', - ProjectionStatus::Active, - ThrowableToErrorContextTransformer::transform($projector->exception), - ), - ), - ], - $projectionStore->updatedProjections, - ); - } - - public function testRunningMarkOutdated(): void - { - $projectionId = 'test'; - - $projectionStore = new DummyStore([ - new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Active, - ), - ]); - - $streamableStore = $this->prophesize(Store::class); - $streamableStore->load($this->criteria())->shouldNotBeCalled(); - - $projectionist = new DefaultProjectionist( - $streamableStore->reveal(), - $projectionStore, - new MetadataProjectorAccessorRepository([]), - ); - - $projectionist->run(); - - self::assertEquals([ - new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Outdated, - 0, - ), - ], $projectionStore->updatedProjections); - } - - public function testRunningWithoutActiveProjectors(): void - { - $projectionId = 'test'; - - $projectionStore = new DummyStore([ - new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Booting, - ), - ]); - - $streamableStore = $this->prophesize(Store::class); - $streamableStore->load($this->criteria())->shouldNotBeCalled(); - - $projectionist = new DefaultProjectionist( - $streamableStore->reveal(), - $projectionStore, - new MetadataProjectorAccessorRepository([]), - ); - - $projectionist->run(); - - self::assertEquals([], $projectionStore->updatedProjections); - } - - public function testRunningWithGabInIndex(): void - { - $projectionId = 'test'; - $projector = new #[ProjectionAttribute('test')] - class { - /** @var list */ - public array $messages = []; - - #[Subscribe(ProfileVisited::class)] - public function handle(Message $message): void - { - $this->messages[] = $message; - } - }; - - $projectionStore = new DummyStore([ - new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Active, - ), - ]); - - $message1 = new Message(new ProfileVisited(ProfileId::fromString('test'))); - $message2 = new Message(new ProfileVisited(ProfileId::fromString('test'))); - - $streamableStore = $this->prophesize(Store::class); - $streamableStore->load($this->criteria())->willReturn(new ArrayStream([1 => $message1, 3 => $message2]))->shouldBeCalledOnce(); - - $projectionist = new DefaultProjectionist( - $streamableStore->reveal(), - $projectionStore, - new MetadataProjectorAccessorRepository([$projector]), - ); - - $projectionist->run(); - - self::assertEquals([ - new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Active, - 3, - ), - ], $projectionStore->updatedProjections); - - self::assertSame([$message1, $message2], $projector->messages); - } - - public function testTeardownDiscoverNewProjectors(): void - { - $projectionId = 'test'; - $projector = new #[ProjectionAttribute('test')] - class { - }; - - $streamableStore = $this->prophesize(Store::class); - $projectionStore = new DummyStore(); - - $projectionist = new DefaultProjectionist( - $streamableStore->reveal(), - $projectionStore, - new MetadataProjectorAccessorRepository([$projector]), - ); - - $projectionist->teardown(); - - self::assertEquals([ - new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::New, - ), - ], $projectionStore->addedProjections); - } - - public function testTeardownWithoutTeardownMethod(): void - { - $projectionId = 'test'; - $projector = new #[ProjectionAttribute('test')] - class { - }; - - $projection = new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Outdated, - ); - - $projectionStore = new DummyStore([$projection]); - - $streamableStore = $this->prophesize(Store::class); - - $projectionist = new DefaultProjectionist( - $streamableStore->reveal(), - $projectionStore, - new MetadataProjectorAccessorRepository([$projector]), - ); - - $projectionist->teardown(); - - self::assertEquals([], $projectionStore->updatedProjections); - self::assertEquals([$projection], $projectionStore->removedProjections); - } - - public function testTeardownWithProjector(): void - { - $projectionId = 'test'; - $projector = new #[ProjectionAttribute('test')] - class { - public Message|null $message = null; - public bool $dropped = false; - - #[Teardown] - public function drop(): void - { - $this->dropped = true; - } - }; - - $projection = new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Outdated, - ); - - $projectionStore = new DummyStore([$projection]); - - $streamableStore = $this->prophesize(Store::class); - - $projectionist = new DefaultProjectionist( - $streamableStore->reveal(), - $projectionStore, - new MetadataProjectorAccessorRepository([$projector]), - ); - - $projectionist->teardown(); - - self::assertEquals([], $projectionStore->updatedProjections); - self::assertEquals([$projection], $projectionStore->removedProjections); - self::assertTrue($projector->dropped); - } - - public function testTeardownWithProjectorAndError(): void - { - $projectionId = 'test'; - $projector = new #[ProjectionAttribute('test')] - class { - public Message|null $message = null; - public bool $dropped = false; - - #[Teardown] - public function drop(): void - { - throw new RuntimeException('ERROR'); - } - }; - - $projectionStore = new DummyStore([ - new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Outdated, - ), - ]); - - $streamableStore = $this->prophesize(Store::class); - - $projectionist = new DefaultProjectionist( - $streamableStore->reveal(), - $projectionStore, - new MetadataProjectorAccessorRepository([$projector]), - ); - - $projectionist->teardown(); - - self::assertEquals([], $projectionStore->updatedProjections); - self::assertEquals([], $projectionStore->removedProjections); - } - - public function testTeardownWithoutProjector(): void - { - $projectorId = 'test'; - - $projectionStore = new DummyStore([ - new Projection( - $projectorId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Outdated, - ), - ]); - - $streamableStore = $this->prophesize(Store::class); - - $projectionist = new DefaultProjectionist( - $streamableStore->reveal(), - $projectionStore, - new MetadataProjectorAccessorRepository([]), - ); - - $projectionist->teardown(); - - self::assertEquals([], $projectionStore->updatedProjections); - self::assertEquals([], $projectionStore->removedProjections); - } - - public function testRemoveDiscoverNewProjectors(): void - { - $projectionId = 'test'; - $projector = new #[ProjectionAttribute('test')] - class { - }; - - $streamableStore = $this->prophesize(Store::class); - $projectionStore = new DummyStore(); - - $projectionist = new DefaultProjectionist( - $streamableStore->reveal(), - $projectionStore, - new MetadataProjectorAccessorRepository([$projector]), - ); - - $projectionist->remove(); - - self::assertEquals([ - new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::New, - ), - ], $projectionStore->addedProjections); - } - - public function testRemoveWithProjector(): void - { - $projectionId = 'test'; - $projector = new #[ProjectionAttribute('test')] - class { - public bool $dropped = false; - - #[Teardown] - public function drop(): void - { - $this->dropped = true; - } - }; - - $projection = new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Outdated, - ); - $projectionStore = new DummyStore([$projection]); - - $streamableStore = $this->prophesize(Store::class); - - $projectionist = new DefaultProjectionist( - $streamableStore->reveal(), - $projectionStore, - new MetadataProjectorAccessorRepository([$projector]), - ); - - $projectionist->remove(); - - self::assertEquals([], $projectionStore->updatedProjections); - self::assertEquals([$projection], $projectionStore->removedProjections); - self::assertTrue($projector->dropped); - } - - public function testRemoveWithoutDropMethod(): void - { - $projectionId = 'test'; - $projector = new #[ProjectionAttribute('test')] - class { - }; - - $projection = new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Outdated, - ); - $projectionStore = new DummyStore([$projection]); - - $streamableStore = $this->prophesize(Store::class); - - $projectionist = new DefaultProjectionist( - $streamableStore->reveal(), - $projectionStore, - new MetadataProjectorAccessorRepository([$projector]), - ); - - $projectionist->remove(); - - self::assertEquals([], $projectionStore->updatedProjections); - self::assertEquals([$projection], $projectionStore->removedProjections); - } - - public function testRemoveWithProjectorAndError(): void - { - $projectionId = 'test'; - $projector = new #[ProjectionAttribute('test')] - class { - public bool $dropped = false; - - #[Teardown] - public function drop(): void - { - throw new RuntimeException('ERROR'); - } - }; - - $projection = new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Outdated, - ); - $projectionStore = new DummyStore([$projection]); - - $streamableStore = $this->prophesize(Store::class); - - $projectionist = new DefaultProjectionist( - $streamableStore->reveal(), - $projectionStore, - new MetadataProjectorAccessorRepository([$projector]), - ); - - $projectionist->remove(); - - self::assertEquals([], $projectionStore->updatedProjections); - self::assertEquals([$projection], $projectionStore->removedProjections); - } - - public function testRemoveWithoutProjector(): void - { - $projectorId = 'test'; - - $projection = new Projection( - $projectorId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Outdated, - ); - $projectionStore = new DummyStore([$projection]); - - $streamableStore = $this->prophesize(Store::class); - - $projectionist = new DefaultProjectionist( - $streamableStore->reveal(), - $projectionStore, - new MetadataProjectorAccessorRepository([]), - ); - - $projectionist->remove(); - - self::assertEquals([], $projectionStore->updatedProjections); - self::assertEquals([$projection], $projectionStore->removedProjections); - } - - public function testReactiveDiscoverNewProjectors(): void - { - $projectionId = 'test'; - $projector = new #[ProjectionAttribute('test')] - class { - }; - - $streamableStore = $this->prophesize(Store::class); - $projectionStore = new DummyStore(); - - $projectionist = new DefaultProjectionist( - $streamableStore->reveal(), - $projectionStore, - new MetadataProjectorAccessorRepository([$projector]), - ); - - $projectionist->reactivate(); - - self::assertEquals([ - new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::New, - ), - ], $projectionStore->addedProjections); - } - - public function testReactivateError(): void - { - $projectionId = 'test'; - $projector = new #[ProjectionAttribute('test')] - class { - }; - - $projectionStore = new DummyStore([ - new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Error, - 0, - new ProjectionError('ERROR', ProjectionStatus::New), - ), - ]); - - $streamableStore = $this->prophesize(Store::class); - - $projectionist = new DefaultProjectionist( - $streamableStore->reveal(), - $projectionStore, - new MetadataProjectorAccessorRepository([$projector]), - ); - - $projectionist->reactivate(); - - self::assertEquals([ - new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::New, - 0, - ), - ], $projectionStore->updatedProjections); - } - - public function testReactivateOutdated(): void - { - $projectionId = 'test'; - $projector = new #[ProjectionAttribute('test')] - class { - }; - - $projectionStore = new DummyStore([ - new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Outdated, - ), - ]); - - $streamableStore = $this->prophesize(Store::class); - - $projectionist = new DefaultProjectionist( - $streamableStore->reveal(), - $projectionStore, - new MetadataProjectorAccessorRepository([$projector]), - ); - - $projectionist->reactivate(); - - self::assertEquals([ - new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Active, - ), - ], $projectionStore->updatedProjections); - } - - public function testReactivatePaused(): void - { - $projectionId = 'test'; - $projector = new #[ProjectionAttribute('test')] - class { - }; - - $projectionStore = new DummyStore([ - new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Paused, - ), - ]); - - $streamableStore = $this->prophesize(Store::class); - - $projectionist = new DefaultProjectionist( - $streamableStore->reveal(), - $projectionStore, - new MetadataProjectorAccessorRepository([$projector]), - ); - - $projectionist->reactivate(); - - self::assertEquals([ - new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Active, - ), - ], $projectionStore->updatedProjections); - } - - public function testReactivateFinished(): void - { - $projectionId = 'test'; - $projector = new #[ProjectionAttribute('test')] - class { - }; - - $projectionStore = new DummyStore([ - new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Finished, - ), - ]); - - $streamableStore = $this->prophesize(Store::class); - - $projectionist = new DefaultProjectionist( - $streamableStore->reveal(), - $projectionStore, - new MetadataProjectorAccessorRepository([$projector]), - ); - - $projectionist->reactivate(); - - self::assertEquals([ - new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Active, - ), - ], $projectionStore->updatedProjections); - } - - public function testPauseDiscoverNewProjectors(): void - { - $projectionId = 'test'; - $projector = new #[ProjectionAttribute('test')] - class { - }; - - $streamableStore = $this->prophesize(Store::class); - $projectionStore = new DummyStore(); - - $projectionist = new DefaultProjectionist( - $streamableStore->reveal(), - $projectionStore, - new MetadataProjectorAccessorRepository([$projector]), - ); - - $projectionist->pause(); - - self::assertEquals([ - new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::New, - ), - ], $projectionStore->addedProjections); - } - - public function testPauseBooting(): void - { - $projectionId = 'test'; - $projector = new #[ProjectionAttribute('test')] - class { - }; - - $projectionStore = new DummyStore([ - new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Booting, - ), - ]); - - $streamableStore = $this->prophesize(Store::class); - - $projectionist = new DefaultProjectionist( - $streamableStore->reveal(), - $projectionStore, - new MetadataProjectorAccessorRepository([$projector]), - ); - - $projectionist->pause(); - - self::assertEquals([ - new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Paused, - ), - ], $projectionStore->updatedProjections); - } - - public function testPauseActive(): void - { - $projectionId = 'test'; - $projector = new #[ProjectionAttribute('test')] - class { - }; - - $projectionStore = new DummyStore([ - new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Active, - ), - ]); - - $streamableStore = $this->prophesize(Store::class); - - $projectionist = new DefaultProjectionist( - $streamableStore->reveal(), - $projectionStore, - new MetadataProjectorAccessorRepository([$projector]), - ); - - $projectionist->pause(); - - self::assertEquals([ - new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Paused, - ), - ], $projectionStore->updatedProjections); - } - - public function testPauseError(): void - { - $projectionId = 'test'; - $projector = new #[ProjectionAttribute('test')] - class { - }; - - $projectionStore = new DummyStore([ - new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Error, - 0, - new ProjectionError('ERROR', ProjectionStatus::New), - ), - ]); - - $streamableStore = $this->prophesize(Store::class); - - $projectionist = new DefaultProjectionist( - $streamableStore->reveal(), - $projectionStore, - new MetadataProjectorAccessorRepository([$projector]), - ); - - $projectionist->pause(); - - self::assertEquals([ - new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Paused, - 0, - new ProjectionError('ERROR', ProjectionStatus::New), - ), - ], $projectionStore->updatedProjections); - } - - public function testGetProjectionAndDiscoverNewProjectors(): void - { - $projectionId = 'test'; - $projector = new #[ProjectionAttribute('test')] - class { - }; - - $streamableStore = $this->prophesize(Store::class); - $projectionStore = new DummyStore(); - - $projectionist = new DefaultProjectionist( - $streamableStore->reveal(), - $projectionStore, - new MetadataProjectorAccessorRepository([$projector]), - ); - - $projections = $projectionist->projections(); - - self::assertEquals([ - new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::New, - ), - ], $projectionStore->addedProjections); - - self::assertEquals([ - new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::New, - ), - ], $projections); - } - - public function testRetry(): void - { - $projectionId = 'test'; - $projector = new #[ProjectionAttribute('test')] - class { - #[Subscribe(ProfileVisited::class)] - public function subscribe(): void - { - throw new RuntimeException('ERROR2'); - } - }; - - $message = new Message(new ProfileVisited(ProfileId::fromString('test'))); - - $streamableStore = $this->prophesize(Store::class); - $streamableStore->load($this->criteria())->willReturn(new ArrayStream([$message]))->shouldBeCalledOnce(); - - $projection = new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Error, - 0, - new ProjectionError('ERROR', ProjectionStatus::Active), - ); - - $projectionStore = new DummyStore([$projection]); - - $retryStrategy = $this->prophesize(RetryStrategy::class); - $retryStrategy->shouldRetry($projection)->willReturn(true); - - $projectionist = new DefaultProjectionist( - $streamableStore->reveal(), - $projectionStore, - new MetadataProjectorAccessorRepository([$projector]), - $retryStrategy->reveal(), - ); - - $projectionist->run(); - - self::assertCount(2, $projectionStore->updatedProjections); - - [$update1, $update2] = $projectionStore->updatedProjections; - - self::assertEquals($update1, new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Active, - 0, - null, - 1, - )); - - self::assertEquals(ProjectionStatus::Error, $update2->status()); - self::assertEquals(ProjectionStatus::Active, $update2->projectionError()?->previousStatus); - self::assertEquals('ERROR2', $update2->projectionError()?->errorMessage); - self::assertEquals(1, $update2->retryAttempt()); - } - - public function testShouldNotRetry(): void - { - $projectionId = 'test'; - $projector = new #[ProjectionAttribute('test')] - class { - }; - - $streamableStore = $this->prophesize(Store::class); - - $projection = new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Error, - 0, - new ProjectionError('ERROR', ProjectionStatus::Active), - ); - - $projectionStore = new DummyStore([$projection]); - - $retryStrategy = $this->prophesize(RetryStrategy::class); - $retryStrategy->shouldRetry($projection)->willReturn(false); - - $projectionist = new DefaultProjectionist( - $streamableStore->reveal(), - $projectionStore, - new MetadataProjectorAccessorRepository([$projector]), - $retryStrategy->reveal(), - ); - - $projectionist->run(); - - self::assertEquals([], $projectionStore->updatedProjections); - } - - #[DataProvider('methodProvider')] - public function testCriteria(string $method): void - { - $projector = new #[ProjectionAttribute('id1')] - class { - }; - - $projectionStore = $this->prophesize(ProjectionStore::class); - $projectionStore->find( - Argument::that( - static fn (ProjectionCriteria $criteria) => $criteria->ids === ['id1'] && $criteria->groups === ['group1'] - ), - )->willReturn([])->shouldBeCalled(); - - $projectionStore->find( - new ProjectionCriteria(), - )->willReturn([ - new Projection('id1'), - ])->shouldBeCalled(); - - $streamableStore = $this->prophesize(Store::class); - $streamableStore->load($this->criteria())->willReturn(new ArrayStream([])); - - $projectionist = new DefaultProjectionist( - $streamableStore->reveal(), - $projectionStore->reveal(), - new MetadataProjectorAccessorRepository([$projector]), - ); - - $projectionistCriteria = new ProjectionistCriteria( - ids: ['id1'], - groups: ['group1'], - ); - - $projectionist->{$method}($projectionistCriteria); - } - - #[DataProvider('methodProvider')] - public function testWithLockableStore(string $method): void - { - $projector = new #[ProjectionAttribute('id1')] - class { - }; - - $projectionStore = $this->prophesize(LockableProjectionStore::class); - $projectionStore->inLock(Argument::type(Closure::class))->will( - /** @param array{Closure} $args */ - static fn (array $args): mixed => $args[0]() - )->shouldBeCalled(); - $projectionStore->find(Argument::any())->willReturn([])->shouldBeCalled(); - - $projectionStore->find( - new ProjectionCriteria(), - )->willReturn([ - new Projection('id1'), - ])->shouldBeCalled(); - - $projectionStore->remove(Argument::type(Projection::class)); - $projectionStore->add(Argument::type(Projection::class)); - - $streamableStore = $this->prophesize(Store::class); - $streamableStore->load($this->criteria())->willReturn(new ArrayStream([])); - - $projectionist = new DefaultProjectionist( - $streamableStore->reveal(), - $projectionStore->reveal(), - new MetadataProjectorAccessorRepository([$projector]), - ); - - $projectionist->{$method}(); - } - - public static function methodProvider(): Generator - { - yield 'boot' => ['boot']; - yield 'run' => ['run']; - yield 'teardown' => ['teardown']; - yield 'remove' => ['remove']; - yield 'reactivate' => ['reactivate']; - yield 'projections' => ['projections']; - } - - private function criteria(int $fromIndex = 0): Criteria - { - return new Criteria(fromIndex: $fromIndex); - } -} diff --git a/tests/Unit/Projection/Projector/MetadataProjectorAccessorRepositoryTest.php b/tests/Unit/Projection/Projector/MetadataProjectorAccessorRepositoryTest.php deleted file mode 100644 index 48615474d..000000000 --- a/tests/Unit/Projection/Projector/MetadataProjectorAccessorRepositoryTest.php +++ /dev/null @@ -1,45 +0,0 @@ -all()); - self::assertNull($repository->get('foo')); - } - - public function testWithProjector(): void - { - $projector = new #[Projector('foo')] - class { - }; - $metadataFactory = new AttributeProjectorMetadataFactory(); - - $repository = new MetadataProjectorAccessorRepository( - [$projector], - $metadataFactory, - ); - - $accessor = new MetadataProjectorAccessor( - $projector, - $metadataFactory->metadata($projector::class), - ); - - self::assertEquals([$accessor], $repository->all()); - self::assertEquals($accessor, $repository->get('foo')); - } -} diff --git a/tests/Unit/Projection/Projector/MetadataProjectorAccessorTest.php b/tests/Unit/Projection/Projector/MetadataProjectorAccessorTest.php deleted file mode 100644 index 9b5d801e4..000000000 --- a/tests/Unit/Projection/Projector/MetadataProjectorAccessorTest.php +++ /dev/null @@ -1,206 +0,0 @@ -metadata($projector::class), - ); - - self::assertEquals('profile', $accessor->id()); - } - - public function testGroup(): void - { - $projector = new #[Projector('profile')] - class { - }; - - $accessor = new MetadataProjectorAccessor( - $projector, - (new AttributeProjectorMetadataFactory())->metadata($projector::class), - ); - - self::assertEquals('default', $accessor->group()); - } - - public function testRunMode(): void - { - $projector = new #[Projector('profile')] - class { - }; - - $accessor = new MetadataProjectorAccessor( - $projector, - (new AttributeProjectorMetadataFactory())->metadata($projector::class), - ); - - self::assertEquals(RunMode::FromBeginning, $accessor->runMode()); - } - - public function testSubscribeMethod(): void - { - $projector = new #[Projector('profile')] - class { - #[Subscribe(ProfileCreated::class)] - public function onProfileCreated(Message $message): void - { - } - }; - - $accessor = new MetadataProjectorAccessor( - $projector, - (new AttributeProjectorMetadataFactory())->metadata($projector::class), - ); - - $result = $accessor->subscribeMethods(ProfileCreated::class); - - self::assertEquals([ - $projector->onProfileCreated(...), - ], $result); - } - - public function testMultipleSubscribeMethod(): void - { - $projector = new #[Projector('profile')] - class { - #[Subscribe(ProfileCreated::class)] - public function onProfileCreated(Message $message): void - { - } - - #[Subscribe(ProfileCreated::class)] - public function onFoo(Message $message): void - { - } - }; - - $accessor = new MetadataProjectorAccessor( - $projector, - (new AttributeProjectorMetadataFactory())->metadata($projector::class), - ); - - $result = $accessor->subscribeMethods(ProfileCreated::class); - - self::assertEquals([ - $projector->onProfileCreated(...), - $projector->onFoo(...), - ], $result); - } - - public function testSubscribeAllMethod(): void - { - $projector = new #[Projector('profile')] - class { - #[Subscribe('*')] - public function onProfileCreated(Message $message): void - { - } - }; - - $accessor = new MetadataProjectorAccessor( - $projector, - (new AttributeProjectorMetadataFactory())->metadata($projector::class), - ); - - $result = $accessor->subscribeMethods(ProfileCreated::class); - - self::assertEquals([ - $projector->onProfileCreated(...), - ], $result); - } - - public function testSetupMethod(): void - { - $projector = new #[Projector('profile')] - class { - #[Setup] - public function method(): void - { - } - }; - - $accessor = new MetadataProjectorAccessor( - $projector, - (new AttributeProjectorMetadataFactory())->metadata($projector::class), - ); - - $result = $accessor->setupMethod(); - - self::assertEquals($projector->method(...), $result); - } - - public function testNotSetupMethod(): void - { - $projector = new #[Projector('profile')] - class { - }; - - $accessor = new MetadataProjectorAccessor( - $projector, - (new AttributeProjectorMetadataFactory())->metadata($projector::class), - ); - - $result = $accessor->setupMethod(); - - self::assertNull($result); - } - - public function testTeardownMethod(): void - { - $projector = new #[Projector('profile')] - class { - #[Teardown] - public function method(): void - { - } - }; - - $accessor = new MetadataProjectorAccessor( - $projector, - (new AttributeProjectorMetadataFactory())->metadata($projector::class), - ); - - $result = $accessor->teardownMethod(); - - self::assertEquals($projector->method(...), $result); - } - - public function testNotTeardownMethod(): void - { - $projector = new #[Projector('profile')] - class { - }; - - $accessor = new MetadataProjectorAccessor( - $projector, - (new AttributeProjectorMetadataFactory())->metadata($projector::class), - ); - - $result = $accessor->teardownMethod(); - - self::assertNull($result); - } -} diff --git a/tests/Unit/Projection/Projector/ProjectorHelperTest.php b/tests/Unit/Projection/Projector/ProjectorHelperTest.php deleted file mode 100644 index 738171c24..000000000 --- a/tests/Unit/Projection/Projector/ProjectorHelperTest.php +++ /dev/null @@ -1,24 +0,0 @@ -projectorId($projector)); - } -} diff --git a/tests/Unit/Projection/RetryStrategy/NoRetryStrategyTest.php b/tests/Unit/Projection/RetryStrategy/NoRetryStrategyTest.php deleted file mode 100644 index dc76fabe8..000000000 --- a/tests/Unit/Projection/RetryStrategy/NoRetryStrategyTest.php +++ /dev/null @@ -1,22 +0,0 @@ -shouldRetry( - new Projection('test'), - )); - } -} diff --git a/tests/Unit/Subscription/DummySubscriptionStore.php b/tests/Unit/Subscription/DummySubscriptionStore.php new file mode 100644 index 000000000..48c90995d --- /dev/null +++ b/tests/Unit/Subscription/DummySubscriptionStore.php @@ -0,0 +1,59 @@ + */ + public array $addedSubscriptions = []; + + /** @var list */ + public array $updatedSubscriptions = []; + + /** @var list */ + public array $removedSubscriptions = []; + + /** @param list $subscriptions */ + public function __construct(array $subscriptions = []) + { + $this->parentStore = new InMemorySubscriptionStore($subscriptions); + } + + public function get(string $subscriptionId): Subscription + { + return $this->parentStore->get($subscriptionId); + } + + /** @return list */ + public function find(SubscriptionCriteria|null $criteria = null): array + { + return $this->parentStore->find($criteria); + } + + public function add(Subscription $subscription): void + { + $this->parentStore->add($subscription); + $this->addedSubscriptions[] = clone $subscription; + } + + public function update(Subscription $subscription): void + { + $this->parentStore->update($subscription); + $this->updatedSubscriptions[] = clone $subscription; + } + + public function remove(Subscription $subscription): void + { + $this->parentStore->remove($subscription); + $this->removedSubscriptions[] = clone $subscription; + } +} diff --git a/tests/Unit/Subscription/Engine/DefaultSubscriptionEngineTest.php b/tests/Unit/Subscription/Engine/DefaultSubscriptionEngineTest.php new file mode 100644 index 000000000..1ac0a5474 --- /dev/null +++ b/tests/Unit/Subscription/Engine/DefaultSubscriptionEngineTest.php @@ -0,0 +1,1873 @@ +prophesize(Store::class); + $streamableStore->load($this->criteria())->shouldNotBeCalled(); + + $store = new DummySubscriptionStore(); + + $engine = new DefaultSubscriptionEngine( + $streamableStore->reveal(), + $store, + new MetadataSubscriberAccessorRepository([]), + ); + + $engine->boot(); + + self::assertEquals([], $store->addedSubscriptions); + self::assertEquals([], $store->updatedSubscriptions); + } + + public function testBootDiscoverNewSubscribers(): void + { + $subscriptionId = 'test'; + $subscriber = new #[Subscriber('test')] + class { + }; + + $streamableStore = $this->prophesize(Store::class); + $streamableStore->load($this->criteria())->willReturn(new ArrayStream([]))->shouldBeCalledOnce(); + + $subscriptionStore = new DummySubscriptionStore(); + + $engine = new DefaultSubscriptionEngine( + $streamableStore->reveal(), + $subscriptionStore, + new MetadataSubscriberAccessorRepository([$subscriber]), + ); + + $engine->boot(); + + self::assertEquals([ + new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::New, + ), + ], $subscriptionStore->addedSubscriptions); + + self::assertEquals([ + new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::Booting, + ), + new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::Active, + ), + ], $subscriptionStore->updatedSubscriptions); + } + + public function testBootWithoutCreateMethod(): void + { + $subscriptionId = 'test'; + $subscriber = new #[Subscriber('test')] + class { + }; + + $subscriptionStore = new DummySubscriptionStore([ + new Subscription($subscriptionId), + ]); + + $message = new Message(new ProfileVisited(ProfileId::fromString('test'))); + + $streamableStore = $this->prophesize(Store::class); + $streamableStore->load($this->criteria())->willReturn(new ArrayStream([$message]))->shouldBeCalledOnce(); + + $engine = new DefaultSubscriptionEngine( + $streamableStore->reveal(), + $subscriptionStore, + new MetadataSubscriberAccessorRepository([$subscriber]), + ); + + $engine->boot(); + + self::assertEquals([ + new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::Booting, + ), + new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::Booting, + 1, + ), + new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::Active, + 1, + ), + ], $subscriptionStore->updatedSubscriptions); + } + + public function testBootWithMethods(): void + { + $subscriptionId = 'test'; + $subscriber = new #[Subscriber('test')] + class { + public Message|null $message = null; + public bool $created = false; + + #[Setup] + public function create(): void + { + $this->created = true; + } + + #[Subscribe(ProfileVisited::class)] + public function handle(Message $message): void + { + $this->message = $message; + } + }; + + $subscriptionStore = new DummySubscriptionStore(); + + $message = new Message(new ProfileVisited(ProfileId::fromString('test'))); + + $streamableStore = $this->prophesize(Store::class); + $streamableStore->load($this->criteria())->willReturn(new ArrayStream([$message]))->shouldBeCalledOnce(); + + $engine = new DefaultSubscriptionEngine( + $streamableStore->reveal(), + $subscriptionStore, + new MetadataSubscriberAccessorRepository([$subscriber]), + ); + + $engine->boot(); + + self::assertEquals([ + new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::New, + ), + ], $subscriptionStore->addedSubscriptions); + + self::assertEquals([ + new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::Booting, + ), + new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::Booting, + 1, + ), + new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::Active, + 1, + ), + ], $subscriptionStore->updatedSubscriptions); + + self::assertTrue($subscriber->created); + self::assertSame($message, $subscriber->message); + } + + public function testBootWithLimit(): void + { + $subscriptionId = 'test'; + $subscriber = new #[Subscriber('test')] + class { + public Message|null $message = null; + public bool $created = false; + + #[Setup] + public function create(): void + { + $this->created = true; + } + + #[Subscribe(ProfileVisited::class)] + public function handle(Message $message): void + { + $this->message = $message; + } + }; + + $subscriptionStore = new DummySubscriptionStore(); + + $message = new Message(new ProfileVisited(ProfileId::fromString('test'))); + + $streamableStore = $this->prophesize(Store::class); + $streamableStore->load($this->criteria())->willReturn(new ArrayStream([$message]))->shouldBeCalledOnce(); + + $engine = new DefaultSubscriptionEngine( + $streamableStore->reveal(), + $subscriptionStore, + new MetadataSubscriberAccessorRepository([$subscriber]), + ); + + $engine->boot(new SubscriptionEngineCriteria(), 1); + + self::assertEquals([ + new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::New, + ), + ], $subscriptionStore->addedSubscriptions); + + self::assertEquals([ + new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::Booting, + ), + new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::Booting, + 1, + ), + ], $subscriptionStore->updatedSubscriptions); + + self::assertTrue($subscriber->created); + self::assertSame($message, $subscriber->message); + } + + public function testBootingWithSkip(): void + { + $subscriptionId1 = 'test1'; + $subscriber1 = new #[Subscriber('test1')] + class { + public Message|null $message = null; + + #[Subscribe(ProfileVisited::class)] + public function handle(Message $message): void + { + $this->message = $message; + } + }; + + $subscriptionId2 = 'test2'; + $subscriber2 = new #[Subscriber('test2')] + class { + public Message|null $message = null; + + #[Subscribe(ProfileVisited::class)] + public function handle(Message $message): void + { + $this->message = $message; + } + }; + + $subscriptionStore = new DummySubscriptionStore([ + new Subscription( + $subscriptionId1, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::Booting, + ), + new Subscription( + $subscriptionId2, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::Booting, + 1, + ), + ]); + + $message = new Message(new ProfileVisited(ProfileId::fromString('test'))); + + $streamableStore = $this->prophesize(Store::class); + $streamableStore->load($this->criteria())->willReturn(new ArrayStream([$message]))->shouldBeCalledOnce(); + + $engine = new DefaultSubscriptionEngine( + $streamableStore->reveal(), + $subscriptionStore, + new MetadataSubscriberAccessorRepository([$subscriber1, $subscriber2]), + ); + + $engine->boot(); + + self::assertEquals([ + new Subscription( + $subscriptionId1, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::Booting, + 1, + ), + new Subscription( + $subscriptionId2, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::Booting, + 1, + ), + new Subscription( + $subscriptionId1, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::Active, + 1, + ), + new Subscription( + $subscriptionId2, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::Active, + 1, + ), + ], $subscriptionStore->updatedSubscriptions); + + self::assertSame($message, $subscriber1->message); + self::assertNull($subscriber2->message); + } + + public function testBootWithCreateError(): void + { + $subscriptionId = 'test'; + $subscriber = new #[Subscriber('test')] + class { + public function __construct( + public readonly RuntimeException $exception = new RuntimeException('ERROR'), + ) { + } + + #[Setup] + public function create(): void + { + throw $this->exception; + } + }; + + $subscriptionStore = new DummySubscriptionStore([ + new Subscription($subscriptionId), + ]); + + $streamableStore = $this->prophesize(Store::class); + $streamableStore->load($this->criteria())->shouldNotBeCalled(); + + $engine = new DefaultSubscriptionEngine( + $streamableStore->reveal(), + $subscriptionStore, + new MetadataSubscriberAccessorRepository([$subscriber]), + ); + + $engine->boot(); + + self::assertEquals( + [ + new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::Error, + 0, + new SubscriptionError( + 'ERROR', + Status::New, + ThrowableToErrorContextTransformer::transform($subscriber->exception), + ), + ), + ], + $subscriptionStore->updatedSubscriptions, + ); + } + + public function testBootingWithGabInIndex(): void + { + $subscriptionId = 'test'; + $subscriber = new #[Subscriber('test')] + class { + /** @var list */ + public array $messages = []; + + #[Subscribe(ProfileVisited::class)] + public function handle(Message $message): void + { + $this->messages[] = $message; + } + }; + + $subscriptionStore = new DummySubscriptionStore([ + new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::Booting, + ), + ]); + + $message1 = new Message(new ProfileVisited(ProfileId::fromString('test'))); + $message2 = new Message(new ProfileVisited(ProfileId::fromString('test'))); + + $streamableStore = $this->prophesize(Store::class); + $streamableStore->load($this->criteria())->willReturn(new ArrayStream([1 => $message1, 3 => $message2]))->shouldBeCalledOnce(); + + $engine = new DefaultSubscriptionEngine( + $streamableStore->reveal(), + $subscriptionStore, + new MetadataSubscriberAccessorRepository([$subscriber]), + ); + + $engine->boot(); + + self::assertEquals([ + new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::Booting, + 3, + ), + new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::Active, + 3, + ), + ], $subscriptionStore->updatedSubscriptions); + + self::assertSame([$message1, $message2], $subscriber->messages); + } + + public function testBootingWithFromNow(): void + { + $subscriptionId = 'test'; + $subscriber = new #[Subscriber('test', runMode: RunMode::FromNow)] + class { + public Message|null $message = null; + + #[Subscribe(ProfileVisited::class)] + public function handle(Message $message): void + { + $this->message = $message; + } + }; + + $subscriptionStore = new DummySubscriptionStore([ + new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromNow, + Status::Booting, + ), + ]); + + $message1 = new Message(new ProfileVisited(ProfileId::fromString('test'))); + + $streamableStore = $this->prophesize(Store::class); + $streamableStore->load(null, 1, null, true)->willReturn(new ArrayStream([$message1]))->shouldBeCalledOnce(); + + $engine = new DefaultSubscriptionEngine( + $streamableStore->reveal(), + $subscriptionStore, + new MetadataSubscriberAccessorRepository([$subscriber]), + ); + + $engine->boot(); + + self::assertEquals([ + new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromNow, + Status::Active, + 1, + ), + ], $subscriptionStore->updatedSubscriptions); + + self::assertNull($subscriber->message); + } + + public function testBootingWithFromNowWithEmtpyStream(): void + { + $subscriptionId = 'test'; + $subscriber = new #[Subscriber('test', runMode: RunMode::FromNow)] + class { + public Message|null $message = null; + + #[Subscribe(ProfileVisited::class)] + public function handle(Message $message): void + { + $this->message = $message; + } + }; + + $subscriptionStore = new DummySubscriptionStore([ + new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromNow, + Status::Booting, + ), + ]); + + $message1 = new Message(new ProfileVisited(ProfileId::fromString('test'))); + + $streamableStore = $this->prophesize(Store::class); + $streamableStore->load(null, 1, null, true)->willReturn(new ArrayStream([]))->shouldBeCalledOnce(); + + $engine = new DefaultSubscriptionEngine( + $streamableStore->reveal(), + $subscriptionStore, + new MetadataSubscriberAccessorRepository([$subscriber]), + ); + + $engine->boot(); + + self::assertEquals([ + new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromNow, + Status::Active, + 0, + ), + ], $subscriptionStore->updatedSubscriptions); + + self::assertNull($subscriber->message); + } + + public function testBootingWithOnlyOnce(): void + { + $subscriptionId = 'test'; + $subscriber = new #[Subscriber('test', runMode: RunMode::Once)] + class { + public Message|null $message = null; + + #[Subscribe(ProfileVisited::class)] + public function handle(Message $message): void + { + $this->message = $message; + } + }; + + $subscriptionStore = new DummySubscriptionStore([ + new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::Once, + Status::Booting, + ), + ]); + + $message1 = new Message(new ProfileVisited(ProfileId::fromString('test'))); + + $streamableStore = $this->prophesize(Store::class); + $streamableStore->load($this->criteria())->willReturn(new ArrayStream([$message1]))->shouldBeCalledOnce(); + + $engine = new DefaultSubscriptionEngine( + $streamableStore->reveal(), + $subscriptionStore, + new MetadataSubscriberAccessorRepository([$subscriber]), + ); + + $engine->boot(); + + self::assertEquals([ + new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::Once, + Status::Booting, + 1, + ), + new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::Once, + Status::Finished, + 1, + ), + ], $subscriptionStore->updatedSubscriptions); + + self::assertEquals($message1, $subscriber->message); + } + + public function testRunDiscoverNewSubscribers(): void + { + $subscriptionId = 'test'; + $subscriber = new #[Subscriber('test')] + class { + }; + + $streamableStore = $this->prophesize(Store::class); + $subscriptionStore = new DummySubscriptionStore(); + + $engine = new DefaultSubscriptionEngine( + $streamableStore->reveal(), + $subscriptionStore, + new MetadataSubscriberAccessorRepository([$subscriber]), + ); + + $engine->run(); + + self::assertEquals([ + new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::New, + ), + ], $subscriptionStore->addedSubscriptions); + } + + public function testRunning(): void + { + $subscriptionId = 'test'; + $subscriber = new #[Subscriber('test')] + class { + public Message|null $message = null; + + #[Subscribe(ProfileVisited::class)] + public function handle(Message $message): void + { + $this->message = $message; + } + }; + + $subscriptionStore = new DummySubscriptionStore([ + new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::Active, + ), + ]); + + $message = new Message(new ProfileVisited(ProfileId::fromString('test'))); + + $streamableStore = $this->prophesize(Store::class); + $streamableStore->load($this->criteria())->willReturn(new ArrayStream([$message]))->shouldBeCalledOnce(); + + $engine = new DefaultSubscriptionEngine( + $streamableStore->reveal(), + $subscriptionStore, + new MetadataSubscriberAccessorRepository([$subscriber]), + ); + + $engine->run(); + + self::assertEquals([ + new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::Active, + 1, + ), + ], $subscriptionStore->updatedSubscriptions); + + self::assertSame($message, $subscriber->message); + } + + public function testRunningWithLimit(): void + { + $subscriptionId = 'test'; + $subscriber = new #[Subscriber('test')] + class { + public Message|null $message = null; + + #[Subscribe(ProfileVisited::class)] + public function handle(Message $message): void + { + $this->message = $message; + } + }; + + $subscriptionStore = new DummySubscriptionStore([ + new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::Active, + ), + ]); + + $message1 = new Message(new ProfileVisited(ProfileId::fromString('test'))); + $message2 = new Message(new ProfileVisited(ProfileId::fromString('test'))); + + $streamableStore = $this->prophesize(Store::class); + $streamableStore + ->load($this->criteria()) + ->willReturn(new ArrayStream([$message1, $message2])) + ->shouldBeCalledOnce(); + + $engine = new DefaultSubscriptionEngine( + $streamableStore->reveal(), + $subscriptionStore, + new MetadataSubscriberAccessorRepository([$subscriber]), + ); + + $engine->run(new SubscriptionEngineCriteria(), 1); + + self::assertEquals([ + new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::Active, + 1, + ), + ], $subscriptionStore->updatedSubscriptions); + + self::assertSame($message1, $subscriber->message); + } + + public function testRunningWithSkip(): void + { + $subscriptionId1 = 'test1'; + $subscriber1 = new #[Subscriber('test1')] + class { + public Message|null $message = null; + + #[Subscribe(ProfileVisited::class)] + public function handle(Message $message): void + { + $this->message = $message; + } + }; + + $subscriptionId2 = 'test2'; + $subscriber2 = new #[Subscriber('test2')] + class { + public Message|null $message = null; + + #[Subscribe(ProfileVisited::class)] + public function handle(Message $message): void + { + $this->message = $message; + } + }; + + $subscriptionStore = new DummySubscriptionStore([ + new Subscription( + $subscriptionId1, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::Active, + ), + new Subscription( + $subscriptionId2, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::Active, + 1, + ), + ]); + + $message = new Message(new ProfileVisited(ProfileId::fromString('test'))); + + $streamableStore = $this->prophesize(Store::class); + $streamableStore->load($this->criteria())->willReturn(new ArrayStream([$message]))->shouldBeCalledOnce(); + + $engine = new DefaultSubscriptionEngine( + $streamableStore->reveal(), + $subscriptionStore, + new MetadataSubscriberAccessorRepository([$subscriber1, $subscriber2]), + ); + + $engine->run(); + + self::assertEquals([ + new Subscription( + $subscriptionId1, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::Active, + 1, + ), + new Subscription( + $subscriptionId2, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::Active, + 1, + ), + ], $subscriptionStore->updatedSubscriptions); + + self::assertSame($message, $subscriber1->message); + self::assertNull($subscriber2->message); + } + + public function testRunningWithError(): void + { + $subscriptionId = 'test'; + $subscriber = new #[Subscriber('test')] + class { + public function __construct( + public readonly RuntimeException $exception = new RuntimeException('ERROR'), + ) { + } + + #[Subscribe(ProfileVisited::class)] + public function handle(Message $message): void + { + throw $this->exception; + } + }; + + $subscriptionStore = new DummySubscriptionStore([ + new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::Active, + ), + ]); + + $message = new Message(new ProfileVisited(ProfileId::fromString('test'))); + + $streamableStore = $this->prophesize(Store::class); + $streamableStore->load($this->criteria())->willReturn(new ArrayStream([$message]))->shouldBeCalledOnce(); + + $engine = new DefaultSubscriptionEngine( + $streamableStore->reveal(), + $subscriptionStore, + new MetadataSubscriberAccessorRepository([$subscriber]), + ); + + $engine->run(); + + self::assertEquals( + [ + new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::Error, + 0, + new SubscriptionError( + 'ERROR', + Status::Active, + ThrowableToErrorContextTransformer::transform($subscriber->exception), + ), + ), + ], + $subscriptionStore->updatedSubscriptions, + ); + } + + public function testRunningMarkOutdated(): void + { + $subscriptionId = 'test'; + + $subscriptionStore = new DummySubscriptionStore([ + new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::Active, + ), + ]); + + $streamableStore = $this->prophesize(Store::class); + $streamableStore->load($this->criteria())->shouldNotBeCalled(); + + $engine = new DefaultSubscriptionEngine( + $streamableStore->reveal(), + $subscriptionStore, + new MetadataSubscriberAccessorRepository([]), + ); + + $engine->run(); + + self::assertEquals([ + new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::Outdated, + 0, + ), + ], $subscriptionStore->updatedSubscriptions); + } + + public function testRunningWithoutActiveSubscribers(): void + { + $subscriptionId = 'test'; + + $subscriptionStore = new DummySubscriptionStore([ + new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::Booting, + ), + ]); + + $streamableStore = $this->prophesize(Store::class); + $streamableStore->load($this->criteria())->shouldNotBeCalled(); + + $engine = new DefaultSubscriptionEngine( + $streamableStore->reveal(), + $subscriptionStore, + new MetadataSubscriberAccessorRepository([]), + ); + + $engine->run(); + + self::assertEquals([], $subscriptionStore->updatedSubscriptions); + } + + public function testRunningWithGabInIndex(): void + { + $subscriptionId = 'test'; + $subscriber = new #[Subscriber('test')] + class { + /** @var list */ + public array $messages = []; + + #[Subscribe(ProfileVisited::class)] + public function handle(Message $message): void + { + $this->messages[] = $message; + } + }; + + $subscriptionStore = new DummySubscriptionStore([ + new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::Active, + ), + ]); + + $message1 = new Message(new ProfileVisited(ProfileId::fromString('test'))); + $message2 = new Message(new ProfileVisited(ProfileId::fromString('test'))); + + $streamableStore = $this->prophesize(Store::class); + $streamableStore->load($this->criteria())->willReturn(new ArrayStream([1 => $message1, 3 => $message2]))->shouldBeCalledOnce(); + + $engine = new DefaultSubscriptionEngine( + $streamableStore->reveal(), + $subscriptionStore, + new MetadataSubscriberAccessorRepository([$subscriber]), + ); + + $engine->run(); + + self::assertEquals([ + new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::Active, + 3, + ), + ], $subscriptionStore->updatedSubscriptions); + + self::assertSame([$message1, $message2], $subscriber->messages); + } + + public function testTeardownDiscoverNewSubscribers(): void + { + $subscriptionId = 'test'; + $subscriber = new #[Subscriber('test')] + class { + }; + + $streamableStore = $this->prophesize(Store::class); + $subscriptionStore = new DummySubscriptionStore(); + + $engine = new DefaultSubscriptionEngine( + $streamableStore->reveal(), + $subscriptionStore, + new MetadataSubscriberAccessorRepository([$subscriber]), + ); + + $engine->teardown(); + + self::assertEquals([ + new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::New, + ), + ], $subscriptionStore->addedSubscriptions); + } + + public function testTeardownWithoutTeardownMethod(): void + { + $subscriptionId = 'test'; + $subscriber = new #[Subscriber('test')] + class { + }; + + $subscription = new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::Outdated, + ); + + $subscriptionStore = new DummySubscriptionStore([$subscription]); + + $streamableStore = $this->prophesize(Store::class); + + $engine = new DefaultSubscriptionEngine( + $streamableStore->reveal(), + $subscriptionStore, + new MetadataSubscriberAccessorRepository([$subscriber]), + ); + + $engine->teardown(); + + self::assertEquals([], $subscriptionStore->updatedSubscriptions); + self::assertEquals([$subscription], $subscriptionStore->removedSubscriptions); + } + + public function testTeardownWithSubscriber(): void + { + $subscriptionId = 'test'; + $subscriber = new #[Subscriber('test')] + class { + public Message|null $message = null; + public bool $dropped = false; + + #[Teardown] + public function drop(): void + { + $this->dropped = true; + } + }; + + $subscription = new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::Outdated, + ); + + $subscriptionStore = new DummySubscriptionStore([$subscription]); + + $streamableStore = $this->prophesize(Store::class); + + $engine = new DefaultSubscriptionEngine( + $streamableStore->reveal(), + $subscriptionStore, + new MetadataSubscriberAccessorRepository([$subscriber]), + ); + + $engine->teardown(); + + self::assertEquals([], $subscriptionStore->updatedSubscriptions); + self::assertEquals([$subscription], $subscriptionStore->removedSubscriptions); + self::assertTrue($subscriber->dropped); + } + + public function testTeardownWithSubscriberAndError(): void + { + $subscriptionId = 'test'; + $subscriber = new #[Subscriber('test')] + class { + public Message|null $message = null; + public bool $dropped = false; + + #[Teardown] + public function drop(): void + { + throw new RuntimeException('ERROR'); + } + }; + + $subscriptionStore = new DummySubscriptionStore([ + new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::Outdated, + ), + ]); + + $streamableStore = $this->prophesize(Store::class); + + $engine = new DefaultSubscriptionEngine( + $streamableStore->reveal(), + $subscriptionStore, + new MetadataSubscriberAccessorRepository([$subscriber]), + ); + + $engine->teardown(); + + self::assertEquals([], $subscriptionStore->updatedSubscriptions); + self::assertEquals([], $subscriptionStore->removedSubscriptions); + } + + public function testTeardownWithoutSubscriber(): void + { + $subscriberId = 'test'; + + $subscriptionStore = new DummySubscriptionStore([ + new Subscription( + $subscriberId, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::Outdated, + ), + ]); + + $streamableStore = $this->prophesize(Store::class); + + $engine = new DefaultSubscriptionEngine( + $streamableStore->reveal(), + $subscriptionStore, + new MetadataSubscriberAccessorRepository([]), + ); + + $engine->teardown(); + + self::assertEquals([], $subscriptionStore->updatedSubscriptions); + self::assertEquals([], $subscriptionStore->removedSubscriptions); + } + + public function testRemoveDiscoverNewSubscribers(): void + { + $subscriptionId = 'test'; + $subscriber = new #[Subscriber('test')] + class { + }; + + $streamableStore = $this->prophesize(Store::class); + $subscriptionStore = new DummySubscriptionStore(); + + $engine = new DefaultSubscriptionEngine( + $streamableStore->reveal(), + $subscriptionStore, + new MetadataSubscriberAccessorRepository([$subscriber]), + ); + + $engine->remove(); + + self::assertEquals([ + new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::New, + ), + ], $subscriptionStore->addedSubscriptions); + } + + public function testRemoveWithSubscriber(): void + { + $subscriptionId = 'test'; + $subscriber = new #[Subscriber('test')] + class { + public bool $dropped = false; + + #[Teardown] + public function drop(): void + { + $this->dropped = true; + } + }; + + $subscription = new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::Outdated, + ); + $subscriptionStore = new DummySubscriptionStore([$subscription]); + + $streamableStore = $this->prophesize(Store::class); + + $engine = new DefaultSubscriptionEngine( + $streamableStore->reveal(), + $subscriptionStore, + new MetadataSubscriberAccessorRepository([$subscriber]), + ); + + $engine->remove(); + + self::assertEquals([], $subscriptionStore->updatedSubscriptions); + self::assertEquals([$subscription], $subscriptionStore->removedSubscriptions); + self::assertTrue($subscriber->dropped); + } + + public function testRemoveWithoutDropMethod(): void + { + $subscriptionId = 'test'; + $subscriber = new #[Subscriber('test')] + class { + }; + + $subscription = new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::Outdated, + ); + $subscriptionStore = new DummySubscriptionStore([$subscription]); + + $streamableStore = $this->prophesize(Store::class); + + $engine = new DefaultSubscriptionEngine( + $streamableStore->reveal(), + $subscriptionStore, + new MetadataSubscriberAccessorRepository([$subscriber]), + ); + + $engine->remove(); + + self::assertEquals([], $subscriptionStore->updatedSubscriptions); + self::assertEquals([$subscription], $subscriptionStore->removedSubscriptions); + } + + public function testRemoveWithSubscriberAndError(): void + { + $subscriptionId = 'test'; + $subscriber = new #[Subscriber('test')] + class { + public bool $dropped = false; + + #[Teardown] + public function drop(): void + { + throw new RuntimeException('ERROR'); + } + }; + + $subscription = new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::Outdated, + ); + $subscriptionStore = new DummySubscriptionStore([$subscription]); + + $streamableStore = $this->prophesize(Store::class); + + $engine = new DefaultSubscriptionEngine( + $streamableStore->reveal(), + $subscriptionStore, + new MetadataSubscriberAccessorRepository([$subscriber]), + ); + + $engine->remove(); + + self::assertEquals([], $subscriptionStore->updatedSubscriptions); + self::assertEquals([$subscription], $subscriptionStore->removedSubscriptions); + } + + public function testRemoveWithoutSubscriber(): void + { + $subscriberId = 'test'; + + $subscription = new Subscription( + $subscriberId, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::Outdated, + ); + $subscriptionStore = new DummySubscriptionStore([$subscription]); + + $streamableStore = $this->prophesize(Store::class); + + $engine = new DefaultSubscriptionEngine( + $streamableStore->reveal(), + $subscriptionStore, + new MetadataSubscriberAccessorRepository([]), + ); + + $engine->remove(); + + self::assertEquals([], $subscriptionStore->updatedSubscriptions); + self::assertEquals([$subscription], $subscriptionStore->removedSubscriptions); + } + + public function testReactiveDiscoverNewSubscribers(): void + { + $subscriptionId = 'test'; + $subscriber = new #[Subscriber('test')] + class { + }; + + $streamableStore = $this->prophesize(Store::class); + $subscriptionStore = new DummySubscriptionStore(); + + $engine = new DefaultSubscriptionEngine( + $streamableStore->reveal(), + $subscriptionStore, + new MetadataSubscriberAccessorRepository([$subscriber]), + ); + + $engine->reactivate(); + + self::assertEquals([ + new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::New, + ), + ], $subscriptionStore->addedSubscriptions); + } + + public function testReactivateError(): void + { + $subscriptionId = 'test'; + $subscriber = new #[Subscriber('test')] + class { + }; + + $subscriptionStore = new DummySubscriptionStore([ + new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::Error, + 0, + new SubscriptionError('ERROR', Status::New), + ), + ]); + + $streamableStore = $this->prophesize(Store::class); + + $engine = new DefaultSubscriptionEngine( + $streamableStore->reveal(), + $subscriptionStore, + new MetadataSubscriberAccessorRepository([$subscriber]), + ); + + $engine->reactivate(); + + self::assertEquals([ + new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::New, + 0, + ), + ], $subscriptionStore->updatedSubscriptions); + } + + public function testReactivateOutdated(): void + { + $subscriptionId = 'test'; + $subscriber = new #[Subscriber('test')] + class { + }; + + $subscriptionStore = new DummySubscriptionStore([ + new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::Outdated, + ), + ]); + + $streamableStore = $this->prophesize(Store::class); + + $engine = new DefaultSubscriptionEngine( + $streamableStore->reveal(), + $subscriptionStore, + new MetadataSubscriberAccessorRepository([$subscriber]), + ); + + $engine->reactivate(); + + self::assertEquals([ + new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::Active, + ), + ], $subscriptionStore->updatedSubscriptions); + } + + public function testReactivatePaused(): void + { + $subscriptionId = 'test'; + $subscriber = new #[Subscriber('test')] + class { + }; + + $subscriptionStore = new DummySubscriptionStore([ + new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::Paused, + ), + ]); + + $streamableStore = $this->prophesize(Store::class); + + $engine = new DefaultSubscriptionEngine( + $streamableStore->reveal(), + $subscriptionStore, + new MetadataSubscriberAccessorRepository([$subscriber]), + ); + + $engine->reactivate(); + + self::assertEquals([ + new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::Active, + ), + ], $subscriptionStore->updatedSubscriptions); + } + + public function testReactivateFinished(): void + { + $subscriptionId = 'test'; + $subscriber = new #[Subscriber('test')] + class { + }; + + $subscriptionStore = new DummySubscriptionStore([ + new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::Finished, + ), + ]); + + $streamableStore = $this->prophesize(Store::class); + + $engine = new DefaultSubscriptionEngine( + $streamableStore->reveal(), + $subscriptionStore, + new MetadataSubscriberAccessorRepository([$subscriber]), + ); + + $engine->reactivate(); + + self::assertEquals([ + new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::Active, + ), + ], $subscriptionStore->updatedSubscriptions); + } + + public function testPauseDiscoverNewSubscribers(): void + { + $subscriptionId = 'test'; + $subscriber = new #[Subscriber('test')] + class { + }; + + $streamableStore = $this->prophesize(Store::class); + $subscriptionStore = new DummySubscriptionStore(); + + $engine = new DefaultSubscriptionEngine( + $streamableStore->reveal(), + $subscriptionStore, + new MetadataSubscriberAccessorRepository([$subscriber]), + ); + + $engine->pause(); + + self::assertEquals([ + new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::New, + ), + ], $subscriptionStore->addedSubscriptions); + } + + public function testPauseBooting(): void + { + $subscriptionId = 'test'; + $subscriber = new #[Subscriber('test')] + class { + }; + + $subscriptionStore = new DummySubscriptionStore([ + new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::Booting, + ), + ]); + + $streamableStore = $this->prophesize(Store::class); + + $engine = new DefaultSubscriptionEngine( + $streamableStore->reveal(), + $subscriptionStore, + new MetadataSubscriberAccessorRepository([$subscriber]), + ); + + $engine->pause(); + + self::assertEquals([ + new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::Paused, + ), + ], $subscriptionStore->updatedSubscriptions); + } + + public function testPauseActive(): void + { + $subscriptionId = 'test'; + $subscriber = new #[Subscriber('test')] + class { + }; + + $subscriptionStore = new DummySubscriptionStore([ + new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::Active, + ), + ]); + + $streamableStore = $this->prophesize(Store::class); + + $engine = new DefaultSubscriptionEngine( + $streamableStore->reveal(), + $subscriptionStore, + new MetadataSubscriberAccessorRepository([$subscriber]), + ); + + $engine->pause(); + + self::assertEquals([ + new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::Paused, + ), + ], $subscriptionStore->updatedSubscriptions); + } + + public function testPauseError(): void + { + $subscriptionId = 'test'; + $subscriber = new #[Subscriber('test')] + class { + }; + + $subscriptionStore = new DummySubscriptionStore([ + new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::Error, + 0, + new SubscriptionError('ERROR', Status::New), + ), + ]); + + $streamableStore = $this->prophesize(Store::class); + + $engine = new DefaultSubscriptionEngine( + $streamableStore->reveal(), + $subscriptionStore, + new MetadataSubscriberAccessorRepository([$subscriber]), + ); + + $engine->pause(); + + self::assertEquals([ + new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::Paused, + 0, + new SubscriptionError('ERROR', Status::New), + ), + ], $subscriptionStore->updatedSubscriptions); + } + + public function testGetSubscriptionAndDiscoverNewSubscribers(): void + { + $subscriptionId = 'test'; + $subscriber = new #[Subscriber('test')] + class { + }; + + $streamableStore = $this->prophesize(Store::class); + $subscriptionStore = new DummySubscriptionStore(); + + $engine = new DefaultSubscriptionEngine( + $streamableStore->reveal(), + $subscriptionStore, + new MetadataSubscriberAccessorRepository([$subscriber]), + ); + + $subscriptions = $engine->subscriptions(); + + self::assertEquals([ + new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::New, + ), + ], $subscriptionStore->addedSubscriptions); + + self::assertEquals([ + new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::New, + ), + ], $subscriptions); + } + + public function testRetry(): void + { + $subscriptionId = 'test'; + $subscriber = new #[Subscriber('test')] + class { + #[Subscribe(ProfileVisited::class)] + public function subscribe(): void + { + throw new RuntimeException('ERROR2'); + } + }; + + $message = new Message(new ProfileVisited(ProfileId::fromString('test'))); + + $streamableStore = $this->prophesize(Store::class); + $streamableStore->load($this->criteria())->willReturn(new ArrayStream([$message]))->shouldBeCalledOnce(); + + $subscription = new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::Error, + 0, + new SubscriptionError('ERROR', Status::Active), + ); + + $subscriptionStore = new DummySubscriptionStore([$subscription]); + + $retryStrategy = $this->prophesize(RetryStrategy::class); + $retryStrategy->shouldRetry($subscription)->willReturn(true); + + $engine = new DefaultSubscriptionEngine( + $streamableStore->reveal(), + $subscriptionStore, + new MetadataSubscriberAccessorRepository([$subscriber]), + $retryStrategy->reveal(), + ); + + $engine->run(); + + self::assertCount(2, $subscriptionStore->updatedSubscriptions); + + [$update1, $update2] = $subscriptionStore->updatedSubscriptions; + + self::assertEquals($update1, new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::Active, + 0, + null, + 1, + )); + + self::assertEquals(Status::Error, $update2->status()); + self::assertEquals(Status::Active, $update2->subscriptionError()?->previousStatus); + self::assertEquals('ERROR2', $update2->subscriptionError()?->errorMessage); + self::assertEquals(1, $update2->retryAttempt()); + } + + public function testShouldNotRetry(): void + { + $subscriptionId = 'test'; + $subscriber = new #[Subscriber('test')] + class { + }; + + $streamableStore = $this->prophesize(Store::class); + + $subscription = new Subscription( + $subscriptionId, + Subscription::DEFAULT_GROUP, + RunMode::FromBeginning, + Status::Error, + 0, + new SubscriptionError('ERROR', Status::Active), + ); + + $subscriptionStore = new DummySubscriptionStore([$subscription]); + + $retryStrategy = $this->prophesize(RetryStrategy::class); + $retryStrategy->shouldRetry($subscription)->willReturn(false); + + $engine = new DefaultSubscriptionEngine( + $streamableStore->reveal(), + $subscriptionStore, + new MetadataSubscriberAccessorRepository([$subscriber]), + $retryStrategy->reveal(), + ); + + $engine->run(); + + self::assertEquals([], $subscriptionStore->updatedSubscriptions); + } + + #[DataProvider('methodProvider')] + public function testCriteria(string $method): void + { + $subscriber = new #[Subscriber('id1')] + class { + }; + + $subscriptionStore = $this->prophesize(SubscriptionStore::class); + $subscriptionStore->find( + Argument::that( + static fn (SubscriptionCriteria $criteria) => $criteria->ids === ['id1'] && $criteria->groups === ['group1'] + ), + )->willReturn([])->shouldBeCalled(); + + $subscriptionStore->find( + new SubscriptionCriteria(), + )->willReturn([ + new Subscription('id1'), + ])->shouldBeCalled(); + + $streamableStore = $this->prophesize(Store::class); + $streamableStore->load($this->criteria())->willReturn(new ArrayStream([])); + + $engine = new DefaultSubscriptionEngine( + $streamableStore->reveal(), + $subscriptionStore->reveal(), + new MetadataSubscriberAccessorRepository([$subscriber]), + ); + + $engineCriteria = new SubscriptionEngineCriteria( + ids: ['id1'], + groups: ['group1'], + ); + + $engine->{$method}($engineCriteria); + } + + #[DataProvider('methodProvider')] + public function testWithLockableStore(string $method): void + { + $subscriber = new #[Subscriber('id1')] + class { + }; + + $subscriptionStore = $this->prophesize(LockableSubscriptionStore::class); + $subscriptionStore->inLock(Argument::type(Closure::class))->will( + /** @param array{Closure} $args */ + static fn (array $args): mixed => $args[0]() + )->shouldBeCalled(); + $subscriptionStore->find(Argument::any())->willReturn([])->shouldBeCalled(); + + $subscriptionStore->find( + new SubscriptionCriteria(), + )->willReturn([ + new Subscription('id1'), + ])->shouldBeCalled(); + + $subscriptionStore->remove(Argument::type(Subscription::class)); + $subscriptionStore->add(Argument::type(Subscription::class)); + + $streamableStore = $this->prophesize(Store::class); + $streamableStore->load($this->criteria())->willReturn(new ArrayStream([])); + + $engine = new DefaultSubscriptionEngine( + $streamableStore->reveal(), + $subscriptionStore->reveal(), + new MetadataSubscriberAccessorRepository([$subscriber]), + ); + + $engine->{$method}(); + } + + public static function methodProvider(): Generator + { + yield 'boot' => ['boot']; + yield 'run' => ['run']; + yield 'teardown' => ['teardown']; + yield 'remove' => ['remove']; + yield 'reactivate' => ['reactivate']; + yield 'subscriptions' => ['subscriptions']; + } + + private function criteria(int $fromIndex = 0): Criteria + { + return new Criteria(fromIndex: $fromIndex); + } +} diff --git a/tests/Unit/Projection/RetryStrategy/ClockBasedRetryStrategyTest.php b/tests/Unit/Subscription/RetryStrategy/ClockBasedRetryStrategyTest.php similarity index 71% rename from tests/Unit/Projection/RetryStrategy/ClockBasedRetryStrategyTest.php rename to tests/Unit/Subscription/RetryStrategy/ClockBasedRetryStrategyTest.php index 420486dc4..588441f36 100644 --- a/tests/Unit/Projection/RetryStrategy/ClockBasedRetryStrategyTest.php +++ b/tests/Unit/Subscription/RetryStrategy/ClockBasedRetryStrategyTest.php @@ -2,19 +2,19 @@ declare(strict_types=1); -namespace Patchlevel\EventSourcing\Tests\Unit\Projection\RetryStrategy; +namespace Patchlevel\EventSourcing\Tests\Unit\Subscription\RetryStrategy; use DateTimeImmutable; use Generator; use Patchlevel\EventSourcing\Clock\FrozenClock; -use Patchlevel\EventSourcing\Projection\Projection\Projection; -use Patchlevel\EventSourcing\Projection\Projection\ProjectionStatus; -use Patchlevel\EventSourcing\Projection\Projection\RunMode; -use Patchlevel\EventSourcing\Projection\RetryStrategy\ClockBasedRetryStrategy; +use Patchlevel\EventSourcing\Subscription\RetryStrategy\ClockBasedRetryStrategy; +use Patchlevel\EventSourcing\Subscription\Subscription\RunMode; +use Patchlevel\EventSourcing\Subscription\Subscription\Status; +use Patchlevel\EventSourcing\Subscription\Subscription\Subscription; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; -/** @covers \Patchlevel\EventSourcing\Projection\RetryStrategy\ClockBasedRetryStrategy */ +/** @covers \Patchlevel\EventSourcing\Subscription\RetryStrategy\ClockBasedRetryStrategy */ final class ClockBasedRetryStrategyTest extends TestCase { private ClockBasedRetryStrategy $strategy; @@ -31,11 +31,11 @@ public function setUp(): void #[DataProvider('attemptProvider')] public function testShouldRetry(int $attempt, int $seconds, bool $expected): void { - $projection = new Projection( + $subscription = new Subscription( 'test', 'default', RunMode::FromBeginning, - ProjectionStatus::Error, + Status::Error, 0, null, $attempt, @@ -46,7 +46,7 @@ public function testShouldRetry(int $attempt, int $seconds, bool $expected): voi self::assertEquals( $expected, - $this->strategy->shouldRetry($projection), + $this->strategy->shouldRetry($subscription), ); } diff --git a/tests/Unit/Subscription/RetryStrategy/NoRetryStrategyTest.php b/tests/Unit/Subscription/RetryStrategy/NoRetryStrategyTest.php new file mode 100644 index 000000000..991cc787e --- /dev/null +++ b/tests/Unit/Subscription/RetryStrategy/NoRetryStrategyTest.php @@ -0,0 +1,22 @@ +shouldRetry( + new Subscription('test'), + )); + } +} diff --git a/tests/Unit/Projection/Projection/Store/ErrorContextTest.php b/tests/Unit/Subscription/Store/ErrorContextTest.php similarity index 80% rename from tests/Unit/Projection/Projection/Store/ErrorContextTest.php rename to tests/Unit/Subscription/Store/ErrorContextTest.php index fe3d29692..81878dc83 100644 --- a/tests/Unit/Projection/Projection/Store/ErrorContextTest.php +++ b/tests/Unit/Subscription/Store/ErrorContextTest.php @@ -2,13 +2,13 @@ declare(strict_types=1); -namespace Patchlevel\EventSourcing\Tests\Unit\Projection\Projection\Store; +namespace Patchlevel\EventSourcing\Tests\Unit\Subscription\Store; -use Patchlevel\EventSourcing\Projection\Projection\ThrowableToErrorContextTransformer; +use Patchlevel\EventSourcing\Subscription\Subscription\ThrowableToErrorContextTransformer; use PHPUnit\Framework\TestCase; use RuntimeException; -/** @covers \Patchlevel\EventSourcing\Projection\Projection\ThrowableToErrorContextTransformer */ +/** @covers \Patchlevel\EventSourcing\Subscription\Subscription\ThrowableToErrorContextTransformer */ final class ErrorContextTest extends TestCase { public function testWithoutPrevious(): void diff --git a/tests/Unit/Subscription/Store/InMemorySubscriptionStoreTest.php b/tests/Unit/Subscription/Store/InMemorySubscriptionStoreTest.php new file mode 100644 index 000000000..cacbfa39f --- /dev/null +++ b/tests/Unit/Subscription/Store/InMemorySubscriptionStoreTest.php @@ -0,0 +1,154 @@ +add($subscription); + + self::assertEquals($subscription, $store->get($id)); + self::assertEquals([$subscription], $store->find()); + } + + public function testAddDuplicated(): void + { + $this->expectException(SubscriptionAlreadyExists::class); + + $id = 'test'; + $subscription = new Subscription($id); + + $store = new InMemorySubscriptionStore([$subscription]); + $store->add($subscription); + } + + public function testUpdate(): void + { + $id = 'test'; + $subscription = new Subscription($id); + + $store = new InMemorySubscriptionStore([$subscription]); + + $store->update($subscription); + + self::assertEquals($subscription, $store->get($id)); + self::assertEquals([$subscription], $store->find()); + } + + public function testUpdateNotFound(): void + { + $this->expectException(SubscriptionNotFound::class); + + $id = 'test'; + $subscription = new Subscription($id); + + $store = new InMemorySubscriptionStore(); + + $store->update($subscription); + } + + public function testNotFound(): void + { + $this->expectException(SubscriptionNotFound::class); + + $store = new InMemorySubscriptionStore(); + $store->get('test'); + } + + public function testRemove(): void + { + $id = 'test'; + $subscription = new Subscription($id); + + $store = new InMemorySubscriptionStore([$subscription]); + + $store->remove($subscription); + + self::assertEquals([], $store->find()); + } + + public function testFind(): void + { + $subscription1 = new Subscription('1'); + $subscription2 = new Subscription('2'); + + $store = new InMemorySubscriptionStore([$subscription1, $subscription2]); + + self::assertEquals([$subscription1, $subscription2], $store->find()); + } + + public function testFindById(): void + { + $subscription1 = new Subscription('1'); + $subscription2 = new Subscription('2'); + + $store = new InMemorySubscriptionStore([$subscription1, $subscription2]); + + $criteria = new SubscriptionCriteria( + ids: ['1'], + ); + + self::assertEquals([$subscription1], $store->find($criteria)); + } + + public function testFindByGroup(): void + { + $subscription1 = new Subscription('1', group: 'group1'); + $subscription2 = new Subscription('2', group: 'group2'); + + $store = new InMemorySubscriptionStore([$subscription1, $subscription2]); + + $criteria = new SubscriptionCriteria( + groups: ['group1'], + ); + + self::assertEquals([$subscription1], $store->find($criteria)); + } + + public function testFindByStatus(): void + { + $subscription1 = new Subscription('1', status: Status::New); + $subscription2 = new Subscription('2', status: Status::Booting); + + $store = new InMemorySubscriptionStore([$subscription1, $subscription2]); + + $criteria = new SubscriptionCriteria( + status: [Status::New], + ); + + self::assertEquals([$subscription1], $store->find($criteria)); + } + + public function testFindByAll(): void + { + $subscription1 = new Subscription('1', group: 'group1', status: Status::New); + $subscription2 = new Subscription('2', group: 'group2', status: Status::Booting); + + $store = new InMemorySubscriptionStore([$subscription1, $subscription2]); + + $criteria = new SubscriptionCriteria( + ids: ['1'], + groups: ['group1'], + status: [Status::New], + ); + + self::assertEquals([$subscription1], $store->find($criteria)); + } +} diff --git a/tests/Unit/Subscription/Store/SubscriptionAlreadyExistsTest.php b/tests/Unit/Subscription/Store/SubscriptionAlreadyExistsTest.php new file mode 100644 index 000000000..dca218c24 --- /dev/null +++ b/tests/Unit/Subscription/Store/SubscriptionAlreadyExistsTest.php @@ -0,0 +1,24 @@ +getMessage(), + ); + + self::assertSame(0, $exception->getCode()); + } +} diff --git a/tests/Unit/Subscription/Store/SubscriptionCriteriaTest.php b/tests/Unit/Subscription/Store/SubscriptionCriteriaTest.php new file mode 100644 index 000000000..29c4e04c0 --- /dev/null +++ b/tests/Unit/Subscription/Store/SubscriptionCriteriaTest.php @@ -0,0 +1,20 @@ +ids); + } +} diff --git a/tests/Unit/Subscription/Store/SubscriptionNotFoundTest.php b/tests/Unit/Subscription/Store/SubscriptionNotFoundTest.php new file mode 100644 index 000000000..a4d456ccd --- /dev/null +++ b/tests/Unit/Subscription/Store/SubscriptionNotFoundTest.php @@ -0,0 +1,23 @@ +getMessage(), + ); + self::assertSame(0, $exception->getCode()); + } +} diff --git a/tests/Unit/Subscription/Subscriber/MetadataSubscriberAccessorRepositoryTest.php b/tests/Unit/Subscription/Subscriber/MetadataSubscriberAccessorRepositoryTest.php new file mode 100644 index 000000000..d371b5018 --- /dev/null +++ b/tests/Unit/Subscription/Subscriber/MetadataSubscriberAccessorRepositoryTest.php @@ -0,0 +1,46 @@ +all()); + self::assertNull($repository->get('foo')); + } + + public function testWithSubscriber(): void + { + $subscriber = new #[Subscriber('foo')] + class { + }; + $metadataFactory = new AttributeSubscriberMetadataFactory(); + + $repository = new MetadataSubscriberAccessorRepository( + [$subscriber], + $metadataFactory, + ); + + $accessor = new MetadataSubscriberAccessor( + $subscriber, + $metadataFactory->metadata($subscriber::class), + ); + + self::assertEquals([$accessor], $repository->all()); + self::assertEquals($accessor, $repository->get('foo')); + } +} diff --git a/tests/Unit/Subscription/Subscriber/MetadataSubscriberAccessorTest.php b/tests/Unit/Subscription/Subscriber/MetadataSubscriberAccessorTest.php new file mode 100644 index 000000000..79613df04 --- /dev/null +++ b/tests/Unit/Subscription/Subscriber/MetadataSubscriberAccessorTest.php @@ -0,0 +1,206 @@ +metadata($subscriber::class), + ); + + self::assertEquals('profile', $accessor->id()); + } + + public function testGroup(): void + { + $subscriber = new #[Subscriber('profile')] + class { + }; + + $accessor = new MetadataSubscriberAccessor( + $subscriber, + (new AttributeSubscriberMetadataFactory())->metadata($subscriber::class), + ); + + self::assertEquals('default', $accessor->group()); + } + + public function testRunMode(): void + { + $subscriber = new #[Subscriber('profile')] + class { + }; + + $accessor = new MetadataSubscriberAccessor( + $subscriber, + (new AttributeSubscriberMetadataFactory())->metadata($subscriber::class), + ); + + self::assertEquals(RunMode::FromBeginning, $accessor->runMode()); + } + + public function testSubscribeMethod(): void + { + $subscriber = new #[Subscriber('profile')] + class { + #[Subscribe(ProfileCreated::class)] + public function onProfileCreated(Message $message): void + { + } + }; + + $accessor = new MetadataSubscriberAccessor( + $subscriber, + (new AttributeSubscriberMetadataFactory())->metadata($subscriber::class), + ); + + $result = $accessor->subscribeMethods(ProfileCreated::class); + + self::assertEquals([ + $subscriber->onProfileCreated(...), + ], $result); + } + + public function testMultipleSubscribeMethod(): void + { + $subscriber = new #[Subscriber('profile')] + class { + #[Subscribe(ProfileCreated::class)] + public function onProfileCreated(Message $message): void + { + } + + #[Subscribe(ProfileCreated::class)] + public function onFoo(Message $message): void + { + } + }; + + $accessor = new MetadataSubscriberAccessor( + $subscriber, + (new AttributeSubscriberMetadataFactory())->metadata($subscriber::class), + ); + + $result = $accessor->subscribeMethods(ProfileCreated::class); + + self::assertEquals([ + $subscriber->onProfileCreated(...), + $subscriber->onFoo(...), + ], $result); + } + + public function testSubscribeAllMethod(): void + { + $subscriber = new #[Subscriber('profile')] + class { + #[Subscribe('*')] + public function onProfileCreated(Message $message): void + { + } + }; + + $accessor = new MetadataSubscriberAccessor( + $subscriber, + (new AttributeSubscriberMetadataFactory())->metadata($subscriber::class), + ); + + $result = $accessor->subscribeMethods(ProfileCreated::class); + + self::assertEquals([ + $subscriber->onProfileCreated(...), + ], $result); + } + + public function testSetupMethod(): void + { + $subscriber = new #[Subscriber('profile')] + class { + #[Setup] + public function method(): void + { + } + }; + + $accessor = new MetadataSubscriberAccessor( + $subscriber, + (new AttributeSubscriberMetadataFactory())->metadata($subscriber::class), + ); + + $result = $accessor->setupMethod(); + + self::assertEquals($subscriber->method(...), $result); + } + + public function testNotSetupMethod(): void + { + $subscriber = new #[Subscriber('profile')] + class { + }; + + $accessor = new MetadataSubscriberAccessor( + $subscriber, + (new AttributeSubscriberMetadataFactory())->metadata($subscriber::class), + ); + + $result = $accessor->setupMethod(); + + self::assertNull($result); + } + + public function testTeardownMethod(): void + { + $subscriber = new #[Subscriber('profile')] + class { + #[Teardown] + public function method(): void + { + } + }; + + $accessor = new MetadataSubscriberAccessor( + $subscriber, + (new AttributeSubscriberMetadataFactory())->metadata($subscriber::class), + ); + + $result = $accessor->teardownMethod(); + + self::assertEquals($subscriber->method(...), $result); + } + + public function testNotTeardownMethod(): void + { + $subscriber = new #[Subscriber('profile')] + class { + }; + + $accessor = new MetadataSubscriberAccessor( + $subscriber, + (new AttributeSubscriberMetadataFactory())->metadata($subscriber::class), + ); + + $result = $accessor->teardownMethod(); + + self::assertNull($result); + } +} diff --git a/tests/Unit/Subscription/Subscriber/SubscriberHelperTest.php b/tests/Unit/Subscription/Subscriber/SubscriberHelperTest.php new file mode 100644 index 000000000..305da7a48 --- /dev/null +++ b/tests/Unit/Subscription/Subscriber/SubscriberHelperTest.php @@ -0,0 +1,24 @@ +subscriberId($subscriber)); + } +} diff --git a/tests/Unit/Projection/Projection/ErrorContextTest.php b/tests/Unit/Subscription/Subscription/ErrorContextTest.php similarity index 93% rename from tests/Unit/Projection/Projection/ErrorContextTest.php rename to tests/Unit/Subscription/Subscription/ErrorContextTest.php index eb6329ca8..c40894a7e 100644 --- a/tests/Unit/Projection/Projection/ErrorContextTest.php +++ b/tests/Unit/Subscription/Subscription/ErrorContextTest.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace Patchlevel\EventSourcing\Tests\Unit\Projection\Projection; +namespace Patchlevel\EventSourcing\Tests\Unit\Subscription\Subscription; use Patchlevel\EventSourcing\Aggregate\CustomId; -use Patchlevel\EventSourcing\Projection\Projection\ThrowableToErrorContextTransformer; +use Patchlevel\EventSourcing\Subscription\Subscription\ThrowableToErrorContextTransformer; use PHPUnit\Framework\TestCase; use RuntimeException; diff --git a/tests/Unit/Subscription/Subscription/SubscriptionErrorTest.php b/tests/Unit/Subscription/Subscription/SubscriptionErrorTest.php new file mode 100644 index 000000000..a3ceeeccb --- /dev/null +++ b/tests/Unit/Subscription/Subscription/SubscriptionErrorTest.php @@ -0,0 +1,26 @@ +errorMessage); + self::assertSame(Status::Active, $error->previousStatus); + self::assertIsArray($error->errorContext); + } +} diff --git a/tests/Unit/Subscription/Subscription/SubscriptionTest.php b/tests/Unit/Subscription/Subscription/SubscriptionTest.php new file mode 100644 index 000000000..f244cf73a --- /dev/null +++ b/tests/Unit/Subscription/Subscription/SubscriptionTest.php @@ -0,0 +1,149 @@ +id()); + self::assertEquals(Status::New, $subscription->status()); + self::assertEquals(0, $subscription->position()); + self::assertTrue($subscription->isNew()); + self::assertFalse($subscription->isBooting()); + self::assertFalse($subscription->isActive()); + self::assertFalse($subscription->isError()); + self::assertFalse($subscription->isOutdated()); + } + + public function testBooting(): void + { + $subscription = new Subscription( + 'test', + ); + + $subscription->booting(); + + self::assertEquals(Status::Booting, $subscription->status()); + self::assertFalse($subscription->isNew()); + self::assertTrue($subscription->isBooting()); + self::assertFalse($subscription->isActive()); + self::assertFalse($subscription->isError()); + self::assertFalse($subscription->isOutdated()); + } + + public function testActive(): void + { + $subscription = new Subscription( + 'test', + ); + + $subscription->active(); + + self::assertEquals(Status::Active, $subscription->status()); + self::assertFalse($subscription->isNew()); + self::assertFalse($subscription->isBooting()); + self::assertTrue($subscription->isActive()); + self::assertFalse($subscription->isError()); + self::assertFalse($subscription->isOutdated()); + } + + public function testError(): void + { + $subscription = new Subscription( + 'test', + ); + + $exception = new RuntimeException('test'); + + $subscription->error($exception); + + self::assertEquals(Status::Error, $subscription->status()); + self::assertFalse($subscription->isNew()); + self::assertFalse($subscription->isBooting()); + self::assertFalse($subscription->isActive()); + self::assertTrue($subscription->isError()); + self::assertFalse($subscription->isOutdated()); + self::assertEquals( + new SubscriptionError( + 'test', + Status::New, + ThrowableToErrorContextTransformer::transform($exception), + ), + $subscription->subscriptionError(), + ); + } + + public function testOutdated(): void + { + $subscription = new Subscription( + 'test', + ); + + $subscription->outdated(); + + self::assertEquals(Status::Outdated, $subscription->status()); + self::assertFalse($subscription->isNew()); + self::assertFalse($subscription->isBooting()); + self::assertFalse($subscription->isActive()); + self::assertFalse($subscription->isError()); + self::assertTrue($subscription->isOutdated()); + } + + public function testChangePosition(): void + { + $subscription = new Subscription( + 'test', + ); + + $subscription->changePosition(10); + + self::assertEquals(10, $subscription->position()); + } + + public function testCanNotRetry(): void + { + $this->expectException(NoErrorToRetry::class); + + $subscription = new Subscription( + 'test', + ); + + $subscription->doRetry(); + } + + public function testDoRetry(): void + { + $subscription = new Subscription( + 'test', + 'default', + RunMode::FromBeginning, + Status::Error, + 0, + new SubscriptionError('test', Status::New, []), + ); + + self::assertEquals(null, $subscription->retryAttempt()); + $subscription->doRetry(); + + self::assertEquals(1, $subscription->retryAttempt()); + $subscription->resetRetry(); + + self::assertEquals(null, $subscription->retryAttempt()); + } +}