Skip to content

Commit 96bd529

Browse files
committed
FEATURE: Migrate flow nodeTemplate:createFromNodeSubtree to Neos9
introduced with #40
1 parent a5be69e commit 96bd529

7 files changed

Lines changed: 147 additions & 86 deletions

File tree

Classes/Application/Command/NodeTemplateCommandController.php

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -61,19 +61,32 @@ class NodeTemplateCommandController extends CommandController
6161
* @param string $workspaceName
6262
* @return void
6363
*/
64-
public function createFromNodeSubtreeCommand(string $startingNodeId, string $workspaceName = 'live'): void
64+
public function createFromNodeSubtreeCommand(string $startingNodeId, ?string $site = null, string $workspaceName = 'live'): void
6565
{
66-
throw new \BadMethodCallException('Not implemented.');
67-
/*
68-
$subgraph = $this->contextFactory->create([
69-
'workspaceName' => $workspaceName
70-
]);
71-
$node = $subgraph->getNodeByIdentifier($startingNodeId);
66+
$siteInstance = $site
67+
? $this->siteRepository->findOneByNodeName($site)
68+
: $this->siteRepository->findDefault();
69+
70+
if (!$siteInstance) {
71+
$this->outputLine(sprintf('<error>Site "%s" does not exist.</error>', $site));
72+
$this->quit(2);
73+
}
74+
75+
$siteConfiguration = $siteInstance->getConfiguration();
76+
77+
$contentRepository = $this->contentRepositoryRegistry->get($siteConfiguration->contentRepositoryId);
78+
79+
// default context? https://github.com/neos/neos-development-collection/issues/5113
80+
$subgraph = $contentRepository->getContentGraph(WorkspaceName::fromString($workspaceName))->getSubgraph(
81+
$siteConfiguration->defaultDimensionSpacePoint,
82+
VisibilityConstraints::frontend()
83+
);
84+
85+
$node = $subgraph->findNodeById(NodeAggregateId::fromString($startingNodeId));
7286
if (!$node) {
7387
throw new \InvalidArgumentException("Node $startingNodeId doesnt exist in workspace $workspaceName.");
7488
}
75-
echo $this->nodeTemplateDumper->createNodeTemplateYamlDumpFromSubtree($node);
76-
*/
89+
echo $this->nodeTemplateDumper->createNodeTemplateYamlDumpFromSubtree($node, $contentRepository);
7790
}
7891

7992
/**

Classes/Domain/NodeCreation/PropertyType.php

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -53,16 +53,6 @@ public static function fromPropertyOfNodeType(
5353
NodeType $nodeType
5454
): self {
5555
$declaration = $nodeType->getPropertyType($propertyName);
56-
if ($declaration === 'reference' || $declaration === 'references') {
57-
throw new \DomainException(
58-
sprintf(
59-
'Given property "%s" is declared as "reference" in node type "%s" and must be treated as such.',
60-
$propertyName,
61-
$nodeType->name->value
62-
),
63-
1685964835205
64-
);
65-
}
6656
$type = self::tryFromString($declaration);
6757
if (!$type) {
6858
throw new \DomainException(

Classes/Domain/NodeTemplateDumper/NodeTemplateDumper.php

Lines changed: 113 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,16 @@
44

55
namespace Flowpack\NodeTemplates\Domain\NodeTemplateDumper;
66

7-
use Neos\ContentRepository\Domain\Model\ArrayPropertyCollection;
8-
use Neos\ContentRepository\Domain\Model\NodeInterface;
9-
use Neos\ContentRepository\Domain\Projection\Content\PropertyCollectionInterface;
7+
use Neos\ContentRepository\Core\ContentRepository;
8+
use Neos\ContentRepository\Core\NodeType\NodeType;
9+
use Neos\ContentRepository\Core\Projection\ContentGraph\ContentSubgraphInterface;
10+
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindChildNodesFilter;
11+
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindReferencesFilter;
12+
use Neos\ContentRepository\Core\Projection\ContentGraph\Node;
13+
use Neos\ContentRepository\Core\Projection\ContentGraph\Nodes;
1014
use Neos\Flow\Annotations as Flow;
1115
use Neos\Flow\I18n\EelHelper\TranslationHelper;
16+
use Neos\Neos\Domain\NodeLabel\NodeLabelGeneratorInterface;
1217
use Symfony\Component\Yaml\Yaml;
1318

1419
/** @Flow\Scope("singleton") */
@@ -20,28 +25,36 @@ class NodeTemplateDumper
2025
*/
2126
protected $translationHelper;
2227

