Skip to content

Commit

Permalink
Merge pull request #5348 from neos/task/serializable-commands
Browse files Browse the repository at this point in the history
!!! TASK: Serializable Commands
  • Loading branch information
mhsdesign authored Nov 12, 2024
2 parents ab7f5f9 + 1aa0e04 commit 7a5a7c4
Show file tree
Hide file tree
Showing 42 changed files with 346 additions and 244 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ Feature: Add New Property
properties:
text:
type: string
dateTime:
type: DateTime
"""
And using identifier "default", I define a content repository
And I am in content repository "default"
Expand Down Expand Up @@ -64,7 +66,7 @@ Feature: Add New Property
-
type: 'AddNewProperty'
settings:
newPropertyName: 'aDateOutsideSchema'
newPropertyName: 'dateTime'
serializedValue: '2013-09-09T12:04:12+00:00'
type: 'DateTime'
"""
Expand All @@ -82,6 +84,46 @@ Feature: Add New Property
| text | "Original text" |
Then I expect a node identified by migration-cs;other;{} to exist in the content graph
And I expect this node to have the following properties:
| Key | Value |
| text | "fixed value" |
| aDateOutsideSchema | Date:2013-09-09T12:04:12+00:00 |
| Key | Value |
| text | "fixed value" |
| dateTime | Date:2013-09-09T12:04:12+00:00 |

Scenario: Adding a property that is not defined in the node type schema
When I run the following node migration for workspace "live", creating target workspace "migration-workspace" on contentStreamId "migration-cs" and exceptions are caught:
"""yaml
migration:
-
filters:
-
type: 'NodeType'
settings:
nodeType: 'Neos.ContentRepository.Testing:Document'
transformations:
-
type: 'AddNewProperty'
settings:
newPropertyName: 'aDateOutsideSchema'
serializedValue: '2013-09-09T12:04:12+00:00'
type: 'DateTime'
"""
Then the last command should have thrown an exception of type "PropertyCannotBeSet"

Scenario: Adding a property with a different type than defined by the node type schema
When I run the following node migration for workspace "live", creating target workspace "migration-workspace" on contentStreamId "migration-cs" and exceptions are caught:
"""yaml
migration:
-
filters:
-
type: 'NodeType'
settings:
nodeType: 'Neos.ContentRepository.Testing:Document'
transformations:
-
type: 'AddNewProperty'
settings:
newPropertyName: 'dateTime'
serializedValue: '2013-09-09T12:04:12+00:00'
type: 'string'
"""
Then the last command should have thrown an exception of type "PropertyCannotBeSet"
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ Feature: Workspace discarding - complex chained functionality
| nodesToDiscard | [{"workspaceName": "user-ws", "dimensionSpacePoint": {"language": "en"}, "nodeAggregateId": "sir-david-nodenborough"}, {"workspaceName": "user-ws", "dimensionSpacePoint": {"language": "en"}, "nodeAggregateId": "sir-david-nodenborough"}] |
| newContentStreamId | "user-cs-id-rebased" |
Then the last command should have thrown the WorkspaceRebaseFailed exception with:
| SequenceNumber | Command | Exception |
| 11 | CreateNodeVariant | NodeAggregateDoesCurrentlyNotCoverDimensionSpacePoint |
| SequenceNumber | Event | Exception |
| 11 | NodeGeneralizationVariantWasCreated | NodeAggregateDoesCurrentlyNotCoverDimensionSpacePoint |

When the command DiscardWorkspace is executed with payload:
| Key | Value |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ Feature: Rebasing auto-created nodes works

# rebase of SetSerializedNodeProperties
When the command RebaseWorkspace is executed with payload:
| Key | Value |
| workspaceName | "user-test" |
| Key | Value |
| workspaceName | "user-test" |
| rebasedContentStreamId | "user-cs-rebased" |
# This should properly work; no error.

Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,8 @@ Feature: Workspace rebasing - conflicting changes
Then I expect the content stream "user-cs-two" to exist
Then I expect the content stream "user-cs-two-rebased" to not exist
Then the last command should have thrown the WorkspaceRebaseFailed exception with:
| SequenceNumber | Command | Exception |
| 13 | SetSerializedNodeProperties | NodeAggregateCurrentlyDoesNotExist |
| SequenceNumber | Event | Exception |
| 13 | NodePropertiesWereSet | NodeAggregateCurrentlyDoesNotExist |

