Skip to content
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

Merged

Conversation

bwaidelich
Copy link
Member

@bwaidelich bwaidelich commented Oct 12, 2017

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 and because
it moves the EventWithMetadata class to a Decorator sub namespace.
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: #6

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;
}
Copy link
Member Author

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;
Copy link
Member Author

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

Copy link
Member

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?

Copy link
Member Author

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?

Copy link
Member

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);
Copy link
Member Author

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'));
Copy link
Member Author

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;
Copy link
Member

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()
Copy link
Member

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

Copy link
Member Author

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?

Copy link
Member

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.

Copy link
Member

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()
Copy link
Member

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 ;)

Copy link
Member

@robertlemke robertlemke left a 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'));
Copy link
Member

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()
Copy link
Member

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.

Copy link
Member

@robertlemke robertlemke left a 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?

@bwaidelich
Copy link
Member Author

can't we call it "correlationIdentifier" instead of "correlationId"?

Yes of course. I find somethingId just easier to comprehend, but you're right that we went the "identifier" way everywhere else, so good point!

correlation identifier and causation identifier appear both in metadata and their own fields

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.

in general it doesn't feel like first-class support for these identifiers yet

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..
does that make sense?

Bastian Waidelich and others added 3 commits October 27, 2017 14:39
* Move `EventWithMetadata` to `Decorator` sub namespace
* Introduce `EventWithMetadataInterface`
* Add `EventWithCorrelationIdentifier`/`EventWithCausationIdentifier` decorators
* Rename "id" to "identifier" where applicable
Copy link
Member

@robertlemke robertlemke left a 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.

@robertlemke robertlemke merged commit 4680470 into neos:master Oct 30, 2017
@robertlemke robertlemke deleted the 6-event-correlation-and-causation-id branch October 30, 2017 09:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants