├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── UPGRADE-2.0.md ├── UPGRADE-3.0.md ├── baseline.xml ├── composer.json ├── deptrac-baseline.yaml ├── deptrac.yaml ├── docker-compose.yaml ├── infection.json.dist ├── phpstan-baseline.neon ├── phpstan.neon.dist ├── psalm_docs.xml ├── renovate.json └── src ├── Aggregate ├── AggregateException.php ├── AggregateHeader.php ├── AggregateRoot.php ├── AggregateRootAttributeBehaviour.php ├── AggregateRootBehaviour.php ├── AggregateRootId.php ├── AggregateRootIdNotSupported.php ├── AggregateRootMetadataAware.php ├── AggregateRootMetadataAwareBehaviour.php ├── ApplyMethodNotFound.php ├── BasicAggregateRoot.php ├── BasicChildAggregate.php ├── ChildAggregate.php ├── ChildAggregateBehaviour.php ├── CustomId.php ├── CustomIdBehaviour.php ├── InvalidAggregateStreamName.php ├── MetadataNotPossible.php ├── RamseyUuidV7Behaviour.php └── Uuid.php ├── Attribute ├── Aggregate.php ├── Answer.php ├── Apply.php ├── ChildAggregate.php ├── Event.php ├── Handle.php ├── Header.php ├── Id.php ├── Inject.php ├── OnFailed.php ├── Processor.php ├── Projector.php ├── RetryAggregateOutdated.php ├── RetryStrategy.php ├── Setup.php ├── Snapshot.php ├── SplitStream.php ├── Stream.php ├── Subscribe.php ├── Subscriber.php ├── SuppressMissingApply.php └── Teardown.php ├── Clock ├── FrozenClock.php └── SystemClock.php ├── CommandBus ├── AggregateHandlerProvider.php ├── ChainHandlerProvider.php ├── CommandBus.php ├── Handler │ ├── AggregateIdNotFound.php │ ├── CreateAggregateHandler.php │ ├── DefaultParameterResolver.php │ ├── ParameterResolver.php │ ├── ServiceNotResolvable.php │ └── UpdateAggregateHandler.php ├── HandlerDescriptor.php ├── HandlerFinder.php ├── HandlerNotFound.php ├── HandlerProvider.php ├── HandlerReference.php ├── InvalidHandleMethod.php ├── MultipleHandlersFound.php ├── RetryOutdatedAggregateCommandBus.php ├── ServiceHandlerProvider.php ├── ServiceLocator.php ├── ServiceNotFound.php └── SyncCommandBus.php ├── Console ├── Command │ ├── DatabaseCreateCommand.php │ ├── DatabaseDropCommand.php │ ├── DebugCommand.php │ ├── SchemaCreateCommand.php │ ├── SchemaDropCommand.php │ ├── SchemaUpdateCommand.php │ ├── ShowAggregateCommand.php │ ├── ShowCommand.php │ ├── SubscriptionBootCommand.php │ ├── SubscriptionCommand.php │ ├── SubscriptionPauseCommand.php │ ├── SubscriptionReactivateCommand.php │ ├── SubscriptionRemoveCommand.php │ ├── SubscriptionRunCommand.php │ ├── SubscriptionSetupCommand.php │ ├── SubscriptionStatusCommand.php │ ├── SubscriptionTeardownCommand.php │ └── WatchCommand.php ├── DoctrineHelper.php ├── InputHelper.php ├── InvalidArgumentGiven.php └── OutputStyle.php ├── Cryptography └── DoctrineCipherKeyStore.php ├── EventBus ├── AttributeListenerProvider.php ├── Consumer.php ├── DefaultConsumer.php ├── DefaultEventBus.php ├── EventBus.php ├── ListenerDescriptor.php ├── ListenerProvider.php └── Psr14EventBus.php ├── Message ├── HeaderNotFound.php ├── Message.php ├── Pipe.php ├── Reducer.php ├── Serializer │ ├── DefaultHeadersSerializer.php │ ├── HeadersSerializer.php │ └── InvalidArgument.php └── Translator │ ├── AggregateToStreamHeaderTranslator.php │ ├── ChainTranslator.php │ ├── ClosureMiddleware.php │ ├── ExcludeEventTranslator.php │ ├── ExcludeEventWithHeaderTranslator.php │ ├── FilterEventTranslator.php │ ├── IncludeEventTranslator.php │ ├── IncludeEventWithHeaderTranslator.php │ ├── RecalculatePlayheadTranslator.php │ ├── ReplaceEventTranslator.php │ ├── Translator.php │ └── UntilEventTranslator.php ├── Metadata ├── AggregateRoot │ ├── AggregateRootAlreadyInRegistry.php │ ├── AggregateRootClassNotRegistered.php │ ├── AggregateRootIdNotFound.php │ ├── AggregateRootMetadata.php │ ├── AggregateRootMetadataAwareMetadataFactory.php │ ├── AggregateRootMetadataFactory.php │ ├── AggregateRootNameNotRegistered.php │ ├── AggregateRootRegistry.php │ ├── AggregateRootRegistryFactory.php │ ├── AggregateWithoutMetadataAware.php │ ├── ArgumentTypeIsMissing.php │ ├── ArgumentTypeIsNotAClass.php │ ├── AttributeAggregateRootMetadataFactory.php │ ├── AttributeAggregateRootRegistryFactory.php │ ├── ClassIsNotAnAggregate.php │ ├── DuplicateApplyMethod.php │ ├── DuplicateEmptyApplyAttribute.php │ ├── MissingAggregateIdForStreamName.php │ ├── MissingDataSubjectId.php │ ├── MixedApplyAttributeUsage.php │ ├── MultipleDataSubjectId.php │ ├── NoAggregateRoot.php │ ├── Psr16AggregateRootMetadataFactory.php │ ├── Psr16AggregateRootRegistryFactory.php │ ├── Psr6AggregateRootMetadataFactory.php │ ├── Psr6AggregateRootRegistryFactory.php │ ├── Snapshot.php │ └── SubjectIdAndPersonalDataConflict.php ├── ClassFinder.php ├── Event │ ├── AttributeEventMetadataFactory.php │ ├── AttributeEventRegistryFactory.php │ ├── ClassIsNotAnEvent.php │ ├── EventAlreadyInRegistry.php │ ├── EventClassNotRegistered.php │ ├── EventMetadata.php │ ├── EventMetadataFactory.php │ ├── EventNameNotRegistered.php │ ├── EventRegistry.php │ ├── EventRegistryFactory.php │ ├── MissingDataSubjectId.php │ ├── MultipleDataSubjectId.php │ ├── Psr16EventMetadataFactory.php │ ├── Psr16EventRegistryFactory.php │ ├── Psr6EventMetadataFactory.php │ ├── Psr6EventRegistryFactory.php │ └── SubjectIdAndPersonalDataConflict.php ├── Message │ ├── AttributeMessageHeaderRegistryFactory.php │ ├── HeaderClassNotRegistered.php │ ├── HeaderNameNotRegistered.php │ ├── MessageHeaderRegistry.php │ └── MessageHeaderRegistryFactory.php ├── MetadataException.php └── Subscriber │ ├── ArgumentMetadata.php │ ├── ArgumentTypeNotSupported.php │ ├── AttributeSubscriberMetadataFactory.php │ ├── ClassIsNotASubscriber.php │ ├── DuplicateFailedMethod.php │ ├── DuplicateSetupMethod.php │ ├── DuplicateSubscribeMethod.php │ ├── DuplicateTeardownMethod.php │ ├── Psr16SubscriberMetadataFactory.php │ ├── Psr6SubscriberMetadataFactory.php │ ├── SubscribeMethodMetadata.php │ ├── SubscriberMetadata.php │ └── SubscriberMetadataFactory.php ├── QueryBus ├── ChainHandlerProvider.php ├── HandlerDescriptor.php ├── HandlerFinder.php ├── HandlerProvider.php ├── HandlerReference.php ├── InvalidHandleMethod.php ├── InvalidQueryHandler.php ├── QueryBus.php ├── ServiceHandlerProvider.php └── SyncQueryBus.php ├── Repository ├── AggregateAlreadyExists.php ├── AggregateDetached.php ├── AggregateNotFound.php ├── AggregateOutdated.php ├── AggregateUnknown.php ├── DefaultRepository.php ├── DefaultRepositoryManager.php ├── MessageDecorator │ ├── ChainMessageDecorator.php │ ├── MessageDecorator.php │ └── SplitStreamDecorator.php ├── PlayheadMismatch.php ├── Repository.php ├── RepositoryException.php ├── RepositoryManager.php ├── SnapshotRebuildFailed.php └── WrongAggregate.php ├── Schema ├── ChainDoctrineSchemaConfigurator.php ├── DoctrineMigrationSchemaProvider.php ├── DoctrineSchemaConfigurator.php ├── DoctrineSchemaDirector.php ├── DoctrineSchemaListener.php ├── DoctrineSchemaProvider.php ├── DoctrineSchemaSubscriber.php ├── DryRunSchemaDirector.php └── SchemaDirector.php ├── Serializer ├── DefaultEventSerializer.php ├── Encoder │ ├── DecodeNotPossible.php │ ├── EncodeNotPossible.php │ ├── Encoder.php │ └── JsonEncoder.php ├── EventSerializer.php ├── Normalizer │ └── IdNormalizer.php ├── SerializeException.php ├── SerializedEvent.php └── Upcast │ ├── Upcast.php │ ├── Upcaster.php │ └── UpcasterChain.php ├── Snapshot ├── Adapter │ ├── InMemorySnapshotAdapter.php │ ├── Psr16SnapshotAdapter.php │ ├── Psr6SnapshotAdapter.php │ ├── SnapshotAdapter.php │ └── SnapshotNotFound.php ├── AdapterNotFound.php ├── AdapterRepository.php ├── ArrayAdapterRepository.php ├── DefaultSnapshotStore.php ├── Snapshot.php ├── SnapshotException.php ├── SnapshotNotConfigured.php ├── SnapshotNotFound.php ├── SnapshotStore.php └── SnapshotVersionInvalid.php ├── Store ├── ArchivedHeader.php ├── ArrayStream.php ├── Criteria │ ├── AggregateIdCriterion.php │ ├── AggregateNameCriterion.php │ ├── ArchivedCriterion.php │ ├── Criteria.php │ ├── CriteriaBuilder.php │ ├── CriterionNotFound.php │ ├── EventIdCriterion.php │ ├── EventsCriterion.php │ ├── FromIndexCriterion.php │ ├── FromPlayheadCriterion.php │ ├── StreamCriterion.php │ ├── ToIndexCriterion.php │ └── ToPlayheadCriterion.php ├── DoctrineDbalStore.php ├── DoctrineDbalStoreStream.php ├── Header │ ├── EventIdHeader.php │ ├── IndexHeader.php │ ├── PlayheadHeader.php │ ├── RecordedOnHeader.php │ └── StreamNameHeader.php ├── InMemoryStore.php ├── InvalidStreamName.php ├── InvalidType.php ├── LockingNotImplemented.php ├── MissingDataForStorage.php ├── ReadOnlyStore.php ├── Store.php ├── StoreException.php ├── StoreIsReadOnly.php ├── Stream.php ├── StreamClosed.php ├── StreamDoctrineDbalStore.php ├── StreamDoctrineDbalStoreStream.php ├── StreamReadOnlyStore.php ├── StreamStartHeader.php ├── StreamStore.php ├── SubscriptionStore.php ├── UniqueConstraintViolation.php ├── UnsupportedCriterion.php └── WrongQueryResult.php ├── Subscription ├── Engine │ ├── AlreadyProcessing.php │ ├── CatchUpSubscriptionEngine.php │ ├── DefaultSubscriptionEngine.php │ ├── Error.php │ ├── ErrorDetected.php │ ├── EventFilteredStoreMessageLoader.php │ ├── GapResolverStoreMessageLoader.php │ ├── GeneratorStream.php │ ├── MessageLoader.php │ ├── ProcessedResult.php │ ├── Result.php │ ├── StoreMessageLoader.php │ ├── SubscriberNotFound.php │ ├── SubscriptionCollection.php │ ├── SubscriptionEngine.php │ ├── SubscriptionEngineCriteria.php │ ├── SubscriptionManager.php │ ├── ThrowOnErrorSubscriptionEngine.php │ └── UnexpectedError.php ├── Lookup │ ├── Lookup.php │ └── MessageNotFound.php ├── NoErrorToRetry.php ├── Repository │ ├── RunSubscriptionEngineRepository.php │ └── RunSubscriptionEngineRepositoryManager.php ├── RetryStrategy │ ├── ClockBasedRetryStrategy.php │ ├── ConditionalRetryStrategy.php │ ├── NoRetryStrategy.php │ ├── RetryStrategy.php │ ├── RetryStrategyNotFound.php │ ├── RetryStrategyRepository.php │ └── UnexpectedError.php ├── RunMode.php ├── Status.php ├── Store │ ├── DoctrineSubscriptionStore.php │ ├── InMemorySubscriptionStore.php │ ├── LockableSubscriptionStore.php │ ├── SubscriptionAlreadyExists.php │ ├── SubscriptionCriteria.php │ ├── SubscriptionNotFound.php │ ├── SubscriptionStore.php │ └── TransactionCommitNotPossible.php ├── Subscriber │ ├── ArgumentResolver │ │ ├── AggregateIdArgumentResolver.php │ │ ├── ArgumentResolver.php │ │ ├── EventArgumentResolver.php │ │ ├── LookupResolver.php │ │ ├── MessageArgumentResolver.php │ │ └── RecordedOnArgumentResolver.php │ ├── BatchableSubscriber.php │ ├── DuplicateSubscriberId.php │ ├── MetadataSubscriberAccessor.php │ ├── MetadataSubscriberAccessorRepository.php │ ├── NoSuitableResolver.php │ ├── RealSubscriberAccessor.php │ ├── SubscriberAccessor.php │ ├── SubscriberAccessorRepository.php │ ├── SubscriberHelper.php │ └── SubscriberUtil.php ├── Subscription.php ├── SubscriptionError.php └── ThrowableToErrorContextTransformer.php └── Test └── IncrementalRamseyUuidFactory.php /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:8.3 2 | 3 | ARG EXTENSIONS="pcntl zip intl bcmath" 4 | 5 | RUN apt-get update && apt-get install -y \ 6 | git \ 7 | zip \ 8 | unzip \ 9 | curl \ 10 | && rm -rf /var/lib/apt/lists/* 11 | 12 | COPY --from=composer /usr/bin/composer /usr/bin/composer 13 | 14 | ADD https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/ 15 | 16 | RUN chmod +x /usr/local/bin/install-php-extensions && \ 17 | install-php-extensions $EXTENSIONS 18 | 19 | RUN mkdir -p /tmp/blackfire \ 20 | && architecture=$(uname -m) \ 21 | && curl -A "Docker" -L https://blackfire.io/api/v1/releases/cli/linux/$architecture | tar zxp -C /tmp/blackfire \ 22 | && mv /tmp/blackfire/blackfire /usr/bin/blackfire \ 23 | && rm -Rf /tmp/blackfire -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Daniel Badura & David Badura 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /UPGRADE-3.0.md: -------------------------------------------------------------------------------- 1 | The upgrade path is located in our docs. Please have a look at 2 | [here](https://patchlevel.github.io/event-sourcing-docs/3.0/UPGRADE-3.0/). 3 | -------------------------------------------------------------------------------- /deptrac-baseline.yaml: -------------------------------------------------------------------------------- 1 | deptrac: 2 | skip_violations: 3 | Patchlevel\EventSourcing\Aggregate\AggregateRootId: 4 | - Patchlevel\EventSourcing\Serializer\Normalizer\IdNormalizer 5 | Patchlevel\EventSourcing\Aggregate\CustomId: 6 | - Patchlevel\EventSourcing\Serializer\Normalizer\IdNormalizer 7 | Patchlevel\EventSourcing\Aggregate\Uuid: 8 | - Patchlevel\EventSourcing\Serializer\Normalizer\IdNormalizer 9 | Patchlevel\EventSourcing\Attribute\Processor: 10 | - Patchlevel\EventSourcing\Subscription\RunMode 11 | Patchlevel\EventSourcing\Attribute\Projector: 12 | - Patchlevel\EventSourcing\Subscription\RunMode 13 | Patchlevel\EventSourcing\Attribute\Stream: 14 | - Patchlevel\EventSourcing\Aggregate\AggregateRoot 15 | Patchlevel\EventSourcing\Attribute\Subscriber: 16 | - Patchlevel\EventSourcing\Subscription\RunMode 17 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | postgres: 3 | image: postgres:alpine 4 | environment: 5 | - POSTGRES_PASSWORD=postgres 6 | - POSTGRES_DB=eventstore 7 | ports: 8 | - 5432:5432 9 | 10 | mysql: 11 | image: mysql:8 12 | environment: 13 | - MYSQL_ALLOW_EMPTY_PASSWORD="yes" 14 | - MYSQL_DATABASE=eventstore 15 | ports: 16 | - 3306:3306 -------------------------------------------------------------------------------- /infection.json.dist: -------------------------------------------------------------------------------- 1 | { 2 | "source": { 3 | "directories": [ 4 | "src" 5 | ] 6 | }, 7 | "logs": { 8 | "text": "infection.log", 9 | "html": "infection.html", 10 | "stryker": { 11 | "report": "/[0-9]+.[0-9]+.x/" 12 | } 13 | }, 14 | "mutators": { 15 | "@default": true 16 | }, 17 | "minMsi": 72, 18 | "minCoveredMsi": 95, 19 | "testFrameworkOptions": "--testsuite=unit" 20 | } 21 | -------------------------------------------------------------------------------- /phpstan.neon.dist: -------------------------------------------------------------------------------- 1 | includes: 2 | - phpstan-baseline.neon 3 | 4 | parameters: 5 | level: max 6 | paths: 7 | - src 8 | ignoreErrors: 9 | - 10 | identifier: missingType.generics 11 | -------------------------------------------------------------------------------- /psalm_docs.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "local>patchlevel/.github:renovate-config" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /src/Aggregate/AggregateException.php: -------------------------------------------------------------------------------- 1 | aggregateName . '-' . $this->aggregateId; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Aggregate/AggregateRoot.php: -------------------------------------------------------------------------------- 1 | $events */ 12 | public function catchUp(iterable $events): void; 13 | 14 | /** @return list */ 15 | public function releaseEvents(): array; 16 | 17 | /** 18 | * @param iterable $events 19 | * @param 0|positive-int $startPlayhead 20 | */ 21 | public static function createFromEvents(iterable $events, int $startPlayhead = 0): static; 22 | 23 | public function playhead(): int; 24 | } 25 | -------------------------------------------------------------------------------- /src/Aggregate/AggregateRootId.php: -------------------------------------------------------------------------------- 1 | */ 18 | public static function metadata(): AggregateRootMetadata 19 | { 20 | if (!self::$metadataFactory) { 21 | self::$metadataFactory = new AttributeAggregateRootMetadataFactory(); 22 | } 23 | 24 | return self::$metadataFactory->metadata(static::class); 25 | } 26 | 27 | public static function setMetadataFactory(AggregateRootMetadataFactory $metadataFactory): void 28 | { 29 | self::$metadataFactory = $metadataFactory; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Aggregate/ApplyMethodNotFound.php: -------------------------------------------------------------------------------- 1 | $aggregateRootClass 13 | * @param class-string $event 14 | */ 15 | public function __construct(string $aggregateRootClass, string $event) 16 | { 17 | parent::__construct( 18 | sprintf( 19 | 'Apply method in "%s" could not be found for the event "%s"', 20 | $aggregateRootClass, 21 | $event, 22 | ), 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Aggregate/BasicAggregateRoot.php: -------------------------------------------------------------------------------- 1 | */ 16 | public static function metadata(): AggregateRootMetadata 17 | { 18 | if (static::class === self::class) { 19 | throw new MetadataNotPossible(); 20 | } 21 | 22 | return static::getMetadata(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Aggregate/BasicChildAggregate.php: -------------------------------------------------------------------------------- 1 | recorder)($event); 19 | } 20 | 21 | /** @param callable(object $event): void $recorder */ 22 | public function setRecorder(callable $recorder): void 23 | { 24 | $this->recorder = $recorder; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Aggregate/CustomId.php: -------------------------------------------------------------------------------- 1 | id; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Aggregate/InvalidAggregateStreamName.php: -------------------------------------------------------------------------------- 1 | id->toString(); 25 | } 26 | 27 | public static function generate(): self 28 | { 29 | return new self(Uuid::uuid7()); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Aggregate/Uuid.php: -------------------------------------------------------------------------------- 1 | */ 15 | public readonly array $aliases = [], 16 | ) { 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Attribute/Handle.php: -------------------------------------------------------------------------------- 1 | $name */ 14 | public function __construct( 15 | public readonly string $name, 16 | ) { 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Attribute/Subscribe.php: -------------------------------------------------------------------------------- 1 | */ 15 | public readonly array $suppressEvents; 16 | public readonly bool $suppressAll; 17 | 18 | /** @param list|self::ALL $suppress */ 19 | public function __construct(string|array $suppress) 20 | { 21 | if ($suppress === self::ALL) { 22 | $this->suppressEvents = []; 23 | $this->suppressAll = true; 24 | 25 | return; 26 | } 27 | 28 | $this->suppressEvents = $suppress; 29 | $this->suppressAll = false; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Attribute/Teardown.php: -------------------------------------------------------------------------------- 1 | frozenDateTime; 22 | } 23 | 24 | public function update(DateTimeImmutable $frozenDateTime): void 25 | { 26 | $this->frozenDateTime = $frozenDateTime; 27 | } 28 | 29 | /** @param positive-int $seconds */ 30 | public function sleep(int $seconds): void 31 | { 32 | $this->frozenDateTime = $this->frozenDateTime->modify(sprintf('+%s seconds', $seconds)); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Clock/SystemClock.php: -------------------------------------------------------------------------------- 1 | $providers */ 10 | public function __construct( 11 | private readonly iterable $providers, 12 | ) { 13 | } 14 | 15 | /** 16 | * @param class-string $commandClass 17 | * 18 | * @return iterable 19 | */ 20 | public function handlerForCommand(string $commandClass): iterable 21 | { 22 | $handlers = []; 23 | 24 | foreach ($this->providers as $provider) { 25 | $handlers = [ 26 | ...$handlers, 27 | ...$provider->handlerForCommand($commandClass), 28 | ]; 29 | } 30 | 31 | return $handlers; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/CommandBus/CommandBus.php: -------------------------------------------------------------------------------- 1 | $aggregateClass */ 15 | public function __construct( 16 | private readonly RepositoryManager $repositoryManager, 17 | private readonly string $aggregateClass, 18 | private readonly string $methodName, 19 | private readonly ParameterResolver $parameterResolver, 20 | ) { 21 | } 22 | 23 | public function __invoke(object $command): void 24 | { 25 | $repository = $this->repositoryManager->get($this->aggregateClass); 26 | 27 | $reflection = new ReflectionClass($this->aggregateClass); 28 | $reflectionMethod = $reflection->getMethod($this->methodName); 29 | 30 | $aggregate = $reflectionMethod->invokeArgs( 31 | null, 32 | [...$this->parameterResolver->resolve($reflectionMethod, $command)], 33 | ); 34 | 35 | if (!$aggregate instanceof AggregateRoot) { 36 | throw new InvalidArgumentException('create method must return an instance of AggregateRoot'); 37 | } 38 | 39 | $repository->save($aggregate); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/CommandBus/Handler/ParameterResolver.php: -------------------------------------------------------------------------------- 1 | */ 12 | public function resolve(ReflectionMethod $method, object $command): iterable; 13 | } 14 | -------------------------------------------------------------------------------- /src/CommandBus/Handler/ServiceNotResolvable.php: -------------------------------------------------------------------------------- 1 | getMessage(), 40 | ), 41 | 0, 42 | $exception, 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/CommandBus/HandlerDescriptor.php: -------------------------------------------------------------------------------- 1 | callable = $callable(...); 18 | $this->name = self::closureName($this->callable); 19 | } 20 | 21 | public function name(): string 22 | { 23 | return $this->name; 24 | } 25 | 26 | public function callable(): callable 27 | { 28 | return $this->callable; 29 | } 30 | 31 | private static function closureName(Closure $closure): string 32 | { 33 | $reflectionFunction = new ReflectionFunction($closure); 34 | 35 | if ($reflectionFunction->isAnonymous()) { 36 | return 'Closure'; 37 | } 38 | 39 | $closureThis = $reflectionFunction->getClosureThis(); 40 | 41 | if (!$closureThis) { 42 | $class = $reflectionFunction->getClosureCalledClass(); 43 | 44 | return ($class ? $class->name . '::' : '') . $reflectionFunction->name; 45 | } 46 | 47 | return $closureThis::class . '::' . $reflectionFunction->name; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/CommandBus/HandlerNotFound.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | public function handlerForCommand(string $commandClass): iterable; 15 | } 16 | -------------------------------------------------------------------------------- /src/CommandBus/HandlerReference.php: -------------------------------------------------------------------------------- 1 | doDispatch($command, 0); 21 | } 22 | 23 | private function doDispatch(object $command, int $retry, int|null $maxRetries = null): void 24 | { 25 | try { 26 | $this->commandBus->dispatch($command); 27 | } catch (AggregateOutdated $exception) { 28 | $maxRetries ??= $this->maxRetries($command); 29 | 30 | if ($retry >= $maxRetries) { 31 | throw $exception; 32 | } 33 | 34 | $this->doDispatch($command, $retry + 1, $maxRetries); 35 | } 36 | } 37 | 38 | private function maxRetries(object $command): int|null 39 | { 40 | $reflectionClass = new ReflectionClass($command); 41 | $attributes = $reflectionClass->getAttributes(RetryAggregateOutdated::class); 42 | 43 | if ($attributes === []) { 44 | return 0; 45 | } 46 | 47 | return $attributes[0]->newInstance()->maxRetries; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/CommandBus/ServiceHandlerProvider.php: -------------------------------------------------------------------------------- 1 | > */ 12 | private array $handlers = []; 13 | 14 | /** @param iterable $services */ 15 | public function __construct( 16 | private readonly iterable $services, 17 | ) { 18 | } 19 | 20 | /** 21 | * @param class-string $commandClass 22 | * 23 | * @return iterable 24 | */ 25 | public function handlerForCommand(string $commandClass): iterable 26 | { 27 | if (!$this->initialized) { 28 | $this->initialize(); 29 | } 30 | 31 | return $this->handlers[$commandClass] ?? []; 32 | } 33 | 34 | private function initialize(): void 35 | { 36 | foreach ($this->services as $service) { 37 | foreach (HandlerFinder::findInClass($service::class) as $handler) { 38 | if ($handler->static) { 39 | $this->handlers[$handler->commandClass][] = new HandlerDescriptor( 40 | $service::{$handler->method}(...), 41 | ); 42 | 43 | continue; 44 | } 45 | 46 | $this->handlers[$handler->commandClass][] = new HandlerDescriptor( 47 | $service->{$handler->method}(...), 48 | ); 49 | } 50 | } 51 | 52 | $this->initialized = true; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/CommandBus/ServiceLocator.php: -------------------------------------------------------------------------------- 1 | $services */ 14 | public function __construct( 15 | private readonly array $services = [], 16 | ) { 17 | } 18 | 19 | public function get(string $id): mixed 20 | { 21 | if (!array_key_exists($id, $this->services)) { 22 | throw new ServiceNotFound($id); 23 | } 24 | 25 | return $this->services[$id]; 26 | } 27 | 28 | public function has(string $id): bool 29 | { 30 | return array_key_exists($id, $this->services); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/CommandBus/ServiceNotFound.php: -------------------------------------------------------------------------------- 1 | subscriptionEngineCriteria($input); 20 | $this->engine->pause($criteria); 21 | 22 | return 0; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Console/Command/SubscriptionReactivateCommand.php: -------------------------------------------------------------------------------- 1 | subscriptionEngineCriteria($input); 20 | $this->engine->reactivate($criteria); 21 | 22 | return 0; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Console/Command/SubscriptionRemoveCommand.php: -------------------------------------------------------------------------------- 1 | subscriptionEngineCriteria($input); 23 | 24 | if ($criteria->ids === null) { 25 | if (!$io->confirm('do you want to remove all subscriptions?', false)) { 26 | return 1; 27 | } 28 | } 29 | 30 | $this->engine->remove($criteria); 31 | 32 | return 0; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Console/Command/SubscriptionSetupCommand.php: -------------------------------------------------------------------------------- 1 | addOption( 25 | 'skip-booting', 26 | null, 27 | InputOption::VALUE_NONE, 28 | 'Skip booting', 29 | ); 30 | } 31 | 32 | protected function execute(InputInterface $input, OutputInterface $output): int 33 | { 34 | $skipBooting = InputHelper::bool($input->getOption('skip-booting')); 35 | 36 | $criteria = $this->subscriptionEngineCriteria($input); 37 | $this->engine->setup($criteria, $skipBooting); 38 | 39 | return 0; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Console/Command/SubscriptionTeardownCommand.php: -------------------------------------------------------------------------------- 1 | subscriptionEngineCriteria($input); 20 | $this->engine->teardown($criteria); 21 | 22 | return 0; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Console/InvalidArgumentGiven.php: -------------------------------------------------------------------------------- 1 | value; 30 | } 31 | 32 | public function need(): string 33 | { 34 | return $this->need; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/EventBus/Consumer.php: -------------------------------------------------------------------------------- 1 | event()::class; 23 | 24 | $this->logger?->debug(sprintf( 25 | 'EventBus: Consume message "%s".', 26 | $eventClass, 27 | )); 28 | 29 | $listeners = $this->listenerProvider->listenersForEvent($eventClass); 30 | 31 | foreach ($listeners as $listener) { 32 | $this->logger?->info(sprintf( 33 | 'EventBus: Listener "%s" consume message with event "%s".', 34 | $listener->name(), 35 | $eventClass, 36 | )); 37 | 38 | ($listener->callable())($message); 39 | } 40 | } 41 | 42 | /** @param iterable $listeners */ 43 | public static function create(iterable $listeners = []): self 44 | { 45 | return new self(new AttributeListenerProvider($listeners)); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/EventBus/EventBus.php: -------------------------------------------------------------------------------- 1 | callable = $callable(...); 20 | $this->name = self::closureName($this->callable); 21 | } 22 | 23 | public function name(): string 24 | { 25 | return $this->name; 26 | } 27 | 28 | public function callable(): callable 29 | { 30 | return $this->callable; 31 | } 32 | 33 | private static function closureName(Closure $closure): string 34 | { 35 | $reflectionFunction = new ReflectionFunction($closure); 36 | 37 | if (method_exists($reflectionFunction, 'isAnonymous') && $reflectionFunction->isAnonymous()) { 38 | return 'Closure'; 39 | } 40 | 41 | $closureThis = $reflectionFunction->getClosureThis(); 42 | 43 | if (!$closureThis) { 44 | $class = $reflectionFunction->getClosureCalledClass(); 45 | 46 | return ($class ? $class->name . '::' : '') . $reflectionFunction->name; 47 | } 48 | 49 | return $closureThis::class . '::' . $reflectionFunction->name; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/EventBus/ListenerProvider.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | public function listenersForEvent(string $eventClass): iterable; 15 | } 16 | -------------------------------------------------------------------------------- /src/EventBus/Psr14EventBus.php: -------------------------------------------------------------------------------- 1 | eventDispatcher->dispatch($message); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Message/HeaderNotFound.php: -------------------------------------------------------------------------------- 1 | */ 17 | final class Pipe implements IteratorAggregate 18 | { 19 | private Translator $translator; 20 | 21 | /** @param iterable $messages */ 22 | public function __construct( 23 | private readonly iterable $messages, 24 | Translator ...$translators, 25 | ) { 26 | $this->translator = new ChainTranslator($translators); 27 | } 28 | 29 | /** @return Traversable */ 30 | public function getIterator(): Traversable 31 | { 32 | return $this->createGenerator( 33 | $this->messages, 34 | $this->translator, 35 | ); 36 | } 37 | 38 | /** @return list */ 39 | public function toArray(): array 40 | { 41 | return array_values( 42 | iterator_to_array($this->getIterator()), 43 | ); 44 | } 45 | 46 | /** 47 | * @param iterable $messages 48 | * 49 | * @return Generator 50 | */ 51 | private function createGenerator(iterable $messages, Translator $translator): Generator 52 | { 53 | foreach ($messages as $message) { 54 | foreach ($translator($message) as $translatedMessage) { 55 | yield $translatedMessage; 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Message/Serializer/HeadersSerializer.php: -------------------------------------------------------------------------------- 1 | $headers 11 | * @param array $options 12 | */ 13 | public function serialize(array $headers, array $options = []): string; 14 | 15 | /** 16 | * @param array $options 17 | * 18 | * @return list 19 | */ 20 | public function deserialize(string $string, array $options = []): array; 21 | } 22 | -------------------------------------------------------------------------------- /src/Message/Serializer/InvalidArgument.php: -------------------------------------------------------------------------------- 1 | */ 16 | public function __invoke(Message $message): array 17 | { 18 | if (!$message->hasHeader(AggregateHeader::class)) { 19 | return [$message]; 20 | } 21 | 22 | $aggregateHeader = $message->header(AggregateHeader::class); 23 | 24 | return [ 25 | $message 26 | ->removeHeader(AggregateHeader::class) 27 | ->withHeader(new StreamNameHeader($aggregateHeader->streamName())) 28 | ->withHeader(new PlayheadHeader($aggregateHeader->playhead)) 29 | ->withHeader(new RecordedOnHeader($aggregateHeader->recordedOn)), 30 | ]; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Message/Translator/ChainTranslator.php: -------------------------------------------------------------------------------- 1 | $translators */ 12 | public function __construct( 13 | private readonly iterable $translators, 14 | ) { 15 | } 16 | 17 | /** @return list */ 18 | public function __invoke(Message $message): array 19 | { 20 | $messages = [$message]; 21 | 22 | foreach ($this->translators as $middleware) { 23 | $messages = $this->process($middleware, $messages); 24 | } 25 | 26 | return $messages; 27 | } 28 | 29 | /** 30 | * @param list $messages 31 | * 32 | * @return list 33 | */ 34 | private function process(Translator $translator, array $messages): array 35 | { 36 | $result = []; 37 | 38 | foreach ($messages as $message) { 39 | $result = [...$result, ...$translator($message)]; 40 | } 41 | 42 | return $result; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Message/Translator/ClosureMiddleware.php: -------------------------------------------------------------------------------- 1 | $callable */ 13 | public function __construct( 14 | private readonly Closure $callable, 15 | ) { 16 | } 17 | 18 | /** @return list */ 19 | public function __invoke(Message $message): array 20 | { 21 | return ($this->callable)($message); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Message/Translator/ExcludeEventTranslator.php: -------------------------------------------------------------------------------- 1 | $classes */ 12 | public function __construct( 13 | private readonly array $classes, 14 | ) { 15 | } 16 | 17 | /** @return list */ 18 | public function __invoke(Message $message): array 19 | { 20 | foreach ($this->classes as $class) { 21 | if ($message->event() instanceof $class) { 22 | return []; 23 | } 24 | } 25 | 26 | return [$message]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Message/Translator/ExcludeEventWithHeaderTranslator.php: -------------------------------------------------------------------------------- 1 | */ 18 | public function __invoke(Message $message): array 19 | { 20 | if ($message->hasHeader($this->header)) { 21 | return []; 22 | } 23 | 24 | return [$message]; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Message/Translator/FilterEventTranslator.php: -------------------------------------------------------------------------------- 1 | callable = $callable; 18 | } 19 | 20 | /** @return list */ 21 | public function __invoke(Message $message): array 22 | { 23 | $result = ($this->callable)($message->event()); 24 | 25 | if ($result) { 26 | return [$message]; 27 | } 28 | 29 | return []; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Message/Translator/IncludeEventTranslator.php: -------------------------------------------------------------------------------- 1 | $classes */ 12 | public function __construct( 13 | private readonly array $classes, 14 | ) { 15 | } 16 | 17 | /** @return list */ 18 | public function __invoke(Message $message): array 19 | { 20 | foreach ($this->classes as $class) { 21 | if ($message->event() instanceof $class) { 22 | return [$message]; 23 | } 24 | } 25 | 26 | return []; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Message/Translator/IncludeEventWithHeaderTranslator.php: -------------------------------------------------------------------------------- 1 | */ 18 | public function __invoke(Message $message): array 19 | { 20 | if ($message->hasHeader($this->header)) { 21 | return [$message]; 22 | } 23 | 24 | return []; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Message/Translator/ReplaceEventTranslator.php: -------------------------------------------------------------------------------- 1 | $class 17 | * @param callable(T $event):object $callable 18 | */ 19 | public function __construct( 20 | private readonly string $class, 21 | callable $callable, 22 | ) { 23 | $this->callable = $callable; 24 | } 25 | 26 | /** @return list */ 27 | public function __invoke(Message $message): array 28 | { 29 | $event = $message->event(); 30 | 31 | if (!$event instanceof $this->class) { 32 | return [$message]; 33 | } 34 | 35 | $callable = $this->callable; 36 | $newEvent = $callable($event); 37 | 38 | return [Message::createWithHeaders($newEvent, $message->headers())]; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Message/Translator/Translator.php: -------------------------------------------------------------------------------- 1 | */ 12 | public function __invoke(Message $message): array; 13 | } 14 | -------------------------------------------------------------------------------- /src/Message/Translator/UntilEventTranslator.php: -------------------------------------------------------------------------------- 1 | */ 20 | public function __invoke(Message $message): array 21 | { 22 | if ($message->hasHeader(AggregateHeader::class)) { 23 | $header = $message->header(AggregateHeader::class); 24 | 25 | if ($header->recordedOn < $this->until) { 26 | return [$message]; 27 | } 28 | 29 | return []; 30 | } 31 | 32 | if ($message->hasHeader(RecordedOnHeader::class)) { 33 | $header = $message->header(RecordedOnHeader::class); 34 | 35 | if ($header->recordedOn < $this->until) { 36 | return [$message]; 37 | } 38 | 39 | return []; 40 | } 41 | 42 | return [$message]; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Metadata/AggregateRoot/AggregateRootAlreadyInRegistry.php: -------------------------------------------------------------------------------- 1 | */ 19 | public readonly string $className, 20 | public readonly string $name, 21 | public readonly string $idProperty, 22 | /** @var array */ 23 | public readonly array $applyMethods, 24 | /** @var array */ 25 | public readonly array $suppressEvents, 26 | public readonly bool $suppressAll, 27 | public readonly Snapshot|null $snapshot, 28 | /** @var list */ 29 | public readonly array $childAggregates = [], 30 | string|null $streamName = null, 31 | ) { 32 | $this->streamName = $streamName ?? $this->name . '-{id}'; 33 | } 34 | 35 | public function streamName(string|null $aggregateId = null): string 36 | { 37 | if ($aggregateId === null) { 38 | if (str_contains($this->streamName, '{id}')) { 39 | throw new MissingAggregateIdForStreamName($this->streamName); 40 | } 41 | 42 | return $this->streamName; 43 | } 44 | 45 | return str_replace('{id}', $aggregateId, $this->streamName); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Metadata/AggregateRoot/AggregateRootMetadataAwareMetadataFactory.php: -------------------------------------------------------------------------------- 1 | $aggregate 16 | * 17 | * @return AggregateRootMetadata 18 | * 19 | * @template T of AggregateRoot 20 | */ 21 | public function metadata(string $aggregate): AggregateRootMetadata 22 | { 23 | if (!is_a($aggregate, AggregateRootMetadataAware::class, true)) { 24 | throw new AggregateWithoutMetadataAware($aggregate); 25 | } 26 | 27 | return $aggregate::metadata(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Metadata/AggregateRoot/AggregateRootMetadataFactory.php: -------------------------------------------------------------------------------- 1 | $aggregate 13 | * 14 | * @return AggregateRootMetadata 15 | * 16 | * @template T of AggregateRoot 17 | */ 18 | public function metadata(string $aggregate): AggregateRootMetadata; 19 | } 20 | -------------------------------------------------------------------------------- /src/Metadata/AggregateRoot/AggregateRootNameNotRegistered.php: -------------------------------------------------------------------------------- 1 | $paths */ 10 | public function create(array $paths): AggregateRootRegistry; 11 | } 12 | -------------------------------------------------------------------------------- /src/Metadata/AggregateRoot/AggregateWithoutMetadataAware.php: -------------------------------------------------------------------------------- 1 | $paths */ 19 | public function create(array $paths): AggregateRootRegistry 20 | { 21 | $classes = (new ClassFinder())->findClassNames($paths); 22 | 23 | $result = []; 24 | 25 | foreach ($classes as $class) { 26 | $reflection = new ReflectionClass($class); 27 | $attributes = $reflection->getAttributes(Aggregate::class); 28 | 29 | if (count($attributes) === 0) { 30 | continue; 31 | } 32 | 33 | if (!is_subclass_of($class, AggregateRoot::class)) { 34 | throw new NoAggregateRoot($class); 35 | } 36 | 37 | $aggregateName = $attributes[0]->newInstance()->name; 38 | 39 | if (array_key_exists($aggregateName, $result)) { 40 | throw new AggregateRootAlreadyInRegistry($aggregateName); 41 | } 42 | 43 | $result[$aggregateName] = $class; 44 | } 45 | 46 | return new AggregateRootRegistry($result); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Metadata/AggregateRoot/ClassIsNotAnAggregate.php: -------------------------------------------------------------------------------- 1 | $aggregateRootClass 16 | * @param class-string $event 17 | */ 18 | public function __construct(string $aggregateRootClass, string $event, string $fistMethod, string $secondMethod) 19 | { 20 | parent::__construct( 21 | sprintf( 22 | 'Two methods "%s" and "%s" on the aggregate "%s" want to apply the same event "%s". Only one method can apply an event.', 23 | $fistMethod, 24 | $secondMethod, 25 | $aggregateRootClass, 26 | $event, 27 | ), 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Metadata/AggregateRoot/DuplicateEmptyApplyAttribute.php: -------------------------------------------------------------------------------- 1 | $aggregate 20 | * 21 | * @return AggregateRootMetadata 22 | * 23 | * @template T of AggregateRoot 24 | */ 25 | public function metadata(string $aggregate): AggregateRootMetadata 26 | { 27 | /** @var ?AggregateRootMetadata $metadata */ 28 | $metadata = $this->cache->get($aggregate); 29 | 30 | if ($metadata !== null) { 31 | return $metadata; 32 | } 33 | 34 | $metadata = $this->aggregateRootMetadataFactory->metadata($aggregate); 35 | 36 | $this->cache->set($aggregate, $metadata); 37 | 38 | return $metadata; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Metadata/AggregateRoot/Psr16AggregateRootRegistryFactory.php: -------------------------------------------------------------------------------- 1 | $paths */ 20 | public function create(array $paths): AggregateRootRegistry 21 | { 22 | /** @var ?AggregateRootRegistry $registry */ 23 | $registry = $this->cache->get(self::CACHE_KEY); 24 | 25 | if ($registry !== null) { 26 | return $registry; 27 | } 28 | 29 | $registry = $this->aggregateRootRegistryFactory->create($paths); 30 | 31 | $this->cache->set(self::CACHE_KEY, $registry); 32 | 33 | return $registry; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Metadata/AggregateRoot/Psr6AggregateRootMetadataFactory.php: -------------------------------------------------------------------------------- 1 | $aggregate 20 | * 21 | * @return AggregateRootMetadata 22 | * 23 | * @template T of AggregateRoot 24 | */ 25 | public function metadata(string $aggregate): AggregateRootMetadata 26 | { 27 | $item = $this->cache->getItem($aggregate); 28 | 29 | if ($item->isHit()) { 30 | /** @var AggregateRootMetadata $data */ 31 | $data = $item->get(); 32 | 33 | return $data; 34 | } 35 | 36 | $metadata = $this->aggregateRootMetadataFactory->metadata($aggregate); 37 | 38 | $item->set($metadata); 39 | $this->cache->save($item); 40 | 41 | return $metadata; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Metadata/AggregateRoot/Psr6AggregateRootRegistryFactory.php: -------------------------------------------------------------------------------- 1 | $paths */ 22 | public function create(array $paths): AggregateRootRegistry 23 | { 24 | $item = $this->cache->getItem(self::CACHE_KEY); 25 | 26 | if ($item->isHit()) { 27 | $data = $item->get(); 28 | assert($data instanceof AggregateRootRegistry); 29 | 30 | return $data; 31 | } 32 | 33 | $registry = $this->aggregateRootRegistryFactory->create($paths); 34 | 35 | $item->set($registry); 36 | $this->cache->save($item); 37 | 38 | return $registry; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Metadata/AggregateRoot/Snapshot.php: -------------------------------------------------------------------------------- 1 | */ 17 | private array $eventMetadata = []; 18 | 19 | /** @param class-string $event */ 20 | public function metadata(string $event): EventMetadata 21 | { 22 | if (array_key_exists($event, $this->eventMetadata)) { 23 | return $this->eventMetadata[$event]; 24 | } 25 | 26 | $reflectionClass = new ReflectionClass($event); 27 | 28 | $attributeReflectionList = $reflectionClass->getAttributes(Event::class); 29 | 30 | if (!$attributeReflectionList) { 31 | throw new ClassIsNotAnEvent($event); 32 | } 33 | 34 | $eventAttribute = $attributeReflectionList[0]->newInstance(); 35 | 36 | $this->eventMetadata[$event] = new EventMetadata( 37 | $eventAttribute->name, 38 | $this->splitStream($reflectionClass), 39 | $eventAttribute->aliases, 40 | ); 41 | 42 | return $this->eventMetadata[$event]; 43 | } 44 | 45 | private function splitStream(ReflectionClass $reflectionClass): bool 46 | { 47 | return count($reflectionClass->getAttributes(SplitStream::class)) !== 0; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Metadata/Event/AttributeEventRegistryFactory.php: -------------------------------------------------------------------------------- 1 | $paths */ 17 | public function create(array $paths): EventRegistry 18 | { 19 | $classes = (new ClassFinder())->findClassNames($paths); 20 | 21 | $result = []; 22 | 23 | foreach ($classes as $class) { 24 | $reflection = new ReflectionClass($class); 25 | $attributes = $reflection->getAttributes(Event::class); 26 | 27 | if (count($attributes) === 0) { 28 | continue; 29 | } 30 | 31 | $attribute = $attributes[0]->newInstance(); 32 | 33 | if (array_key_exists($attribute->name, $result)) { 34 | throw new EventAlreadyInRegistry($attribute->name); 35 | } 36 | 37 | $result[$attribute->name] = $class; 38 | 39 | foreach ($attribute->aliases as $alias) { 40 | if (array_key_exists($alias, $result)) { 41 | throw new EventAlreadyInRegistry($alias); 42 | } 43 | 44 | $result[$alias] = $class; 45 | } 46 | } 47 | 48 | return new EventRegistry($result); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Metadata/Event/ClassIsNotAnEvent.php: -------------------------------------------------------------------------------- 1 | */ 13 | public readonly array $aliases = [], 14 | ) { 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Metadata/Event/EventMetadataFactory.php: -------------------------------------------------------------------------------- 1 | $paths */ 10 | public function create(array $paths): EventRegistry; 11 | } 12 | -------------------------------------------------------------------------------- /src/Metadata/Event/MissingDataSubjectId.php: -------------------------------------------------------------------------------- 1 | cache->get($event); 22 | 23 | if ($metadata !== null) { 24 | return $metadata; 25 | } 26 | 27 | $metadata = $this->eventMetadataFactory->metadata($event); 28 | 29 | $this->cache->set($event, $metadata); 30 | 31 | return $metadata; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Metadata/Event/Psr16EventRegistryFactory.php: -------------------------------------------------------------------------------- 1 | $paths */ 20 | public function create(array $paths): EventRegistry 21 | { 22 | /** @var ?EventRegistry $registry */ 23 | $registry = $this->cache->get(self::CACHE_KEY); 24 | 25 | if ($registry !== null) { 26 | return $registry; 27 | } 28 | 29 | $registry = $this->eventRegistryFactory->create($paths); 30 | 31 | $this->cache->set(self::CACHE_KEY, $registry); 32 | 33 | return $registry; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Metadata/Event/Psr6EventMetadataFactory.php: -------------------------------------------------------------------------------- 1 | cache->getItem($event); 23 | 24 | if ($item->isHit()) { 25 | $data = $item->get(); 26 | assert($data instanceof EventMetadata); 27 | 28 | return $data; 29 | } 30 | 31 | $metadata = $this->eventMetadataFactory->metadata($event); 32 | 33 | $item->set($metadata); 34 | $this->cache->save($item); 35 | 36 | return $metadata; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Metadata/Event/Psr6EventRegistryFactory.php: -------------------------------------------------------------------------------- 1 | $paths */ 22 | public function create(array $paths): EventRegistry 23 | { 24 | $item = $this->cache->getItem(self::CACHE_KEY); 25 | 26 | if ($item->isHit()) { 27 | $data = $item->get(); 28 | assert($data instanceof EventRegistry); 29 | 30 | return $data; 31 | } 32 | 33 | $registry = $this->eventRegistryFactory->create($paths); 34 | 35 | $item->set($registry); 36 | $this->cache->save($item); 37 | 38 | return $registry; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Metadata/Event/SubjectIdAndPersonalDataConflict.php: -------------------------------------------------------------------------------- 1 | $paths */ 16 | public function create(array $paths): MessageHeaderRegistry 17 | { 18 | $classes = (new ClassFinder())->findClassNames($paths); 19 | 20 | $result = []; 21 | 22 | foreach ($classes as $class) { 23 | $reflection = new ReflectionClass($class); 24 | $attributes = $reflection->getAttributes(Header::class); 25 | 26 | if (count($attributes) === 0) { 27 | continue; 28 | } 29 | 30 | $aggregateName = $attributes[0]->newInstance()->name; 31 | $result[$aggregateName] = $class; 32 | } 33 | 34 | return MessageHeaderRegistry::createWithInternalHeaders($result); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Metadata/Message/HeaderClassNotRegistered.php: -------------------------------------------------------------------------------- 1 | $paths */ 10 | public function create(array $paths): MessageHeaderRegistry; 11 | } 12 | -------------------------------------------------------------------------------- /src/Metadata/MetadataException.php: -------------------------------------------------------------------------------- 1 | cache->get($subscriber); 22 | 23 | if ($metadata !== null) { 24 | return $metadata; 25 | } 26 | 27 | $metadata = $this->subscriberMetadataFactory->metadata($subscriber); 28 | 29 | $this->cache->set($subscriber, $metadata); 30 | 31 | return $metadata; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Metadata/Subscriber/Psr6SubscriberMetadataFactory.php: -------------------------------------------------------------------------------- 1 | cache->getItem($subscriber); 23 | 24 | if ($item->isHit()) { 25 | $data = $item->get(); 26 | assert($data instanceof SubscriberMetadata); 27 | 28 | return $data; 29 | } 30 | 31 | $metadata = $this->subscriberMetadataFactory->metadata($subscriber); 32 | 33 | $item->set($metadata); 34 | $this->cache->save($item); 35 | 36 | return $metadata; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Metadata/Subscriber/SubscribeMethodMetadata.php: -------------------------------------------------------------------------------- 1 | $arguments */ 10 | public function __construct( 11 | public readonly string $name, 12 | public readonly array $arguments = [], 13 | ) { 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Metadata/Subscriber/SubscriberMetadata.php: -------------------------------------------------------------------------------- 1 | > */ 17 | public readonly array $subscribeMethods = [], 18 | public readonly string|null $setupMethod = null, 19 | public readonly string|null $teardownMethod = null, 20 | public readonly string|null $failedMethod = null, 21 | public readonly string|null $retryStrategy = null, 22 | ) { 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Metadata/Subscriber/SubscriberMetadataFactory.php: -------------------------------------------------------------------------------- 1 | $providers */ 10 | public function __construct( 11 | private readonly iterable $providers, 12 | ) { 13 | } 14 | 15 | /** 16 | * @param class-string $queryClass 17 | * 18 | * @return iterable 19 | */ 20 | public function handlerForQuery(string $queryClass): iterable 21 | { 22 | $handlers = []; 23 | 24 | foreach ($this->providers as $provider) { 25 | $handlers = [ 26 | ...$handlers, 27 | ...$provider->handlerForQuery($queryClass), 28 | ]; 29 | } 30 | 31 | return $handlers; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/QueryBus/HandlerDescriptor.php: -------------------------------------------------------------------------------- 1 | callable = $callable(...); 18 | $this->name = self::closureName($this->callable); 19 | } 20 | 21 | public function name(): string 22 | { 23 | return $this->name; 24 | } 25 | 26 | public function callable(): callable 27 | { 28 | return $this->callable; 29 | } 30 | 31 | private static function closureName(Closure $closure): string 32 | { 33 | $reflectionFunction = new ReflectionFunction($closure); 34 | 35 | if ($reflectionFunction->isAnonymous()) { 36 | return 'Closure'; 37 | } 38 | 39 | $closureThis = $reflectionFunction->getClosureThis(); 40 | 41 | if (!$closureThis) { 42 | $class = $reflectionFunction->getClosureCalledClass(); 43 | 44 | return ($class ? $class->name . '::' : '') . $reflectionFunction->name; 45 | } 46 | 47 | return $closureThis::class . '::' . $reflectionFunction->name; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/QueryBus/HandlerProvider.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | public function handlerForQuery(string $queryClass): iterable; 15 | } 16 | -------------------------------------------------------------------------------- /src/QueryBus/HandlerReference.php: -------------------------------------------------------------------------------- 1 | > */ 12 | private array $handlers = []; 13 | 14 | /** @param iterable $services */ 15 | public function __construct( 16 | private readonly iterable $services, 17 | ) { 18 | } 19 | 20 | /** 21 | * @param class-string $queryClass 22 | * 23 | * @return iterable 24 | */ 25 | public function handlerForQuery(string $queryClass): iterable 26 | { 27 | if (!$this->initialized) { 28 | $this->initialize(); 29 | } 30 | 31 | return $this->handlers[$queryClass] ?? []; 32 | } 33 | 34 | private function initialize(): void 35 | { 36 | foreach ($this->services as $service) { 37 | foreach (HandlerFinder::findInClass($service::class) as $handler) { 38 | if ($handler->static) { 39 | $this->handlers[$handler->queryClass][] = new HandlerDescriptor( 40 | $service::{$handler->method}(...), 41 | ); 42 | 43 | continue; 44 | } 45 | 46 | $this->handlers[$handler->queryClass][] = new HandlerDescriptor( 47 | $service->{$handler->method}(...), 48 | ); 49 | } 50 | } 51 | 52 | $this->initialized = true; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/QueryBus/SyncQueryBus.php: -------------------------------------------------------------------------------- 1 | |HandlerProvider $handlerProviders */ 18 | public function __construct( 19 | iterable|HandlerProvider $handlerProviders, 20 | private readonly LoggerInterface|null $logger = null, 21 | ) { 22 | if (!$handlerProviders instanceof HandlerProvider) { 23 | $this->handlerProvider = new ChainHandlerProvider($handlerProviders); 24 | } else { 25 | $this->handlerProvider = $handlerProviders; 26 | } 27 | } 28 | 29 | /** @throws InvalidQueryHandler */ 30 | public function dispatch(object $query): mixed 31 | { 32 | $this->logger?->debug('QueryBus: dispatch query', ['query' => $query::class]); 33 | 34 | $handlers = $this->handlerProvider->handlerForQuery($query::class); 35 | 36 | if (!is_array($handlers)) { 37 | $handlers = iterator_to_array($handlers); 38 | } 39 | 40 | $count = count($handlers); 41 | 42 | if ($count === 0) { 43 | throw InvalidQueryHandler::noHandler($query::class); 44 | } 45 | 46 | if ($count > 1) { 47 | throw InvalidQueryHandler::multipleHandler($query::class); 48 | } 49 | 50 | return ($handlers[0]->callable())($query); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Repository/AggregateAlreadyExists.php: -------------------------------------------------------------------------------- 1 | $aggregate */ 15 | public function __construct(string $aggregate, AggregateRootId $id) 16 | { 17 | parent::__construct( 18 | sprintf( 19 | 'aggregate %s with id %s already exists', 20 | $aggregate, 21 | $id->toString(), 22 | ), 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Repository/AggregateDetached.php: -------------------------------------------------------------------------------- 1 | $aggregateRootClass */ 15 | public function __construct(string $aggregateRootClass, AggregateRootId $aggregateRootId) 16 | { 17 | parent::__construct( 18 | sprintf( 19 | 'An error occurred while saving the aggregate "%s" with the ID "%s", causing the uncommitted events to be lost. Please reload the aggregate.', 20 | $aggregateRootClass, 21 | $aggregateRootId->toString(), 22 | ), 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Repository/AggregateNotFound.php: -------------------------------------------------------------------------------- 1 | toString())); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Repository/AggregateOutdated.php: -------------------------------------------------------------------------------- 1 | $aggregate */ 15 | public function __construct(string $aggregate, AggregateRootId $id) 16 | { 17 | parent::__construct( 18 | sprintf( 19 | 'Aggregate %s with id %s is outdated. There are new events in the store. Please reload the aggregate.', 20 | $aggregate, 21 | $id->toString(), 22 | ), 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Repository/AggregateUnknown.php: -------------------------------------------------------------------------------- 1 | $aggregateRootClass */ 15 | public function __construct(string $aggregateRootClass, AggregateRootId $aggregateRootId) 16 | { 17 | parent::__construct( 18 | sprintf( 19 | 'The aggregate %s with the ID "%s" was not loaded from this repository. Please reload the aggregate.', 20 | $aggregateRootClass, 21 | $aggregateRootId->toString(), 22 | ), 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Repository/MessageDecorator/ChainMessageDecorator.php: -------------------------------------------------------------------------------- 1 | $messageDecorators */ 12 | public function __construct( 13 | private readonly iterable $messageDecorators, 14 | ) { 15 | } 16 | 17 | public function __invoke(Message $message): Message 18 | { 19 | foreach ($this->messageDecorators as $messageDecorator) { 20 | $message = ($messageDecorator)($message); 21 | } 22 | 23 | return $message; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Repository/MessageDecorator/MessageDecorator.php: -------------------------------------------------------------------------------- 1 | event(); 21 | $metadata = $this->eventMetadataFactory->metadata($event::class); 22 | 23 | if (!$metadata->splitStream) { 24 | return $message; 25 | } 26 | 27 | return $message->withHeader(new StreamStartHeader()); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Repository/PlayheadMismatch.php: -------------------------------------------------------------------------------- 1 | toString(), 21 | )); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Repository/Repository.php: -------------------------------------------------------------------------------- 1 | $aggregateClass 13 | * 14 | * @return Repository 15 | * 16 | * @template T of AggregateRoot 17 | */ 18 | public function get(string $aggregateClass): Repository; 19 | } 20 | -------------------------------------------------------------------------------- /src/Repository/SnapshotRebuildFailed.php: -------------------------------------------------------------------------------- 1 | $aggregateRootClass */ 16 | public function __construct( 17 | private string $aggregateRootClass, 18 | private AggregateRootId $aggregateRootId, 19 | Throwable $previous, 20 | ) { 21 | parent::__construct( 22 | sprintf( 23 | 'Rebuild from snapshot of aggregate "%s" with the id "%s" failed', 24 | $aggregateRootClass, 25 | $aggregateRootId->toString(), 26 | ), 27 | 0, 28 | $previous, 29 | ); 30 | } 31 | 32 | /** @return class-string */ 33 | public function aggregateClass(): string 34 | { 35 | return $this->aggregateRootClass; 36 | } 37 | 38 | public function aggregateRootId(): AggregateRootId 39 | { 40 | return $this->aggregateRootId; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Repository/WrongAggregate.php: -------------------------------------------------------------------------------- 1 | $schemaConfigurator */ 13 | public function __construct( 14 | private readonly iterable $schemaConfigurator, 15 | ) { 16 | } 17 | 18 | public function configureSchema(Schema $schema, Connection $connection): void 19 | { 20 | foreach ($this->schemaConfigurator as $schemaConfigurator) { 21 | $schemaConfigurator->configureSchema($schema, $connection); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Schema/DoctrineMigrationSchemaProvider.php: -------------------------------------------------------------------------------- 1 | doctrineSchemaProvider->schema(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Schema/DoctrineSchemaConfigurator.php: -------------------------------------------------------------------------------- 1 | schemaConfigurator->configureSchema( 19 | $event->getSchema(), 20 | $event->getEntityManager()->getConnection(), 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Schema/DoctrineSchemaProvider.php: -------------------------------------------------------------------------------- 1 | schemaConfigurator->configureSchema( 24 | $event->getSchema(), 25 | $event->getEntityManager()->getConnection(), 26 | ); 27 | } 28 | 29 | /** @return list */ 30 | public function getSubscribedEvents(): array 31 | { 32 | $subscribedEvents = []; 33 | 34 | if (class_exists(ToolEvents::class)) { 35 | $subscribedEvents[] = ToolEvents::postGenerateSchema; 36 | } 37 | 38 | return $subscribedEvents; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Schema/DryRunSchemaDirector.php: -------------------------------------------------------------------------------- 1 | */ 10 | public function dryRunCreate(): array; 11 | 12 | /** @return list */ 13 | public function dryRunUpdate(): array; 14 | 15 | /** @return list */ 16 | public function dryRunDrop(): array; 17 | } 18 | -------------------------------------------------------------------------------- /src/Schema/SchemaDirector.php: -------------------------------------------------------------------------------- 1 | data; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Serializer/Encoder/EncodeNotPossible.php: -------------------------------------------------------------------------------- 1 | $data */ 13 | public function __construct( 14 | private array $data, 15 | Throwable|null $previous = null, 16 | ) { 17 | parent::__construct( 18 | 'serialization is not possible', 19 | 0, 20 | $previous, 21 | ); 22 | } 23 | 24 | /** @return array */ 25 | public function data(): array 26 | { 27 | return $this->data; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Serializer/Encoder/Encoder.php: -------------------------------------------------------------------------------- 1 | $data 13 | * @param array $options 14 | * 15 | * @throws EncodeNotPossible 16 | */ 17 | public function encode(array $data, array $options = []): string; 18 | 19 | /** 20 | * @param array $options 21 | * 22 | * @return array 23 | * 24 | * @throws DecodeNotPossible 25 | */ 26 | public function decode(string $data, array $options = []): array; 27 | } 28 | -------------------------------------------------------------------------------- /src/Serializer/Encoder/JsonEncoder.php: -------------------------------------------------------------------------------- 1 | $data 20 | * @param array $options 21 | */ 22 | public function encode(array $data, array $options = []): string 23 | { 24 | $flags = JSON_THROW_ON_ERROR; 25 | 26 | if (array_key_exists(self::OPTION_PRETTY_PRINT, $options) && $options[self::OPTION_PRETTY_PRINT] === true) { 27 | $flags |= JSON_PRETTY_PRINT; 28 | } 29 | 30 | try { 31 | return json_encode($data, $flags); 32 | } catch (JsonException $e) { 33 | throw new EncodeNotPossible($data, $e); 34 | } 35 | } 36 | 37 | /** 38 | * @param array $options 39 | * 40 | * @return array 41 | */ 42 | public function decode(string $data, array $options = []): array 43 | { 44 | try { 45 | /** @var array $result */ 46 | $result = json_decode($data, true, 512, JSON_THROW_ON_ERROR); 47 | 48 | return $result; 49 | } catch (JsonException $e) { 50 | throw new DecodeNotPossible($data, $e); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Serializer/EventSerializer.php: -------------------------------------------------------------------------------- 1 | $options */ 10 | public function serialize(object $event, array $options = []): SerializedEvent; 11 | 12 | /** @param array $options */ 13 | public function deserialize(SerializedEvent $data, array $options = []): object; 14 | } 15 | -------------------------------------------------------------------------------- /src/Serializer/SerializeException.php: -------------------------------------------------------------------------------- 1 | $payload */ 11 | public function __construct( 12 | public readonly string $eventName, 13 | public readonly array $payload, 14 | ) { 15 | } 16 | 17 | public function replaceEventName(string $eventName): self 18 | { 19 | return new self($eventName, $this->payload); 20 | } 21 | 22 | /** @param array $payload */ 23 | public function replacePayload(array $payload): self 24 | { 25 | return new self($this->eventName, $payload); 26 | } 27 | 28 | public function replacePayloadByKey(string $key, mixed $data): self 29 | { 30 | $payload = $this->payload; 31 | $payload[$key] = $data; 32 | 33 | return new self($this->eventName, $payload); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Serializer/Upcast/Upcaster.php: -------------------------------------------------------------------------------- 1 | $upcaster */ 10 | public function __construct( 11 | private readonly iterable $upcaster, 12 | ) { 13 | } 14 | 15 | public function __invoke(Upcast $upcast): Upcast 16 | { 17 | foreach ($this->upcaster as $upcaster) { 18 | $upcast = $upcaster($upcast); 19 | } 20 | 21 | return $upcast; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Snapshot/Adapter/InMemorySnapshotAdapter.php: -------------------------------------------------------------------------------- 1 | > */ 12 | private array $snapshots = []; 13 | 14 | /** @param array $data */ 15 | public function save(string $key, array $data): void 16 | { 17 | $this->snapshots[$key] = $data; 18 | } 19 | 20 | /** @return array */ 21 | public function load(string $key): array 22 | { 23 | if (!array_key_exists($key, $this->snapshots)) { 24 | throw new SnapshotNotFound($key); 25 | } 26 | 27 | return $this->snapshots[$key]; 28 | } 29 | 30 | public function clear(): void 31 | { 32 | $this->snapshots = []; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Snapshot/Adapter/Psr16SnapshotAdapter.php: -------------------------------------------------------------------------------- 1 | $data */ 17 | public function save(string $key, array $data): void 18 | { 19 | $this->cache->set($key, $data); 20 | } 21 | 22 | /** @return array */ 23 | public function load(string $key): array 24 | { 25 | /** @var ?array $data */ 26 | $data = $this->cache->get($key); 27 | 28 | if ($data === null) { 29 | throw new SnapshotNotFound($key); 30 | } 31 | 32 | return $data; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Snapshot/Adapter/Psr6SnapshotAdapter.php: -------------------------------------------------------------------------------- 1 | $data */ 17 | public function save(string $key, array $data): void 18 | { 19 | $item = $this->cache->getItem($key); 20 | $item->set($data); 21 | $this->cache->save($item); 22 | } 23 | 24 | /** 25 | * @return array 26 | * 27 | * @throws SnapshotNotFound 28 | */ 29 | public function load(string $key): array 30 | { 31 | $item = $this->cache->getItem($key); 32 | 33 | if (!$item->isHit()) { 34 | throw new SnapshotNotFound($key); 35 | } 36 | 37 | /** @var array $data */ 38 | $data = $item->get(); 39 | 40 | return $data; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Snapshot/Adapter/SnapshotAdapter.php: -------------------------------------------------------------------------------- 1 | $data */ 10 | public function save(string $key, array $data): void; 11 | 12 | /** 13 | * @return array 14 | * 15 | * @throws SnapshotNotFound 16 | */ 17 | public function load(string $key): array; 18 | } 19 | -------------------------------------------------------------------------------- /src/Snapshot/Adapter/SnapshotNotFound.php: -------------------------------------------------------------------------------- 1 | $snapshotAdapters */ 14 | public function __construct( 15 | private readonly array $snapshotAdapters, 16 | ) { 17 | } 18 | 19 | public function get(string $name): SnapshotAdapter 20 | { 21 | if (!array_key_exists($name, $this->snapshotAdapters)) { 22 | throw new AdapterNotFound($name); 23 | } 24 | 25 | return $this->snapshotAdapters[$name]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Snapshot/Snapshot.php: -------------------------------------------------------------------------------- 1 | $aggregate 13 | * @param array $payload 14 | */ 15 | public function __construct( 16 | private readonly string $aggregate, 17 | private readonly string $id, 18 | private readonly int $playhead, 19 | private readonly array $payload, 20 | ) { 21 | } 22 | 23 | /** @return class-string */ 24 | public function aggregate(): string 25 | { 26 | return $this->aggregate; 27 | } 28 | 29 | public function id(): string 30 | { 31 | return $this->id; 32 | } 33 | 34 | public function playhead(): int 35 | { 36 | return $this->playhead; 37 | } 38 | 39 | /** @return array */ 40 | public function payload(): array 41 | { 42 | return $this->payload; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Snapshot/SnapshotException.php: -------------------------------------------------------------------------------- 1 | $aggregateRootClass */ 14 | public function __construct(string $aggregateRootClass) 15 | { 16 | parent::__construct( 17 | sprintf( 18 | 'Missing snapshot configuration for the aggregate class "%s"', 19 | $aggregateRootClass, 20 | ), 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Snapshot/SnapshotNotFound.php: -------------------------------------------------------------------------------- 1 | $aggregateRootClass */ 16 | public function __construct(string $aggregateRootClass, AggregateRootId $rootId, Throwable|null $previous = null) 17 | { 18 | parent::__construct( 19 | sprintf( 20 | 'snapshot for aggregate "%s" with the id "%s" not found', 21 | $aggregateRootClass, 22 | $rootId->toString(), 23 | ), 24 | 0, 25 | $previous, 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Snapshot/SnapshotStore.php: -------------------------------------------------------------------------------- 1 | $aggregateClass 21 | * 22 | * @return T 23 | * 24 | * @throws SnapshotNotFound 25 | * @throws SnapshotVersionInvalid 26 | * @throws SnapshotNotConfigured 27 | * @throws AdapterNotFound 28 | * 29 | * @template T of AggregateRoot 30 | */ 31 | public function load(string $aggregateClass, AggregateRootId $id): AggregateRoot; 32 | } 33 | -------------------------------------------------------------------------------- /src/Snapshot/SnapshotVersionInvalid.php: -------------------------------------------------------------------------------- 1 | */ 13 | private readonly array $criteria; 14 | 15 | public function __construct(object ...$criteria) 16 | { 17 | $result = []; 18 | 19 | foreach ($criteria as $criterion) { 20 | $result[$criterion::class] = $criterion; 21 | } 22 | 23 | $this->criteria = $result; 24 | } 25 | 26 | /** 27 | * @param class-string $criteriaClass 28 | * 29 | * @return T 30 | * 31 | * @template T of object 32 | */ 33 | public function get(string $criteriaClass): object 34 | { 35 | if (!array_key_exists($criteriaClass, $this->criteria)) { 36 | throw new CriterionNotFound($criteriaClass); 37 | } 38 | 39 | return $this->criteria[$criteriaClass]; 40 | } 41 | 42 | public function has(string $criteriaClass): bool 43 | { 44 | return array_key_exists($criteriaClass, $this->criteria); 45 | } 46 | 47 | /** @return list */ 48 | public function all(): array 49 | { 50 | return array_values($this->criteria); 51 | } 52 | 53 | public function add(object $criteria): self 54 | { 55 | return new self( 56 | ...$this->all(), 57 | ...[$criteria], 58 | ); 59 | } 60 | 61 | public function remove(string $criteriaClass): self 62 | { 63 | $criteria = $this->criteria; 64 | unset($criteria[$criteriaClass]); 65 | 66 | return new self(...$criteria); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Store/Criteria/CriterionNotFound.php: -------------------------------------------------------------------------------- 1 | */ 11 | public readonly array $events, 12 | ) { 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Store/Criteria/FromIndexCriterion.php: -------------------------------------------------------------------------------- 1 | */ 10 | public readonly array $streamName; 11 | 12 | public function __construct( 13 | string ...$streamName, 14 | ) { 15 | $this->streamName = $streamName; 16 | } 17 | 18 | public static function startWith(string $streamName): self 19 | { 20 | return new self($streamName . '*'); 21 | } 22 | 23 | public function all(): bool 24 | { 25 | foreach ($this->streamName as $name) { 26 | if ($name === '*') { 27 | return true; 28 | } 29 | } 30 | 31 | return false; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Store/Criteria/ToIndexCriterion.php: -------------------------------------------------------------------------------- 1 | store instanceof StreamStore) { 20 | throw new InvalidArgumentException('store must not be a StreamStore. use StreamReadOnlyStore instead'); 21 | } 22 | } 23 | 24 | public function load( 25 | Criteria|null $criteria = null, 26 | int|null $limit = null, 27 | int|null $offset = null, 28 | bool $backwards = false, 29 | ): Stream { 30 | return $this->store->load($criteria, $limit, $offset, $backwards); 31 | } 32 | 33 | public function count(Criteria|null $criteria = null): int 34 | { 35 | return $this->store->count($criteria); 36 | } 37 | 38 | public function save(Message ...$messages): void 39 | { 40 | foreach ($messages as $message) { 41 | $this->logger?->info('tried to save message in read only store', ['message' => $message]); 42 | } 43 | 44 | throw new StoreIsReadOnly(); 45 | } 46 | 47 | public function transactional(Closure $function): void 48 | { 49 | $this->store->transactional($function); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Store/Store.php: -------------------------------------------------------------------------------- 1 | */ 11 | interface Stream extends Traversable 12 | { 13 | public function close(): void; 14 | 15 | /** @throws StreamClosed */ 16 | public function next(): void; 17 | 18 | /** @throws StreamClosed */ 19 | public function current(): Message|null; 20 | 21 | /** @throws StreamClosed */ 22 | public function end(): bool; 23 | 24 | /** 25 | * @return positive-int|0|null 26 | * 27 | * @throws StreamClosed 28 | */ 29 | public function position(): int|null; 30 | 31 | /** 32 | * @return positive-int|null 33 | * 34 | * @throws StreamClosed 35 | */ 36 | public function index(): int|null; 37 | } 38 | -------------------------------------------------------------------------------- /src/Store/StreamClosed.php: -------------------------------------------------------------------------------- 1 | */ 12 | public function streams(): array; 13 | 14 | public function remove(Criteria|null $criteria = null): void; 15 | 16 | public function archive(Criteria|null $criteria = null): void; 17 | } 18 | -------------------------------------------------------------------------------- /src/Store/SubscriptionStore.php: -------------------------------------------------------------------------------- 1 | $errors */ 16 | public function __construct( 17 | public readonly array $errors, 18 | ) { 19 | $sentences = array_map( 20 | static fn (Error $error) => sprintf( 21 | 'Subscription %s: %s', 22 | $error->subscriptionId, 23 | $error->message, 24 | ), 25 | $errors, 26 | ); 27 | 28 | parent::__construct("Error in subscription engine detected.\n" . implode("\n", $sentences)); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Subscription/Engine/MessageLoader.php: -------------------------------------------------------------------------------- 1 | $subscriptions */ 13 | public function load(int $startIndex, array $subscriptions): Stream; 14 | 15 | public function lastIndex(): int; 16 | } 17 | -------------------------------------------------------------------------------- /src/Subscription/Engine/ProcessedResult.php: -------------------------------------------------------------------------------- 1 | $errors */ 10 | public function __construct( 11 | public readonly int $processedMessages, 12 | public readonly bool $finished = false, 13 | public readonly array $errors = [], 14 | ) { 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Subscription/Engine/Result.php: -------------------------------------------------------------------------------- 1 | $errors */ 10 | public function __construct( 11 | public readonly array $errors = [], 12 | ) { 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Subscription/Engine/StoreMessageLoader.php: -------------------------------------------------------------------------------- 1 | $subscriptions */ 21 | public function load(int $startIndex, array $subscriptions): Stream 22 | { 23 | return $this->store->load(new Criteria(new FromIndexCriterion($startIndex))); 24 | } 25 | 26 | public function lastIndex(): int 27 | { 28 | $stream = $this->store->load(null, 1, null, true); 29 | 30 | return $stream->index() ?: 0; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Subscription/Engine/SubscriberNotFound.php: -------------------------------------------------------------------------------- 1 | */ 44 | public function subscriptions(SubscriptionEngineCriteria|null $criteria = null): array; 45 | } 46 | -------------------------------------------------------------------------------- /src/Subscription/Engine/SubscriptionEngineCriteria.php: -------------------------------------------------------------------------------- 1 | |null $ids 12 | * @param list|null $groups 13 | */ 14 | public function __construct( 15 | public readonly array|null $ids = null, 16 | public readonly array|null $groups = null, 17 | ) { 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Subscription/Engine/UnexpectedError.php: -------------------------------------------------------------------------------- 1 | |null $ids 16 | * @param list|null $groups 17 | * @param positive-int|null $limit 18 | */ 19 | public function __construct( 20 | private readonly RepositoryManager $repositoryManager, 21 | private readonly SubscriptionEngine $engine, 22 | private readonly array|null $ids = null, 23 | private readonly array|null $groups = null, 24 | private readonly int|null $limit = null, 25 | ) { 26 | } 27 | 28 | /** 29 | * @param class-string $aggregateClass 30 | * 31 | * @return Repository 32 | * 33 | * @template T of AggregateRoot 34 | */ 35 | public function get(string $aggregateClass): Repository 36 | { 37 | return new RunSubscriptionEngineRepository( 38 | $this->repositoryManager->get($aggregateClass), 39 | $this->engine, 40 | $this->ids, 41 | $this->groups, 42 | $this->limit, 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Subscription/RetryStrategy/ConditionalRetryStrategy.php: -------------------------------------------------------------------------------- 1 | $strategies */ 12 | public function __construct( 13 | private readonly array $strategies, 14 | private readonly string $defaultStrategy = self::DEFAULT_STRATEGY_NAME, 15 | ) { 16 | } 17 | 18 | public function get(string $name): RetryStrategy 19 | { 20 | if (!isset($this->strategies[$name])) { 21 | throw new RetryStrategyNotFound($name); 22 | } 23 | 24 | return $this->strategies[$name]; 25 | } 26 | 27 | public function getDefaultRetryStrategy(): RetryStrategy 28 | { 29 | return $this->get($this->defaultStrategy); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Subscription/RetryStrategy/UnexpectedError.php: -------------------------------------------------------------------------------- 1 | |null $ids 14 | * @param list|null $groups 15 | * @param list|null $status 16 | */ 17 | public function __construct( 18 | public readonly array|null $ids = null, 19 | public readonly array|null $groups = null, 20 | public readonly array|null $status = null, 21 | ) { 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Subscription/Store/SubscriptionNotFound.php: -------------------------------------------------------------------------------- 1 | */ 15 | public function find(SubscriptionCriteria|null $criteria = null): array; 16 | 17 | /** @throws SubscriptionAlreadyExists */ 18 | public function add(Subscription $subscription): void; 19 | 20 | /** @throws SubscriptionNotFound */ 21 | public function update(Subscription $subscription): void; 22 | 23 | /** @throws SubscriptionNotFound */ 24 | public function remove(Subscription $subscription): void; 25 | } 26 | -------------------------------------------------------------------------------- /src/Subscription/Store/TransactionCommitNotPossible.php: -------------------------------------------------------------------------------- 1 | $class */ 21 | $class = $argument->type; 22 | $id = $message->header(AggregateHeader::class)->aggregateId; 23 | 24 | return $class::fromString($id); 25 | } 26 | 27 | public function support(ArgumentMetadata $argument, string $eventClass): bool 28 | { 29 | return class_exists($argument->type) && is_a($argument->type, AggregateRootId::class, true); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Subscription/Subscriber/ArgumentResolver/ArgumentResolver.php: -------------------------------------------------------------------------------- 1 | event(); 18 | } 19 | 20 | public function support(ArgumentMetadata $argument, string $eventClass): bool 21 | { 22 | return class_exists($argument->type) && is_a($eventClass, $argument->type, true); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Subscription/Subscriber/ArgumentResolver/LookupResolver.php: -------------------------------------------------------------------------------- 1 | store, 25 | $message, 26 | $this->eventRegistry, 27 | ); 28 | } 29 | 30 | public function support(ArgumentMetadata $argument, string $eventClass): bool 31 | { 32 | return $argument->type === Lookup::class; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Subscription/Subscriber/ArgumentResolver/MessageArgumentResolver.php: -------------------------------------------------------------------------------- 1 | type === Message::class; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Subscription/Subscriber/ArgumentResolver/RecordedOnArgumentResolver.php: -------------------------------------------------------------------------------- 1 | header(AggregateHeader::class)->recordedOn; 17 | } 18 | 19 | public function support(ArgumentMetadata $argument, string $eventClass): bool 20 | { 21 | return $argument->type === DateTimeImmutable::class; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Subscription/Subscriber/BatchableSubscriber.php: -------------------------------------------------------------------------------- 1 | 28 | */ 29 | public function subscribeMethods(string $eventClass): array; 30 | } 31 | -------------------------------------------------------------------------------- /src/Subscription/Subscriber/SubscriberAccessorRepository.php: -------------------------------------------------------------------------------- 1 | */ 10 | public function all(): iterable; 11 | 12 | public function get(string $id): SubscriberAccessor|null; 13 | } 14 | -------------------------------------------------------------------------------- /src/Subscription/Subscriber/SubscriberHelper.php: -------------------------------------------------------------------------------- 1 | metadata($subscriber)->id; 21 | } 22 | 23 | private function metadata(object $subscriber): SubscriberMetadata 24 | { 25 | return $this->metadataFactory->metadata($subscriber::class); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Subscription/Subscriber/SubscriberUtil.php: -------------------------------------------------------------------------------- 1 | subscriberHelper()->subscriberId($this); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Subscription/SubscriptionError.php: -------------------------------------------------------------------------------- 1 | } 11 | * @psalm-type Context = array{namespace: string, short_name: string, class: class-string, message: string, code: int|string, file: string, line: int, trace: list} 12 | */ 13 | final class SubscriptionError 14 | { 15 | /** @param list|null $errorContext */ 16 | public function __construct( 17 | public readonly string $errorMessage, 18 | public readonly Status $previousStatus, 19 | public readonly array|null $errorContext = null, 20 | ) { 21 | } 22 | 23 | public static function fromThrowable(Status $status, Throwable $error): self 24 | { 25 | return new self($error->getMessage(), $status, ThrowableToErrorContextTransformer::transform($error)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Test/IncrementalRamseyUuidFactory.php: -------------------------------------------------------------------------------- 1 | counter; 24 | 25 | $string = sprintf( 26 | '10000000-7000-0000-0000-%s', 27 | str_pad((string)$number, 12, '0', STR_PAD_LEFT), 28 | ); 29 | 30 | return Uuid::fromString($string); 31 | } 32 | 33 | public function reset(): void 34 | { 35 | $this->counter = 0; 36 | } 37 | } 38 | --------------------------------------------------------------------------------