When the command RebaseWorkspace is executed with payload:
| Key | Value |
Expand Down Expand Up @@ -169,9 +169,9 @@ Feature: Workspace rebasing - conflicting changes
Then I expect the content stream "user-cs-identifier" to exist
Then I expect the content stream "user-cs-identifier-rebased" to not exist
Then the last command should have thrown the WorkspaceRebaseFailed exception with:
| SequenceNumber | Command | Exception |
| 12 | SetSerializedNodeProperties | NodeAggregateCurrentlyDoesNotExist |
| 14 | SetSerializedNodeProperties | NodeAggregateCurrentlyDoesNotExist |
| SequenceNumber | Event | Exception |
| 12 | NodePropertiesWereSet | NodeAggregateCurrentlyDoesNotExist |
| 14 | NodePropertiesWereSet | NodeAggregateCurrentlyDoesNotExist |

When the command RebaseWorkspace is executed with payload:
| Key | Value |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,9 @@ Feature: Workspace publication - complex chained functionality
| nodesToPublish | [{"dimensionSpacePoint": {"language": "de"}, "nodeAggregateId": "sir-nodebelig"}] |
| newContentStreamId | "user-cs-id-rebased" |
Then the last command should have thrown the WorkspaceRebaseFailed exception with:
| SequenceNumber | Command | Exception |
| 13 | SetSerializedNodeProperties | NodeAggregateCurrentlyDoesNotExist |
| 14 | SetSerializedNodeProperties | NodeAggregateCurrentlyDoesNotExist |
| SequenceNumber | Event | Exception |
| 13 | NodePropertiesWereSet | NodeAggregateCurrentlyDoesNotExist |
| 14 | NodePropertiesWereSet | NodeAggregateCurrentlyDoesNotExist |

Scenario: Vary to generalization, then publish only the child node so that an exception is thrown. Ensure that the workspace recovers from this
When the command CreateNodeVariant is executed with payload:
Expand All @@ -107,8 +107,8 @@ Feature: Workspace publication - complex chained functionality
| nodesToPublish | [{"workspaceName": "user-ws", "dimensionSpacePoint": {"language": "en"}, "nodeAggregateId": "nody-mc-nodeface"}] |
| newContentStreamId | "user-cs-id-rebased" |
Then the last command should have thrown the WorkspaceRebaseFailed exception with:
| SequenceNumber | Command | Exception |
| 13 | CreateNodeVariant | NodeAggregateDoesCurrentlyNotCoverDimensionSpacePoint |
| SequenceNumber | Event | Exception |
| 13 | NodeGeneralizationVariantWasCreated | NodeAggregateDoesCurrentlyNotCoverDimensionSpacePoint |

When the command PublishWorkspace is executed with payload:
| Key | Value |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,8 +201,8 @@ Feature: Publishing individual nodes (basics)
| nodesToPublish | [{"dimensionSpacePoint": {}, "nodeAggregateId": "sir-unchanged"}] |
| contentStreamIdForRemainingPart | "user-cs-identifier-remaining" |
Then the last command should have thrown the WorkspaceRebaseFailed exception with:
| SequenceNumber | Command | Exception |
| 14 | TagSubtree | SubtreeIsAlreadyTagged |
| SequenceNumber | Event | Exception |
| 14 | SubtreeWasTagged | SubtreeIsAlreadyTagged |
Scenario: It is possible to publish all nodes
When the command PublishIndividualNodesFromWorkspace is executed with payload:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,8 @@ Feature: Workspace discarding - basic functionality
| workspaceName | "user-ws-two" |
| rebasedContentStreamId | "user-cs-two-rebased" |
Then the last command should have thrown the WorkspaceRebaseFailed exception with:
| SequenceNumber | Command | Exception |
| 13 | SetSerializedNodeProperties | NodeAggregateCurrentlyDoesNotExist |
| SequenceNumber | Event | Exception |
| 13 | NodePropertiesWereSet | NodeAggregateCurrentlyDoesNotExist |

Then workspace user-ws-two has status OUTDATED

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Neos\ContentRepository\Core\ContentRepository;
use Neos\ContentRepository\Core\EventStore\EventsToPublish;
use Neos\ContentRepository\Core\Feature\Common\RebasableToOtherWorkspaceInterface;

