Skip to content

Commit

Permalink
FEATURE: Migrate flow nodeTemplate:createFromNodeSubtree to Neos9
Browse files Browse the repository at this point in the history
introduced with #40
  • Loading branch information
mhsdesign committed Jun 21, 2024
1 parent a5be69e commit 96bd529
Show file tree
Hide file tree
Showing 7 changed files with 147 additions and 86 deletions.
31 changes: 22 additions & 9 deletions Classes/Application/Command/NodeTemplateCommandController.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,19 +61,32 @@ class NodeTemplateCommandController extends CommandController
* @param string $workspaceName
* @return void
*/
public function createFromNodeSubtreeCommand(string $startingNodeId, string $workspaceName = 'live'): void
public function createFromNodeSubtreeCommand(string $startingNodeId, ?string $site = null, string $workspaceName = 'live'): void
{
throw new \BadMethodCallException('Not implemented.');
/*
$subgraph = $this->contextFactory->create([
'workspaceName' => $workspaceName
]);
$node = $subgraph->getNodeByIdentifier($startingNodeId);
$siteInstance = $site
? $this->siteRepository->findOneByNodeName($site)
: $this->siteRepository->findDefault();

if (!$siteInstance) {
$this->outputLine(sprintf('<error>Site "%s" does not exist.</error>', $site));
$this->quit(2);
}

$siteConfiguration = $siteInstance->getConfiguration();

$contentRepository = $this->contentRepositoryRegistry->get($siteConfiguration->contentRepositoryId);

// default context? https://github.com/neos/neos-development-collection/issues/5113
$subgraph = $contentRepository->getContentGraph(WorkspaceName::fromString($workspaceName))->getSubgraph(
$siteConfiguration->defaultDimensionSpacePoint,
VisibilityConstraints::frontend()
);

$node = $subgraph->findNodeById(NodeAggregateId::fromString($startingNodeId));
if (!$node) {
throw new \InvalidArgumentException("Node $startingNodeId doesnt exist in workspace $workspaceName.");
}
echo $this->nodeTemplateDumper->createNodeTemplateYamlDumpFromSubtree($node);
*/
echo $this->nodeTemplateDumper->createNodeTemplateYamlDumpFromSubtree($node, $contentRepository);
}

/**
Expand Down
10 changes: 0 additions & 10 deletions Classes/Domain/NodeCreation/PropertyType.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,16 +53,6 @@ public static function fromPropertyOfNodeType(
NodeType $nodeType
): self {
$declaration = $nodeType->getPropertyType($propertyName);
if ($declaration === 'reference' || $declaration === 'references') {
throw new \DomainException(
sprintf(
'Given property "%s" is declared as "reference" in node type "%s" and must be treated as such.',
$propertyName,
$nodeType->name->value
),
1685964835205
);
}
$type = self::tryFromString($declaration);
if (!$type) {
throw new \DomainException(
Expand Down
171 changes: 113 additions & 58 deletions Classes/Domain/NodeTemplateDumper/NodeTemplateDumper.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,16 @@

namespace Flowpack\NodeTemplates\Domain\NodeTemplateDumper;

use Neos\ContentRepository\Domain\Model\ArrayPropertyCollection;
use Neos\ContentRepository\Domain\Model\NodeInterface;
use Neos\ContentRepository\Domain\Projection\Content\PropertyCollectionInterface;
use Neos\ContentRepository\Core\ContentRepository;
use Neos\ContentRepository\Core\NodeType\NodeType;
use Neos\ContentRepository\Core\Projection\ContentGraph\ContentSubgraphInterface;
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindChildNodesFilter;
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindReferencesFilter;
use Neos\ContentRepository\Core\Projection\ContentGraph\Node;
use Neos\ContentRepository\Core\Projection\ContentGraph\Nodes;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\I18n\EelHelper\TranslationHelper;
use Neos\Neos\Domain\NodeLabel\NodeLabelGeneratorInterface;
use Symfony\Component\Yaml\Yaml;

/** @Flow\Scope("singleton") */
Expand All @@ -20,28 +25,36 @@ class NodeTemplateDumper
*/
protected $translationHelper;

/**
* @var NodeLabelGeneratorInterface
* @Flow\Inject
*/
protected $nodeLabelGenerator;

