Skip to content

Commit

Permalink
Merge pull request #4186 from dlubitz/feature-workspace-changes
Browse files Browse the repository at this point in the history
FEATURE: Edit, delete and switch workspaces
  • Loading branch information
skurfuerst authored Apr 16, 2023
2 parents 2ea8da6 + f342d4c commit 8f496c4
Show file tree
Hide file tree
Showing 20 changed files with 672 additions and 102 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,13 @@
use Neos\ContentRepository\Core\Feature\WorkspacePublication\Event\WorkspaceWasPublished;
use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Event\WorkspaceRebaseFailed;
use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Event\WorkspaceWasRebased;
use Neos\ContentRepository\Core\Feature\WorkspaceModification\Event\WorkspaceWasRenamed;
use Neos\ContentRepository\Core\Feature\WorkspaceModification\Event\WorkspaceWasRemoved;
use Neos\ContentRepository\Core\Feature\WorkspaceModification\Event\WorkspaceOwnerWasChanged;
use Neos\EventStore\Model\Event\EventData;
use Neos\EventStore\Model\Event;
use Neos\EventStore\Model\Event\EventType;
use Neos\ContentRepository\Core\Feature\WorkspaceModification\Event\WorkspaceBaseWorkspaceWasChanged;

/**
* Central authority to convert Content Repository domain events to Event Store EventData and EventType, vice versa.
Expand Down Expand Up @@ -85,11 +89,15 @@ public function __construct()
RootNodeAggregateDimensionsWereUpdated::class,
WorkspaceRebaseFailed::class,
WorkspaceWasCreated::class,
WorkspaceWasRenamed::class,
WorkspaceWasDiscarded::class,
WorkspaceWasPartiallyDiscarded::class,
WorkspaceWasPartiallyPublished::class,
WorkspaceWasPublished::class,
WorkspaceWasRebased::class
WorkspaceWasRebased::class,
WorkspaceWasRemoved::class,
WorkspaceOwnerWasChanged::class,
WorkspaceBaseWorkspaceWasChanged::class,
];

foreach ($supportedEventClassNames as $fullEventClassName) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@ private function handleForkContentStream(
$sourceContentStreamVersion->unwrap(),
),
),
ExpectedVersion::ANY()
// NO_STREAM to ensure the "fork" happens as the first event of the new content stream
ExpectedVersion::NO_STREAM()
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
use Neos\ContentRepository\Core\Feature\ContentStreamCreation\Command\CreateContentStream;
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\SharedModel\Exception\ContentStreamAlreadyExists;
use Neos\ContentRepository\Core\SharedModel\Exception\ContentStreamDoesNotExistYet;
use Neos\ContentRepository\Core\Feature\Common\RebasableToOtherContentStreamsInterface;
Expand All @@ -51,6 +52,17 @@
use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Exception\BaseWorkspaceDoesNotExist;
use Neos\ContentRepository\Core\Feature\WorkspacePublication\Exception\BaseWorkspaceHasBeenModifiedInTheMeantime;
use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Exception\WorkspaceAlreadyExists;
use Neos\ContentRepository\Core\Feature\WorkspaceModification\Command\RenameWorkspace;
use Neos\ContentRepository\Core\Feature\WorkspaceModification\Command\DeleteWorkspace;
use Neos\ContentRepository\Core\Feature\WorkspaceModification\Event\WorkspaceWasRemoved;
use Neos\ContentRepository\Core\Feature\WorkspaceModification\Event\WorkspaceWasRenamed;
use Neos\ContentRepository\Core\Feature\WorkspaceModification\Command\ChangeWorkspaceOwner;
use Neos\ContentRepository\Core\Feature\WorkspaceModification\Event\WorkspaceOwnerWasChanged;
use Neos\ContentRepository\Core\Feature\WorkspaceModification\Command\ChangeBaseWorkspace;
use Neos\ContentRepository\Core\Feature\WorkspaceModification\Event\WorkspaceBaseWorkspaceWasChanged;
use Neos\ContentRepository\Core\Feature\WorkspaceModification\Exception\WorkspaceIsNotEmptyException;
use Neos\ContentRepository\Core\Feature\WorkspaceModification\Exception\BaseWorkspaceEqualsWorkspaceException;
use Neos\ContentRepository\Core\Feature\WorkspaceModification\Exception\CircularRelationBetweenWorkspacesException;
use Neos\ContentRepository\Core\SharedModel\Exception\WorkspaceDoesNotExist;
use Neos\ContentRepository\Core\SharedModel\Exception\WorkspaceHasNoBaseWorkspaceName;
use Neos\ContentRepository\Core\Projection\Workspace\Workspace;
Expand All @@ -61,6 +73,7 @@
use Neos\EventStore\Exception\ConcurrencyException;
use Neos\EventStore\Model\EventEnvelope;
use Neos\EventStore\Model\EventStream\ExpectedVersion;
use Neos\EventStore\Model\Event\EventType;

/**
* @internal from userland, you'll use ContentRepository::handle to dispatch commands
Expand All @@ -84,12 +97,16 @@ public function handle(CommandInterface $command, ContentRepository $contentRepo
/** @phpstan-ignore-next-line */
return match ($command::class) {
CreateWorkspace::class => $this->handleCreateWorkspace($command, $contentRepository),
RenameWorkspace::class => $this->handleRenameWorkspace($command, $contentRepository),
CreateRootWorkspace::class => $this->handleCreateRootWorkspace($command, $contentRepository),
PublishWorkspace::class => $this->handlePublishWorkspace($command, $contentRepository),
RebaseWorkspace::class => $this->handleRebaseWorkspace($command, $contentRepository),
PublishIndividualNodesFromWorkspace::class => $this->handlePublishIndividualNodesFromWorkspace($command, $contentRepository),
DiscardIndividualNodesFromWorkspace::class => $this->handleDiscardIndividualNodesFromWorkspace($command, $contentRepository),
DiscardWorkspace::class => $this->handleDiscardWorkspace($command, $contentRepository),
DeleteWorkspace::class => $this->handleDeleteWorkspace($command, $contentRepository),
ChangeWorkspaceOwner::class => $this->handleChangeWorkspaceOwner($command, $contentRepository),
ChangeBaseWorkspace::class => $this->handleChangeBaseWorkspace($command, $contentRepository),
};
}

