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: Edit, delete and switch workspaces #4186

Merged
merged 8 commits into from
Apr 16, 2023
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\WorkspaceWasDeleted;
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,
WorkspaceWasDeleted::class,
dlubitz marked this conversation as resolved.
Show resolved Hide resolved
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,7 @@ private function handleForkContentStream(
$sourceContentStreamVersion->unwrap(),
),
),
ExpectedVersion::ANY()
ExpectedVersion::NO_STREAM()
dlubitz marked this conversation as resolved.
Show resolved Hide resolved
);
}

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\WorkspaceWasDeleted;
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 Down Expand Up @@ -84,12 +96,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 +162,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 +406,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 +731,106 @@ private function handleDiscardWorkspace(
);
}

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

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

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

// TODO Check workspace is empty before forking
$changedNodes = 0;
if ($changedNodes > 0) {
throw new WorkspaceIsNotEmptyException('The user workspace needs to be empty before forking.', 1681455989);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needs to be empty before switching the base workspace (naming)

}

$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 WorkspaceWasDeleted(
$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->workspaceOwner
)
);

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

/**
* @throws WorkspaceDoesNotExist
*/
Expand Down Expand Up @@ -723,4 +861,23 @@ 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);
}
}
}
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.
dlubitz marked this conversation as resolved.
Show resolved Hide resolved
*
* @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,22 @@
<?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 the title or description of a workspace
dlubitz marked this conversation as resolved.
Show resolved Hide resolved
*
* @api commands are the write-API of the ContentRepository
*/
final class ChangeWorkspaceOwner implements CommandInterface
{
public function __construct(
public readonly WorkspaceName $workspaceName,
public readonly ?string $workspaceOwner,
) {
}
}
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,
) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

declare(strict_types=1);

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

use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName;
use Neos\ContentRepository\Core\EventStore\EventInterface;
use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId;

/**
* Event triggered when the base workspace of a given workspace has changed.
*
* @api events are the persistence-API of the content repository
*/
final class WorkspaceBaseWorkspaceWasChanged implements EventInterface
{
public function __construct(
public readonly WorkspaceName $workspaceName,
public readonly WorkspaceName $baseWorkspaceName,
public readonly ContentStreamId $newContentStreamId,
) {
}

public static function fromArray(array $values): self
{
return new self(
WorkspaceName::fromString($values['workspaceName']),
WorkspaceName::fromString($values['baseWorkspaceName']),
ContentStreamId::fromString($values['newContentStreamId']),
);
}

public function jsonSerialize(): array
{
return get_object_vars($this);
}
}
Loading