diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ContentStreamForking/01-ForkContentStream_ConstraintChecks.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ContentStreamForking/01-ForkContentStream_ConstraintChecks.feature index 8e1d7e8a71d..5b3a1fd7723 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ContentStreamForking/01-ForkContentStream_ConstraintChecks.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ContentStreamForking/01-ForkContentStream_ConstraintChecks.feature @@ -49,12 +49,13 @@ Feature: ForkContentStream Without Dimensions | propertyValues | {"text": {"value": "original value", "type": "string"}} | | propertiesToUnset | {} | - Scenario: Try to fork a content stream that is closed: + Scenario: Try to create a workspace with the base workspace referring to a closed content stream When the command CloseContentStream is executed with payload: | Key | Value | | contentStreamId | "cs-identifier" | - When the command ForkContentStream is executed with payload and exceptions are caught: - | Key | Value | - | contentStreamId | "user-cs-identifier" | - | sourceContentStreamId | "cs-identifier" | + When the command CreateWorkspace is executed with payload and exceptions are caught: + | Key | Value | + | workspaceName | "user-test" | + | baseWorkspaceName | "live" | + | newContentStreamId | "user-cs-identifier" | Then the last command should have thrown an exception of type "ContentStreamIsClosed" diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W8-IndividualNodePublication/02-BasicFeatures.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W8-IndividualNodePublication/02-BasicFeatures.feature index 487007ba37d..f5e31578cf4 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W8-IndividualNodePublication/02-BasicFeatures.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W8-IndividualNodePublication/02-BasicFeatures.feature @@ -41,7 +41,6 @@ Feature: Individual node publication Scenario: It is possible to publish a single node; and only this one is live. # create nodes in user WS Given I am in workspace "user-test" - And I am in workspace "user-test" And I am in dimension space point {} And the following CreateNodeAggregateWithNode commands are executed: | nodeAggregateId | nodeTypeName | parentNodeAggregateId | nodeName | tetheredDescendantNodeAggregateIds | diff --git a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandBus.php b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandBus.php index b77ce854cf6..3d959ebcd51 100644 --- a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandBus.php +++ b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandBus.php @@ -7,6 +7,7 @@ use Neos\ContentRepository\Core\CommandHandlingDependencies; use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\EventStore\EventsToPublish; +use Neos\ContentRepository\Core\EventStore\EventsToPublishFailed; /** * Implementation Detail of {@see ContentRepository::handle}, which does the command dispatching to the different @@ -26,7 +27,10 @@ public function __construct(CommandHandlerInterface ...$handlers) $this->handlers = $handlers; } - public function handle(CommandInterface $command, CommandHandlingDependencies $commandHandlingDependencies): EventsToPublish + /** + * @return EventsToPublish|\Generator + */ + public function handle(CommandInterface $command, CommandHandlingDependencies $commandHandlingDependencies): EventsToPublish|\Generator { // TODO fail if multiple handlers can handle the same command foreach ($this->handlers as $handler) { diff --git a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandHandlerInterface.php b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandHandlerInterface.php index e190337c370..9a82c678b15 100644 --- a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandHandlerInterface.php +++ b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandHandlerInterface.php @@ -5,7 +5,6 @@ namespace Neos\ContentRepository\Core\CommandHandler; use Neos\ContentRepository\Core\CommandHandlingDependencies; -use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\EventStore\EventsToPublish; /** @@ -19,5 +18,9 @@ interface CommandHandlerInterface { public function canHandle(CommandInterface $command): bool; - public function handle(CommandInterface $command, CommandHandlingDependencies $commandHandlingDependencies): EventsToPublish; + + /** + * @return EventsToPublish|\Generator + */ + public function handle(CommandInterface $command, CommandHandlingDependencies $commandHandlingDependencies): EventsToPublish|\Generator; } diff --git a/Neos.ContentRepository.Core/Classes/ContentRepository.php b/Neos.ContentRepository.Core/Classes/ContentRepository.php index 696b2de305d..0db25916435 100644 --- a/Neos.ContentRepository.Core/Classes/ContentRepository.php +++ b/Neos.ContentRepository.Core/Classes/ContentRepository.php @@ -47,6 +47,7 @@ use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepository\Core\SharedModel\Workspace\Workspaces; use Neos\EventStore\EventStoreInterface; +use Neos\EventStore\Exception\ConcurrencyException; use Neos\EventStore\Model\Event\EventMetadata; use Neos\EventStore\Model\EventEnvelope; use Neos\EventStore\Model\EventStream\VirtualStreamName; @@ -101,38 +102,36 @@ public function handle(CommandInterface $command): void { // the commands only calculate which events they want to have published, but do not do the // publishing themselves - $eventsToPublish = $this->commandBus->handle($command, $this->commandHandlingDependencies); - - // TODO meaningful exception message - $initiatingUserId = $this->userIdProvider->getUserId(); - $initiatingTimestamp = $this->clock->now()->format(\DateTimeInterface::ATOM); - - // Add "initiatingUserId" and "initiatingTimestamp" metadata to all events. - // This is done in order to keep information about the _original_ metadata when an - // event is re-applied during publishing/rebasing - // "initiatingUserId": The identifier of the user that originally triggered this event. This will never - // be overridden if it is set once. - // "initiatingTimestamp": The timestamp of the original event. The "recordedAt" timestamp will always be - // re-created and reflects the time an event was actually persisted in a stream, - // the "initiatingTimestamp" will be kept and is never overridden again. - // TODO: cleanup - $eventsToPublish = new EventsToPublish( - $eventsToPublish->streamName, - Events::fromArray( - $eventsToPublish->events->map(function (EventInterface|DecoratedEvent $event) use ( - $initiatingUserId, - $initiatingTimestamp - ) { - $metadata = $event instanceof DecoratedEvent ? $event->eventMetadata?->value ?? [] : []; - $metadata['initiatingUserId'] ??= $initiatingUserId; - $metadata['initiatingTimestamp'] ??= $initiatingTimestamp; - return DecoratedEvent::create($event, metadata: EventMetadata::fromArray($metadata)); - }) - ), - $eventsToPublish->expectedVersion, - ); - - $this->eventPersister->publishEvents($this, $eventsToPublish); + $eventsToPublishOrGenerator = $this->commandBus->handle($command, $this->commandHandlingDependencies); + + if ($eventsToPublishOrGenerator instanceof EventsToPublish) { + $eventsToPublish = $this->enrichEventsToPublishWithMetadata($eventsToPublishOrGenerator); + $this->eventPersister->publishEvents($this, $eventsToPublish); + } else { + foreach ($eventsToPublishOrGenerator as $eventsToPublish) { + assert($eventsToPublish instanceof EventsToPublish); // just for the ide + $eventsToPublish = $this->enrichEventsToPublishWithMetadata($eventsToPublish); + try { + $this->eventPersister->publishEvents($this, $eventsToPublish); + } catch (ConcurrencyException $e) { + // we pass the exception into the generator, so it could be try-caught and reacted upon: + // + // try { + // yield EventsToPublish(); + // } catch (ConcurrencyException $e) { + // yield $restoreState(); + // throw $e; + // } + + $errorStrategy = $eventsToPublishOrGenerator->throw($e); + + if ($errorStrategy instanceof EventsToPublish) { + $eventsToPublish = $this->enrichEventsToPublishWithMetadata($errorStrategy); + $this->eventPersister->publishEvents($this, $this->enrichEventsToPublishWithMetadata($eventsToPublish)); + } + } + } + } } @@ -305,4 +304,37 @@ public function getContentDimensionSource(): ContentDimensionSourceInterface { return $this->contentDimensionSource; } + + /** + * Add "initiatingUserId" and "initiatingTimestamp" metadata to all events. + * This is done in order to keep information about the _original_ metadata when an + * event is re-applied during publishing/rebasing + * "initiatingUserId": The identifier of the user that originally triggered this event. This will never + * be overridden if it is set once. + * "initiatingTimestamp": The timestamp of the original event. The "recordedAt" timestamp will always be + * re-created and reflects the time an event was actually persisted in a stream, + * the "initiatingTimestamp" will be kept and is never overridden again. + */ + private function enrichEventsToPublishWithMetadata(EventsToPublish $eventsToPublish): EventsToPublish + { + $initiatingUserId = $this->userIdProvider->getUserId(); + $initiatingTimestamp = $this->clock->now()->format(\DateTimeInterface::ATOM); + + return new EventsToPublish( + $eventsToPublish->streamName, + Events::fromArray( + $eventsToPublish->events->map(function (EventInterface|DecoratedEvent $event) use ( + $initiatingUserId, + $initiatingTimestamp + ) { + $metadata = $event instanceof DecoratedEvent ? $event->eventMetadata?->value ?? [] : []; + $metadata['initiatingUserId'] ??= $initiatingUserId; + $metadata['initiatingTimestamp'] ??= $initiatingTimestamp; + + return DecoratedEvent::create($event, metadata: EventMetadata::fromArray($metadata)); + }) + ), + $eventsToPublish->expectedVersion, + ); + } } diff --git a/Neos.ContentRepository.Core/Classes/EventStore/EventPersister.php b/Neos.ContentRepository.Core/Classes/EventStore/EventPersister.php index 7c53549dac8..c01aa27b45e 100644 --- a/Neos.ContentRepository.Core/Classes/EventStore/EventPersister.php +++ b/Neos.ContentRepository.Core/Classes/EventStore/EventPersister.php @@ -24,7 +24,6 @@ public function __construct( } /** - * @param EventsToPublish $eventsToPublish * @throws ConcurrencyException in case the expectedVersion does not match */ public function publishEvents(ContentRepository $contentRepository, EventsToPublish $eventsToPublish): void diff --git a/Neos.ContentRepository.Core/Classes/Feature/ContentStreamClosing/Command/CloseContentStream.php b/Neos.ContentRepository.Core/Classes/Feature/ContentStreamClosing/Command/CloseContentStream.php index 16dfd711342..3234b633586 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/ContentStreamClosing/Command/CloseContentStream.php +++ b/Neos.ContentRepository.Core/Classes/Feature/ContentStreamClosing/Command/CloseContentStream.php @@ -18,9 +18,8 @@ use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; /** - * @internal implementation detail. You must not use this command directly. - * Direct use may lead to hard to revert senseless state in your content repository. - * Please use the higher level workspace commands instead. + * @internal only exposed for testing purposes. You must not use this command directly. + * Direct use will leave your content stream in a closed state. */ final readonly class CloseContentStream implements CommandInterface { diff --git a/Neos.ContentRepository.Core/Classes/Feature/ContentStreamCommandHandler.php b/Neos.ContentRepository.Core/Classes/Feature/ContentStreamCommandHandler.php index 76d48439e76..2b129e8286a 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/ContentStreamCommandHandler.php +++ b/Neos.ContentRepository.Core/Classes/Feature/ContentStreamCommandHandler.php @@ -18,33 +18,24 @@ use Neos\ContentRepository\Core\CommandHandler\CommandInterface; use Neos\ContentRepository\Core\CommandHandlingDependencies; use Neos\ContentRepository\Core\ContentRepository; -use Neos\ContentRepository\Core\EventStore\Events; use Neos\ContentRepository\Core\EventStore\EventsToPublish; use Neos\ContentRepository\Core\Feature\ContentStreamClosing\Command\CloseContentStream; use Neos\ContentRepository\Core\Feature\ContentStreamClosing\Command\ReopenContentStream; -use Neos\ContentRepository\Core\Feature\ContentStreamClosing\Event\ContentStreamWasClosed; -use Neos\ContentRepository\Core\Feature\ContentStreamClosing\Event\ContentStreamWasReopened; -use Neos\ContentRepository\Core\Feature\ContentStreamCreation\Command\CreateContentStream; -use Neos\ContentRepository\Core\Feature\ContentStreamCreation\Event\ContentStreamWasCreated; use Neos\ContentRepository\Core\Feature\ContentStreamForking\Command\ForkContentStream; -use Neos\ContentRepository\Core\Feature\ContentStreamForking\Event\ContentStreamWasForked; use Neos\ContentRepository\Core\Feature\ContentStreamRemoval\Command\RemoveContentStream; -use Neos\ContentRepository\Core\Feature\ContentStreamRemoval\Event\ContentStreamWasRemoved; use Neos\ContentRepository\Core\SharedModel\Exception\ContentStreamAlreadyExists; use Neos\ContentRepository\Core\SharedModel\Exception\ContentStreamDoesNotExistYet; -use Neos\ContentRepository\Core\SharedModel\Exception\ContentStreamIsClosed; -use Neos\ContentRepository\Core\SharedModel\Exception\ContentStreamIsNotClosed; -use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; -use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamStatus; -use Neos\EventStore\Model\EventStream\ExpectedVersion; /** * INTERNALS. Only to be used from WorkspaceCommandHandler!!! * * @internal from userland, you'll use ContentRepository::handle to dispatch commands + * FIXME try to fully get rid of this handler :D and the external commands! */ final class ContentStreamCommandHandler implements CommandHandlerInterface { + use ContentStreamHandling; + public function canHandle(CommandInterface $command): bool { return method_exists($this, 'handle' . (new \ReflectionClass($command))->getShortName()); @@ -53,7 +44,6 @@ public function canHandle(CommandInterface $command): bool public function handle(CommandInterface $command, CommandHandlingDependencies $commandHandlingDependencies): EventsToPublish { return match ($command::class) { - CreateContentStream::class => $this->handleCreateContentStream($command, $commandHandlingDependencies), CloseContentStream::class => $this->handleCloseContentStream($command, $commandHandlingDependencies), ReopenContentStream::class => $this->handleReopenContentStream($command, $commandHandlingDependencies), ForkContentStream::class => $this->handleForkContentStream($command, $commandHandlingDependencies), @@ -62,67 +52,18 @@ public function handle(CommandInterface $command, CommandHandlingDependencies $c }; } - /** - * @throws ContentStreamAlreadyExists - */ - private function handleCreateContentStream( - CreateContentStream $command, - CommandHandlingDependencies $commandHandlingDependencies, - ): EventsToPublish { - $this->requireContentStreamToNotExistYet($command->contentStreamId, $commandHandlingDependencies); - $streamName = ContentStreamEventStreamName::fromContentStreamId($command->contentStreamId) - ->getEventStreamName(); - - return new EventsToPublish( - $streamName, - Events::with( - new ContentStreamWasCreated( - $command->contentStreamId, - ) - ), - ExpectedVersion::NO_STREAM() - ); - } - private function handleCloseContentStream( CloseContentStream $command, CommandHandlingDependencies $commandHandlingDependencies, ): EventsToPublish { - $this->requireContentStreamToExist($command->contentStreamId, $commandHandlingDependencies); - $expectedVersion = $this->getExpectedVersionOfContentStream($command->contentStreamId, $commandHandlingDependencies); - $this->requireContentStreamToNotBeClosed($command->contentStreamId, $commandHandlingDependencies); - $streamName = ContentStreamEventStreamName::fromContentStreamId($command->contentStreamId)->getEventStreamName(); - - return new EventsToPublish( - $streamName, - Events::with( - new ContentStreamWasClosed( - $command->contentStreamId, - ), - ), - $expectedVersion - ); + return $this->closeContentStream($command->contentStreamId, $commandHandlingDependencies); } private function handleReopenContentStream( ReopenContentStream $command, CommandHandlingDependencies $commandHandlingDependencies, ): EventsToPublish { - $this->requireContentStreamToExist($command->contentStreamId, $commandHandlingDependencies); - $expectedVersion = $this->getExpectedVersionOfContentStream($command->contentStreamId, $commandHandlingDependencies); - $this->requireContentStreamToBeClosed($command->contentStreamId, $commandHandlingDependencies); - $streamName = ContentStreamEventStreamName::fromContentStreamId($command->contentStreamId)->getEventStreamName(); - - return new EventsToPublish( - $streamName, - Events::with( - new ContentStreamWasReopened( - $command->contentStreamId, - $command->previousState, - ), - ), - $expectedVersion - ); + return $this->reopenContentStream($command->contentStreamId, $command->previousState, $commandHandlingDependencies); } /** @@ -133,114 +74,13 @@ private function handleForkContentStream( ForkContentStream $command, CommandHandlingDependencies $commandHandlingDependencies ): EventsToPublish { - $this->requireContentStreamToExist($command->sourceContentStreamId, $commandHandlingDependencies); - $this->requireContentStreamToNotBeClosed($command->sourceContentStreamId, $commandHandlingDependencies); - $this->requireContentStreamToNotExistYet($command->newContentStreamId, $commandHandlingDependencies); - - $sourceContentStreamVersion = $commandHandlingDependencies->getContentStreamVersion($command->sourceContentStreamId); - - $streamName = ContentStreamEventStreamName::fromContentStreamId($command->newContentStreamId) - ->getEventStreamName(); - - return new EventsToPublish( - $streamName, - Events::with( - new ContentStreamWasForked( - $command->newContentStreamId, - $command->sourceContentStreamId, - $sourceContentStreamVersion, - ), - ), - // NO_STREAM to ensure the "fork" happens as the first event of the new content stream - ExpectedVersion::NO_STREAM() - ); + return $this->forkContentStream($command->newContentStreamId, $command->sourceContentStreamId, $commandHandlingDependencies); } private function handleRemoveContentStream( RemoveContentStream $command, CommandHandlingDependencies $commandHandlingDependencies ): EventsToPublish { - $this->requireContentStreamToExist($command->contentStreamId, $commandHandlingDependencies); - $expectedVersion = $this->getExpectedVersionOfContentStream($command->contentStreamId, $commandHandlingDependencies); - - $streamName = ContentStreamEventStreamName::fromContentStreamId( - $command->contentStreamId - )->getEventStreamName(); - - return new EventsToPublish( - $streamName, - Events::with( - new ContentStreamWasRemoved( - $command->contentStreamId, - ), - ), - $expectedVersion - ); - } - - /** - * @param ContentStreamId $contentStreamId - * @param CommandHandlingDependencies $commandHandlingDependencies - * @throws ContentStreamAlreadyExists - */ - protected function requireContentStreamToNotExistYet( - ContentStreamId $contentStreamId, - CommandHandlingDependencies $commandHandlingDependencies - ): void { - if ($commandHandlingDependencies->contentStreamExists($contentStreamId)) { - throw new ContentStreamAlreadyExists( - 'Content stream "' . $contentStreamId->value . '" already exists.', - 1521386345 - ); - } - } - - /** - * @param ContentStreamId $contentStreamId - * @param CommandHandlingDependencies $commandHandlingDependencies - * @throws ContentStreamDoesNotExistYet - */ - protected function requireContentStreamToExist( - ContentStreamId $contentStreamId, - CommandHandlingDependencies $commandHandlingDependencies - ): void { - if (!$commandHandlingDependencies->contentStreamExists($contentStreamId)) { - throw new ContentStreamDoesNotExistYet( - 'Content stream "' . $contentStreamId->value . '" does not exist yet.', - 1521386692 - ); - } - } - - protected function requireContentStreamToNotBeClosed( - ContentStreamId $contentStreamId, - CommandHandlingDependencies $commandHandlingDependencies - ): void { - if ($commandHandlingDependencies->getContentStreamStatus($contentStreamId) === ContentStreamStatus::CLOSED) { - throw new ContentStreamIsClosed( - 'Content stream "' . $contentStreamId->value . '" is closed.', - 1710260081 - ); - } - } - - protected function requireContentStreamToBeClosed( - ContentStreamId $contentStreamId, - CommandHandlingDependencies $commandHandlingDependencies - ): void { - if ($commandHandlingDependencies->getContentStreamStatus($contentStreamId) !== ContentStreamStatus::CLOSED) { - throw new ContentStreamIsNotClosed( - 'Content stream "' . $contentStreamId->value . '" is not closed.', - 1710405911 - ); - } - } - - protected function getExpectedVersionOfContentStream( - ContentStreamId $contentStreamId, - CommandHandlingDependencies $commandHandlingDependencies - ): ExpectedVersion { - $version = $commandHandlingDependencies->getContentStreamVersion($contentStreamId); - return ExpectedVersion::fromVersion($version); + return $this->removeContentStream($command->contentStreamId, $commandHandlingDependencies); } } diff --git a/Neos.ContentRepository.Core/Classes/Feature/ContentStreamCreation/Command/CreateContentStream.php b/Neos.ContentRepository.Core/Classes/Feature/ContentStreamCreation/Command/CreateContentStream.php deleted file mode 100644 index 8644048817e..00000000000 --- a/Neos.ContentRepository.Core/Classes/Feature/ContentStreamCreation/Command/CreateContentStream.php +++ /dev/null @@ -1,45 +0,0 @@ -requireContentStreamToNotExistYet($contentStreamId, $commandHandlingDependencies); + $streamName = ContentStreamEventStreamName::fromContentStreamId($contentStreamId) + ->getEventStreamName(); + + return new EventsToPublish( + $streamName, + Events::with( + new ContentStreamWasCreated( + $contentStreamId, + ) + ), + ExpectedVersion::NO_STREAM() + ); + } + + /** + * @param ContentStreamId $contentStreamId The id of the content stream to close + * @param CommandHandlingDependencies $commandHandlingDependencies + * @return EventsToPublish + */ + private function closeContentStream( + ContentStreamId $contentStreamId, + CommandHandlingDependencies $commandHandlingDependencies, + ): EventsToPublish { + $this->requireContentStreamToExist($contentStreamId, $commandHandlingDependencies); + $expectedVersion = $this->getExpectedVersionOfContentStream($contentStreamId, $commandHandlingDependencies); + $this->requireContentStreamToNotBeClosed($contentStreamId, $commandHandlingDependencies); + $streamName = ContentStreamEventStreamName::fromContentStreamId($contentStreamId)->getEventStreamName(); + + return new EventsToPublish( + $streamName, + Events::with( + new ContentStreamWasClosed( + $contentStreamId, + ), + ), + $expectedVersion + ); + } + + /** + * @param ContentStreamId $contentStreamId The id of the content stream to reopen + * @param ContentStreamStatus $previousState The state the content stream was in before closing and is to be reset to + */ + private function reopenContentStream( + ContentStreamId $contentStreamId, + ContentStreamStatus $previousState, + CommandHandlingDependencies $commandHandlingDependencies, + ): EventsToPublish { + $this->requireContentStreamToExist($contentStreamId, $commandHandlingDependencies); + $this->requireContentStreamToBeClosed($contentStreamId, $commandHandlingDependencies); + $streamName = ContentStreamEventStreamName::fromContentStreamId($contentStreamId)->getEventStreamName(); + + return new EventsToPublish( + $streamName, + Events::with( + new ContentStreamWasReopened( + $contentStreamId, + $previousState, + ), + ), + ExpectedVersion::ANY() + ); + } + + /** + * @param ContentStreamId $newContentStreamId The id of the new content stream + * @param ContentStreamId $sourceContentStreamId The id of the content stream to fork + * @throws ContentStreamAlreadyExists + * @throws ContentStreamDoesNotExistYet + */ + private function forkContentStream( + ContentStreamId $newContentStreamId, + ContentStreamId $sourceContentStreamId, + CommandHandlingDependencies $commandHandlingDependencies + ): EventsToPublish { + $this->requireContentStreamToExist($sourceContentStreamId, $commandHandlingDependencies); + $this->requireContentStreamToNotBeClosed($sourceContentStreamId, $commandHandlingDependencies); + $this->requireContentStreamToNotExistYet($newContentStreamId, $commandHandlingDependencies); + + $sourceContentStreamVersion = $commandHandlingDependencies->getContentStreamVersion($sourceContentStreamId); + + $streamName = ContentStreamEventStreamName::fromContentStreamId($newContentStreamId) + ->getEventStreamName(); + + return new EventsToPublish( + $streamName, + Events::with( + new ContentStreamWasForked( + $newContentStreamId, + $sourceContentStreamId, + $sourceContentStreamVersion, + ), + ), + // NO_STREAM to ensure the "fork" happens as the first event of the new content stream + ExpectedVersion::NO_STREAM() + ); + } + + /** + * @param ContentStreamId $contentStreamId The id of the content stream to remove + */ + private function removeContentStream( + ContentStreamId $contentStreamId, + CommandHandlingDependencies $commandHandlingDependencies + ): EventsToPublish { + $this->requireContentStreamToExist($contentStreamId, $commandHandlingDependencies); + $expectedVersion = $this->getExpectedVersionOfContentStream($contentStreamId, $commandHandlingDependencies); + + $streamName = ContentStreamEventStreamName::fromContentStreamId( + $contentStreamId + )->getEventStreamName(); + + return new EventsToPublish( + $streamName, + Events::with( + new ContentStreamWasRemoved( + $contentStreamId, + ), + ), + $expectedVersion + ); + } + + /** + * @param ContentStreamId $contentStreamId + * @param CommandHandlingDependencies $commandHandlingDependencies + * @throws ContentStreamAlreadyExists + */ + private function requireContentStreamToNotExistYet( + ContentStreamId $contentStreamId, + CommandHandlingDependencies $commandHandlingDependencies + ): void { + if ($commandHandlingDependencies->contentStreamExists($contentStreamId)) { + throw new ContentStreamAlreadyExists( + 'Content stream "' . $contentStreamId->value . '" already exists.', + 1521386345 + ); + } + } + + /** + * @param ContentStreamId $contentStreamId + * @param CommandHandlingDependencies $commandHandlingDependencies + * @throws ContentStreamDoesNotExistYet + */ + private function requireContentStreamToExist( + ContentStreamId $contentStreamId, + CommandHandlingDependencies $commandHandlingDependencies + ): void { + if (!$commandHandlingDependencies->contentStreamExists($contentStreamId)) { + throw new ContentStreamDoesNotExistYet( + 'Content stream "' . $contentStreamId->value . '" does not exist yet.', + 1521386692 + ); + } + } + + private function requireContentStreamToNotBeClosed( + ContentStreamId $contentStreamId, + CommandHandlingDependencies $commandHandlingDependencies + ): void { + if ($commandHandlingDependencies->getContentStreamStatus($contentStreamId) === ContentStreamStatus::CLOSED) { + throw new ContentStreamIsClosed( + 'Content stream "' . $contentStreamId->value . '" is closed.', + 1710260081 + ); + } + } + + private function requireContentStreamToBeClosed( + ContentStreamId $contentStreamId, + CommandHandlingDependencies $commandHandlingDependencies + ): void { + if ($commandHandlingDependencies->getContentStreamStatus($contentStreamId) !== ContentStreamStatus::CLOSED) { + throw new ContentStreamIsNotClosed( + 'Content stream "' . $contentStreamId->value . '" is not closed.', + 1710405911 + ); + } + } + + private function getExpectedVersionOfContentStream( + ContentStreamId $contentStreamId, + CommandHandlingDependencies $commandHandlingDependencies + ): ExpectedVersion { + $version = $commandHandlingDependencies->getContentStreamVersion($contentStreamId); + return ExpectedVersion::fromVersion($version); + } +} diff --git a/Neos.ContentRepository.Core/Classes/Feature/ContentStreamRemoval/Command/RemoveContentStream.php b/Neos.ContentRepository.Core/Classes/Feature/ContentStreamRemoval/Command/RemoveContentStream.php index f79c0fe1da8..932a488a842 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/ContentStreamRemoval/Command/RemoveContentStream.php +++ b/Neos.ContentRepository.Core/Classes/Feature/ContentStreamRemoval/Command/RemoveContentStream.php @@ -15,14 +15,14 @@ */ use Neos\ContentRepository\Core\CommandHandler\CommandInterface; +use Neos\ContentRepository\Core\Service\ContentStreamPruner; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; /** * Command to remove an existing content stream * - * @internal implementation detail. You must not use this command directly. - * Direct use may lead to hard to revert senseless state in your content repository. - * Please use the higher level workspace commands instead. + * @internal implementation detail of the {@see ContentStreamPruner}. You must not use this command directly. + * Direct use would throw away the content stream and may leave a broken state. */ final readonly class RemoveContentStream implements CommandInterface { diff --git a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php index 1851a976560..daaf635fede 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php +++ b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php @@ -24,6 +24,7 @@ use Neos\ContentRepository\Core\EventStore\EventPersister; use Neos\ContentRepository\Core\EventStore\Events; use Neos\ContentRepository\Core\EventStore\EventsToPublish; +use Neos\ContentRepository\Core\EventStore\EventsToPublishFailed; use Neos\ContentRepository\Core\Feature\Common\MatchableWithNodeIdToPublishOrDiscardInterface; use Neos\ContentRepository\Core\Feature\Common\PublishableToWorkspaceInterface; use Neos\ContentRepository\Core\Feature\Common\RebasableToOtherWorkspaceInterface; @@ -62,17 +63,16 @@ use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Dto\RebaseErrorHandlingStrategy; use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Event\WorkspaceWasRebased; use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Exception\WorkspaceRebaseFailed; -use Neos\ContentRepository\Core\SharedModel\Workspace\Workspace; use Neos\ContentRepository\Core\SharedModel\Exception\ContentStreamAlreadyExists; use Neos\ContentRepository\Core\SharedModel\Exception\ContentStreamDoesNotExistYet; use Neos\ContentRepository\Core\SharedModel\Exception\WorkspaceDoesNotExist; use Neos\ContentRepository\Core\SharedModel\Exception\WorkspaceHasNoBaseWorkspaceName; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; +use Neos\ContentRepository\Core\SharedModel\Workspace\Workspace; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\EventStore\EventStoreInterface; use Neos\EventStore\Exception\ConcurrencyException; use Neos\EventStore\Model\Event\EventType; -use Neos\EventStore\Model\EventEnvelope; use Neos\EventStore\Model\EventStream\ExpectedVersion; /** @@ -80,6 +80,8 @@ */ final readonly class WorkspaceCommandHandler implements CommandHandlerInterface { + use ContentStreamHandling; + public function __construct( private EventPersister $eventPersister, private EventStoreInterface $eventStore, @@ -92,7 +94,7 @@ public function canHandle(CommandInterface $command): bool return method_exists($this, 'handle' . (new \ReflectionClass($command))->getShortName()); } - public function handle(CommandInterface $command, CommandHandlingDependencies $commandHandlingDependencies): EventsToPublish + public function handle(CommandInterface $command, CommandHandlingDependencies $commandHandlingDependencies): EventsToPublish|\Generator { /** @phpstan-ignore-next-line */ return match ($command::class) { @@ -117,7 +119,7 @@ public function handle(CommandInterface $command, CommandHandlingDependencies $c private function handleCreateWorkspace( CreateWorkspace $command, CommandHandlingDependencies $commandHandlingDependencies, - ): EventsToPublish { + ): \Generator { $this->requireWorkspaceToNotExist($command->workspaceName, $commandHandlingDependencies); if ($commandHandlingDependencies->findWorkspaceByName($command->baseWorkspaceName) === null) { throw new BaseWorkspaceDoesNotExist(sprintf( @@ -129,57 +131,50 @@ private function handleCreateWorkspace( $baseWorkspaceContentGraph = $commandHandlingDependencies->getContentGraph($command->baseWorkspaceName); // When the workspace is created, we first have to fork the content stream - $commandHandlingDependencies->handle( - ForkContentStream::create( - $command->newContentStreamId, - $baseWorkspaceContentGraph->getContentStreamId(), - ) - ); - - $events = Events::with( - new WorkspaceWasCreated( - $command->workspaceName, - $command->baseWorkspaceName, - $command->newContentStreamId, - ) + yield $this->forkContentStream( + $command->newContentStreamId, + $baseWorkspaceContentGraph->getContentStreamId(), + $commandHandlingDependencies ); - return new EventsToPublish( + yield new EventsToPublish( WorkspaceEventStreamName::fromWorkspaceName($command->workspaceName)->getEventStreamName(), - $events, + Events::with( + new WorkspaceWasCreated( + $command->workspaceName, + $command->baseWorkspaceName, + $command->newContentStreamId, + ) + ), ExpectedVersion::ANY() ); } /** * @param CreateRootWorkspace $command - * @return EventsToPublish * @throws WorkspaceAlreadyExists * @throws ContentStreamAlreadyExists */ private function handleCreateRootWorkspace( CreateRootWorkspace $command, CommandHandlingDependencies $commandHandlingDependencies, - ): EventsToPublish { + ): \Generator { $this->requireWorkspaceToNotExist($command->workspaceName, $commandHandlingDependencies); $newContentStreamId = $command->newContentStreamId; - $commandHandlingDependencies->handle( - CreateContentStream::create( - $newContentStreamId, - ) + yield $this->createContentStream( + $newContentStreamId, + $commandHandlingDependencies ); - $events = Events::with( - new RootWorkspaceWasCreated( - $command->workspaceName, - $newContentStreamId - ) - ); - - return new EventsToPublish( + yield new EventsToPublish( WorkspaceEventStreamName::fromWorkspaceName($command->workspaceName)->getEventStreamName(), - $events, + Events::with( + new RootWorkspaceWasCreated( + $command->workspaceName, + $newContentStreamId + ) + ), ExpectedVersion::ANY() ); } @@ -196,44 +191,109 @@ private function handleCreateRootWorkspace( private function handlePublishWorkspace( PublishWorkspace $command, CommandHandlingDependencies $commandHandlingDependencies, - ): EventsToPublish { + ): \Generator { $workspace = $this->requireWorkspace($command->workspaceName, $commandHandlingDependencies); $baseWorkspace = $this->requireBaseWorkspace($workspace, $commandHandlingDependencies); - $this->publishContentStream( - $commandHandlingDependencies, + $publishContentStream = $this->getCopiedEventsToPublishForContentStream( $workspace->currentContentStreamId, $baseWorkspace->workspaceName, $baseWorkspace->currentContentStreamId ); + try { + yield $publishContentStream; + } catch (ConcurrencyException $exception) { + throw new BaseWorkspaceHasBeenModifiedInTheMeantime(sprintf( + 'The base workspace has been modified in the meantime; please rebase.' + . ' Expected version %d of source content stream %s', + $publishContentStream->expectedVersion->value, + $baseWorkspace->currentContentStreamId + )); + } + // After publishing a workspace, we need to again fork from Base. - $commandHandlingDependencies->handle( - ForkContentStream::create( - $command->newContentStreamId, - $baseWorkspace->currentContentStreamId, - ) + yield $this->forkContentStream( + $command->newContentStreamId, + $baseWorkspace->currentContentStreamId, + $commandHandlingDependencies ); - $streamName = WorkspaceEventStreamName::fromWorkspaceName($command->workspaceName)->getEventStreamName(); - $events = Events::with( - new WorkspaceWasPublished( - $command->workspaceName, - $baseWorkspace->workspaceName, - $command->newContentStreamId, - $workspace->currentContentStreamId, - ) + // if we got so far without an Exception, we can switch the Workspace's active Content stream. + yield new EventsToPublish( + WorkspaceEventStreamName::fromWorkspaceName($command->workspaceName)->getEventStreamName(), + Events::with( + new WorkspaceWasPublished( + $command->workspaceName, + $baseWorkspace->workspaceName, + $command->newContentStreamId, + $workspace->currentContentStreamId, + ) + ), + ExpectedVersion::ANY() ); + } + + /** + * @throws BaseWorkspaceHasBeenModifiedInTheMeantime + * @throws \Exception + */ + private function getCopiedEventsToPublishForContentStream( + ContentStreamId $contentStreamId, + WorkspaceName $baseWorkspaceName, + ContentStreamId $baseContentStreamId, + ): EventsToPublish { + $baseWorkspaceContentStreamName = ContentStreamEventStreamName::fromContentStreamId( + $baseContentStreamId + ); + + // TODO: please check the code below in-depth. it does: + // - copy all events from the "user" content stream which implement @see{}"PublishableToOtherContentStreamsInterface" + // - extract the initial ContentStreamWasForked event, + // to read the version of the source content stream when the fork occurred + // - ensure that no other changes have been done in the meantime in the base content stream + + $workspaceContentStream = iterator_to_array($this->eventStore->load( + ContentStreamEventStreamName::fromContentStreamId($contentStreamId)->getEventStreamName() + )); + + $events = []; + $contentStreamWasForkedEvent = null; + foreach ($workspaceContentStream as $eventEnvelope) { + $event = $this->eventNormalizer->denormalize($eventEnvelope->event); + + if ($event instanceof ContentStreamWasForked) { + if ($contentStreamWasForkedEvent !== null) { + throw new \RuntimeException( + 'Invariant violation: The content stream "' . $contentStreamId->value + . '" has two forked events.', + 1658740373 + ); + } + $contentStreamWasForkedEvent = $event; + } elseif ($event instanceof PublishableToWorkspaceInterface) { + /** @var EventInterface $copiedEvent */ + $copiedEvent = $event->withWorkspaceNameAndContentStreamId($baseWorkspaceName, $baseContentStreamId); + // We need to add the event metadata here for rebasing in nested workspace situations + // (and for exporting) + $events[] = DecoratedEvent::create($copiedEvent, metadata: $eventEnvelope->event->metadata, causationId: $eventEnvelope->event->causationId, correlationId: $eventEnvelope->event->correlationId); + } + } + + if ($contentStreamWasForkedEvent === null) { + throw new \RuntimeException('Invariant violation: The content stream "' . $contentStreamId->value + . '" has NO forked event.', 1658740407); + } - // if we got so far without an Exception, we can switch the Workspace's active Content stream. return new EventsToPublish( - $streamName, - $events, - ExpectedVersion::ANY() + $baseWorkspaceContentStreamName->getEventStreamName(), + Events::fromArray($events), + ExpectedVersion::fromVersion($contentStreamWasForkedEvent->versionOfSourceContentStream) ); } /** + * @deprecated FIXME REMOVE with https://github.com/neos/neos-development-collection/pull/5301 * @throws BaseWorkspaceHasBeenModifiedInTheMeantime * @throws \Exception */ @@ -256,7 +316,6 @@ private function publishContentStream( $workspaceContentStream = iterator_to_array($this->eventStore->load( ContentStreamEventStreamName::fromContentStreamId($contentStreamId)->getEventStreamName() )); - /** @var array $workspaceContentStream */ $events = []; $contentStreamWasForkedEvent = null; @@ -404,7 +463,7 @@ function () use ($originalCommands, $commandHandlingDependencies, &$commandsThat } /** - * @return array + * @return array */ private function extractCommandsFromContentStreamMetadata( ContentStreamEventStreamName $workspaceContentStreamName, @@ -419,17 +478,18 @@ private function extractCommandsFromContentStreamMetadata( if (isset($metadata['commandClass'])) { $commandToRebaseClass = $metadata['commandClass']; $commandToRebasePayload = $metadata['commandPayload']; - if (!method_exists($commandToRebaseClass, 'fromArray')) { + + if (array_diff(class_implements($commandToRebaseClass) ?: [], [CommandInterface::class, RebasableToOtherWorkspaceInterface::class]) === []) { throw new \RuntimeException(sprintf( - 'Command "%s" can\'t be rebased because it does not implement a static "fromArray" constructor', - $commandToRebaseClass + 'Command "%s" can\'t be rebased because it does not implement %s', + $commandToRebaseClass, + RebasableToOtherWorkspaceInterface::class ), 1547815341); } - /** - * The "fromArray" might be declared via {@see RebasableToOtherWorkspaceInterface::fromArray()} - * or any other command can just implement it. - */ - $commands[$eventEnvelope->sequenceNumber->value] = $commandToRebaseClass::fromArray($commandToRebasePayload); + /** @var class-string $commandToRebaseClass */ + /** @var CommandInterface&RebasableToOtherWorkspaceInterface $commandInstance */ + $commandInstance = $commandToRebaseClass::fromArray($commandToRebasePayload); + $commands[$eventEnvelope->sequenceNumber->value] = $commandInstance; } } @@ -469,8 +529,6 @@ private function handlePublishIndividualNodesFromWorkspace( $matchingCommands = []; $remainingCommands = []; $this->separateMatchingAndRemainingCommands($command, $workspace, $matchingCommands, $remainingCommands); - /** @var array $matchingCommands */ - /** @var array $remainingCommands */ // 3) fork a new contentStream, based on the base WS, and apply MATCHING $commandHandlingDependencies->handle( @@ -608,9 +666,7 @@ private function handleDiscardIndividualNodesFromWorkspace( // 2) filter commands, only keeping the ones NOT MATCHING the nodes from the command // (i.e. the modifications we want to keep) - /** @var array $commandsToDiscard */ $commandsToDiscard = []; - /** @var array $commandsToKeep */ $commandsToKeep = []; $this->separateMatchingAndRemainingCommands($command, $workspace, $commandsToDiscard, $commandsToKeep); @@ -629,13 +685,6 @@ private function handleDiscardIndividualNodesFromWorkspace( $command->newContentStreamId, function () use ($commandsToKeep, $commandHandlingDependencies, $baseWorkspace): void { foreach ($commandsToKeep as $matchingCommand) { - if (!($matchingCommand instanceof RebasableToOtherWorkspaceInterface)) { - throw new \RuntimeException( - 'ERROR: The command ' . get_class($matchingCommand) - . ' does not implement ' . RebasableToOtherWorkspaceInterface::class . '; but it should!' - ); - } - $commandHandlingDependencies->handle($matchingCommand->createCopyForWorkspace( $baseWorkspace->workspaceName, )); @@ -683,6 +732,8 @@ function () use ($commandsToKeep, $commandHandlingDependencies, $baseWorkspace): /** * @param array &$matchingCommands * @param array &$remainingCommands + * @param-out array $matchingCommands + * @param-out array $remainingCommands */ private function separateMatchingAndRemainingCommands( PublishIndividualNodesFromWorkspace|DiscardIndividualNodesFromWorkspace $command, @@ -736,31 +787,27 @@ private function commandMatchesAtLeastOneNode( private function handleDiscardWorkspace( DiscardWorkspace $command, CommandHandlingDependencies $commandHandlingDependencies, - ): EventsToPublish { + ): \Generator { $workspace = $this->requireWorkspace($command->workspaceName, $commandHandlingDependencies); $baseWorkspace = $this->requireBaseWorkspace($workspace, $commandHandlingDependencies); $newContentStream = $command->newContentStreamId; - $commandHandlingDependencies->handle( - ForkContentStream::create( - $newContentStream, - $baseWorkspace->currentContentStreamId, - ) + yield $this->forkContentStream( + $newContentStream, + $baseWorkspace->currentContentStreamId, + $commandHandlingDependencies ); // if we got so far without an Exception, we can switch the Workspace's active Content stream. - $streamName = WorkspaceEventStreamName::fromWorkspaceName($command->workspaceName)->getEventStreamName(); - $events = Events::with( - new WorkspaceWasDiscarded( - $command->workspaceName, - $newContentStream, - $workspace->currentContentStreamId, - ) - ); - - return new EventsToPublish( - $streamName, - $events, + yield new EventsToPublish( + WorkspaceEventStreamName::fromWorkspaceName($command->workspaceName)->getEventStreamName(), + Events::with( + new WorkspaceWasDiscarded( + $command->workspaceName, + $newContentStream, + $workspace->currentContentStreamId, + ) + ), ExpectedVersion::ANY() ); } @@ -776,7 +823,7 @@ private function handleDiscardWorkspace( private function handleChangeBaseWorkspace( ChangeBaseWorkspace $command, CommandHandlingDependencies $commandHandlingDependencies, - ): EventsToPublish { + ): \Generator { $workspace = $this->requireWorkspace($command->workspaceName, $commandHandlingDependencies); $this->requireEmptyWorkspace($workspace); $this->requireBaseWorkspace($workspace, $commandHandlingDependencies); @@ -784,25 +831,21 @@ private function handleChangeBaseWorkspace( $this->requireNonCircularRelationBetweenWorkspaces($workspace, $baseWorkspace, $commandHandlingDependencies); - $commandHandlingDependencies->handle( - ForkContentStream::create( - $command->newContentStreamId, - $baseWorkspace->currentContentStreamId, - ) - ); - - $streamName = WorkspaceEventStreamName::fromWorkspaceName($command->workspaceName)->getEventStreamName(); - $events = Events::with( - new WorkspaceBaseWorkspaceWasChanged( - $command->workspaceName, - $command->baseWorkspaceName, - $command->newContentStreamId, - ) + yield $this->forkContentStream( + $command->newContentStreamId, + $baseWorkspace->currentContentStreamId, + $commandHandlingDependencies ); - return new EventsToPublish( - $streamName, - $events, + yield new EventsToPublish( + WorkspaceEventStreamName::fromWorkspaceName($command->workspaceName)->getEventStreamName(), + Events::with( + new WorkspaceBaseWorkspaceWasChanged( + $command->workspaceName, + $command->baseWorkspaceName, + $command->newContentStreamId, + ) + ), ExpectedVersion::ANY() ); } @@ -813,25 +856,21 @@ private function handleChangeBaseWorkspace( private function handleDeleteWorkspace( DeleteWorkspace $command, CommandHandlingDependencies $commandHandlingDependencies, - ): EventsToPublish { + ): \Generator { $workspace = $this->requireWorkspace($command->workspaceName, $commandHandlingDependencies); - $commandHandlingDependencies->handle( - RemoveContentStream::create( - $workspace->currentContentStreamId - ) - ); - - $events = Events::with( - new WorkspaceWasRemoved( - $command->workspaceName, - ) + yield $this->removeContentStream( + $workspace->currentContentStreamId, + $commandHandlingDependencies ); - $streamName = WorkspaceEventStreamName::fromWorkspaceName($command->workspaceName)->getEventStreamName(); - return new EventsToPublish( - $streamName, - $events, + yield new EventsToPublish( + WorkspaceEventStreamName::fromWorkspaceName($command->workspaceName)->getEventStreamName(), + Events::with( + new WorkspaceWasRemoved( + $command->workspaceName, + ) + ), ExpectedVersion::ANY() ); } diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteTrait.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteTrait.php index 0384232e778..75f11288a99 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteTrait.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteTrait.php @@ -35,7 +35,6 @@ use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamStatus; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\Features\ContentStreamClosing; -use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\Features\ContentStreamForking; use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\Features\NodeCopying; use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\Features\NodeCreation; use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\Features\NodeDisabling; @@ -66,7 +65,6 @@ trait CRTestSuiteTrait use ProjectedNodeTrait; use GenericCommandExecutionAndEventPublication; - use ContentStreamForking; use ContentStreamClosing; use NodeCreation; diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/ContentStreamForking.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/ContentStreamForking.php deleted file mode 100644 index dc859e092d4..00000000000 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/ContentStreamForking.php +++ /dev/null @@ -1,60 +0,0 @@ -readPayloadTable($payloadTable); - $command = ForkContentStream::create( - ContentStreamId::fromString($commandArguments['contentStreamId']), - ContentStreamId::fromString($commandArguments['sourceContentStreamId']), - ); - - $this->currentContentRepository->handle($command); - } - - /** - * @Given /^the command ForkContentStream is executed with payload and exceptions are caught:$/ - * @param TableNode $payloadTable - * @throws \Exception - */ - public function theCommandForkContentStreamIsExecutedWithPayloadAndExceptionsAreCaught(TableNode $payloadTable): void - { - try { - $this->theCommandForkContentStreamIsExecutedWithPayload($payloadTable); - } catch (\Exception $exception) { - $this->lastCommandException = $exception; - } - } -} diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/WorkspaceCreation.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/WorkspaceCreation.php index b876e98a7b4..91122fe5dba 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/WorkspaceCreation.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/WorkspaceCreation.php @@ -21,6 +21,7 @@ use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Command\CreateWorkspace; use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Command\RebaseWorkspace; use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Dto\RebaseErrorHandlingStrategy; +use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Exception\WorkspaceRebaseFailed; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\CRTestSuiteRuntimeVariables; @@ -37,22 +38,6 @@ abstract protected function readPayloadTable(TableNode $payloadTable): array; abstract protected function publishEvent(string $eventType, StreamName $streamName, array $eventPayload): void; - /** - * @When /^the command CreateContentStream is executed with payload:$/ - * @param TableNode $payloadTable - * @throws \Exception - */ - public function theCommandCreateContentStreamIsExecutedWithPayload(TableNode $payloadTable) - { - $commandArguments = $this->readPayloadTable($payloadTable); - - $command = CreateContentStream::create( - ContentStreamId::fromString($commandArguments['contentStreamId']), - ); - - $this->currentContentRepository->handle($command); - } - /** * @When /^the command CreateRootWorkspace is executed with payload:$/ * @param TableNode $payloadTable @@ -100,6 +85,19 @@ public function theCommandCreateWorkspaceIsExecutedWithPayload(TableNode $payloa $this->currentContentRepository->handle($command); } + /** + * @When /^the command CreateWorkspace is executed with payload and exceptions are caught:$/ + * @param TableNode $payloadTable + * @throws \Exception + */ + public function theCommandCreateWorkspaceIsExecutedWithPayloadAndExceptionsAreCaught(TableNode $payloadTable) + { + try { + $this->theCommandCreateWorkspaceIsExecutedWithPayload($payloadTable); + } catch (\Exception $e) { + $this->lastCommandException = $e; + } + } /** * @When /^the command RebaseWorkspace is executed with payload:$/