/**
* 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
* @param Node $startingNode specified root node of the node tree to dump
* @return string YAML representation of the node template
*/
public function createNodeTemplateYamlDumpFromSubtree(NodeInterface $startingNode): string
public function createNodeTemplateYamlDumpFromSubtree(Node $startingNode, ContentRepository $contentRepository): string
{
$comments = Comments::empty();

$nodeType = $startingNode->getNodeType();
$nodeType = $contentRepository->getNodeTypeManager()->getNodeType($startingNode->nodeTypeName);

if (
!$nodeType->isOfType('Neos.Neos:Document')
&& !$nodeType->isOfType('Neos.Neos:Content')
&& !$nodeType->isOfType('Neos.Neos:ContentCollection')
!$nodeType || (
!$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.");
throw new \InvalidArgumentException("Node {$startingNode->aggregateId->value} must be one of Neos.Neos:Document,Neos.Neos:Content,Neos.Neos:ContentCollection.");
}

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

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


$templateInNodeTypeOptions = [
$nodeType->getName() => [
$nodeType->name->value => [
'options' => [
'template' => array_filter([
'properties' => $properties,
Expand All @@ -68,21 +81,32 @@ public function createNodeTemplateYamlDumpFromSubtree(NodeInterface $startingNod
return $comments->renderCommentsInYamlDump($yamlWithSerializedComments);
}

/** @param array<NodeInterface> $nodes */
private function nodeTemplateFromNodes(array $nodes, Comments $comments): array
/** @return array<string, array<string, mixed>> */
private function nodeTemplateFromNodes(Nodes $nodes, Comments $comments, ContentRepository $contentRepository): array
{
$subgraph = null;

$documentNodeTemplates = [];
$contentNodeTemplates = [];
foreach ($nodes as $index => $node) {
assert($node instanceof NodeInterface);
$nodeType = $node->getNodeType();
$subgraph ??= $contentRepository->getContentGraph($node->workspaceName)->getSubgraph(
$node->dimensionSpacePoint,
$node->visibilityConstraints
);

$nodeType = $contentRepository->getNodeTypeManager()->getNodeType($node->nodeTypeName);
if (!$nodeType) {
throw new \RuntimeException("NodeType {$node->nodeTypeName->value} of Node {$node->aggregateId->value} doesnt exist.");
}

$isDocumentNode = $nodeType->isOfType('Neos.Neos:Document');

$templatePart = array_filter([
'properties' => $this->nonDefaultConfiguredNodeProperties($node, $comments),
'properties' => $this->nonDefaultConfiguredNodeProperties($node, $nodeType, $comments, $subgraph),
'childNodes' => $this->nodeTemplateFromNodes(
$node->getChildNodes('Neos.Neos:Node'),
$comments
$subgraph->findChildNodes($node->aggregateId, FindChildNodesFilter::create('Neos.Neos:Node')),
$comments,
$contentRepository
)
]);

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

if ($isDocumentNode) {
if ($node->isTethered()) {
$documentNodeTemplates[$node->getLabel() ?: $node->getName()] = array_merge([
'name' => $node->getName()
if ($node->classification->isTethered()) {
$tetheredName = $node->name;
assert($tetheredName !== null);

$documentNodeTemplates[$this->nodeLabelGenerator->getLabel($node) ?: $tetheredName->value] = array_merge([
'name' => $tetheredName->value
], $templatePart);
continue;
}

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

if ($node->isTethered()) {
$contentNodeTemplates[$node->getLabel() ?: $node->getName()] = array_merge([
'name' => $node->getName()
if ($node->classification->isTethered()) {
$tetheredName = $node->name;
assert($tetheredName !== null);

$contentNodeTemplates[$this->nodeLabelGenerator->getLabel($node) ?: $tetheredName->value] = array_merge([
'name' => $tetheredName->value
], $templatePart);
continue;
}

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

return array_merge($contentNodeTemplates, $documentNodeTemplates);
}

private function nonDefaultConfiguredNodeProperties(NodeInterface $node, Comments $comments): array
/** @return array<string, string> */
private function nonDefaultConfiguredNodeProperties(Node $node, NodeType $nodeType, Comments $comments, ContentSubgraphInterface $subgraph): array
{
$nodeType = $node->getNodeType();
$nodeProperties = $node->getProperties();
$nodeProperties = $node->properties;

$filteredProperties = [];
foreach ($nodeType->getProperties() as $propertyName => $configuration) {
Expand Down Expand Up @@ -170,22 +200,12 @@ function ($indentation, $propertyName) use ($dataSourceIdentifier, $propertyValu
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, ' ...]')
. mb_strimwidth(json_encode($selectBoxValues, JSON_THROW_ON_ERROR), 0, 60, ' ...]')
. ' with value ' . $this->valueToDebugString($propertyValue);
}
)));
Expand All @@ -208,36 +228,71 @@ function ($indentation, $propertyName) use ($propertyValue) {
)));
}

if ($nodeType->getReferences() === []) {
return $filteredProperties;
}

$references = $subgraph->findReferences($node->aggregateId, FindReferencesFilter::create());
$referencesArray = [];
foreach ($references as $reference) {
if (!isset($referencesArray[$reference->name->value])) {
$referencesArray[$reference->name->value] = $reference->node->aggregateId->value;
continue;
}
$referencesArray[$reference->name->value] .= ', ' . $reference->node->aggregateId->value;
}

foreach ($nodeType->getReferences() as $referenceName => $configuration) {
$referenceValue = $referencesArray[$referenceName] ?? null;
if (!$referenceValue) {
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, $label) {
return $indentation . '# ' . $label . "\n" .
$comment->toYamlComment($indentation, $propertyName);
}
);
}

if (($configuration['constraints']['maxItems'] ?? null) === 1) {
$nodeTypesInReference = $configuration['ui']['inspector']['editorOptions']['nodeTypes'] ?? ['Neos.Neos:Document'];
$filteredProperties[$referenceName] = $comments->addCommentAndGetMarker($augmentCommentWithLabel(Comment::fromRenderer(
function ($indentation, $propertyName) use ($nodeTypesInReference, $referenceValue) {
return $indentation . '# ' . $propertyName . ' -> Reference of NodeTypes (' . join(', ', $nodeTypesInReference) . ') with Node: ' . $referenceValue;
}
)));
continue;
}

$filteredProperties[$referenceName] = $comments->addCommentAndGetMarker($augmentCommentWithLabel(Comment::fromRenderer(
function ($indentation, $propertyName) use ($referenceValue) {
return $indentation . '# ' . $propertyName . ' -> References with Nodes: ' . $referenceValue;
}
)));
}

return $filteredProperties;
}

private function valueToDebugString($value): string
private function valueToDebugString(mixed $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) . ')';
return 'array(' . join(', ', $entries) . ')';
}

if (is_object($value)) {
return 'object(' . get_class($value) . ')';
}
return json_encode($value);
return json_encode($value, JSON_THROW_ON_ERROR);
}
}
7 changes: 2 additions & 5 deletions Tests/Functional/AbstractNodeTemplateTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -236,12 +236,9 @@ protected function assertNodeDumpAndTemplateDumpMatchSnapshot(string $snapShotNa
unset($serializedNodes['nodeTypeName']);
$this->assertJsonStringEqualsJsonFileOrCreateSnapshot($this->fixturesDir . '/' . $snapShotName . '.nodes.json', json_encode($serializedNodes, JSON_PRETTY_PRINT));

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

$dumpedYamlTemplate = $this->nodeTemplateDumper->createNodeTemplateYamlDumpFromSubtree($node);

$yamlTemplateWithoutOriginNodeTypeName = '\'{nodeTypeName}\'' . substr($dumpedYamlTemplate, strlen($node->getNodeType()->getName()) + 2);
$yamlTemplateWithoutOriginNodeTypeName = '\'{nodeTypeName}\'' . substr($dumpedYamlTemplate, strlen($node->nodeTypeName->value) + 2);

$this->assertStringEqualsFileOrCreateSnapshot($this->fixturesDir . '/' . $snapShotName . '.yaml', $yamlTemplateWithoutOriginNodeTypeName);
}
Expand Down
8 changes: 7 additions & 1 deletion Tests/Functional/FakeNodeTypeManagerTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Flowpack\NodeTemplates\Tests\Functional;

use Neos\ContentRepository\Core\NodeType\NodeTypeManager;
use Neos\ContentRepositoryRegistry\Configuration\NodeTypeEnrichmentService;
use Neos\Flow\Configuration\ConfigurationManager;
use Neos\Flow\ObjectManagement\ObjectManagerInterface;
use Neos\Utility\Arrays;
Expand Down Expand Up @@ -34,6 +35,11 @@ private function loadFakeNodeTypes(): void
);
}

$this->nodeTypeManager->overrideNodeTypes($configuration);
$this->nodeTypeManager->overrideNodeTypes(
// hack, we use the service here to expand the `i18n` magic label
$this->objectManager->get(NodeTypeEnrichmentService::class)->enrichNodeTypeLabelsConfiguration(
$configuration
)
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@
# Select Box
# selectBox -> SelectBox of ["karma","longLive"] with value "karma"
# Reference
# reference -> Reference of NodeTypes (Flowpack.NodeTemplates:Content.Properties) with value Node(7f7bac1c-9400-4db5-bbaa-2b8251d127c5)
# reference -> Reference of NodeTypes (Flowpack.NodeTemplates:Content.Properties) with Node: 7f7bac1c-9400-4db5-bbaa-2b8251d127c5
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@
properties:
# asset -> object(Neos\Media\Domain\Model\Asset)
# images -> array(Neos\Media\Domain\Model\Image)
# reference -> Reference of NodeTypes (Neos.Neos:Document) with value Node(some-node-id)
# references -> Nodes(some-node-id, other-node-id, real-node-id)
# reference -> Reference of NodeTypes (Neos.Neos:Document) with Node: some-node-id
# references -> References with Nodes: some-node-id, other-node-id, real-node-id

0 comments on commit 96bd529

Please sign in to comment.