Expand Down Expand Up @@ -146,6 +163,28 @@ private function handleCreateWorkspace(
);
}

/**
* @throws WorkspaceDoesNotExist
*/
private function handleRenameWorkspace(RenameWorkspace $command, ContentRepository $contentRepository): EventsToPublish
{
$this->requireWorkspace($command->workspaceName, $contentRepository);

$events = Events::with(
new WorkspaceWasRenamed(
$command->workspaceName,
$command->workspaceTitle,
$command->workspaceDescription,
)
);

return new EventsToPublish(
WorkspaceEventStreamName::fromWorkspaceName($command->workspaceName)->getEventStreamName(),
$events,
ExpectedVersion::STREAM_EXISTS()
);
}

/**
* @param CreateRootWorkspace $command
* @return EventsToPublish
Expand Down Expand Up @@ -368,8 +407,8 @@ private function handleRebaseWorkspace(

$rebaseStatistics->commandRebaseError(sprintf(
"The content stream %s cannot be rebased. Error with command %d (%s)"
. " - see nested exception for details.\n\n The base workspace %s is at content stream %s."
. "\n The full list of commands applied so far is: %s",
. " - see nested exception for details.\n\n The base workspace %s is at content stream %s."
. "\n The full list of commands applied so far is: %s",
$workspaceContentStreamName->value,
$i,
get_class($commandToRebase),
Expand Down Expand Up @@ -693,6 +732,102 @@ private function handleDiscardWorkspace(
);
}

/**
* @throws BaseWorkspaceDoesNotExist
* @throws WorkspaceDoesNotExist
* @throws WorkspaceHasNoBaseWorkspaceName
* @throws WorkspaceIsNotEmptyException
* @throws BaseWorkspaceEqualsWorkspaceException
* @throws CircularRelationBetweenWorkspacesException
*/
private function handleChangeBaseWorkspace(
ChangeBaseWorkspace $command,
ContentRepository $contentRepository,
): EventsToPublish {
$workspace = $this->requireWorkspace($command->workspaceName, $contentRepository);
$this->requireEmptyWorkspace($workspace);
$this->requireBaseWorkspace($workspace, $contentRepository);

$baseWorkspace = $this->requireWorkspace($command->baseWorkspaceName, $contentRepository);

$this->requireNonCircularRelationBetweenWorkspaces($workspace, $baseWorkspace, $contentRepository);

$contentRepository->handle(
new ForkContentStream(
$command->newContentStreamId,
$baseWorkspace->currentContentStreamId,
)
)->block();

$streamName = WorkspaceEventStreamName::fromWorkspaceName($command->workspaceName)->getEventStreamName();
$events = Events::with(
new WorkspaceBaseWorkspaceWasChanged(
$command->workspaceName,
$command->baseWorkspaceName,
$command->newContentStreamId,
)
);

return new EventsToPublish(
$streamName,
$events,
ExpectedVersion::ANY()
);
}

/**
* @throws WorkspaceDoesNotExist
*/
private function handleDeleteWorkspace(
DeleteWorkspace $command,
ContentRepository $contentRepository,
): EventsToPublish {
$workspace = $this->requireWorkspace($command->workspaceName, $contentRepository);

$contentRepository->handle(
new RemoveContentStream(
$workspace->currentContentStreamId
)
)->block();

$events = Events::with(
new WorkspaceWasRemoved(
$command->workspaceName,
)
);

$streamName = WorkspaceEventStreamName::fromWorkspaceName($command->workspaceName)->getEventStreamName();
return new EventsToPublish(
$streamName,
$events,
ExpectedVersion::ANY()
);
}

