New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
!!!FEATURE: Add notion of correlation and causation Id #157
!!!FEATURE: Add notion of correlation and causation Id #157
Conversation
This is a new approach to introduce the notion of optional correlation/causation ids to events. Usage: $eventWithCorrelationId = new EventWithMetadata($originalEvent, ['correlationId' => $id]); $eventWithCausationId = new EventWithMetadata($originalEvent, ['causationId' => $id]); This also reworks the `EventStreamFilterInterface` to be easier to extend/implement and adds a new implementation to filter for the `correlationId` (as preparation for event-sourced Process Managers). Breaking Change: This is a breaking change because it changes the `EventStreamFilterInterface`. Besides, the schema of the doctrine based event-store is updated, adding the two columns `correlationid` and `causationid`. To update the db accordingly, just run ./flow eventstore:setupall again. Resolves: neos#6
} else { | ||
$this->event = $event; | ||
$this->metadata = $metadata; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I sneaked in this improvement as follow-up to #156, so that EventWithMetadata
can actually be nested
* | ||
* @return array | ||
*/ | ||
public function getFilterValues(): array; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed this so that we don't have to implement/update loads of empty methods when creating new dedicated filters
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm unsure about this. I see the hassle of the empty methods, but with this we forfeit all of the type safety and implementation clarity for some additional flexibility/conciseness. Also, the actual filtering method now is much more technical due to the array_key_exists
checks with the constants.
Can we maybe just bring down the hassle with an AllEventsStreamFilter
implementation, so that concrete filters only need to extend that and override the specifc methods that they want to filter on?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just to clarify: I see the problem with the current implementation not so much in the hassle of implementing methods but in the lack of extensibility.
To implement a new filter we currently have to change the interface which will break every implementation.
we forfeit all of the type safety
Yes, there's slightly reduced type safety but I considered it a pragmatic solution as this won't be used in highlevel userland code anyways.
the actual filtering method now is much more technical
is that a problem? That happens only within the EventStore implementation, which is very technical anyways.
Can we maybe just bring down the hassle with an AllEventsStreamFilter
I would really like to avoid abstract base classes to solve this and a QOM/DSL would be overkill IMO.
What I could imagine is a purely interface-based approach like
interface FiltersStreamNameInterface
{
public function getStreamName(): string
}
interface FiltersStreamNamePrefixInterface
{
public function getStreamNamePrefix(): string
}
// ...
but is that really worth it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To implement a new filter we currently have to change the interface which will break every implementation.
Fair point, but my take would be, that there is an upper boundary on what the EventFilter interface should contain, because there's only so much that makes (generically) sense to filter events by. Anything on top can still be solved with an untyped "additionalFilterProperties(): array" method, but the basic filters should be expressive and type safe.
is that a problem? That happens only within the EventStore implementation, which is very technical anyways.
You're right, I just prefer to to read something like if ($foo->hasSomething())
over if (array_key_exists($foo, FooClass::SOMETHING))
any time, even inside technical code.
I would really like to avoid abstract base classes
Doesn't need to be abstract even. Think of it like this: Any (custom) filter is a specialization of an AllEventsStreamFilter
. It is a real is-a relationship, so inheritance is a plausible modelling pattern.
What I could imagine is a purely interface-based approach like
TBH I would even prefer that over a single array typed filter specification interface. But it would lead to lots of if ($foo instanceof Bar)
checks - PHP finally needs some real pattern matching soon :/
@@ -109,7 +110,7 @@ public function load(EventStreamFilterInterface $filter): EventStream | |||
$this->applyEventStreamFilter($query, $filter); | |||
|
|||
$streamIterator = new DoctrineStreamIterator($query); | |||
return new EventStream($streamIterator, $this->getStreamVersion($filter)); | |||
return new EventStream($streamIterator); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The second argument was not used
$result->addNotice(new Notice($this->connection->getDriver()->getName(), null, [], 'Driver')); | ||
$result->addNotice(new Notice($this->connection->getUsername(), null, [], 'Username')); | ||
$result->addNotice(new Notice((string)$this->connection->getHost(), null, [], 'Host')); | ||
$result->addNotice(new Notice((string)$this->connection->getPort(), null, [], 'Port')); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Without this cast, an exception is thrown if the port is not set because of the recently introduced type hints on the Notice
class
* | ||
* @return array | ||
*/ | ||
public function getFilterValues(): array; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm unsure about this. I see the hassle of the empty methods, but with this we forfeit all of the type safety and implementation clarity for some additional flexibility/conciseness. Also, the actual filtering method now is much more technical due to the array_key_exists
checks with the constants.
Can we maybe just bring down the hassle with an AllEventsStreamFilter
implementation, so that concrete filters only need to extend that and override the specifc methods that they want to filter on?
* | ||
* @return Schema | ||
*/ | ||
private function createEventStoreSchema() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any reason to have the schema moved from the dedicated file? I found it better to contain the schema specification in an own file
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There used to be more functionality in the Schema
folder and in this file. Now there's only one static method left that is invoked in a single place.
What's the advantage of a separate file for this one method?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Less "visual debt" :P Nah, really, I just prefer to have different concerns separated in different locations. Schema is a different concern than how the EventStorage is implemented IMO.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Personally, I like the new way. Maybe it's just a matter of taste.
/** | ||
* @test | ||
*/ | ||
public function eventIsUnwrappedWhenNestingEventsWithMetadata() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 for test coverage of sneaked in feature ;)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll test this with my project now …
$schemaDiff = (new Comparator())->compare($fromSchema, $this->createEventStoreSchema()); | ||
$statements = $schemaDiff->toSaveSql($this->connection->getDatabasePlatform()); | ||
if ($statements !== []) { | ||
$result->addWarning(new Warning('The schama of table %s is not up-to-date', null, [$this->eventTableName], 'Table schema')); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
a typo ("schama")
* | ||
* @return Schema | ||
*/ | ||
private function createEventStoreSchema() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Personally, I like the new way. Maybe it's just a matter of taste.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a good step ahead, however, I have the following concerns / thoughts:
- can't we call it "correlationIdentifier" instead of "correlationId"? It really sticks out from all my other code where I have value objects called "somethingIdentifier" …
- it's a bit weird that the correlation identifier and causation identifier appear both in metadata and their own fields
- in general it doesn't feel like first-class support for these identifiers yet, because the way to provide it is so implicit. It's easy to produce bugs because just mistyping it ("corelationId") won't give you any errors
Should we discuss this further or is it too much discussion for this feature?
Yes of course. I find
Conceptually they are part of the event metadata (and that's how it is in all other implementations I saw). In the doctrine store we just put them into a separate column additionally in order to make it possible to look up events by the correlation id. It's an implementation detail if you will.
That's true and maybe we can work around this with more explicit decorators and value objects for the different ids: $eventWithCorrelationId = new EventWithCorrelationId($originalEvent, $correlationId);
$eventWithCorrelationAndCausationId = new EventWithCausationId($eventWithCorrelationId, $causationId); and maybe even additional interfaces so you can just implement support for those ids in your domain events.. |
* Move `EventWithMetadata` to `Decorator` sub namespace * Introduce `EventWithMetadataInterface` * Add `EventWithCorrelationIdentifier`/`EventWithCausationIdentifier` decorators * Rename "id" to "identifier" where applicable
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That looks great and I just tested it - it works, too.
This is a new approach to introduce the notion of optional
correlation/causation ids to events.
Usage:
This also reworks the
EventStreamFilterInterface
to be easier to extend/implementand adds a new implementation to filter for the
correlationId
(as preparation for event-sourcedProcess Managers).
Breaking Change:
This is a breaking change because it changes the
EventStreamFilterInterface
and becauseit moves the
EventWithMetadata
class to aDecorator
sub namespace.Besides, the schema of the doctrine based event-store is updated, adding the
two columns
correlationid
andcausationid
.To update the db accordingly, just run
again.
Resolves: #6