28+
/**
29+
* @var NodeLabelGeneratorInterface
30+
* @Flow\Inject
31+
*/
32+
protected $nodeLabelGenerator;
33+
2334
/**
2435
* Dump the node tree structure into a NodeTemplate YAML structure.
2536
* References to Nodes and non-primitive property values are commented out in the YAML.
2637
*
27-
* @param NodeInterface $startingNode specified root node of the node tree to dump
38+
* @param Node $startingNode specified root node of the node tree to dump
2839
* @return string YAML representation of the node template
2940
*/
30-
public function createNodeTemplateYamlDumpFromSubtree(NodeInterface $startingNode): string
41+
public function createNodeTemplateYamlDumpFromSubtree(Node $startingNode, ContentRepository $contentRepository): string
3142
{
3243
$comments = Comments::empty();
3344

34-
$nodeType = $startingNode->getNodeType();
45+
$nodeType = $contentRepository->getNodeTypeManager()->getNodeType($startingNode->nodeTypeName);
3546

3647
if (
37-
!$nodeType->isOfType('Neos.Neos:Document')
38-
&& !$nodeType->isOfType('Neos.Neos:Content')
39-
&& !$nodeType->isOfType('Neos.Neos:ContentCollection')
48+
!$nodeType || (
49+
!$nodeType->isOfType('Neos.Neos:Document')
50+
&& !$nodeType->isOfType('Neos.Neos:Content')
51+
&& !$nodeType->isOfType('Neos.Neos:ContentCollection')
52+
)
4053
) {
41-
throw new \InvalidArgumentException("Node {$startingNode->getIdentifier()} must be one of Neos.Neos:Document,Neos.Neos:Content,Neos.Neos:ContentCollection.");
54+
throw new \InvalidArgumentException("Node {$startingNode->aggregateId->value} must be one of Neos.Neos:Document,Neos.Neos:Content,Neos.Neos:ContentCollection.");
4255
}
4356

44-
$template = $this->nodeTemplateFromNodes([$startingNode], $comments);
57+
$template = $this->nodeTemplateFromNodes(Nodes::fromArray([$startingNode]), $comments, $contentRepository);
4558

4659
$firstEntry = null;
4760
foreach ($template as $firstEntry) {
@@ -53,7 +66,7 @@ public function createNodeTemplateYamlDumpFromSubtree(NodeInterface $startingNod
5366

5467

5568
$templateInNodeTypeOptions = [
56-
$nodeType->getName() => [
69+
$nodeType->name->value => [
5770
'options' => [
5871
'template' => array_filter([
5972
'properties' => $properties,
@@ -68,21 +81,32 @@ public function createNodeTemplateYamlDumpFromSubtree(NodeInterface $startingNod
6881
return $comments->renderCommentsInYamlDump($yamlWithSerializedComments);
6982
}
7083

71-
/** @param array<NodeInterface> $nodes */
72-
private function nodeTemplateFromNodes(array $nodes, Comments $comments): array
84+
/** @return array<string, array<string, mixed>> */
85+
private function nodeTemplateFromNodes(Nodes $nodes, Comments $comments, ContentRepository $contentRepository): array
7386
{
87+
$subgraph = null;
88+
7489
$documentNodeTemplates = [];
7590
$contentNodeTemplates = [];
7691
foreach ($nodes as $index => $node) {
77-
assert($node instanceof NodeInterface);
78-
$nodeType = $node->getNodeType();
92+
$subgraph ??= $contentRepository->getContentGraph($node->workspaceName)->getSubgraph(
93+
$node->dimensionSpacePoint,
94+
$node->visibilityConstraints
95+
);
96+
97+
$nodeType = $contentRepository->getNodeTypeManager()->getNodeType($node->nodeTypeName);
98+
if (!$nodeType) {
99+
throw new \RuntimeException("NodeType {$node->nodeTypeName->value} of Node {$node->aggregateId->value} doesnt exist.");
100+
}
101+
79102
$isDocumentNode = $nodeType->isOfType('Neos.Neos:Document');
80103

81104
$templatePart = array_filter([
82-
'properties' => $this->nonDefaultConfiguredNodeProperties($node, $comments),
105+
'properties' => $this->nonDefaultConfiguredNodeProperties($node, $nodeType, $comments, $subgraph),
83106
'childNodes' => $this->nodeTemplateFromNodes(
84-
$node->getChildNodes('Neos.Neos:Node'),
85-
$comments
107+
$subgraph->findChildNodes($node->aggregateId, FindChildNodesFilter::create('Neos.Neos:Node')),
108+
$comments,
109+
$contentRepository
86110
)
87111
]);
88112

@@ -91,38 +115,44 @@ private function nodeTemplateFromNodes(array $nodes, Comments $comments): array
91115
}
92116

93117
if ($isDocumentNode) {
94-
if ($node->isTethered()) {
95-
$documentNodeTemplates[$node->getLabel() ?: $node->getName()] = array_merge([
96-
'name' => $node->getName()
118+
if ($node->classification->isTethered()) {
119+
$tetheredName = $node->name;
120+
assert($tetheredName !== null);
121+
122+
$documentNodeTemplates[$this->nodeLabelGenerator->getLabel($node) ?: $tetheredName->value] = array_merge([
123+
'name' => $tetheredName->value
97124
], $templatePart);
98125
continue;
99126
}
100127

101128
$documentNodeTemplates["page$index"] = array_merge([
102-
'type' => $node->getNodeType()->getName()
129+
'type' => $node->nodeTypeName->value
103130
], $templatePart);
104131
continue;
105132
}
106133

107-
if ($node->isTethered()) {
108-
$contentNodeTemplates[$node->getLabel() ?: $node->getName()] = array_merge([
109-
'name' => $node->getName()
134+
if ($node->classification->isTethered()) {
135+
$tetheredName = $node->name;
136+
assert($tetheredName !== null);
137+
138+
$contentNodeTemplates[$this->nodeLabelGenerator->getLabel($node) ?: $tetheredName->value] = array_merge([
139+
'name' => $tetheredName->value
110140
], $templatePart);
111141
continue;
112142
}
113143

114144
$contentNodeTemplates["content$index"] = array_merge([
115-
'type' => $node->getNodeType()->getName()
145+
'type' => $node->nodeTypeName->value
116146
], $templatePart);
117147
}
118148

119149
return array_merge($contentNodeTemplates, $documentNodeTemplates);
120150
}
121151

122-
private function nonDefaultConfiguredNodeProperties(NodeInterface $node, Comments $comments): array
152+
/** @return array<string, string> */
153+
private function nonDefaultConfiguredNodeProperties(Node $node, NodeType $nodeType, Comments $comments, ContentSubgraphInterface $subgraph): array
123154
{
124-
$nodeType = $node->getNodeType();
125-
$nodeProperties = $node->getProperties();
155+
$nodeProperties = $node->properties;
126156

127157
$filteredProperties = [];
128158
foreach ($nodeType->getProperties() as $propertyName => $configuration) {
@@ -170,22 +200,12 @@ function ($indentation, $propertyName) use ($dataSourceIdentifier, $propertyValu
170200
continue;
171201
}
172202

173-
if (($configuration['type'] ?? null) === 'reference') {
174-
$nodeTypesInReference = $configuration['ui']['inspector']['editorOptions']['nodeTypes'] ?? ['Neos.Neos:Document'];
175-
$filteredProperties[$propertyName] = $comments->addCommentAndGetMarker($augmentCommentWithLabel(Comment::fromRenderer(
176-
function ($indentation, $propertyName) use ($nodeTypesInReference, $propertyValue) {
177-
return $indentation . '# ' . $propertyName . ' -> Reference of NodeTypes (' . join(', ', $nodeTypesInReference) . ') with value ' . $this->valueToDebugString($propertyValue);
178-
}
179-
)));
180-
continue;
181-
}
182-
183203
if (($configuration['ui']['inspector']['editor'] ?? null) === 'Neos.Neos/Inspector/Editors/SelectBoxEditor') {
184204
$selectBoxValues = array_keys($configuration['ui']['inspector']['editorOptions']['values'] ?? []);
185205
$filteredProperties[$propertyName] = $comments->addCommentAndGetMarker($augmentCommentWithLabel(Comment::fromRenderer(
186206
function ($indentation, $propertyName) use ($selectBoxValues, $propertyValue) {
187207
return $indentation . '# ' . $propertyName . ' -> SelectBox of '
188-
. mb_strimwidth(json_encode($selectBoxValues), 0, 60, ' ...]')
208+
. mb_strimwidth(json_encode($selectBoxValues, JSON_THROW_ON_ERROR), 0, 60, ' ...]')
189209
. ' with value ' . $this->valueToDebugString($propertyValue);
190210
}
191211
)));
@@ -208,36 +228,71 @@ function ($indentation, $propertyName) use ($propertyValue) {
208228
)));
209229
}
210230

231+
if ($nodeType->getReferences() === []) {
232+
return $filteredProperties;
233+
}
234+
235+
$references = $subgraph->findReferences($node->aggregateId, FindReferencesFilter::create());
236+
$referencesArray = [];
237+
foreach ($references as $reference) {
238+
if (!isset($referencesArray[$reference->name->value])) {
239+
$referencesArray[$reference->name->value] = $reference->node->aggregateId->value;
240+
continue;
241+
}
242+
$referencesArray[$reference->name->value] .= ', ' . $reference->node->aggregateId->value;
243+
}
244+
245+
foreach ($nodeType->getReferences() as $referenceName => $configuration) {
246+
$referenceValue = $referencesArray[$referenceName] ?? null;
247+
if (!$referenceValue) {
248+
continue;
249+
}
250+
251+
$label = $configuration['ui']['label'] ?? null;
252+
$augmentCommentWithLabel = fn (Comment $comment) => $comment;
253+
if ($label) {
254+
$label = $this->translationHelper->translate($label);
255+
$augmentCommentWithLabel = fn (Comment $comment) => Comment::fromRenderer(
256+
function ($indentation, $propertyName) use($comment, $label) {
257+
return $indentation . '# ' . $label . "\n" .
258+
$comment->toYamlComment($indentation, $propertyName);
259+
}
260+
);
261+
}
262+
263+
if (($configuration['constraints']['maxItems'] ?? null) === 1) {
264+
$nodeTypesInReference = $configuration['ui']['inspector']['editorOptions']['nodeTypes'] ?? ['Neos.Neos:Document'];
265+
$filteredProperties[$referenceName] = $comments->addCommentAndGetMarker($augmentCommentWithLabel(Comment::fromRenderer(
266+
function ($indentation, $propertyName) use ($nodeTypesInReference, $referenceValue) {
267+
return $indentation . '# ' . $propertyName . ' -> Reference of NodeTypes (' . join(', ', $nodeTypesInReference) . ') with Node: ' . $referenceValue;
268+
}
269+
)));
270+
continue;
271+
}
272+
273+
$filteredProperties[$referenceName] = $comments->addCommentAndGetMarker($augmentCommentWithLabel(Comment::fromRenderer(
274+
function ($indentation, $propertyName) use ($referenceValue) {
275+
return $indentation . '# ' . $propertyName . ' -> References with Nodes: ' . $referenceValue;
276+
}
277+
)));
278+
}
279+
211280
return $filteredProperties;
212281
}
213282

214-
private function valueToDebugString($value): string
283+
private function valueToDebugString(mixed $value): string
215284
{
216-
if ($value instanceof NodeInterface) {
217-
return 'Node(' . $value->getIdentifier() . ')';
218-
}
219285
if (is_iterable($value)) {
220-
$name = null;
221286
$entries = [];
222287
foreach ($value as $key => $item) {
223-
if ($item instanceof NodeInterface) {
224-
if ($name === null || $name === 'Nodes') {
225-
$name = 'Nodes';
226-
} else {
227-
$name = 'array';
228-
}
229-
$entries[$key] = $item->getIdentifier();
230-
continue;
231-
}
232-
$name = 'array';
233288
$entries[$key] = is_object($item) ? get_class($item) : json_encode($item);
234289
}
235-
return $name . '(' . join(', ', $entries) . ')';
290+
return 'array(' . join(', ', $entries) . ')';
236291
}
237292

238293
if (is_object($value)) {
239294
return 'object(' . get_class($value) . ')';
240295
}
241-
return json_encode($value);
296+
return json_encode($value, JSON_THROW_ON_ERROR);
242297
}
243298
}

Tests/Functional/AbstractNodeTemplateTestCase.php

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -236,12 +236,9 @@ protected function assertNodeDumpAndTemplateDumpMatchSnapshot(string $snapShotNa
236236
unset($serializedNodes['nodeTypeName']);
237237
$this->assertJsonStringEqualsJsonFileOrCreateSnapshot($this->fixturesDir . '/' . $snapShotName . '.nodes.json', json_encode($serializedNodes, JSON_PRETTY_PRINT));
238238

239-
// todo test dumper
240-
return;
239+
$dumpedYamlTemplate = $this->nodeTemplateDumper->createNodeTemplateYamlDumpFromSubtree($node, $this->contentRepository);
241240

242-
$dumpedYamlTemplate = $this->nodeTemplateDumper->createNodeTemplateYamlDumpFromSubtree($node);
243-
244-
$yamlTemplateWithoutOriginNodeTypeName = '\'{nodeTypeName}\'' . substr($dumpedYamlTemplate, strlen($node->getNodeType()->getName()) + 2);
241+
$yamlTemplateWithoutOriginNodeTypeName = '\'{nodeTypeName}\'' . substr($dumpedYamlTemplate, strlen($node->nodeTypeName->value) + 2);
245242

246243
$this->assertStringEqualsFileOrCreateSnapshot($this->fixturesDir . '/' . $snapShotName . '.yaml', $yamlTemplateWithoutOriginNodeTypeName);
247244
}

Tests/Functional/FakeNodeTypeManagerTrait.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Flowpack\NodeTemplates\Tests\Functional;
66

77
use Neos\ContentRepository\Core\NodeType\NodeTypeManager;
8+
use Neos\ContentRepositoryRegistry\Configuration\NodeTypeEnrichmentService;
89
use Neos\Flow\Configuration\ConfigurationManager;
910
use Neos\Flow\ObjectManagement\ObjectManagerInterface;
1011
use Neos\Utility\Arrays;
@@ -34,6 +35,11 @@ private function loadFakeNodeTypes(): void
3435
);
3536
}
3637

37-
$this->nodeTypeManager->overrideNodeTypes($configuration);
38+
$this->nodeTypeManager->overrideNodeTypes(
39+
// hack, we use the service here to expand the `i18n` magic label
40+
$this->objectManager->get(NodeTypeEnrichmentService::class)->enrichNodeTypeLabelsConfiguration(
41+
$configuration
42+
)
43+
);
3844
}
3945
}

Tests/Functional/Features/Properties/Snapshots/Properties.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@
99
# Select Box
1010
# selectBox -> SelectBox of ["karma","longLive"] with value "karma"
1111
# Reference
12-
# reference -> Reference of NodeTypes (Flowpack.NodeTemplates:Content.Properties) with value Node(7f7bac1c-9400-4db5-bbaa-2b8251d127c5)
12+
# reference -> Reference of NodeTypes (Flowpack.NodeTemplates:Content.Properties) with Node: 7f7bac1c-9400-4db5-bbaa-2b8251d127c5

Tests/Functional/Features/ResolvableProperties/Snapshots/ResolvableProperties.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@
44
properties:
55
# asset -> object(Neos\Media\Domain\Model\Asset)
66
# images -> array(Neos\Media\Domain\Model\Image)
7-
# reference -> Reference of NodeTypes (Neos.Neos:Document) with value Node(some-node-id)
8-
# references -> Nodes(some-node-id, other-node-id, real-node-id)
7+
# reference -> Reference of NodeTypes (Neos.Neos:Document) with Node: some-node-id
8+
# references -> References with Nodes: some-node-id, other-node-id, real-node-id

0 commit comments

Comments
 (0)