/**
* Implementation Detail of {@see ContentRepository::handle}, which does the command dispatching to the different
Expand All @@ -31,7 +32,7 @@ public function __construct(
/**
* @return EventsToPublish|\Generator<int, EventsToPublish>
*/
public function handle(CommandInterface $command): EventsToPublish|\Generator
public function handle(CommandInterface|RebasableToOtherWorkspaceInterface $command): EventsToPublish|\Generator
{
// multiple handlers must not handle the same command
foreach ($this->handlers as $handler) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Neos\ContentRepository\Core\CommandHandler;

use Neos\ContentRepository\Core\EventStore\EventsToPublish;
use Neos\ContentRepository\Core\Feature\Common\RebasableToOtherWorkspaceInterface;

/**
* Common interface for all Content Repository command handlers
Expand All @@ -15,7 +16,7 @@
*/
interface CommandHandlerInterface
{
public function canHandle(CommandInterface $command): bool;
public function canHandle(CommandInterface|RebasableToOtherWorkspaceInterface $command): bool;

/**
* "simple" command handlers return EventsToPublish directly
Expand All @@ -25,5 +26,5 @@ public function canHandle(CommandInterface $command): bool;
*
* @return EventsToPublish|\Generator<int, EventsToPublish>
*/
public function handle(CommandInterface $command, CommandHandlingDependencies $commandHandlingDependencies): EventsToPublish|\Generator;
public function handle(CommandInterface|RebasableToOtherWorkspaceInterface $command, CommandHandlingDependencies $commandHandlingDependencies): EventsToPublish|\Generator;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@

namespace Neos\ContentRepository\Core\CommandHandler;

use Neos\ContentRepository\Core\Feature\Common\RebasableToOtherWorkspaceInterface;

/**
* Common interface for all commands of the Content Repository
* Common interface for all commands of the content repository
*
* Note: Some commands also implement the {@see RebasableToOtherWorkspaceInterface}
* others are converted to the rebasable counter-part at command handling time, serializing their state to make them deterministic
*
* @internal because extra commands are no extension point
* @internal sealed interface. Custom commands cannot be handled and are no extension point!
*/
interface CommandInterface
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
use Neos\ContentRepository\Core\EventStore\EventNormalizer;
use Neos\ContentRepository\Core\EventStore\EventsToPublish;
use Neos\ContentRepository\Core\Feature\RebaseableCommand;
use Neos\ContentRepository\Core\Feature\WorkspaceRebase\CommandsThatFailedDuringRebase;
use Neos\ContentRepository\Core\Feature\WorkspaceRebase\CommandThatFailedDuringRebase;
use Neos\ContentRepository\Core\Feature\WorkspaceRebase\ConflictingEvents;
use Neos\ContentRepository\Core\Feature\WorkspaceRebase\ConflictingEvent;
use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphProjectionInterface;
use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName;
use Neos\EventStore\Helper\InMemoryEventStore;
Expand Down Expand Up @@ -45,7 +45,7 @@
*/
final class CommandSimulator
{
private CommandsThatFailedDuringRebase $commandsThatFailedDuringRebase;
private ConflictingEvents $conflictingEvents;

private readonly InMemoryEventStore $inMemoryEventStore;

Expand All @@ -56,7 +56,7 @@ public function __construct(
private readonly WorkspaceName $workspaceNameToSimulateIn,
) {
$this->inMemoryEventStore = new InMemoryEventStore();
$this->commandsThatFailedDuringRebase = new CommandsThatFailedDuringRebase();
$this->conflictingEvents = new ConflictingEvents();
}

/**
Expand Down Expand Up @@ -86,9 +86,11 @@ private function handle(RebaseableCommand $rebaseableCommand): void
try {
$eventsToPublish = $this->commandBus->handle($commandInWorkspace);
} catch (\Exception $exception) {
$this->commandsThatFailedDuringRebase = $this->commandsThatFailedDuringRebase->withAppended(
new CommandThatFailedDuringRebase(
$rebaseableCommand->originalCommand,
$originalEvent = $this->eventNormalizer->denormalize($rebaseableCommand->originalEvent);

$this->conflictingEvents = $this->conflictingEvents->withAppended(
new ConflictingEvent(
$originalEvent,
$exception,
$rebaseableCommand->originalSequenceNumber
)
Expand Down Expand Up @@ -155,13 +157,13 @@ public function eventStream(): EventStreamInterface
return $this->inMemoryEventStore->load(VirtualStreamName::all());
}

public function hasCommandsThatFailed(): bool
public function hasConflicts(): bool
{
return !$this->commandsThatFailedDuringRebase->isEmpty();
return !$this->conflictingEvents->isEmpty();
}

public function getCommandsThatFailed(): CommandsThatFailedDuringRebase
public function getConflictingEvents(): ConflictingEvents
{
return $this->commandsThatFailedDuringRebase;
return $this->conflictingEvents;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,30 @@
namespace Neos\ContentRepository\Core\Feature\Common;

use Neos\ContentRepository\Core\CommandHandler\CommandInterface;
use Neos\ContentRepository\Core\CommandHandler\CommandSimulator;
use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName;

/**
* This interface is implemented by **commands** which can be rebased to other Content Streams. This is basically all
* node-based commands.
* Common (marker) interface for all **commands** that need to be serialized for rebasing to other workspaces
*
* If the api command {@see CommandInterface} is serializable on its own it will directly implement this interface.
* For complex commands a serialized counterpart - which is not api - will be build which implements this interface.
*
* During a rebase, the command (either the original {@see CommandInterface} or its serialized counterpart) will be deserialized
* from array {@see RebasableToOtherWorkspaceInterface::fromArray()} and reapplied via the {@see CommandSimulator}
*
* Reminder: a rebase can fail, because the target content stream might contain conflicting changes.
*
* @internal used internally for the rebasing mechanism of content streams
*/
interface RebasableToOtherWorkspaceInterface extends CommandInterface
interface RebasableToOtherWorkspaceInterface extends \JsonSerializable
{
public function createCopyForWorkspace(
WorkspaceName $targetWorkspaceName,
): self;

/**
* @param array<string,mixed> $array
*/
public static function fromArray(array $array): self;
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,6 @@ public static function create(
return new self($workspaceName, $source, $target);
}

/**
* @param array<string,mixed> $array
*/
public static function fromArray(array $array): self
{
return new self(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
*/

use Neos\ContentRepository\Core\CommandHandler\CommandHandlerInterface;
use Neos\ContentRepository\Core\CommandHandler\CommandInterface;
use Neos\ContentRepository\Core\CommandHandler\CommandHandlingDependencies;
use Neos\ContentRepository\Core\CommandHandler\CommandInterface;
use Neos\ContentRepository\Core\ContentRepository;
use Neos\ContentRepository\Core\DimensionSpace\ContentDimensionZookeeper;
use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint;
Expand All @@ -26,13 +26,14 @@
use Neos\ContentRepository\Core\DimensionSpace\VariantType;
use Neos\ContentRepository\Core\EventStore\Events;
use Neos\ContentRepository\Core\EventStore\EventsToPublish;
use Neos\ContentRepository\Core\Feature\RebaseableCommand;
use Neos\ContentRepository\Core\Feature\Common\RebasableToOtherWorkspaceInterface;
use Neos\ContentRepository\Core\Feature\ContentStreamEventStreamName;
use Neos\ContentRepository\Core\Feature\DimensionSpaceAdjustment\Command\AddDimensionShineThrough;
use Neos\ContentRepository\Core\Feature\DimensionSpaceAdjustment\Command\MoveDimensionSpacePoint;
use Neos\ContentRepository\Core\Feature\DimensionSpaceAdjustment\Event\DimensionShineThroughWasAdded;
use Neos\ContentRepository\Core\Feature\DimensionSpaceAdjustment\Event\DimensionSpacePointWasMoved;
use Neos\ContentRepository\Core\Feature\DimensionSpaceAdjustment\Exception\DimensionSpacePointAlreadyExists;
use Neos\ContentRepository\Core\Feature\RebaseableCommand;
use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphInterface;
use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints;
use Neos\EventStore\Model\EventStream\ExpectedVersion;
Expand All @@ -48,12 +49,12 @@ public function __construct(
) {
}

public function canHandle(CommandInterface $command): bool
public function canHandle(CommandInterface|RebasableToOtherWorkspaceInterface $command): bool
{
return method_exists($this, 'handle' . (new \ReflectionClass($command))->getShortName());
}

public function handle(CommandInterface $command, CommandHandlingDependencies $commandHandlingDependencies): EventsToPublish
public function handle(CommandInterface|RebasableToOtherWorkspaceInterface $command, CommandHandlingDependencies $commandHandlingDependencies): EventsToPublish
{
/** @phpstan-ignore-next-line */
return match ($command::class) {
Expand Down
Loading

0 comments on commit 7a5a7c4

Please sign in to comment.