diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 8fa1a3b0..01fc51f6 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -33,3 +33,4 @@ /src/SonsOfPHP/Component/Queue @JoshuaEstes /src/SonsOfPHP/Component/Version @JoshuaEstes /src/SonsOfPHP/Contract/Core @JoshuaEstes +/src/SonsOfPHP/Contract/EventSourcing @JoshuaEstes diff --git a/.github/labeler.yml b/.github/labeler.yml index d4062aca..62bb4d48 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -56,3 +56,4 @@ Contracts: documentation: - docs/** + - mkdocs.yml diff --git a/docs/components/event-sourcing/aggregates/aggregate-id.md b/docs/components/event-sourcing/aggregates/aggregate-id.md new file mode 100644 index 00000000..0e22a2bf --- /dev/null +++ b/docs/components/event-sourcing/aggregates/aggregate-id.md @@ -0,0 +1,65 @@ +--- +title: Aggregate IDs +--- + +# Aggregate IDs + +Each Aggregate will need to have it's own unique ID. You can use the same +AggregateId class for all your aggregates or you can make you own for each one +to ensure data consistency. + +### Working with an Aggregate ID + +```php +toString(); +$id = (string) $aggregateId; + +// To compare two Aggregate IDs +if ($aggregateId->equals($anotherAggregateId)) { + // they are the same +} +``` + +If you need to create an Aggregate ID Class in order to use type-hinting, just +extend the `AbstractAggregateId` class. + +```php +toString()); // true + +// It also still supports setting the ID via the constructor +$ulid = new AggregateId((string) new Ulid()); +``` diff --git a/docs/components/event-sourcing/aggregates/aggregate-version.md b/docs/components/event-sourcing/aggregates/aggregate-version.md new file mode 100644 index 00000000..a6db60e4 --- /dev/null +++ b/docs/components/event-sourcing/aggregates/aggregate-version.md @@ -0,0 +1,26 @@ +--- +title: Aggregate Versions +--- + +# Aggregate Version + +An Aggregate also has a version. Each event that is raised will increase the +version. You will generally not have to work much with versions as they are +mostly handled internally. + +### Working with an Aggregate Version + +```php +toInt(); + +// Comparing Versions +if ($aggregateVersion->equals($anotherAggregateVersion)) { + // they are the same +} +``` diff --git a/docs/components/event-sourcing/aggregates/index.md b/docs/components/event-sourcing/aggregates/index.md index 1e931979..f14c83d6 100644 --- a/docs/components/event-sourcing/aggregates/index.md +++ b/docs/components/event-sourcing/aggregates/index.md @@ -6,90 +6,6 @@ title: Aggregates Aggregates are the primary objects that you will be working with. -## Aggregate ID - -Each Aggregate will need to have it's own unique ID. You can use the same -AggregateId class for all your aggregates or you can make you own for each one -to ensure data consistency. - -### Working with an Aggregate ID - -```php -toString(); -$id = (string) $aggregateId; - -// To compare two Aggregate IDs -if ($aggregateId->equals($anotherAggregateId)) { - // they are the same -} -``` - -If you need to create an Aggregate ID Class in order to use type-hinting, just -extend the `AbstractAggregateId` class. - -```php -toString()); // true - -// It also still supports setting the ID via the constructor -$ulid = new AggregateId((string) new Ulid()); -``` - - -## Aggregate Version - -An Aggregate also has a version. Each event that is raised will increase the -version. You will generally not have to work much with versions as they are -mostly handled internally. - -### Working with an Aggregate Version - -```php -toInt(); - -// Comparing Versions -if ($aggregateVersion->equals($anotherAggregateVersion)) { - // they are the same -} -``` - ## Creating an Aggregate Pretty simple to create an aggregate. diff --git a/docs/contracts/index.md b/docs/contracts/index.md index 7a160344..57600bb3 100644 --- a/docs/contracts/index.md +++ b/docs/contracts/index.md @@ -8,7 +8,7 @@ Contracts are interfaces that can be reusable across multiple different libraries and projects. The actual implementation of the interfaces is left up to you. -The interfaces also enhance existing PSRs. +The interfaces may also enhance existing PSRs. Whenever possible, the components and projects created by Sons of PHP will implement these interfaces. diff --git a/mkdocs.yml b/mkdocs.yml index 01d84f2f..12affce6 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -71,6 +71,8 @@ nav: - components/event-sourcing/index.md - Aggregates: - components/event-sourcing/aggregates/index.md + - Aggregate ID: components/event-sourcing/aggregates/aggregate-id.md + - Aggregate Version: components/event-sourcing/aggregates/aggregate-version.md - Storage: - components/event-sourcing/aggregates/storage/index.md - Event Messages: diff --git a/src/SonsOfPHP/Component/EventSourcing/Aggregate/AbstractAggregate.php b/src/SonsOfPHP/Component/EventSourcing/Aggregate/AbstractAggregate.php index fbd98229..6ade4968 100644 --- a/src/SonsOfPHP/Component/EventSourcing/Aggregate/AbstractAggregate.php +++ b/src/SonsOfPHP/Component/EventSourcing/Aggregate/AbstractAggregate.php @@ -27,23 +27,16 @@ final public function __construct(AggregateIdInterface|string $id) } /** - * @param AggregateIdInterface|string $id - * - * @return static + * {@inheritdoc} */ - final public static function new($id) - { - @trigger_error(sprintf('"%s::new()" is deprecated, use "new %s()" instead.', static::class, static::class), \E_USER_DEPRECATED); - $static = new static($id); - - return $static; - } - final public function getAggregateId(): AggregateIdInterface { return $this->id; } + /** + * {@inheritdoc} + */ final public function getAggregateVersion(): AggregateVersionInterface { return $this->version; @@ -59,6 +52,9 @@ final public function hasPendingEvents(): bool return \count($this->pendingEvents) > 0; } + /** + * {@inheritdoc} + */ final public function getPendingEvents(): iterable { $events = $this->pendingEvents; @@ -67,12 +63,18 @@ final public function getPendingEvents(): iterable return $events; } + /** + * {@inheritdoc} + */ final public function peekPendingEvents(): iterable { return $this->pendingEvents; } - final public static function buildFromEvents(AggregateIdInterface $id, \Generator $events): AggregateInterface + /** + * {@inheritdoc} + */ + final public static function buildFromEvents(AggregateIdInterface $id, iterable $events): AggregateInterface { $aggregate = new static($id); foreach ($events as $event) { @@ -124,7 +126,7 @@ final protected function applyEvent(MessageInterface $event): void $method = 'apply' . end($parts); if (method_exists($this, $method)) { - $this->{$method}($event); // @phpstan-ignore-line + $this->{$method}($event); } $this->version = $this->version->next(); diff --git a/src/SonsOfPHP/Component/EventSourcing/Aggregate/AbstractAggregateId.php b/src/SonsOfPHP/Component/EventSourcing/Aggregate/AbstractAggregateId.php index 96af6b80..1216d27f 100644 --- a/src/SonsOfPHP/Component/EventSourcing/Aggregate/AbstractAggregateId.php +++ b/src/SonsOfPHP/Component/EventSourcing/Aggregate/AbstractAggregateId.php @@ -24,21 +24,30 @@ public function __construct( } } + /** + * {@inheritdoc} + */ final public function __toString(): string { return $this->toString(); } - final public function toString(): string + final public static function fromString(string $id): AggregateIdInterface { - return $this->id; + return new static($id); } - final public static function fromString(string $id): AggregateIdInterface + /** + * {@inheritdoc} + */ + final public function toString(): string { - return new static($id); + return $this->id; } + /** + * {@inheritdoc} + */ final public function equals(AggregateIdInterface $that): bool { return $this->toString() === $that->toString(); diff --git a/src/SonsOfPHP/Component/EventSourcing/Aggregate/AggregateIdInterface.php b/src/SonsOfPHP/Component/EventSourcing/Aggregate/AggregateIdInterface.php index 85a40f59..df3b2cb7 100644 --- a/src/SonsOfPHP/Component/EventSourcing/Aggregate/AggregateIdInterface.php +++ b/src/SonsOfPHP/Component/EventSourcing/Aggregate/AggregateIdInterface.php @@ -32,22 +32,11 @@ public function __toString(): string; */ public function toString(): string; - /** - * Creates an instance of AggregateIdInterface with the passed in - * value. - * - * Example: - * $id = AggregateId::fromString('unique-uuid'); - * - * @throws EventSourcingException - */ - public static function fromString(string $id): self; - /** * Compares two Aggregate ID objects and returns true if they are * equal. * * @throws EventSourcingException */ - public function equals(self $that): bool; + public function equals(AggregateIdInterface $that): bool; } diff --git a/src/SonsOfPHP/Component/EventSourcing/Aggregate/AggregateInterface.php b/src/SonsOfPHP/Component/EventSourcing/Aggregate/AggregateInterface.php index f69bab38..d5f04cfc 100644 --- a/src/SonsOfPHP/Component/EventSourcing/Aggregate/AggregateInterface.php +++ b/src/SonsOfPHP/Component/EventSourcing/Aggregate/AggregateInterface.php @@ -47,9 +47,9 @@ public function peekPendingEvents(): iterable; /** * Build Aggregate from a collection of Domain Events. * - * @param \Generator $events yields MessageInterface objects + * @param iterable $events yields MessageInterface objects * * @throws EventSourcingException */ - public static function buildFromEvents(AggregateIdInterface $id, \Generator $events): self; + public static function buildFromEvents(AggregateIdInterface $id, iterable $events): self; } diff --git a/src/SonsOfPHP/Component/EventSourcing/Aggregate/SnapshotableAggregateInterface.php b/src/SonsOfPHP/Component/EventSourcing/Aggregate/SnapshotableAggregateInterface.php index dea67c7e..f108d700 100644 --- a/src/SonsOfPHP/Component/EventSourcing/Aggregate/SnapshotableAggregateInterface.php +++ b/src/SonsOfPHP/Component/EventSourcing/Aggregate/SnapshotableAggregateInterface.php @@ -16,9 +16,15 @@ */ interface SnapshotableAggregateInterface extends AggregateInterface { + /** + */ public function createSnapshot(): SnapshotInterface; + /** + */ public static function buildFromSnapshot(SnapshotInterface $snapshot): self; - public static function buildFromSnapshotAndEvents(SnapshotInterface $snapshot, \Generator $events): self; + /** + */ + public static function buildFromSnapshotAndEvents(SnapshotInterface $snapshot, iterable $events): self; } diff --git a/src/SonsOfPHP/Component/EventSourcing/AggregateClassMetadata.php b/src/SonsOfPHP/Component/EventSourcing/AggregateClassMetadata.php new file mode 100644 index 00000000..b723a6c5 --- /dev/null +++ b/src/SonsOfPHP/Component/EventSourcing/AggregateClassMetadata.php @@ -0,0 +1,42 @@ + + */ +final class AggregateClassMetadata implements AggregateClassMetadataInterface +{ + public function __construct( + private string $name, + ) {} + + public function getName(): string + { + return $this->name; + } + + public function getReflectionClass(): \ReflectionClass + { + return new \ReflectionClass($this->getName()); + } + + public function getAggregateRepositoryClass(): AggregateRepositoryInterface + { + return AggregateRepository::class; + } + + public function getMessageRepositoryClass(): MessageRepositoryInterface + { + return InMemoryMessageRepository::class; + } +} diff --git a/src/SonsOfPHP/Component/EventSourcing/AggregateClassMetadataInterface.php b/src/SonsOfPHP/Component/EventSourcing/AggregateClassMetadataInterface.php new file mode 100644 index 00000000..fc0bca0c --- /dev/null +++ b/src/SonsOfPHP/Component/EventSourcing/AggregateClassMetadataInterface.php @@ -0,0 +1,37 @@ + + */ +interface AggregateClassMetadataInterface +{ + // aggregate class name + //public function getName(): string; + + // property name for aggregate id + //public function getAggregateId(): string; + + // property name for aggregate version + //public function getAggregateVersion(): string; + + //public function getReflectinClass(): \ReflectionClass; + + // If any upserters, return them + //public function getUpserters(): iterable; + + // returns the serializer to use + //public function getSerializer(): MessageSerializerInterface; + + //public function getAggregateRepository(): AggregateRepositoryInterface; + + //public function getMessageRepository(): MessageRepositoryInterface; + + //public function getMessageProviderInterface(): MessageProviderInterface; +} diff --git a/src/SonsOfPHP/Component/EventSourcing/AggregateManager.php b/src/SonsOfPHP/Component/EventSourcing/AggregateManager.php new file mode 100644 index 00000000..2ea055a8 --- /dev/null +++ b/src/SonsOfPHP/Component/EventSourcing/AggregateManager.php @@ -0,0 +1,57 @@ + + */ +final class AggregateManager implements AggregateManagerInterface +{ + private array $aggregates = []; + + public function __construct( + private ConfigurationInterface $config, + ) {} + + public function registerAggregate(string $aggregate): void + { + foreach ($this->config->getDriver()->getClassAttributes($aggregate) as $attribute) { + if ($attribute instanceof AsAggregate) { + $this->aggregates[$aggregate] = new AggregateClassMetadata($aggregate); + return; + } + } + + throw new \Exception('Invalid Aggregate'); + } + + public function find(AggregateInterface $class, AggregateIdInterface|string $id): ?AggregateInterface + { + if (!array_key_exists($class, $this->aggregates)) { + $this->registerAggregate($aggregate); + } + + return $this->config->getContainer()->get($this->aggregate[$class]->getAggregateRepositoryClass())->find($id); + } + + public function persist(AggregateInterface $aggregate): void + { + if (!array_key_exists($class, $this->aggregates)) { + $this->registerAggregate($aggregate); + } + + $events = $aggregate->getPendingEvents(); + if (0 === count($events)) { + return; + } + + $this->config->getContainer()->get($this->aggregate[$class]->getMessageRepositoryClass())->persist($aggregate); + } +} diff --git a/src/SonsOfPHP/Component/EventSourcing/AggregateManagerInterface.php b/src/SonsOfPHP/Component/EventSourcing/AggregateManagerInterface.php new file mode 100644 index 00000000..7c57bc16 --- /dev/null +++ b/src/SonsOfPHP/Component/EventSourcing/AggregateManagerInterface.php @@ -0,0 +1,29 @@ +find(UserAggregate::class, 'unique-id'); + * ... + * $manager->persist($aggregate); + * + * @author Joshua Estes + */ +interface AggregateManagerInterface +{ + /** + * Usage: + * $manager->find(UserAggregate::class, 'unique-id'); + */ + public function find(AggregateInterface $class, AggregateIdInterface|string $id): ?AggregateInterface; + + public function persist(AggregateInterface $aggregate): void; +} diff --git a/src/SonsOfPHP/Component/EventSourcing/Configuration.php b/src/SonsOfPHP/Component/EventSourcing/Configuration.php new file mode 100644 index 00000000..c1117df9 --- /dev/null +++ b/src/SonsOfPHP/Component/EventSourcing/Configuration.php @@ -0,0 +1,55 @@ + + */ +final class Configuration implements ConfigurationInterface +{ + private ContainerInterface $container; + private DriverInterface $driver; + private EventDispatcherInterface $eventDispatcher; + + public function __construct( + array $config = [], + ) { + if (array_key_exists('container', $config)) { + $this->container = $config['container']; + } + + if (array_key_exists('driver', $config)) { + $this->driver = $config['driver']; + } else { + $this->driver = new AttributeDriver(); + } + + if (array_key_exists('event_dispatcher', $config)) { + $this->eventDispatcher = $config['event_dispatcher']; + } else { + $this->eventDispatcher = new EventDispatcher(); + } + } + + public function getDriver(): DriverInterface + { + return $this->driver; + } + + public function getEventDispatcher(): EventDispatcherInterface + { + return $this->eventDispatcher; + } + + public function getContainer(): ContainerInterface + { + return $this->container; + } +} diff --git a/src/SonsOfPHP/Component/EventSourcing/ConfigurationInterface.php b/src/SonsOfPHP/Component/EventSourcing/ConfigurationInterface.php new file mode 100644 index 00000000..f29b4437 --- /dev/null +++ b/src/SonsOfPHP/Component/EventSourcing/ConfigurationInterface.php @@ -0,0 +1,13 @@ + + */ +interface ConfigurationInterface +{ + public function getDriver(); +} diff --git a/src/SonsOfPHP/Component/EventSourcing/Mapping/AggregateId.php b/src/SonsOfPHP/Component/EventSourcing/Mapping/AggregateId.php new file mode 100644 index 00000000..ff873351 --- /dev/null +++ b/src/SonsOfPHP/Component/EventSourcing/Mapping/AggregateId.php @@ -0,0 +1,22 @@ + + */ +#[Attribute(Attribute::TARGET_PROPERTY)] +final class AggregateId +{ +} diff --git a/src/SonsOfPHP/Component/EventSourcing/Mapping/AggregateVersion.php b/src/SonsOfPHP/Component/EventSourcing/Mapping/AggregateVersion.php new file mode 100644 index 00000000..7495bb67 --- /dev/null +++ b/src/SonsOfPHP/Component/EventSourcing/Mapping/AggregateVersion.php @@ -0,0 +1,22 @@ + + */ +#[Attribute(Attribute::TARGET_PROPERTY)] +final class AggregateVersion +{ +} diff --git a/src/SonsOfPHP/Component/EventSourcing/Mapping/ApplyEvent.php b/src/SonsOfPHP/Component/EventSourcing/Mapping/ApplyEvent.php new file mode 100644 index 00000000..7af339f7 --- /dev/null +++ b/src/SonsOfPHP/Component/EventSourcing/Mapping/ApplyEvent.php @@ -0,0 +1,26 @@ + + */ +#[Attribute(Attribute::TARGET_METHOD)] +final class ApplyEvent +{ + /** + * @codeCoverageIgnore + */ + public function __construct(public readonly string $event) {} +} diff --git a/src/SonsOfPHP/Component/EventSourcing/Mapping/AsAggregate.php b/src/SonsOfPHP/Component/EventSourcing/Mapping/AsAggregate.php new file mode 100644 index 00000000..9f7cfff7 --- /dev/null +++ b/src/SonsOfPHP/Component/EventSourcing/Mapping/AsAggregate.php @@ -0,0 +1,19 @@ + + */ +#[Attribute(Attribute::TARGET_CLASS)] +final class AsAggregate +{ +} diff --git a/src/SonsOfPHP/Component/EventSourcing/Mapping/AsAggregateId.php b/src/SonsOfPHP/Component/EventSourcing/Mapping/AsAggregateId.php new file mode 100644 index 00000000..b0f38ffb --- /dev/null +++ b/src/SonsOfPHP/Component/EventSourcing/Mapping/AsAggregateId.php @@ -0,0 +1,19 @@ + + */ +#[Attribute(Attribute::TARGET_CLASS)] +final class AsAggregateId +{ +} diff --git a/src/SonsOfPHP/Component/EventSourcing/Mapping/AsAggregateRepository.php b/src/SonsOfPHP/Component/EventSourcing/Mapping/AsAggregateRepository.php new file mode 100644 index 00000000..dca4be87 --- /dev/null +++ b/src/SonsOfPHP/Component/EventSourcing/Mapping/AsAggregateRepository.php @@ -0,0 +1,19 @@ + + */ +#[Attribute(Attribute::TARGET_CLASS)] +final class AsAggregateRepository +{ +} diff --git a/src/SonsOfPHP/Component/EventSourcing/Mapping/AsAggregateVersion.php b/src/SonsOfPHP/Component/EventSourcing/Mapping/AsAggregateVersion.php new file mode 100644 index 00000000..cf7cdeac --- /dev/null +++ b/src/SonsOfPHP/Component/EventSourcing/Mapping/AsAggregateVersion.php @@ -0,0 +1,19 @@ + + */ +#[Attribute(Attribute::TARGET_CLASS)] +final class AsAggregateVersion +{ +} diff --git a/src/SonsOfPHP/Component/EventSourcing/Mapping/AsEnricherHandler.php b/src/SonsOfPHP/Component/EventSourcing/Mapping/AsEnricherHandler.php new file mode 100644 index 00000000..05c34e53 --- /dev/null +++ b/src/SonsOfPHP/Component/EventSourcing/Mapping/AsEnricherHandler.php @@ -0,0 +1,19 @@ + + */ +#[Attribute(Attribute::TARGET_CLASS)] +final class AsEnricherHandler +{ +} diff --git a/src/SonsOfPHP/Component/EventSourcing/Mapping/AsEnricherProvider.php b/src/SonsOfPHP/Component/EventSourcing/Mapping/AsEnricherProvider.php new file mode 100644 index 00000000..8eb8dac7 --- /dev/null +++ b/src/SonsOfPHP/Component/EventSourcing/Mapping/AsEnricherProvider.php @@ -0,0 +1,19 @@ + + */ +#[Attribute(Attribute::TARGET_CLASS)] +final class AsEnricherProvider +{ +} diff --git a/src/SonsOfPHP/Component/EventSourcing/Mapping/AsMessage.php b/src/SonsOfPHP/Component/EventSourcing/Mapping/AsMessage.php new file mode 100644 index 00000000..a6580912 --- /dev/null +++ b/src/SonsOfPHP/Component/EventSourcing/Mapping/AsMessage.php @@ -0,0 +1,19 @@ + + */ +#[Attribute(Attribute::TARGET_CLASS)] +final class AsMessage +{ +} diff --git a/src/SonsOfPHP/Component/EventSourcing/Mapping/AsMessageProvider.php b/src/SonsOfPHP/Component/EventSourcing/Mapping/AsMessageProvider.php new file mode 100644 index 00000000..26357a2f --- /dev/null +++ b/src/SonsOfPHP/Component/EventSourcing/Mapping/AsMessageProvider.php @@ -0,0 +1,19 @@ + + */ +#[Attribute(Attribute::TARGET_CLASS)] +final class AsMessageProvider +{ +} diff --git a/src/SonsOfPHP/Component/EventSourcing/Mapping/AsMessageRepository.php b/src/SonsOfPHP/Component/EventSourcing/Mapping/AsMessageRepository.php new file mode 100644 index 00000000..e9a110b5 --- /dev/null +++ b/src/SonsOfPHP/Component/EventSourcing/Mapping/AsMessageRepository.php @@ -0,0 +1,19 @@ + + */ +#[Attribute(Attribute::TARGET_CLASS)] +final class AsMessageRepository +{ +} diff --git a/src/SonsOfPHP/Component/EventSourcing/Mapping/AsSerializer.php b/src/SonsOfPHP/Component/EventSourcing/Mapping/AsSerializer.php new file mode 100644 index 00000000..2ce2dc10 --- /dev/null +++ b/src/SonsOfPHP/Component/EventSourcing/Mapping/AsSerializer.php @@ -0,0 +1,19 @@ + + */ +#[Attribute(Attribute::TARGET_CLASS)] +final class AsSerializer +{ +} diff --git a/src/SonsOfPHP/Component/EventSourcing/Mapping/AsUpcasterHandler.php b/src/SonsOfPHP/Component/EventSourcing/Mapping/AsUpcasterHandler.php new file mode 100644 index 00000000..ece50619 --- /dev/null +++ b/src/SonsOfPHP/Component/EventSourcing/Mapping/AsUpcasterHandler.php @@ -0,0 +1,19 @@ + + */ +#[Attribute(Attribute::TARGET_CLASS)] +final class AsUpcasterHandler +{ +} diff --git a/src/SonsOfPHP/Component/EventSourcing/Mapping/AsUpcasterProvider.php b/src/SonsOfPHP/Component/EventSourcing/Mapping/AsUpcasterProvider.php new file mode 100644 index 00000000..53bb9f30 --- /dev/null +++ b/src/SonsOfPHP/Component/EventSourcing/Mapping/AsUpcasterProvider.php @@ -0,0 +1,19 @@ + + */ +#[Attribute(Attribute::TARGET_CLASS)] +final class AsUpcasterProvider +{ +} diff --git a/src/SonsOfPHP/Component/EventSourcing/Mapping/Driver/AttributeDriver.php b/src/SonsOfPHP/Component/EventSourcing/Mapping/Driver/AttributeDriver.php new file mode 100644 index 00000000..9424b805 --- /dev/null +++ b/src/SonsOfPHP/Component/EventSourcing/Mapping/Driver/AttributeDriver.php @@ -0,0 +1,55 @@ + + */ +final class AttributeDriver implements DriverInterface +{ + public function getReflectionClass($class) + { + return new \ReflectionClass($class); + } + + public function getClassAttributes($class): iterable + { + foreach ($this->getReflectionClass($class)->getAttributes() as $refAttribute) { + // @todo only return supported attributes + yield $refAttribute->newInstance(); + } + } + + public function getMethodAttributes($class): iterable + { + foreach ($this->getReflectionClass($class)->getMethods() as $refMethod) { + foreach ($refMethod->getAttributes() as $attribute) { + // @todo only return supported attributes + yield $refMethod->getName() => $attribute->newInstance(); + } + } + } + + public function getPropertyAttributes($class): iterable + { + foreach ($this->getReflectionClass($class)->getProperties() as $refProperty) { + foreach ($refProperty->getAttributes() as $attribute) { + // @todo only return supported attributes + yield $refProperty->getName() => $attribute->newInstance(); + } + } + } + + public function getPropertyAttribute($class, $property) + { + foreach ($this->getPropertyAttributes($class) as $i => $attribute) { + if ($i === $property) { + return $attribute; + } + } + + return null; + } +} diff --git a/src/SonsOfPHP/Component/EventSourcing/Mapping/Driver/DriverInterface.php b/src/SonsOfPHP/Component/EventSourcing/Mapping/Driver/DriverInterface.php new file mode 100644 index 00000000..2f3a1d60 --- /dev/null +++ b/src/SonsOfPHP/Component/EventSourcing/Mapping/Driver/DriverInterface.php @@ -0,0 +1,13 @@ + + */ +interface DriverInterface +{ + //public function getClassAttributes(): array; +} diff --git a/src/SonsOfPHP/Component/EventSourcing/Message/AbstractMessage.php b/src/SonsOfPHP/Component/EventSourcing/Message/AbstractMessage.php index 89ff924b..5717534a 100644 --- a/src/SonsOfPHP/Component/EventSourcing/Message/AbstractMessage.php +++ b/src/SonsOfPHP/Component/EventSourcing/Message/AbstractMessage.php @@ -27,7 +27,7 @@ final public function __construct( private MessageMetadata $metadata = new MessageMetadata(), ) {} - final public static function new(): MessageInterface + final public static function new(): static { return new static(new MessagePayload(), new MessageMetadata()); } diff --git a/src/SonsOfPHP/Component/EventSourcing/Message/MessageInterface.php b/src/SonsOfPHP/Component/EventSourcing/Message/MessageInterface.php index 540e7892..a92ae396 100644 --- a/src/SonsOfPHP/Component/EventSourcing/Message/MessageInterface.php +++ b/src/SonsOfPHP/Component/EventSourcing/Message/MessageInterface.php @@ -25,7 +25,7 @@ interface MessageInterface * * @return static */ - public static function new(): self; + public static function new(): static; /** * Returns the Aggregate ID, if the aggregate ID is unknown, it diff --git a/src/SonsOfPHP/Component/EventSourcing/Tests/Aggregate/AbstractAggregateTest.php b/src/SonsOfPHP/Component/EventSourcing/Tests/Aggregate/AbstractAggregateTest.php index 37423234..55a2d5c8 100644 --- a/src/SonsOfPHP/Component/EventSourcing/Tests/Aggregate/AbstractAggregateTest.php +++ b/src/SonsOfPHP/Component/EventSourcing/Tests/Aggregate/AbstractAggregateTest.php @@ -91,6 +91,26 @@ public function testItWillRaiseExceptionWithInvalidId(): void $this->getMockForAbstractClass(AbstractAggregate::class, [new \stdClass()]); } + /** + * @covers ::__construct + */ + public function testConstructWorksWithStrings(): void + { + $aggregate = $this->getMockForAbstractClass(AbstractAggregate::class, ['unique-id']); + + $this->assertSame('unique-id', $aggregate->getAggregateId()->toString()); + } + + /** + * @covers ::__construct + */ + public function testConstructWorksWithAggregateId(): void + { + $aggregate = $this->getMockForAbstractClass(AbstractAggregate::class, [new AggregateId('unique-id')]); + + $this->assertSame('unique-id', $aggregate->getAggregateId()->toString()); + } + /** * @covers ::peekPendingEvents */ diff --git a/src/SonsOfPHP/Component/EventSourcing/Tests/AggregateClassMetadataTest.php b/src/SonsOfPHP/Component/EventSourcing/Tests/AggregateClassMetadataTest.php new file mode 100644 index 00000000..1d969caf --- /dev/null +++ b/src/SonsOfPHP/Component/EventSourcing/Tests/AggregateClassMetadataTest.php @@ -0,0 +1,35 @@ +assertInstanceOf(AggregateClassMetadataInterface::class, new AggregateClassMetadata(FakeAggregate::class)); + } + + /** + * @coversNothing + */ + public function testItWorks(): void + { + $metadata = new AggregateClassMetadata(FakeAggregate::class); + + //dd($metadata); + $this->assertTrue(true); + } +} diff --git a/src/SonsOfPHP/Component/EventSourcing/Tests/AggregateManagerTest.php b/src/SonsOfPHP/Component/EventSourcing/Tests/AggregateManagerTest.php new file mode 100644 index 00000000..217c15d1 --- /dev/null +++ b/src/SonsOfPHP/Component/EventSourcing/Tests/AggregateManagerTest.php @@ -0,0 +1,47 @@ +config = $this->createMock(ConfigurationInterface::class); + } + + /** + * @coversNothing + */ + public function testItImplementsCorrectInterface(): void + { + $this->assertInstanceOf(AggregateManagerInterface::class, new AggregateManager($this->config)); + } + + /** + * @coversNothing + */ + public function testItWorks(): void + { + $this->config->method('getDriver')->willReturn(new AttributeDriver()); + + $manager = new AggregateManager($this->config); + $manager->registerAggregate(FakeAggregate::class); + + $this->assertTrue(true); + } +} diff --git a/src/SonsOfPHP/Component/EventSourcing/Tests/ConfigurationTest.php b/src/SonsOfPHP/Component/EventSourcing/Tests/ConfigurationTest.php new file mode 100644 index 00000000..de0c317f --- /dev/null +++ b/src/SonsOfPHP/Component/EventSourcing/Tests/ConfigurationTest.php @@ -0,0 +1,23 @@ +assertInstanceOf(ConfigurationInterface::class, new Configuration()); + } +} diff --git a/src/SonsOfPHP/Component/EventSourcing/Tests/FakeAggregate.php b/src/SonsOfPHP/Component/EventSourcing/Tests/FakeAggregate.php index 5d9785fc..bec4610c 100644 --- a/src/SonsOfPHP/Component/EventSourcing/Tests/FakeAggregate.php +++ b/src/SonsOfPHP/Component/EventSourcing/Tests/FakeAggregate.php @@ -5,11 +5,21 @@ namespace SonsOfPHP\Component\EventSourcing\Tests; use SonsOfPHP\Component\EventSourcing\Aggregate\AbstractAggregate; +use SonsOfPHP\Component\EventSourcing\Mapping\AsAggregate; +use SonsOfPHP\Component\EventSourcing\Mapping\AggregateId; +use SonsOfPHP\Component\EventSourcing\Mapping\ApplyEvent; +#[AsAggregate] class FakeAggregate extends AbstractAggregate { + #[AggregateId] + public $id; + public function raiseThisEvent($event): void { $this->raiseEvent($event); } + + #[ApplyEvent('stdClass')] + protected function applyExampleEvent($event) {} } diff --git a/src/SonsOfPHP/Component/EventSourcing/Tests/Mapping/Driver/AttributeDriverTest.php b/src/SonsOfPHP/Component/EventSourcing/Tests/Mapping/Driver/AttributeDriverTest.php new file mode 100644 index 00000000..b0f9fbe5 --- /dev/null +++ b/src/SonsOfPHP/Component/EventSourcing/Tests/Mapping/Driver/AttributeDriverTest.php @@ -0,0 +1,40 @@ +assertInstanceOf(DriverInterface::class, new AttributeDriver()); + } + + /** + * @coversNothing + */ + public function testItWorks(): void + { + $driver = new AttributeDriver(); + + //dd( + // iterator_to_array($driver->getClassAttributes(FakeAggregate::class)), + // iterator_to_array($driver->getMethodAttributes(FakeAggregate::class)), + // iterator_to_array($driver->getPropertyAttributes(FakeAggregate::class)), + // $driver->getPropertyAttribute(FakeAggregate::class, 'id'), + //); + $this->assertTrue(true); + } +} diff --git a/src/SonsOfPHP/Component/EventSourcing/composer.json b/src/SonsOfPHP/Component/EventSourcing/composer.json index 4a60f666..50671b17 100644 --- a/src/SonsOfPHP/Component/EventSourcing/composer.json +++ b/src/SonsOfPHP/Component/EventSourcing/composer.json @@ -36,6 +36,9 @@ "require-dev": { "phpunit/phpunit": "^10.4" }, + "provide": { + "sonsofphp/event-sourcing-implementation": "^0.3.x-dev" + }, "suggest": { "sonsofphp/event-sourcing-doctrine": "Adds additional functionality using Doctrine", "sonsofphp/event-sourcing-symfony": "Adds additional functionality using Symfony Components" diff --git a/src/SonsOfPHP/Contract/EventSourcing/LICENSE b/src/SonsOfPHP/Contract/EventSourcing/LICENSE new file mode 100644 index 00000000..39238382 --- /dev/null +++ b/src/SonsOfPHP/Contract/EventSourcing/LICENSE @@ -0,0 +1,19 @@ +Copyright 2022 to Present Joshua Estes + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/SonsOfPHP/Contract/EventSourcing/README.md b/src/SonsOfPHP/Contract/EventSourcing/README.md new file mode 100644 index 00000000..9a69cdc5 --- /dev/null +++ b/src/SonsOfPHP/Contract/EventSourcing/README.md @@ -0,0 +1,16 @@ +Sons of PHP - Event Sourcing Contract +===================================== + +## Learn More + +* [Documentation][docs] +* [Contributing][contributing] +* [Report Issues][issues] and [Submit Pull Requests][pull-requests] in the [Mother Repository][mother-repo] +* Get Help & Support using [Discussions][discussions] + +[discussions]: https://github.com/orgs/SonsOfPHP/discussions +[mother-repo]: https://github.com/SonsOfPHP/sonsofphp +[contributing]: https://docs.sonsofphp.com/contributing/ +[docs]: https://docs.sonsofphp.com/components/http-message/ +[issues]: https://github.com/SonsOfPHP/sonsofphp/issues?q=is%3Aopen+is%3Aissue+label%3AEventSourcing +[pull-requests]: https://github.com/SonsOfPHP/sonsofphp/pulls?q=is%3Aopen+is%3Apr+label%3AEventSourcing diff --git a/src/SonsOfPHP/Contract/EventSourcing/composer.json b/src/SonsOfPHP/Contract/EventSourcing/composer.json new file mode 100644 index 00000000..80d0eaae --- /dev/null +++ b/src/SonsOfPHP/Contract/EventSourcing/composer.json @@ -0,0 +1,45 @@ +{ + "name": "sonsofphp/event-sourcing-contract", + "type": "library", + "description": "Event Sourcing Contracts", + "keywords": ["abstractions", "contracts", "decoupling", "interfaces", "interoperability", "standards"], + "homepage": "https://github.com/SonsOfPHP/event-sourcing-contract", + "license": "MIT", + "authors": [ + { + "name": "Joshua Estes", + "email": "joshua@sonsofphp.com" + } + ], + "support": { + "issues": "https://github.com/SonsOfPHP/sonsofphp/issues", + "forum": "https://github.com/orgs/SonsOfPHP/discussions", + "docs": "https://docs.sonsofphp.com" + }, + "autoload": { + "psr-4": { + "SonsOfPHP\\Contract\\EventSourcing\\": "" + } + }, + "minimum-stability": "dev", + "prefer-stable": true, + "require": { + "php": ">=8.1" + }, + "extra": { + "sort-packages": true, + "branch-alias": { + "dev-main": "0.3.x-dev" + } + }, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/JoshuaEstes" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/packagist-sonsofphp-sonsofphp" + } + ] +}