Skip to content

Commit

Permalink
Merge pull request #40 from Flowpack/feature/37-nodeTemplateYamlDumpF…
Browse files Browse the repository at this point in the history
…romNodeSubtree

FEATURE: Create node template definition yaml dump from node subtree
  • Loading branch information
Sebobo committed Jun 1, 2023
2 parents c0905ea + 4670035 commit 9042e5a
Show file tree
Hide file tree
Showing 8 changed files with 688 additions and 2 deletions.
45 changes: 45 additions & 0 deletions Classes/Command/NodeTemplateCommandController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

declare(strict_types=1);

namespace Flowpack\NodeTemplates\Command;

use Flowpack\NodeTemplates\NodeTemplateDumper\NodeTemplateDumper;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\Cli\CommandController;
use Neos\Neos\Domain\Service\ContentContextFactory;

class NodeTemplateCommandController extends CommandController
{
/**
* @Flow\Inject
* @var ContentContextFactory
*/
protected $contentContextFactory;

/**
* @Flow\Inject
* @var NodeTemplateDumper
*/
protected $nodeTemplateDumper;

/**
* Dump the node tree structure into a NodeTemplate YAML structure.
* References to Nodes and non-primitive property values are commented out in the YAML.
*
* @param string $startingNodeId specified root node of the node tree
* @param string $workspaceName
* @return void
*/
public function createFromNodeSubtree(string $startingNodeId, string $workspaceName = 'live'): void
{
$subgraph = $this->contentContextFactory->create([
'workspaceName' => $workspaceName
]);
$node = $subgraph->getNodeByIdentifier($startingNodeId);
if (!$node) {
throw new \InvalidArgumentException("Node $startingNodeId doesnt exist in workspace $workspaceName.");
}
echo $this->nodeTemplateDumper->createNodeTemplateYamlDumpFromSubtree($node);
}
}
36 changes: 36 additions & 0 deletions Classes/NodeTemplateDumper/Comment.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace Flowpack\NodeTemplates\NodeTemplateDumper;

use Neos\Flow\Annotations as Flow;

/**
* Wrapper around a comment render function
* {@see Comments}
*
* @Flow\Proxy(false)
*/
class Comment
{
private \Closure $renderFunction;

private function __construct(\Closure $renderFunction)
{
$this->renderFunction = $renderFunction;
}

/**
* @psalm-param callable(string $indentation, string $propertyName): string $renderFunction
*/
public static function fromRenderer($renderFunction): self
{
return new self($renderFunction);
}

public function toYamlComment(string $indentation, string $propertyName): string
{
return ($this->renderFunction)($indentation, $propertyName);
}
}
58 changes: 58 additions & 0 deletions Classes/NodeTemplateDumper/Comments.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

declare(strict_types=1);

namespace Flowpack\NodeTemplates\NodeTemplateDumper;

use Neos\Flow\Annotations as Flow;
use Neos\Flow\Utility\Algorithms;

/**
* Since the yaml dumper doesn't support comments, we insert `Comment<id>` markers into the array via {@see Comments::addCommentAndGetMarker}
* that will be dumped and later can be processed via {@see Comments::renderCommentsInYamlDump}
*
* A comment is just a wrapper around a render function that will be called during {@see Comments::renderCommentsInYamlDump}
*
* @Flow\Proxy(false)
*/
class Comments
{
private const SERIALIZED_PATTERN = <<<'REGEX'
/(?<indentation>[ ]*)(?<property>.*?): Comment<(?<identifier>[a-z0-9\-]{1,255})>/
REGEX;

/** @var array<Comment> */
private array $comments;

private function __construct()
{
}

public static function empty(): self
{
return new self();
}

public function addCommentAndGetMarker(Comment $comment): string
{
$identifier = Algorithms::generateUUID();
$this->comments[$identifier] = $comment;
return 'Comment<' . $identifier . '>';
}

public function renderCommentsInYamlDump(string $yamlDump): string
{
return preg_replace_callback(self::SERIALIZED_PATTERN, function (array $matches) {
[
'indentation' => $indentation,
'property' => $property,
'identifier' => $identifier
] = $matches;
$comment = $this->comments[$identifier] ?? null;
if (!$comment instanceof Comment) {
throw new \Exception('Error while trying to render comment ' . $matches[0] . '. Reason: comment id doesnt exist.', 1684309524383);
}
return $comment->toYamlComment($indentation, $property);
}, $yamlDump);
}
}
242 changes: 242 additions & 0 deletions Classes/NodeTemplateDumper/NodeTemplateDumper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
<?php

declare(strict_types=1);

namespace Flowpack\NodeTemplates\NodeTemplateDumper;

use Neos\ContentRepository\Domain\Model\ArrayPropertyCollection;
use Neos\ContentRepository\Domain\Model\NodeInterface;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\I18n\EelHelper\TranslationHelper;
use Symfony\Component\Yaml\Yaml;

/** @Flow\Scope("singleton") */
class NodeTemplateDumper
{
/**
* @var TranslationHelper
* @Flow\Inject
*/
protected $translationHelper;

/**
* Dump the node tree structure into a NodeTemplate YAML structure.
* References to Nodes and non-primitive property values are commented out in the YAML.
*
* @param NodeInterface $startingNode specified root node of the node tree to dump
* @return string YAML representation of the node template
*/
public function createNodeTemplateYamlDumpFromSubtree(NodeInterface $startingNode): string
{
$comments = Comments::empty();

$nodeType = $startingNode->getNodeType();

if (
!$nodeType->isOfType('Neos.Neos:Document')
&& !$nodeType->isOfType('Neos.Neos:Content')
&& !$nodeType->isOfType('Neos.Neos:ContentCollection')
) {
throw new \InvalidArgumentException("Node {$startingNode->getIdentifier()} must be one of Neos.Neos:Document,Neos.Neos:Content,Neos.Neos:ContentCollection.");
}

$template = $this->nodeTemplateFromNodes([$startingNode], $comments);

foreach ($template as $firstEntry) {
break;
}
assert(isset($firstEntry));

$templateInNodeTypeOptions = [
$nodeType->getName() => [
'options' => [
'template' => array_filter([
'properties' => $firstEntry['properties'] ?? null,
'childNodes' => $firstEntry['childNodes'] ?? null,
])
]
]
];

$yamlWithSerializedComments = Yaml::dump($templateInNodeTypeOptions, 99, 2, Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE | Yaml::DUMP_NULL_AS_TILDE);

return $comments->renderCommentsInYamlDump($yamlWithSerializedComments);
}

/** @param array<NodeInterface> $nodes */
private function nodeTemplateFromNodes(array $nodes, Comments $comments): array
{
$documentNodeTemplates = [];
$contentNodeTemplates = [];
foreach ($nodes as $index => $node) {
assert($node instanceof NodeInterface);
$nodeType = $node->getNodeType();
$isDocumentNode = $nodeType->isOfType('Neos.Neos:Document');

$templatePart = array_filter([
'properties' => $this->nonDefaultConfiguredNodeProperties($node, $comments),
'childNodes' => $this->nodeTemplateFromNodes(
$isDocumentNode
? $node->getChildNodes('Neos.Neos:Content,Neos.Neos:ContentCollection,Neos.Neos:Document')
: $node->getChildNodes('Neos.Neos:Content,Neos.Neos:ContentCollection'),
$comments
)
]);

if ($templatePart === []) {
continue;
}

if ($isDocumentNode) {
if ($node->isTethered()) {
$documentNodeTemplates[$node->getLabel() ?: $node->getName()] = array_merge([
'name' => $node->getName()
], $templatePart);
continue;
}

$documentNodeTemplates["page$index"] = array_merge([
'type' => $node->getNodeType()->getName()
], $templatePart);
continue;
}

if ($node->isTethered()) {
$contentNodeTemplates[$node->getLabel() ?: $node->getName()] = array_merge([
'name' => $node->getName()
], $templatePart);
continue;
}

$contentNodeTemplates["content$index"] = array_merge([
'type' => $node->getNodeType()->getName()
], $templatePart);
}

return array_merge($contentNodeTemplates, $documentNodeTemplates);
}

private function nonDefaultConfiguredNodeProperties(NodeInterface $node, Comments $comments): array
{
$nodeType = $node->getNodeType();
$nodeProperties = $node->getProperties();

$filteredProperties = [];
foreach ($nodeType->getProperties() as $propertyName => $configuration) {
if (
$nodeProperties instanceof ArrayPropertyCollection
? !$nodeProperties->offsetExists($propertyName)
: !array_key_exists($propertyName, $nodeProperties)
) {
// node doesn't have the property set
continue;
}

if (
array_key_exists('defaultValue', $configuration)
&& $configuration['defaultValue'] === $nodeProperties[$propertyName]
) {
// node property is the same as default
continue;
}

$propertyValue = $nodeProperties[$propertyName];
if ($propertyValue === null || $propertyValue === []) {
continue;
}
if (is_string($propertyValue) && trim($propertyValue) === '') {
continue;
}

$label = $configuration['ui']['label'] ?? null;
$augmentCommentWithLabel = fn (Comment $comment) => $comment;
if ($label) {
$label = $this->translationHelper->translate($label);
$augmentCommentWithLabel = fn (Comment $comment) => Comment::fromRenderer(
function ($indentation, $propertyName) use($comment, $propertyValue, $label) {
return $indentation . '# ' . $label . "\n" .
$comment->toYamlComment($indentation, $propertyName);
}
);
}

if ($dataSourceIdentifier = $configuration['ui']['inspector']['editorOptions']['dataSourceIdentifier'] ?? null) {
$filteredProperties[$propertyName] = $comments->addCommentAndGetMarker($augmentCommentWithLabel(Comment::fromRenderer(
function ($indentation, $propertyName) use ($dataSourceIdentifier, $propertyValue) {
return $indentation . '# ' . $propertyName . ' -> Datasource "' . $dataSourceIdentifier . '" with value ' . $this->valueToDebugString($propertyValue);
}
)));
continue;
}

if (($configuration['type'] ?? null) === 'reference') {
$nodeTypesInReference = $configuration['ui']['inspector']['editorOptions']['nodeTypes'] ?? ['Neos.Neos:Document'];
$filteredProperties[$propertyName] = $comments->addCommentAndGetMarker($augmentCommentWithLabel(Comment::fromRenderer(
function ($indentation, $propertyName) use ($nodeTypesInReference, $propertyValue) {
return $indentation . '# ' . $propertyName . ' -> Reference of NodeTypes (' . join(', ', $nodeTypesInReference) . ') with value ' . $this->valueToDebugString($propertyValue);
}
)));
continue;
}

if (($configuration['ui']['inspector']['editor'] ?? null) === 'Neos.Neos/Inspector/Editors/SelectBoxEditor') {
$selectBoxValues = array_keys($configuration['ui']['inspector']['editorOptions']['values'] ?? []);
$filteredProperties[$propertyName] = $comments->addCommentAndGetMarker($augmentCommentWithLabel(Comment::fromRenderer(
function ($indentation, $propertyName) use ($selectBoxValues, $propertyValue) {
return $indentation . '# ' . $propertyName . ' -> SelectBox of '
. mb_strimwidth(json_encode($selectBoxValues), 0, 60, ' ...]')
. ' with value ' . $this->valueToDebugString($propertyValue);
}
)));
continue;
}

if (is_object($propertyValue) || (is_array($propertyValue) && is_object(array_values($propertyValue)[0] ?? null))) {
$filteredProperties[$propertyName] = $comments->addCommentAndGetMarker($augmentCommentWithLabel(Comment::fromRenderer(
function ($indentation, $propertyName) use ($propertyValue) {
return $indentation . '# ' . $propertyName . ' -> ' . $this->valueToDebugString($propertyValue);
}
)));
continue;
}

$filteredProperties[$propertyName] = $comments->addCommentAndGetMarker($augmentCommentWithLabel(Comment::fromRenderer(
function ($indentation, $propertyName) use ($propertyValue) {
return $indentation . $propertyName . ': ' . Yaml::dump($propertyValue);
}
)));
}

return $filteredProperties;
}

private function valueToDebugString($value): string
{
if ($value instanceof NodeInterface) {
return 'Node(' . $value->getIdentifier() . ')';
}
if (is_iterable($value)) {
$name = null;
$entries = [];
foreach ($value as $key => $item) {
if ($item instanceof NodeInterface) {
if ($name === null || $name === 'Nodes') {
$name = 'Nodes';
} else {
$name = 'array';
}
$entries[$key] = $item->getIdentifier();
continue;
}
$name = 'array';
$entries[$key] = is_object($item) ? get_class($item) : json_encode($item);
}
return $name . '(' . join(', ', $entries) . ')';
}

if (is_object($value)) {
return 'object(' . get_class($value) . ')';
}
return json_encode($value);
}
}

0 comments on commit 9042e5a

Please sign in to comment.