-
-
Notifications
You must be signed in to change notification settings - Fork 224
/
Copy pathNodeMigrationService.php
207 lines (188 loc) · 9.13 KB
/
NodeMigrationService.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
<?php
declare(strict_types=1);
namespace Neos\ContentRepository\NodeMigration;
use Neos\ContentRepository\Core\ContentRepository;
use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceInterface;
use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Command\CreateWorkspace;
use Neos\ContentRepository\Core\Feature\WorkspaceModification\Command\DeleteWorkspace;
use Neos\ContentRepository\Core\Feature\WorkspacePublication\Command\PublishWorkspace;
use Neos\ContentRepository\Core\Projection\ContentGraph\NodeAggregate;
use Neos\ContentRepository\Core\SharedModel\Exception\WorkspaceDoesNotExist;
use Neos\ContentRepository\Core\SharedModel\Workspace\Workspace;
use Neos\ContentRepository\NodeMigration\Command\ExecuteMigration;
use Neos\ContentRepository\NodeMigration\Filter\FiltersFactory;
use Neos\ContentRepository\NodeMigration\Filter\InvalidMigrationFilterSpecified;
use Neos\ContentRepository\NodeMigration\Transformation\TransformationsFactory;
use Neos\Neos\PendingChangesProjection\ChangeFinder;
/**
* Node Migrations are manually written adjustments to the Node tree;
* stored in "Migrations/ContentRepository" in a package.
*
* They are used to transform properties on nodes, or change the dimension space points in the system to others.
*
* Internally, these migrations can be applied on three levels:
*
* - globally, like changing dimensions
* - on a NodeAggregate, like changing a NodeAggregate type
* - on a (materialized) Node, like changing node properties.
*
* In a single migration, only transformations belonging to a single "level" can be applied;
* as otherwise, the order (and semantics) becomes non-obvious.
*
* All migrations are applied in an empty, new ContentStream,
* which is forked off the target workspace where the migrations are done.
* This way, migrations can be easily rolled back by discarding the content stream instead of publishing it.
*
* A migration file is structured like this:
* migrations: [
* {filters: ... transformations: ...},
* {filters: ... transformations: ...}
* ]
*
* Every pair of filters/transformations is a "submigration". Inside a submigration,
* you'll operate on the result state of all *previous* submigrations;
* but you do not see the modified state of the current submigration while you are running it.
*/
readonly class NodeMigrationService implements ContentRepositoryServiceInterface
{
public function __construct(
private ContentRepository $contentRepository,
private FiltersFactory $filterFactory,
private TransformationsFactory $transformationFactory
) {
}
public function executeMigration(ExecuteMigration $command): void
{
$sourceWorkspace = $this->contentRepository->findWorkspaceByName($command->sourceWorkspaceName);
if ($sourceWorkspace === null) {
throw new WorkspaceDoesNotExist(sprintf(
'The workspace %s does not exist',
$command->sourceWorkspaceName->value
), 1611688225);
}
$targetWorkspaceWasCreated = false;
if ($targetWorkspace = $this->contentRepository->findWorkspaceByName($command->targetWorkspaceName)) {
if (!$this->workspaceIsEmpty($targetWorkspace)) {
throw new MigrationException(sprintf('Target workspace "%s" already exists an is not empty. Please clear the workspace before.', $targetWorkspace->workspaceName->value));
}
} else {
$this->contentRepository->handle(
CreateWorkspace::create(
$command->targetWorkspaceName,
$sourceWorkspace->workspaceName,
$command->contentStreamId,
)
);
$targetWorkspace = $this->contentRepository->findWorkspaceByName($command->targetWorkspaceName);
$targetWorkspaceWasCreated = true;
}
if($targetWorkspace === null) {
throw new MigrationException(sprintf('Target workspace "%s" could not loaded nor created.', $command->targetWorkspaceName->value));
}
foreach ($command->migrationConfiguration->getMigration() as $migrationDescription) {
$this->executeSubMigration(
$migrationDescription,
$sourceWorkspace,
$targetWorkspace
);
}
if ($command->publishOnSuccess === true) {
$this->contentRepository->handle(
PublishWorkspace::create($command->targetWorkspaceName)
);
if ($targetWorkspaceWasCreated === true) {
$this->contentRepository->handle(
DeleteWorkspace::create($command->targetWorkspaceName)
);
}
}
}
/**
* Execute a single "filters / transformation" pair, i.e. a single sub-migration
*
* @param array<string,mixed> $migrationDescription
* @throws MigrationException
*/
protected function executeSubMigration(
array $migrationDescription,
Workspace $workspaceForReading,
Workspace $workspaceForWriting,
): void {
$filters = $this->filterFactory->buildFilterConjunction($migrationDescription['filters'] ?? []);
$transformations = $this->transformationFactory->buildTransformation(
$migrationDescription['transformations'] ?? []
);
if ($transformations->containsMoreThanOneTransformationType()) {
throw new InvalidMigrationFilterSpecified('more than one transformation type', 1617389468);
}
if (
$transformations->containsGlobal()
&& ($filters->containsNodeAggregateBased() || $filters->containsNodeBased())
) {
throw new InvalidMigrationFilterSpecified(
'Global transformations are only supported without any filters',
1617389474
);
}
if ($transformations->containsNodeAggregateBased() && $filters->containsNodeBased()) {
throw new InvalidMigrationFilterSpecified(
'NodeAggregate Based transformations are only supported without any node based filters',
1617389479
);
}
if ($transformations->containsGlobal()) {
$transformations->executeGlobal($workspaceForWriting->workspaceName);
} elseif ($transformations->containsNodeAggregateBased()) {
$contentGraph = $this->contentRepository->getContentGraph($workspaceForReading->workspaceName);
foreach ($contentGraph->findUsedNodeTypeNames() as $nodeTypeName) {
foreach (
$contentGraph->findNodeAggregatesByType(
$nodeTypeName
) as $nodeAggregate
) {
if ($filters->matchesNodeAggregate($nodeAggregate)) {
$transformations->executeNodeAggregateBased($nodeAggregate, $workspaceForWriting->workspaceName, $workspaceForWriting->currentContentStreamId);
}
}
}
} elseif ($transformations->containsNodeBased()) {
$contentGraph = $this->contentRepository->getContentGraph($workspaceForReading->workspaceName);
foreach ($contentGraph->findUsedNodeTypeNames() as $nodeTypeName) {
foreach (
$contentGraph->findNodeAggregatesByType(
$nodeTypeName
) as $nodeAggregate
) {
/* @var $nodeAggregate NodeAggregate */
// we *also* apply the node-aggregate-based filters on the node based transformations,
// so that you can filter Nodes e.g. based on node type
if ($filters->matchesNodeAggregate($nodeAggregate)) {
foreach ($nodeAggregate->occupiedDimensionSpacePoints as $originDimensionSpacePoint) {
$node = $nodeAggregate->getNodeByOccupiedDimensionSpacePoint($originDimensionSpacePoint);
// The node at $contentStreamId and $originDimensionSpacePoint
// *really* exists at this point, and is no shine-through.
$coveredDimensionSpacePoints = $nodeAggregate->getCoverageByOccupant(
$originDimensionSpacePoint
);
if ($filters->matchesNode($node)) {
$transformations->executeNodeBased(
$node,
$coveredDimensionSpacePoints,
$workspaceForWriting->workspaceName,
$workspaceForWriting->currentContentStreamId
);
}
}
}
}
}
}
}
private function workspaceIsEmpty(Workspace $workspace): bool
{
// todo introduce Workspace::hasPendingChanges
return $this->contentRepository
->projectionState(ChangeFinder::class)
->countByContentStreamId($workspace->currentContentStreamId) === 0;
}
}