Skip to content

Commit 9042e5a

Browse files
authored
Merge pull request #40 from Flowpack/feature/37-nodeTemplateYamlDumpFromNodeSubtree
FEATURE: Create node template definition yaml dump from node subtree
2 parents c0905ea + 4670035 commit 9042e5a

File tree

8 files changed

+688
-2
lines changed

8 files changed

+688
-2
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Flowpack\NodeTemplates\Command;
6+
7+
use Flowpack\NodeTemplates\NodeTemplateDumper\NodeTemplateDumper;
8+
use Neos\Flow\Annotations as Flow;
9+
use Neos\Flow\Cli\CommandController;
10+
use Neos\Neos\Domain\Service\ContentContextFactory;
11+
12+
class NodeTemplateCommandController extends CommandController
13+
{
14+
/**
15+
* @Flow\Inject
16+
* @var ContentContextFactory
17+
*/
18+
protected $contentContextFactory;
19+
20+
/**
21+
* @Flow\Inject
22+
* @var NodeTemplateDumper
23+
*/
24+
protected $nodeTemplateDumper;
25+
26+
/**
27+
* Dump the node tree structure into a NodeTemplate YAML structure.
28+
* References to Nodes and non-primitive property values are commented out in the YAML.
29+
*
30+
* @param string $startingNodeId specified root node of the node tree
31+
* @param string $workspaceName
32+
* @return void
33+
*/
34+
public function createFromNodeSubtree(string $startingNodeId, string $workspaceName = 'live'): void
35+
{
36+
$subgraph = $this->contentContextFactory->create([
37+
'workspaceName' => $workspaceName
38+
]);
39+
$node = $subgraph->getNodeByIdentifier($startingNodeId);
40+
if (!$node) {
41+
throw new \InvalidArgumentException("Node $startingNodeId doesnt exist in workspace $workspaceName.");
42+
}
43+
echo $this->nodeTemplateDumper->createNodeTemplateYamlDumpFromSubtree($node);
44+
}
45+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Flowpack\NodeTemplates\NodeTemplateDumper;
6+
7+
use Neos\Flow\Annotations as Flow;
8+
9+
/**
10+
* Wrapper around a comment render function
11+
* {@see Comments}
12+
*
13+
* @Flow\Proxy(false)
14+
*/
15+
class Comment
16+
{
17+
private \Closure $renderFunction;
18+
19+
private function __construct(\Closure $renderFunction)
20+
{
21+
$this->renderFunction = $renderFunction;
22+
}
23+
24+
/**
25+
* @psalm-param callable(string $indentation, string $propertyName): string $renderFunction
26+
*/
27+
public static function fromRenderer($renderFunction): self
28+
{
29+
return new self($renderFunction);
30+
}
31+
32+
public function toYamlComment(string $indentation, string $propertyName): string
33+
{
34+
return ($this->renderFunction)($indentation, $propertyName);
35+
}
36+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Flowpack\NodeTemplates\NodeTemplateDumper;
6+
7+
use Neos\Flow\Annotations as Flow;
8+
use Neos\Flow\Utility\Algorithms;
9+
10+
/**
11+
* Since the yaml dumper doesn't support comments, we insert `Comment<id>` markers into the array via {@see Comments::addCommentAndGetMarker}
12+
* that will be dumped and later can be processed via {@see Comments::renderCommentsInYamlDump}
13+
*
14+
* A comment is just a wrapper around a render function that will be called during {@see Comments::renderCommentsInYamlDump}
15+
*
16+
* @Flow\Proxy(false)
17+
*/
18+
class Comments
19+
{
20+
private const SERIALIZED_PATTERN = <<<'REGEX'
21+
/(?<indentation>[ ]*)(?<property>.*?): Comment<(?<identifier>[a-z0-9\-]{1,255})>/
22+
REGEX;
23+
24+
/** @var array<Comment> */
25+
private array $comments;
26+
27+
private function __construct()
28+
{
29+
}
30+
31+
public static function empty(): self
32+
{
33+
return new self();
34+
}
35+
36+
public function addCommentAndGetMarker(Comment $comment): string
37+
{
38+
$identifier = Algorithms::generateUUID();
39+
$this->comments[$identifier] = $comment;
40+
return 'Comment<' . $identifier . '>';
41+
}
42+
43+
public function renderCommentsInYamlDump(string $yamlDump): string
44+
{
45+
return preg_replace_callback(self::SERIALIZED_PATTERN, function (array $matches) {
46+
[
47+
'indentation' => $indentation,
48+
'property' => $property,
49+
'identifier' => $identifier
50+
] = $matches;
51+
$comment = $this->comments[$identifier] ?? null;
52+
if (!$comment instanceof Comment) {
53+
throw new \Exception('Error while trying to render comment ' . $matches[0] . '. Reason: comment id doesnt exist.', 1684309524383);
54+
}
55+
return $comment->toYamlComment($indentation, $property);
56+
}, $yamlDump);
57+
}
58+
}
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Flowpack\NodeTemplates\NodeTemplateDumper;
6+
7+
use Neos\ContentRepository\Domain\Model\ArrayPropertyCollection;
8+
use Neos\ContentRepository\Domain\Model\NodeInterface;
9+
use Neos\Flow\Annotations as Flow;
10+
use Neos\Flow\I18n\EelHelper\TranslationHelper;
11+
use Symfony\Component\Yaml\Yaml;
12+
13+
/** @Flow\Scope("singleton") */
14+
class NodeTemplateDumper
15+
{
16+
/**
17+
* @var TranslationHelper
18+
* @Flow\Inject
19+
*/
20+
protected $translationHelper;
21+
22+
/**
23+
* Dump the node tree structure into a NodeTemplate YAML structure.
24+
* References to Nodes and non-primitive property values are commented out in the YAML.
25+
*
26+
* @param NodeInterface $startingNode specified root node of the node tree to dump
27+
* @return string YAML representation of the node template
28+
*/
29+
public function createNodeTemplateYamlDumpFromSubtree(NodeInterface $startingNode): string
30+
{
31+
$comments = Comments::empty();
32+
33+
$nodeType = $startingNode->getNodeType();
34+
35+
if (
36+
!$nodeType->isOfType('Neos.Neos:Document')
37+
&& !$nodeType->isOfType('Neos.Neos:Content')
38+
&& !$nodeType->isOfType('Neos.Neos:ContentCollection')
39+
) {
40+
throw new \InvalidArgumentException("Node {$startingNode->getIdentifier()} must be one of Neos.Neos:Document,Neos.Neos:Content,Neos.Neos:ContentCollection.");
41+
}
42+
43+
$template = $this->nodeTemplateFromNodes([$startingNode], $comments);
44+
45+
foreach ($template as $firstEntry) {
46+
break;
47+
}
48+
assert(isset($firstEntry));
49+
50+
$templateInNodeTypeOptions = [
51+
$nodeType->getName() => [
52+
'options' => [
53+
'template' => array_filter([
54+
'properties' => $firstEntry['properties'] ?? null,
55+
'childNodes' => $firstEntry['childNodes'] ?? null,
56+
])
57+
]
58+
]
59+
];
60+
61+
$yamlWithSerializedComments = Yaml::dump($templateInNodeTypeOptions, 99, 2, Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE | Yaml::DUMP_NULL_AS_TILDE);
62+
63+
return $comments->renderCommentsInYamlDump($yamlWithSerializedComments);
64+
}
65+
66+
/** @param array<NodeInterface> $nodes */
67+
private function nodeTemplateFromNodes(array $nodes, Comments $comments): array
68+
{
69+
$documentNodeTemplates = [];
70+
$contentNodeTemplates = [];
71+
foreach ($nodes as $index => $node) {
72+
assert($node instanceof NodeInterface);
73+
$nodeType = $node->getNodeType();
74+
$isDocumentNode = $nodeType->isOfType('Neos.Neos:Document');
75+
76+
$templatePart = array_filter([
77+
'properties' => $this->nonDefaultConfiguredNodeProperties($node, $comments),
78+
'childNodes' => $this->nodeTemplateFromNodes(
79+
$isDocumentNode
80+
? $node->getChildNodes('Neos.Neos:Content,Neos.Neos:ContentCollection,Neos.Neos:Document')
81+
: $node->getChildNodes('Neos.Neos:Content,Neos.Neos:ContentCollection'),
82+
$comments
83+
)
84+
]);
85+
86+
if ($templatePart === []) {
87+
continue;
88+
}
89+
90+
if ($isDocumentNode) {
91+
if ($node->isTethered()) {
92+
$documentNodeTemplates[$node->getLabel() ?: $node->getName()] = array_merge([
93+
'name' => $node->getName()
94+
], $templatePart);
95+
continue;
96+
}
97+
98+
$documentNodeTemplates["page$index"] = array_merge([
99+
'type' => $node->getNodeType()->getName()
100+
], $templatePart);
101+
continue;
102+
}
103+
104+
if ($node->isTethered()) {
105+
$contentNodeTemplates[$node->getLabel() ?: $node->getName()] = array_merge([
106+
'name' => $node->getName()
107+
], $templatePart);
108+
continue;
109+
}
110+
111+
$contentNodeTemplates["content$index"] = array_merge([
112+
'type' => $node->getNodeType()->getName()
113+
], $templatePart);
114+
}
115+
116+
return array_merge($contentNodeTemplates, $documentNodeTemplates);
117+
}
118+
119+
private function nonDefaultConfiguredNodeProperties(NodeInterface $node, Comments $comments): array
120+
{
121+
$nodeType = $node->getNodeType();
122+
$nodeProperties = $node->getProperties();
123+
124+
$filteredProperties = [];
125+
foreach ($nodeType->getProperties() as $propertyName => $configuration) {
126+
if (
127+
$nodeProperties instanceof ArrayPropertyCollection
128+
? !$nodeProperties->offsetExists($propertyName)
129+
: !array_key_exists($propertyName, $nodeProperties)
130+
) {
131+
// node doesn't have the property set
132+
continue;
133+
}
134+
135+
if (
136+
array_key_exists('defaultValue', $configuration)
137+
&& $configuration['defaultValue'] === $nodeProperties[$propertyName]
138+
) {
139+
// node property is the same as default
140+
continue;
141+
}
142+
143+
$propertyValue = $nodeProperties[$propertyName];
144+
if ($propertyValue === null || $propertyValue === []) {
145+
continue;
146+
}
147+
if (is_string($propertyValue) && trim($propertyValue) === '') {
148+
continue;
149+
}
150+
151+
$label = $configuration['ui']['label'] ?? null;
152+
$augmentCommentWithLabel = fn (Comment $comment) => $comment;
153+
if ($label) {
154+
$label = $this->translationHelper->translate($label);
155+
$augmentCommentWithLabel = fn (Comment $comment) => Comment::fromRenderer(
156+
function ($indentation, $propertyName) use($comment, $propertyValue, $label) {
157+
return $indentation . '# ' . $label . "\n" .
158+
$comment->toYamlComment($indentation, $propertyName);
159+
}
160+
);
161+
}
162+
163+
if ($dataSourceIdentifier = $configuration['ui']['inspector']['editorOptions']['dataSourceIdentifier'] ?? null) {
164+
$filteredProperties[$propertyName] = $comments->addCommentAndGetMarker($augmentCommentWithLabel(Comment::fromRenderer(
165+
function ($indentation, $propertyName) use ($dataSourceIdentifier, $propertyValue) {
166+
return $indentation . '# ' . $propertyName . ' -> Datasource "' . $dataSourceIdentifier . '" with value ' . $this->valueToDebugString($propertyValue);
167+
}
168+
)));
169+
continue;
170+
}
171+
172+
if (($configuration['type'] ?? null) === 'reference') {
173+
$nodeTypesInReference = $configuration['ui']['inspector']['editorOptions']['nodeTypes'] ?? ['Neos.Neos:Document'];
174+
$filteredProperties[$propertyName] = $comments->addCommentAndGetMarker($augmentCommentWithLabel(Comment::fromRenderer(
175+
function ($indentation, $propertyName) use ($nodeTypesInReference, $propertyValue) {
176+
return $indentation . '# ' . $propertyName . ' -> Reference of NodeTypes (' . join(', ', $nodeTypesInReference) . ') with value ' . $this->valueToDebugString($propertyValue);
177+
}
178+
)));
179+
continue;
180+
}
181+
182+
if (($configuration['ui']['inspector']['editor'] ?? null) === 'Neos.Neos/Inspector/Editors/SelectBoxEditor') {
183+
$selectBoxValues = array_keys($configuration['ui']['inspector']['editorOptions']['values'] ?? []);
184+
$filteredProperties[$propertyName] = $comments->addCommentAndGetMarker($augmentCommentWithLabel(Comment::fromRenderer(
185+
function ($indentation, $propertyName) use ($selectBoxValues, $propertyValue) {
186+
return $indentation . '# ' . $propertyName . ' -> SelectBox of '
187+
. mb_strimwidth(json_encode($selectBoxValues), 0, 60, ' ...]')
188+
. ' with value ' . $this->valueToDebugString($propertyValue);
189+
}
190+
)));
191+
continue;
192+
}
193+
194+
if (is_object($propertyValue) || (is_array($propertyValue) && is_object(array_values($propertyValue)[0] ?? null))) {
195+
$filteredProperties[$propertyName] = $comments->addCommentAndGetMarker($augmentCommentWithLabel(Comment::fromRenderer(
196+
function ($indentation, $propertyName) use ($propertyValue) {
197+
return $indentation . '# ' . $propertyName . ' -> ' . $this->valueToDebugString($propertyValue);
198+
}
199+
)));
200+
continue;
201+
}
202+
203+
$filteredProperties[$propertyName] = $comments->addCommentAndGetMarker($augmentCommentWithLabel(Comment::fromRenderer(
204+
function ($indentation, $propertyName) use ($propertyValue) {
205+
return $indentation . $propertyName . ': ' . Yaml::dump($propertyValue);
206+
}
207+
)));
208+
}
209+
210+
return $filteredProperties;
211+
}
212+
213+
private function valueToDebugString($value): string
214+
{
215+
if ($value instanceof NodeInterface) {
216+
return 'Node(' . $value->getIdentifier() . ')';
217+
}
218+
if (is_iterable($value)) {
219+
$name = null;
220+
$entries = [];
221+
foreach ($value as $key => $item) {
222+
if ($item instanceof NodeInterface) {
223+
if ($name === null || $name === 'Nodes') {
224+
$name = 'Nodes';
225+
} else {
226+
$name = 'array';
227+
}
228+
$entries[$key] = $item->getIdentifier();
229+
continue;
230+
}
231+
$name = 'array';
232+
$entries[$key] = is_object($item) ? get_class($item) : json_encode($item);
233+
}
234+
return $name . '(' . join(', ', $entries) . ')';
235+
}
236+
237+
if (is_object($value)) {
238+
return 'object(' . get_class($value) . ')';
239+
}
240+
return json_encode($value);
241+
}
242+
}

0 commit comments

Comments
 (0)