Skip to content

Commit

Permalink
FEATURE: Extract workspace metadata and user-assignment to Neos
Browse files Browse the repository at this point in the history
Introduces `WorkspacePublishingService` as replacement for the current Neos `Workspace` "active record" model.

Introduces `WorkspaceService` as central authority to manage Neos workspaces.

Related: #4726
  • Loading branch information
bwaidelich committed Jun 17, 2024
1 parent 8cdcc5f commit 5f1e296
Show file tree
Hide file tree
Showing 33 changed files with 1,265 additions and 1,036 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
*/
final class WorkspaceName implements \JsonSerializable
{
private const PATTERN = '/^[a-z0-9\-]{1,30}$/';

public const WORKSPACE_NAME_LIVE = 'live';

/**
Expand All @@ -33,7 +35,7 @@ final class WorkspaceName implements \JsonSerializable
private function __construct(
public readonly string $value
) {
if (preg_match('/^[\p{L}\p{P}\d \.]{1,200}$/u', $value) !== 1) {
if (!self::hasValidFormat($value)) {
throw new \InvalidArgumentException('Invalid workspace name given.', 1505826610318);
}
}
Expand All @@ -48,6 +50,11 @@ public static function fromString(string $value): self
return self::instance($value);
}

public static function tryFromString(string $value): ?self
{
return self::hasValidFormat($value) ? self::instance($value) : null;
}

public static function forLive(): self
{
return self::instance(self::WORKSPACE_NAME_LIVE);
Expand All @@ -61,19 +68,16 @@ public static function forLive(): self
*/
public static function transliterateFromString(string $name): self
{
try {
// Check if name already match name pattern to prevent unnecessary transliteration
if (self::hasValidFormat($name)) {
return self::fromString($name);
} catch (\InvalidArgumentException $e) {
// Okay, let's transliterate
}

$originalName = $name;
$originalName = strtolower($name);

// Transliterate (transform 北京 to 'Bei Jing')
$name = Transliterator::transliterate($name);

// Urlization (replace spaces with dash, special special characters)
// Urlization (replace spaces with dash, special characters)
$name = Transliterator::urlize($name);

// Ensure only allowed characters are left
Expand All @@ -84,7 +88,7 @@ public static function transliterateFromString(string $name): self
$name = 'workspace-' . strtolower(md5($originalName));
}

return new self($name);
return self::fromString($name);
}

public function isLive(): bool
Expand All @@ -101,4 +105,9 @@ public function equals(self $other): bool
{
return $this === $other;
}

private static function hasValidFormat(string $value): bool
{
return preg_match(self::PATTERN, $value) === 1;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,14 @@ public function get(ContentRepositoryId $contentRepositoryId): ContentRepository
return $this->getFactory($contentRepositoryId)->getOrBuild();
}

/**
* @return array<ContentRepositoryId>
*/
public function getContentRepositoryIds(): array
{
return array_map(ContentRepositoryId::fromString(...), array_keys($this->settings['contentRepositories']));
}

/**
* @internal for test cases only
*/
Expand Down
88 changes: 49 additions & 39 deletions Neos.Neos/Classes/Command/WorkspaceCommandController.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@
use Neos\Flow\Cli\CommandController;
use Neos\Flow\Cli\Exception\StopCommandException;
use Neos\Flow\Persistence\PersistenceManagerInterface;
use Neos\Neos\Domain\Model\WorkspaceClassification;
use Neos\Neos\Domain\Service\UserService;
use Neos\Neos\Domain\Workspace\WorkspaceProvider;
use Neos\Neos\Domain\Service\WorkspaceService;
use Neos\Neos\Domain\Service\WorkspacePublishingService;
use Neos\Neos\PendingChangesProjection\ChangeFinder;

/**
Expand All @@ -56,7 +58,10 @@ class WorkspaceCommandController extends CommandController
protected ContentRepositoryRegistry $contentRepositoryRegistry;

#[Flow\Inject]
protected WorkspaceProvider $workspaceProvider;
protected WorkspacePublishingService $workspacePublishingService;

#[Flow\Inject]
protected WorkspaceService $workspaceService;

/**
* Publish changes of a workspace
Expand All @@ -68,16 +73,14 @@ class WorkspaceCommandController extends CommandController
*/
public function publishCommand(string $workspace, string $contentRepository = 'default'): void
{
// @todo: bypass access control
$workspace = $this->workspaceProvider->provideForWorkspaceName(
$this->workspacePublishingService->publishWorkspace(
ContentRepositoryId::fromString($contentRepository),
WorkspaceName::fromString($workspace)
);
$workspace->publishAllChanges();

$this->outputLine(
'Published all nodes in workspace %s to its base workspace',
[$workspace->name->value]
[$workspace]
);
}

Expand All @@ -92,19 +95,17 @@ public function publishCommand(string $workspace, string $contentRepository = 'd
*/
public function discardCommand(string $workspace, string $contentRepository = 'default'): void
{
// @todo: bypass access control
$workspace = $this->workspaceProvider->provideForWorkspaceName(
ContentRepositoryId::fromString($contentRepository),
WorkspaceName::fromString($workspace)
);

try {
$workspace->discardAllChanges();
// @todo: bypass access control
$this->workspacePublishingService->discardAllWorkspaceChanges(
ContentRepositoryId::fromString($contentRepository),
WorkspaceName::fromString($workspace)
);
} catch (WorkspaceDoesNotExist $exception) {
$this->outputLine('Workspace "%s" does not exist', [$workspace->name->value]);
$this->outputLine('Workspace "%s" does not exist', [$workspace]);
$this->quit(1);
}
$this->outputLine('Discarded all nodes in workspace %s', [$workspace->name->value]);
$this->outputLine('Discarded all nodes in workspace %s', [$workspace]);
}

/**
Expand All @@ -121,11 +122,11 @@ public function rebaseCommand(string $workspace, string $contentRepository = 'de
{
try {
// @todo: bypass access control
$workspace = $this->workspaceProvider->provideForWorkspaceName(
$this->workspacePublishingService->rebaseWorkspace(
ContentRepositoryId::fromString($contentRepository),
WorkspaceName::fromString($workspace)
WorkspaceName::fromString($workspace),
$force ? RebaseErrorHandlingStrategy::STRATEGY_FORCE : RebaseErrorHandlingStrategy::STRATEGY_FAIL,
);
$workspace->rebase($force ? RebaseErrorHandlingStrategy::STRATEGY_FORCE : RebaseErrorHandlingStrategy::STRATEGY_FAIL);
} catch (WorkspaceDoesNotExist $exception) {
$this->outputLine('Workspace "%s" does not exist', [$workspace]);
$this->quit(1);
Expand All @@ -134,7 +135,7 @@ public function rebaseCommand(string $workspace, string $contentRepository = 'de
$this->quit(1);
}

$this->outputLine('Rebased workspace %s', [$workspace->name->value]);
$this->outputLine('Rebased workspace %s', [$workspace]);
}

/**
Expand Down Expand Up @@ -178,13 +179,23 @@ public function createCommand(
string $contentRepository = 'default'
): void {
$contentRepositoryId = ContentRepositoryId::fromString($contentRepository);

$this->workspaceService->createWorkspace(
$contentRepositoryId,
\Neos\Neos\Domain\Model\WorkspaceTitle::fromString($title),
\Neos\Neos\Domain\Model\WorkspaceDescription::fromString($description ?? ''),
WorkspaceName::fromString($baseWorkspace),
\Neos\Neos\Domain\Model\UserId::fromString($owner),
WorkspaceClassification::PERSONAL,
);

$contentRepositoryInstance = $this->contentRepositoryRegistry->get($contentRepositoryId);

if ($owner === '') {
$workspaceOwnerUserId = null;
} else {
$workspaceOwnerUserId = UserId::fromString($owner);
$workspaceOwner = $this->userService->findByUserIdentifier($workspaceOwnerUserId);
$workspaceOwner = $this->userService->findUserById($workspaceOwnerUserId);
if ($workspaceOwner === null) {
$this->outputLine('The user "%s" specified as owner does not exist', [$owner]);
$this->quit(3);
Expand Down Expand Up @@ -243,13 +254,14 @@ public function deleteCommand(string $workspace, bool $force = false, string $co
$this->quit(2);
}

$workspace = $contentRepositoryInstance->getWorkspaceFinder()->findOneByName($workspaceName);
if (!$workspace instanceof Workspace) {
$crWorkspace = $contentRepositoryInstance->getWorkspaceFinder()->findOneByName($workspaceName);
if ($crWorkspace === null) {
$this->outputLine('Workspace "%s" does not exist', [$workspaceName->value]);
$this->quit(1);
}
$workspaceMetadata = $this->workspaceService->getWorkspaceMetadata($contentRepositoryId, $workspaceName);

if ($workspace->isPersonalWorkspace()) {
if ($workspaceMetadata->classification === WorkspaceClassification::PERSONAL) {
$this->outputLine(
'Did not delete workspace "%s" because it is a personal workspace.'
. ' Personal workspaces cannot be deleted manually.',
Expand All @@ -264,28 +276,28 @@ public function deleteCommand(string $workspace, bool $force = false, string $co
'Workspace "%s" cannot be deleted because the following workspaces are based on it:',
[$workspaceName->value]
);

$this->outputLine();
$tableRows = [];
$headerRow = ['Name', 'Title', 'Description'];

foreach ($dependentWorkspaces as $dependentWorkspace) {
$dependentWorkspaceMetadata = $this->workspaceService->getWorkspaceMetadata($contentRepositoryId, $dependentWorkspace->workspaceName);
$tableRows[] = [
$dependentWorkspace->workspaceName->value,
$dependentWorkspace->workspaceTitle->value,
$dependentWorkspace->workspaceDescription->value
$dependentWorkspaceMetadata->title->value,
$dependentWorkspaceMetadata->description->value
];
}
$this->output->outputTable($tableRows, $headerRow);
$this->quit(3);
}


try {
$nodesCount = $contentRepositoryInstance->projectionState(ChangeFinder::class)
->countByContentStreamId(
$workspace->currentContentStreamId
);
$nodesCount = $this->workspacePublishingService->countPendingWorkspaceChanges($contentRepositoryId, $workspaceName);
} catch (\Exception $exception) {
$this->outputLine('Could not fetch unpublished nodes for workspace %s, nothing was deleted. %s', [$workspace->workspaceName->value, $exception->getMessage()]);
$this->outputLine('Could not fetch unpublished nodes for workspace %s, nothing was deleted. %s', [$workspaceName->value, $exception->getMessage()]);
$this->quit(4);
}

Expand All @@ -299,11 +311,7 @@ public function deleteCommand(string $workspace, bool $force = false, string $co
$this->quit(5);
}
// @todo bypass access control?
$workspace = $this->workspaceProvider->provideForWorkspaceName(
$contentRepositoryId,
$workspaceName
);
$workspace->discardAllChanges();
$this->workspacePublishingService->discardAllWorkspaceChanges($contentRepositoryId, $workspaceName);
}

$contentRepositoryInstance->handle(
Expand Down Expand Up @@ -359,16 +367,18 @@ public function listCommand(string $contentRepository = 'default'): void
}

$tableRows = [];
$headerRow = ['Name', 'Base Workspace', 'Title', 'Owner', 'Description', 'Status', 'Content Stream'];
$headerRow = ['Name', 'Classification', 'Base Workspace', 'Title', 'Description', 'Status', 'Content Stream'];

foreach ($workspaces as $workspace) {
$workspaceMetadata = $this->workspaceService->getWorkspaceMetadata($contentRepositoryId, $workspace->workspaceName);

/* @var Workspace $workspace */
$tableRows[] = [
$workspace->workspaceName->value,
$workspaceMetadata->classification->name,
$workspace->baseWorkspaceName?->value ?: '',
$workspace->workspaceTitle->value,
$workspace->workspaceOwner ?: '',
$workspace->workspaceDescription->value,
$workspaceMetadata->title->value,
$workspaceMetadata->description->value,
$workspace->status->value,
$workspace->currentContentStreamId->value,
];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

declare(strict_types=1);

namespace Neos\Neos\Domain\Workspace;
namespace Neos\Neos\Domain\Model;

use Neos\Flow\Annotations as Flow;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@

declare(strict_types=1);

namespace Neos\Neos\Domain\Workspace;
namespace Neos\Neos\Domain\Model;

use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName;
use Neos\Flow\Annotations as Flow;

/**
Expand All @@ -26,6 +27,7 @@
{
public function __construct(
public int $numberOfPublishedChanges,
public WorkspaceName $targetWorkspaceName,
) {
}
}
10 changes: 10 additions & 0 deletions Neos.Neos/Classes/Domain/Model/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ class User extends Person implements UserInterface
*/
protected $preferences;

/**
* @var string
*/
protected $Persistence_Object_Identifier;

/**
* Constructs this User object
*/
Expand All @@ -45,6 +50,11 @@ public function __construct()
$this->preferences = new UserPreferences();
}

public function getId(): UserId
{
return UserId::fromString($this->Persistence_Object_Identifier);
}

/**
* Returns a label which can be used as a human-friendly identifier for this user.
*
Expand Down
36 changes: 36 additions & 0 deletions Neos.Neos/Classes/Domain/Model/UserId.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace Neos\Neos\Domain\Model;

/**
* Globally unique identifier of a Neos user
*
* @api
*/
final readonly class UserId implements \JsonSerializable
{
public function __construct(
public string $value
) {
if (!preg_match('/^([a-z0-9\-]{1,40})$/', $value)) {
throw new \InvalidArgumentException(sprintf('Invalid user id "%s" (a user id must only contain lowercase characters, numbers and the "-" sign).', 1718293224));
}
}

public static function fromString(string $value): self
{
return new self($value);
}

public function jsonSerialize(): string
{
return $this->value;
}

public function equals(self $other): bool
{
return $this->value === $other->value;
}
}
6 changes: 6 additions & 0 deletions Neos.Neos/Classes/Domain/Model/UserInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@
*/
interface UserInterface
{

/**
* Returns the globally unique identifier for this user
*/
public function getId(): UserId;

/**
* Returns a label which can be used as a human-friendly identifier for this user, for example his or her first
* and last name.
Expand Down
Loading

0 comments on commit 5f1e296

Please sign in to comment.