/**
* @throws WorkspaceDoesNotExist
*/
private function handleChangeWorkspaceOwner(
ChangeWorkspaceOwner $command,
ContentRepository $contentRepository,
): EventsToPublish {
$this->requireWorkspace($command->workspaceName, $contentRepository);

$events = Events::with(
new WorkspaceOwnerWasChanged(
$command->workspaceName,
$command->newWorkspaceOwner
)
);

$streamName = WorkspaceEventStreamName::fromWorkspaceName($command->workspaceName)->getEventStreamName();
return new EventsToPublish(
$streamName,
$events,
ExpectedVersion::STREAM_EXISTS()
);
}

/**
* @throws WorkspaceDoesNotExist
*/
Expand Down Expand Up @@ -723,4 +858,57 @@ private function requireBaseWorkspace(Workspace $workspace, ContentRepository $c

return $baseWorkspace;
}

/**
* @throws BaseWorkspaceEqualsWorkspaceException
* @throws CircularRelationBetweenWorkspacesException
*/
private function requireNonCircularRelationBetweenWorkspaces(Workspace $workspace, Workspace $baseWorkspace, ContentRepository $contentRepository): void
{
if ($workspace->workspaceName->equals($baseWorkspace->workspaceName)) {
throw new BaseWorkspaceEqualsWorkspaceException(sprintf('The base workspace of the target must be different from the given workspace "%s".', $workspace->workspaceName->value));
}

$nextBaseWorkspace = $baseWorkspace;
while ($nextBaseWorkspace?->baseWorkspaceName !== null) {
if ($workspace->workspaceName->equals($nextBaseWorkspace->baseWorkspaceName)) {
throw new CircularRelationBetweenWorkspacesException(sprintf('The workspace "%s" is already on the path of the target workspace "%s".', $workspace->workspaceName->value, $baseWorkspace->workspaceName->value));
}
$nextBaseWorkspace = $contentRepository->getWorkspaceFinder()->findOneByName($nextBaseWorkspace->baseWorkspaceName);
}
}

/**
* @throws WorkspaceIsNotEmptyException
*/
private function requireEmptyWorkspace(Workspace $workspace): void
{
$workspaceContentStreamName = ContentStreamEventStreamName::fromContentStreamId(
$workspace->currentContentStreamId
);
if ($this->hasEventsInContentStreamExceptForking($workspaceContentStreamName)) {
throw new WorkspaceIsNotEmptyException('The user workspace needs to be empty before switching the base workspace.', 1681455989);
}
}

/**
* @return bool
*/
private function hasEventsInContentStreamExceptForking(
ContentStreamEventStreamName $workspaceContentStreamName,
): bool {
$workspaceContentStream = $this->eventStore->load($workspaceContentStreamName->getEventStreamName());

$fullQualifiedEventClassName = ContentStreamWasForked::class;
$shortEventClassName = substr($fullQualifiedEventClassName, strrpos($fullQualifiedEventClassName, '\\') + 1);

foreach ($workspaceContentStream as $eventEnvelope) {
if ($eventEnvelope->event->type->value === EventType::fromString($shortEventClassName)->value) {
continue;
}
return true;
}

return false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace Neos\ContentRepository\Core\Feature\WorkspaceModification\Command;

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

/**
* Changes the base workspace of a given workspace, identified by $workspaceName.
*
* @api commands are the write-API of the ContentRepository
*/
final class ChangeBaseWorkspace implements CommandInterface
{
public function __construct(
public readonly WorkspaceName $workspaceName,
public readonly WorkspaceName $baseWorkspaceName,
public readonly ContentStreamId $newContentStreamId,
) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace Neos\ContentRepository\Core\Feature\WorkspaceModification\Command;

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

/**
* Change workspace owner of a workspace, identified by $workspaceName.
* Setting $newWorkspaceOwner to null, removes the current workspace owner.
*
* @api commands are the write-API of the ContentRepository
*/
final class ChangeWorkspaceOwner implements CommandInterface
{
public function __construct(
public readonly WorkspaceName $workspaceName,
public readonly ?string $newWorkspaceOwner,
) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace Neos\ContentRepository\Core\Feature\WorkspaceModification\Command;

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

/**
* Delete a workspace
*
* @api commands are the write-API of the ContentRepository
*/
final class DeleteWorkspace implements CommandInterface
{
public function __construct(
public readonly WorkspaceName $workspaceName,
) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

namespace Neos\ContentRepository\Core\Feature\WorkspaceModification\Command;

use Neos\ContentRepository\Core\CommandHandler\CommandInterface;
use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceDescription;
use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName;
use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceTitle;

/**
* Change the title or description of a workspace
*
* @api commands are the write-API of the ContentRepository
*/
final class RenameWorkspace implements CommandInterface
{
public function __construct(
public readonly WorkspaceName $workspaceName,
public readonly WorkspaceTitle $workspaceTitle,
public readonly WorkspaceDescription $workspaceDescription,
) {
}
}
Loading

0 comments on commit 8f496c4

Please sign in to comment.