From b4394ba64d1b67175702eef645db289e00fd26c5 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Tue, 13 Jun 2023 00:20:34 +0200 Subject: [PATCH 01/24] BUGFIX: TransliterateNodeName to allow setting properties on a childNode with invalid name A valid node name must match this expression /^[a-z0-9\-]+$/ But Neos is so kind to still allow invalid configuration, and it transliterates the string We must make sure that the same transliteration is applied to our childNode names to match the node names from the CR --- .../NodeCreation/NodeCreationService.php | 5 ++- .../TemplateConfigurationProcessor.php | 5 ++- .../NodeTypes.TransliterateNodeName.yaml | 44 ++++++++++++++++++ .../Fixtures/TransliterateNodeName.nodes.json | 39 ++++++++++++++++ .../TransliterateNodeName.template.json | 45 +++++++++++++++++++ .../Fixtures/TransliterateNodeName.yaml | 29 ++++++++++++ Tests/Functional/NodeTemplateTest.php | 19 ++++++++ 7 files changed, 183 insertions(+), 3 deletions(-) create mode 100644 Configuration/Testing/NodeTypes.TransliterateNodeName.yaml create mode 100644 Tests/Functional/Fixtures/TransliterateNodeName.nodes.json create mode 100644 Tests/Functional/Fixtures/TransliterateNodeName.template.json create mode 100644 Tests/Functional/Fixtures/TransliterateNodeName.yaml diff --git a/Classes/Domain/NodeCreation/NodeCreationService.php b/Classes/Domain/NodeCreation/NodeCreationService.php index a0aafee..80ce510 100644 --- a/Classes/Domain/NodeCreation/NodeCreationService.php +++ b/Classes/Domain/NodeCreation/NodeCreationService.php @@ -59,8 +59,11 @@ public function apply(RootTemplate $template, NodeInterface $node, CaughtExcepti private function applyTemplateRecursively(Templates $templates, NodeInterface $parentNode, CaughtExceptions $caughtExceptions): void { + // `hasAutoCreatedChildNode` actually has a bug; it looks up the NodeName parameter against the raw configuration instead of the transliterated NodeName + // https://github.com/neos/neos-ui/issues/3527 + $parentNodesAutoCreatedChildNodes = $parentNode->getNodeType()->getAutoCreatedChildNodes(); foreach ($templates as $template) { - if ($template->getName() && $parentNode->getNodeType()->hasAutoCreatedChildNode($template->getName())) { + if ($template->getName() && isset($parentNodesAutoCreatedChildNodes[$template->getName()->__toString()])) { $node = $parentNode->getNode($template->getName()->__toString()); if ($template->getType() !== null) { $caughtExceptions->add( diff --git a/Classes/Domain/TemplateConfiguration/TemplateConfigurationProcessor.php b/Classes/Domain/TemplateConfiguration/TemplateConfigurationProcessor.php index d2a6b5f..a4d4ba7 100644 --- a/Classes/Domain/TemplateConfiguration/TemplateConfigurationProcessor.php +++ b/Classes/Domain/TemplateConfiguration/TemplateConfigurationProcessor.php @@ -9,6 +9,7 @@ use Flowpack\NodeTemplates\Domain\Template\Templates; use Neos\ContentRepository\Domain\NodeAggregate\NodeName; use Neos\ContentRepository\Domain\NodeType\NodeTypeName; +use Neos\ContentRepository\Utility; use Neos\Flow\Annotations as Flow; /** @@ -119,8 +120,8 @@ private function createTemplateFromTemplatePart(TemplatePart $templatePart): Tem $type = $templatePart->processConfiguration('type'); $name = $templatePart->processConfiguration('name'); return new Template( - $type ? NodeTypeName::fromString($type) : null, - $name ? NodeName::fromString($name) : null, + $type !== null ? NodeTypeName::fromString($type) : null, + $name !== null ? NodeName::fromString(Utility::renderValidNodeName($name)) : null, $processedProperties, $childNodeTemplates ); diff --git a/Configuration/Testing/NodeTypes.TransliterateNodeName.yaml b/Configuration/Testing/NodeTypes.TransliterateNodeName.yaml new file mode 100644 index 0000000..4231df2 --- /dev/null +++ b/Configuration/Testing/NodeTypes.TransliterateNodeName.yaml @@ -0,0 +1,44 @@ +# A valid node name must match this expression /^[a-z0-9\-]+$/ +# But Neos is so kind to still allow invalid configuration, and it transliterates the string +# We must make sure that the same transliteration is applied to our childNodes to match the original +--- + +'Flowpack.NodeTemplates:Content.TransliterateNodeName': + superTypes: + 'Neos.Neos:ContentCollection': true + childNodes: + my-node: + type: 'Flowpack.NodeTemplates:Content.Text' + fooBar: + type: 'Flowpack.NodeTemplates:Content.Text' + ö: + type: 'Flowpack.NodeTemplates:Content.Text' + 'äBla北京=.§$Hä': + type: 'Flowpack.NodeTemplates:Content.Text' + '': + type: 'Flowpack.NodeTemplates:Content.Text' + + options: + template: + childNodes: + legalNodeName: + name: my-node + properties: + text: "legalNodeName" + capitalsInName: + # leave out the type so we would provoke an error if it doesnt match the above childNode + name: fooBar + properties: + text: "capitalsInName" + onlyOneIllegalCharacter: + name: ö + properties: + text: "ö - was soll das" + everythingMixedTogether: + name: 'äBla北京=.§$Hä' + properties: + text: "everythingMixedTogether" + emptyString: + name: '' + properties: + text: "emptyString" diff --git a/Tests/Functional/Fixtures/TransliterateNodeName.nodes.json b/Tests/Functional/Fixtures/TransliterateNodeName.nodes.json new file mode 100644 index 0000000..f4f71a7 --- /dev/null +++ b/Tests/Functional/Fixtures/TransliterateNodeName.nodes.json @@ -0,0 +1,39 @@ +{ + "childNodes": [ + { + "nodeTypeName": "Flowpack.NodeTemplates:Content.Text", + "nodeName": "my-node", + "properties": { + "text": "legalNodeName" + } + }, + { + "nodeTypeName": "Flowpack.NodeTemplates:Content.Text", + "nodeName": "foobar", + "properties": { + "text": "capitalsInName" + } + }, + { + "nodeTypeName": "Flowpack.NodeTemplates:Content.Text", + "nodeName": "o", + "properties": { + "text": "\u00f6 - was soll das" + } + }, + { + "nodeTypeName": "Flowpack.NodeTemplates:Content.Text", + "nodeName": "ablabei-jing-ss-ha", + "properties": { + "text": "everythingMixedTogether" + } + }, + { + "nodeTypeName": "Flowpack.NodeTemplates:Content.Text", + "nodeName": "node-d41d8cd98f00b204e9800998ecf8427e", + "properties": { + "text": "emptyString" + } + } + ] +} diff --git a/Tests/Functional/Fixtures/TransliterateNodeName.template.json b/Tests/Functional/Fixtures/TransliterateNodeName.template.json new file mode 100644 index 0000000..e7320bc --- /dev/null +++ b/Tests/Functional/Fixtures/TransliterateNodeName.template.json @@ -0,0 +1,45 @@ +{ + "properties": [], + "childNodes": [ + { + "type": null, + "name": "my-node", + "properties": { + "text": "legalNodeName" + }, + "childNodes": [] + }, + { + "type": null, + "name": "foobar", + "properties": { + "text": "capitalsInName" + }, + "childNodes": [] + }, + { + "type": null, + "name": "o", + "properties": { + "text": "\u00f6 - was soll das" + }, + "childNodes": [] + }, + { + "type": null, + "name": "ablabei-jing-ss-ha", + "properties": { + "text": "everythingMixedTogether" + }, + "childNodes": [] + }, + { + "type": null, + "name": "node-d41d8cd98f00b204e9800998ecf8427e", + "properties": { + "text": "emptyString" + }, + "childNodes": [] + } + ] +} diff --git a/Tests/Functional/Fixtures/TransliterateNodeName.yaml b/Tests/Functional/Fixtures/TransliterateNodeName.yaml new file mode 100644 index 0000000..8f96e46 --- /dev/null +++ b/Tests/Functional/Fixtures/TransliterateNodeName.yaml @@ -0,0 +1,29 @@ +'{nodeTypeName}': + options: + template: + childNodes: + legalNodeName: + name: my-node + properties: + # Text + text: legalNodeName + capitalsInName: + name: foobar + properties: + # Text + text: capitalsInName + 'ö - was soll das': + name: o + properties: + # Text + text: 'ö - was soll das' + everythingMixedTogether: + name: ablabei-jing-ss-ha + properties: + # Text + text: everythingMixedTogether + emptyString: + name: node-d41d8cd98f00b204e9800998ecf8427e + properties: + # Text + text: emptyString diff --git a/Tests/Functional/NodeTemplateTest.php b/Tests/Functional/NodeTemplateTest.php index 3ad81c6..dcea19f 100644 --- a/Tests/Functional/NodeTemplateTest.php +++ b/Tests/Functional/NodeTemplateTest.php @@ -178,6 +178,25 @@ public function testNodeCreationMatchesSnapshot3(): void $this->assertNodeDumpAndTemplateDumpMatchSnapshot('TwoColumnPreset', $createdNode); } + + /** @test */ + public function transliterateNodeName(): void + { + $this->createNodeInto( + $targetNode = $this->homePageNode->getNode('main'), + $toBeCreatedNodeTypeName = NodeTypeName::fromString('Flowpack.NodeTemplates:Content.TransliterateNodeName'), + [] + ); + + $this->assertLastCreatedTemplateMatchesSnapshot('TransliterateNodeName'); + + $createdNode = $targetNode->getChildNodes($toBeCreatedNodeTypeName->getValue())[0]; + + self::assertSame([], $this->getMessagesOfFeedbackCollection()); + $this->assertNodeDumpAndTemplateDumpMatchSnapshot('TransliterateNodeName', $createdNode); + } + + /** @test */ public function testNodeCreationWithDifferentPropertyTypes(): void { From 86586a6c0acab3f6d0d2ec5aba254019aea7267c Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Tue, 13 Jun 2023 08:34:51 +0200 Subject: [PATCH 02/24] TASK: NodeCreation: Check if node is abstract or if node is not allowed explicitly Backport of https://github.com/Flowpack/Flowpack.NodeTemplates/pull/51 The idea is we move more and more to separating the "command" creation and the actual apply should go through flawless. This will enable us validating the template without applying it. --- .../NodeCreation/NodeCreationService.php | 18 ++++++++++++++++++ Configuration/Testing/NodeTypes.Malformed.yaml | 4 +++- .../WithEvaluationExceptions.messages.json | 6 +++++- .../WithEvaluationExceptions.template.json | 8 +++++++- Tests/Functional/NodeTemplateTest.php | 6 +++--- Tests/Functional/SnapshotTrait.php | 12 ++++++++++++ 6 files changed, 48 insertions(+), 6 deletions(-) diff --git a/Classes/Domain/NodeCreation/NodeCreationService.php b/Classes/Domain/NodeCreation/NodeCreationService.php index 80ce510..12eec36 100644 --- a/Classes/Domain/NodeCreation/NodeCreationService.php +++ b/Classes/Domain/NodeCreation/NodeCreationService.php @@ -84,6 +84,24 @@ private function applyTemplateRecursively(Templates $templates, NodeInterface $p ); continue; } + + $nodeType = $this->nodeTypeManager->getNodeType($template->getType()->getValue()); + + if ($nodeType->isAbstract()) { + $caughtExceptions->add( + CaughtException::fromException(new \RuntimeException(sprintf('Template requires type to be a non abstract NodeType. Got: "%s".', $template->getType()->getValue()), 1686417628976)) + ); + continue; + } + + if (!$parentNode->getNodeType()->allowsChildNodeType($nodeType)) { + $caughtExceptions->add( + CaughtException::fromException(new \RuntimeException(sprintf('Node type "%s" is not allowed for child nodes of type %s', $template->getType()->getValue(), $parentNode->getNodeType()->getName()), 1686417627173)) + ); + continue; + } + + // todo maybe check also explicitly for allowsGrandchildNodeType (we do this currently like below) try { $node = $this->nodeOperations->create( $parentNode, diff --git a/Configuration/Testing/NodeTypes.Malformed.yaml b/Configuration/Testing/NodeTypes.Malformed.yaml index 1d5cd1e..08c96bf 100644 --- a/Configuration/Testing/NodeTypes.Malformed.yaml +++ b/Configuration/Testing/NodeTypes.Malformed.yaml @@ -54,8 +54,10 @@ type: 'Flowpack.NodeTemplates:Content.Text' properties: text: bar + abstractNodeAbort: + type: 'Neos.Neos:Node' illegalNodeAbort: - type: 'Neos.Neos:Document' + type: 'Flowpack.NodeTemplates:Document.Page.Static' name: 'illegal' properties: text: huhu diff --git a/Tests/Functional/Fixtures/WithEvaluationExceptions.messages.json b/Tests/Functional/Fixtures/WithEvaluationExceptions.messages.json index 0e0b812..fdf9b33 100644 --- a/Tests/Functional/Fixtures/WithEvaluationExceptions.messages.json +++ b/Tests/Functional/Fixtures/WithEvaluationExceptions.messages.json @@ -80,7 +80,11 @@ "severity": "ERROR" }, { - "message": "NodeConstraintException(Cannot create new node \"illegal\" of Type \"Neos.Neos:Document\" in Node \/sites\/test-site\/homepage\/main\/new-node@live[Flowpack.NodeTemplates:Content.WithEvaluationExceptions], 1400782413)", + "message": "RuntimeException(Template requires type to be a non abstract NodeType. Got: \"Neos.Neos:Node\"., 1686417628976)", + "severity": "ERROR" + }, + { + "message": "RuntimeException(Node type \"Flowpack.NodeTemplates:Document.Page.Static\" is not allowed for child nodes of type Flowpack.NodeTemplates:Content.WithEvaluationExceptions, 1686417627173)", "severity": "ERROR" }, { diff --git a/Tests/Functional/Fixtures/WithEvaluationExceptions.template.json b/Tests/Functional/Fixtures/WithEvaluationExceptions.template.json index dc99bca..9ff7fd5 100644 --- a/Tests/Functional/Fixtures/WithEvaluationExceptions.template.json +++ b/Tests/Functional/Fixtures/WithEvaluationExceptions.template.json @@ -21,7 +21,13 @@ "childNodes": [] }, { - "type": "Neos.Neos:Document", + "type": "Neos.Neos:Node", + "name": null, + "properties": [], + "childNodes": [] + }, + { + "type": "Flowpack.NodeTemplates:Document.Page.Static", "name": "illegal", "properties": { "text": "huhu" diff --git a/Tests/Functional/NodeTemplateTest.php b/Tests/Functional/NodeTemplateTest.php index dcea19f..996be2d 100644 --- a/Tests/Functional/NodeTemplateTest.php +++ b/Tests/Functional/NodeTemplateTest.php @@ -229,7 +229,7 @@ public function exceptionsAreCaughtAndPartialTemplateIsBuild(): void $createdNode = $targetNode->getChildNodes($toBeCreatedNodeTypeName->getValue())[0]; - $this->assertStringEqualsFileOrCreateSnapshot(__DIR__ . '/Fixtures/WithEvaluationExceptions.messages.json', json_encode($this->getMessagesOfFeedbackCollection(), JSON_PRETTY_PRINT)); + $this->assertJsonStringEqualsJsonFileOrCreateSnapshot(__DIR__ . '/Fixtures/WithEvaluationExceptions.messages.json', json_encode($this->getMessagesOfFeedbackCollection(), JSON_PRETTY_PRINT)); $this->assertNodeDumpAndTemplateDumpMatchSnapshot('WithEvaluationExceptions', $createdNode); } @@ -312,14 +312,14 @@ private function assertLastCreatedTemplateMatchesSnapshot(string $snapShotName): $lastCreatedTemplate = $this->serializeValuesInArray( $this->lastCreatedRootTemplate->jsonSerialize() ); - $this->assertStringEqualsFileOrCreateSnapshot(__DIR__ . '/Fixtures/' . $snapShotName . '.template.json', json_encode($lastCreatedTemplate, JSON_PRETTY_PRINT)); + $this->assertJsonStringEqualsJsonFileOrCreateSnapshot(__DIR__ . '/Fixtures/' . $snapShotName . '.template.json', json_encode($lastCreatedTemplate, JSON_PRETTY_PRINT)); } private function assertNodeDumpAndTemplateDumpMatchSnapshot(string $snapShotName, NodeInterface $node): void { $serializedNodes = $this->jsonSerializeNodeAndDescendents($node); unset($serializedNodes['nodeTypeName']); - $this->assertStringEqualsFileOrCreateSnapshot(__DIR__ . '/Fixtures/' . $snapShotName . '.nodes.json', json_encode($serializedNodes, JSON_PRETTY_PRINT)); + $this->assertJsonStringEqualsJsonFileOrCreateSnapshot(__DIR__ . '/Fixtures/' . $snapShotName . '.nodes.json', json_encode($serializedNodes, JSON_PRETTY_PRINT)); $dumpedYamlTemplate = $this->nodeTemplateDumper->createNodeTemplateYamlDumpFromSubtree($node); diff --git a/Tests/Functional/SnapshotTrait.php b/Tests/Functional/SnapshotTrait.php index a1a994f..032b3cb 100644 --- a/Tests/Functional/SnapshotTrait.php +++ b/Tests/Functional/SnapshotTrait.php @@ -12,7 +12,19 @@ private function assertStringEqualsFileOrCreateSnapshot(string $snapshotFileName if (getenv('CREATE_SNAPSHOT') === '1') { file_put_contents($snapshotFileName, $expectedString); $this->addWarning('Created snapshot.'); + return; } Assert::assertStringEqualsFile($snapshotFileName, $expectedString); } + + private function assertJsonStringEqualsJsonFileOrCreateSnapshot(string $snapshotFileName, string $expectedJsonString): void + { + $expectedJsonString = rtrim($expectedJsonString, "\n") . "\n"; + if (getenv('CREATE_SNAPSHOT') === '1') { + file_put_contents($snapshotFileName, $expectedJsonString); + $this->addWarning('Created snapshot.'); + return; + } + Assert::assertJsonStringEqualsJsonFile($snapshotFileName, $expectedJsonString); + } } From 122f59b278cf9147451a5823d427be3370aaf200 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Tue, 13 Jun 2023 10:06:11 +0200 Subject: [PATCH 03/24] FEATURE: Separate NodeCreation into creating "commands" (NodeMutators) We queue changes on nodes instead of making them directly. This allows us to check if the template is valid by cr perspective, without actually applying it, as all checks are executed before the Mutators are run. A mutator should only queue a mutation, that is guaranteed to succeed, as there is no exception handing. --- .../NodeCreation/NodeCreationService.php | 195 ++++++++++-------- Classes/Domain/NodeCreation/NodeMutator.php | 108 ++++++++++ Classes/Domain/NodeCreation/NodeMutators.php | 49 +++++ .../Domain/NodeCreation/ToBeCreatedNode.php | 25 +++ .../Domain/TemplateNodeCreationHandler.php | 9 +- 5 files changed, 295 insertions(+), 91 deletions(-) create mode 100644 Classes/Domain/NodeCreation/NodeMutator.php create mode 100644 Classes/Domain/NodeCreation/NodeMutators.php create mode 100644 Classes/Domain/NodeCreation/ToBeCreatedNode.php diff --git a/Classes/Domain/NodeCreation/NodeCreationService.php b/Classes/Domain/NodeCreation/NodeCreationService.php index 12eec36..54379cd 100644 --- a/Classes/Domain/NodeCreation/NodeCreationService.php +++ b/Classes/Domain/NodeCreation/NodeCreationService.php @@ -8,20 +8,13 @@ use Flowpack\NodeTemplates\Domain\Template\Template; use Flowpack\NodeTemplates\Domain\Template\Templates; use Neos\ContentRepository\Domain\Model\NodeInterface; +use Neos\ContentRepository\Domain\Service\Context; use Neos\ContentRepository\Domain\Service\NodeTypeManager; -use Neos\ContentRepository\Exception\NodeConstraintException; use Neos\Flow\Annotations as Flow; -use Neos\Neos\Service\NodeOperations; use Neos\Neos\Utility\NodeUriPathSegmentGenerator; class NodeCreationService { - /** - * @var NodeOperations - * @Flow\Inject - */ - protected $nodeOperations; - /** * @var NodeTypeManager * @Flow\Inject @@ -34,106 +27,138 @@ class NodeCreationService */ protected $nodeUriPathSegmentGenerator; + private Context $subgraph; + + public function __construct(Context $subgraph) + { + $this->subgraph = $subgraph; + } + /** * Applies the root template and its descending configured child node templates on the given node. * @throws \InvalidArgumentException */ - public function apply(RootTemplate $template, NodeInterface $node, CaughtExceptions $caughtExceptions): void + public function apply(RootTemplate $template, NodeInterface $node, CaughtExceptions $caughtExceptions): NodeMutators { $nodeType = $node->getNodeType(); - $propertiesAndReferences = PropertiesAndReferences::createFromArrayAndTypeDeclarations($template->getProperties(), $nodeType); - - // set properties - foreach ($propertiesAndReferences->requireValidProperties($nodeType, $caughtExceptions) as $key => $value) { - $node->setProperty($key, $value); - } - // set references - foreach ($propertiesAndReferences->requireValidReferences($nodeType, $node->getContext(), $caughtExceptions) as $key => $value) { - $node->setProperty($key, $value); - } + $propertiesAndReferences = PropertiesAndReferences::createFromArrayAndTypeDeclarations($template->getProperties(), $nodeType); - $this->ensureNodeHasUriPathSegment($node, $template); - $this->applyTemplateRecursively($template->getChildNodes(), $node, $caughtExceptions); + $properties = array_merge( + $propertiesAndReferences->requireValidProperties($nodeType, $caughtExceptions), + $propertiesAndReferences->requireValidReferences($nodeType, $this->subgraph, $caughtExceptions) + ); + + $nodeMutators = NodeMutators::create( + NodeMutator::setProperties($properties), + $this->ensureNodeHasUriPathSegment($template), + NodeMutator::isolated( + $this->applyTemplateRecursively( + $template->getChildNodes(), + new ToBeCreatedNode($nodeType), + $caughtExceptions + ) + ) + ); + + return $nodeMutators; } - private function applyTemplateRecursively(Templates $templates, NodeInterface $parentNode, CaughtExceptions $caughtExceptions): void + private function applyTemplateRecursively(Templates $templates, ToBeCreatedNode $parentNode, CaughtExceptions $caughtExceptions): NodeMutators { + $nodeMutators = NodeMutators::empty(); + // `hasAutoCreatedChildNode` actually has a bug; it looks up the NodeName parameter against the raw configuration instead of the transliterated NodeName // https://github.com/neos/neos-ui/issues/3527 $parentNodesAutoCreatedChildNodes = $parentNode->getNodeType()->getAutoCreatedChildNodes(); foreach ($templates as $template) { if ($template->getName() && isset($parentNodesAutoCreatedChildNodes[$template->getName()->__toString()])) { - $node = $parentNode->getNode($template->getName()->__toString()); if ($template->getType() !== null) { $caughtExceptions->add( CaughtException::fromException(new \RuntimeException(sprintf('Template cant mutate type of auto created child nodes. Got: "%s"', $template->getType()->getValue()), 1685999829307)) ); // we continue processing the node } - } else { - if ($template->getType() === null) { - $caughtExceptions->add( - CaughtException::fromException(new \RuntimeException(sprintf('Template requires type to be set for non auto created child nodes.'), 1685999829307)) - ); - continue; - } - if (!$this->nodeTypeManager->hasNodeType($template->getType()->getValue())) { - $caughtExceptions->add( - CaughtException::fromException(new \RuntimeException(sprintf('Template requires type to be a valid NodeType. Got: "%s".', $template->getType()->getValue()), 1685999795564)) - ); - continue; - } - $nodeType = $this->nodeTypeManager->getNodeType($template->getType()->getValue()); + $nodeType = $parentNodesAutoCreatedChildNodes[$template->getName()->__toString()]; + $propertiesAndReferences = PropertiesAndReferences::createFromArrayAndTypeDeclarations($template->getProperties(), $nodeType); + + $properties = array_merge( + $propertiesAndReferences->requireValidProperties($nodeType, $caughtExceptions), + $propertiesAndReferences->requireValidReferences($nodeType, $this->subgraph, $caughtExceptions) + ); + + $nodeMutators = $nodeMutators->withNodeMutators( + NodeMutator::isolated( + NodeMutators::create( + NodeMutator::selectChildNode($template->getName()), + NodeMutator::setProperties($properties) + )->merge($this->applyTemplateRecursively( + $template->getChildNodes(), + new ToBeCreatedNode($nodeType), + $caughtExceptions + )) + ) + ); + + continue; - if ($nodeType->isAbstract()) { - $caughtExceptions->add( - CaughtException::fromException(new \RuntimeException(sprintf('Template requires type to be a non abstract NodeType. Got: "%s".', $template->getType()->getValue()), 1686417628976)) - ); - continue; - } + } + if ($template->getType() === null) { + $caughtExceptions->add( + CaughtException::fromException(new \RuntimeException(sprintf('Template requires type to be set for non auto created child nodes.'), 1685999829307)) + ); + continue; + } + if (!$this->nodeTypeManager->hasNodeType($template->getType()->getValue())) { + $caughtExceptions->add( + CaughtException::fromException(new \RuntimeException(sprintf('Template requires type to be a valid NodeType. Got: "%s".', $template->getType()->getValue()), 1685999795564)) + ); + continue; + } - if (!$parentNode->getNodeType()->allowsChildNodeType($nodeType)) { - $caughtExceptions->add( - CaughtException::fromException(new \RuntimeException(sprintf('Node type "%s" is not allowed for child nodes of type %s', $template->getType()->getValue(), $parentNode->getNodeType()->getName()), 1686417627173)) - ); - continue; - } + $nodeType = $this->nodeTypeManager->getNodeType($template->getType()->getValue()); - // todo maybe check also explicitly for allowsGrandchildNodeType (we do this currently like below) - try { - $node = $this->nodeOperations->create( - $parentNode, - [ - 'nodeType' => $template->getType()->getValue(), - 'nodeName' => $template->getName() ? $template->getName()->__toString() : null - ], - 'into' - ); - } catch (NodeConstraintException $nodeConstraintException) { - $caughtExceptions->add( - CaughtException::fromException($nodeConstraintException) - ); - continue; // try the next childNode - } + if ($nodeType->isAbstract()) { + $caughtExceptions->add( + CaughtException::fromException(new \RuntimeException(sprintf('Template requires type to be a non abstract NodeType. Got: "%s".', $template->getType()->getValue()), 1686417628976)) + ); + continue; } - $nodeType = $node->getNodeType(); - $propertiesAndReferences = PropertiesAndReferences::createFromArrayAndTypeDeclarations($template->getProperties(), $nodeType); - // set properties - foreach ($propertiesAndReferences->requireValidProperties($nodeType, $caughtExceptions) as $key => $value) { - $node->setProperty($key, $value); + if (!$parentNode->getNodeType()->allowsChildNodeType($nodeType)) { + $caughtExceptions->add( + CaughtException::fromException(new \RuntimeException(sprintf('Node type "%s" is not allowed for child nodes of type %s', $template->getType()->getValue(), $parentNode->getNodeType()->getName()), 1686417627173)) + ); + continue; } - // set references - foreach ($propertiesAndReferences->requireValidReferences($nodeType, $node->getContext(), $caughtExceptions) as $key => $value) { - $node->setProperty($key, $value); - } + // todo maybe check also explicitly for allowsGrandchildNodeType (we do this currently like below) + + $propertiesAndReferences = PropertiesAndReferences::createFromArrayAndTypeDeclarations($template->getProperties(), $nodeType); + + $properties = array_merge( + $propertiesAndReferences->requireValidProperties($nodeType, $caughtExceptions), + $propertiesAndReferences->requireValidReferences($nodeType, $this->subgraph, $caughtExceptions) + ); + + $nodeMutators = $nodeMutators->withNodeMutators( + NodeMutator::isolated( + NodeMutators::create( + NodeMutator::createIntoAndSelectNode($template->getType(), $template->getName()), + NodeMutator::setProperties($properties), + $this->ensureNodeHasUriPathSegment($template) + )->merge($this->applyTemplateRecursively( + $template->getChildNodes(), + new ToBeCreatedNode($nodeType), + $caughtExceptions + )) + ) + ); - $this->ensureNodeHasUriPathSegment($node, $template); - $this->applyTemplateRecursively($template->getChildNodes(), $node, $caughtExceptions); } + + return $nodeMutators; } /** @@ -142,15 +167,17 @@ private function applyTemplateRecursively(Templates $templates, NodeInterface $p * * @param Template|RootTemplate $template */ - private function ensureNodeHasUriPathSegment(NodeInterface $node, $template) + private function ensureNodeHasUriPathSegment($template): NodeMutator { - if (!$node->getNodeType()->isOfType('Neos.Neos:Document')) { - return; - } $properties = $template->getProperties(); - if (isset($properties['uriPathSegment'])) { - return; - } - $node->setProperty('uriPathSegment', $this->nodeUriPathSegmentGenerator->generateUriPathSegment($node, $properties['title'] ?? null)); + return NodeMutator::bind(function (NodeInterface $previousNode) use ($properties) { + if (!$previousNode->getNodeType()->isOfType('Neos.Neos:Document')) { + return; + } + if (isset($properties['uriPathSegment'])) { + return; + } + $previousNode->setProperty('uriPathSegment', $this->nodeUriPathSegmentGenerator->generateUriPathSegment($previousNode, $properties['title'] ?? null)); + }); } } diff --git a/Classes/Domain/NodeCreation/NodeMutator.php b/Classes/Domain/NodeCreation/NodeMutator.php new file mode 100644 index 0000000..f2acc0f --- /dev/null +++ b/Classes/Domain/NodeCreation/NodeMutator.php @@ -0,0 +1,108 @@ +mutator = $mutator; + } + + /** + * Queues to execute this mutator on the current node + * + * @param \Closure(NodeInterface $currentNode): ?NodeInterface $mutator + */ + public static function bind(\Closure $mutator): self + { + return new self($mutator); + } + + /** + * Queues to execute the {@see NodeMutators} on the current node but the operations wont change the current node. + */ + public static function isolated(NodeMutators $nodeMutators): self + { + return new self(function (NodeInterface $currentNode) use($nodeMutators) { + $nodeMutators->apply($currentNode); + }); + } + + /** + * Queues to select a child node of the current node + */ + public static function selectChildNode(NodeName $nodeName): self + { + return new self(function (NodeInterface $currentNode) use($nodeName) { + $nextNode = $currentNode->getNode($nodeName->__toString()); + if (!$nextNode instanceof NodeInterface) { + throw new \RuntimeException(sprintf('Could not select childNode %s from %s', $nodeName->__toString(), $currentNode)); + } + return $nextNode; + }); + } + + /** + * Queues to create a new node into the current node and select it + */ + public static function createIntoAndSelectNode(NodeTypeName $nodeTypeName, ?NodeName $nodeName): self + { + return new static(function (NodeInterface $currentNode) use($nodeTypeName, $nodeName) { + $nodeOperations = Bootstrap::$staticObjectManager->get(NodeOperations::class); // hack + return $nodeOperations->create( + $currentNode, + [ + 'nodeType' => $nodeTypeName->getValue(), + 'nodeName' => $nodeName ? $nodeName->__toString() : null + ], + 'into' + ); + }); + } + + /** + * Queues to set properties on the current node + */ + public static function setProperties(array $properties): self + { + return new static(function (NodeInterface $currentNode) use ($properties) { + foreach ($properties as $key => $value) { + $currentNode->setProperty($key, $value); + } + }); + } + + /** + * Applies the operations. The $currentNode being the current node for all operations + * It will return a new selected/created node or the current node in case only properties were set + */ + public function apply(NodeInterface $currentNode): NodeInterface + { + return ($this->mutator)($currentNode) ?? $currentNode; + } +} diff --git a/Classes/Domain/NodeCreation/NodeMutators.php b/Classes/Domain/NodeCreation/NodeMutators.php new file mode 100644 index 0000000..9b301b9 --- /dev/null +++ b/Classes/Domain/NodeCreation/NodeMutators.php @@ -0,0 +1,49 @@ +items = $items; + } + + public static function create(NodeMutator ...$items): self + { + return new self(...$items); + } + + public static function empty(): self + { + return new self(); + } + + public function withNodeMutators(NodeMutator ...$items): self + { + return new self(...$this->items, ...$items); + } + + public function apply(NodeInterface $node): void + { + foreach ($this->items as $mutator) { + $node = $mutator->apply($node); + } + } + + public function merge(self $other): self + { + return new self(...$this->items, ...$other->items); + } +} diff --git a/Classes/Domain/NodeCreation/ToBeCreatedNode.php b/Classes/Domain/NodeCreation/ToBeCreatedNode.php new file mode 100644 index 0000000..dfc9cda --- /dev/null +++ b/Classes/Domain/NodeCreation/ToBeCreatedNode.php @@ -0,0 +1,25 @@ +nodeType = $nodeType; + } + + public function getNodeType(): NodeType + { + return $this->nodeType; + } +} diff --git a/Classes/Domain/TemplateNodeCreationHandler.php b/Classes/Domain/TemplateNodeCreationHandler.php index 55f1573..44bae02 100644 --- a/Classes/Domain/TemplateNodeCreationHandler.php +++ b/Classes/Domain/TemplateNodeCreationHandler.php @@ -20,12 +20,6 @@ class TemplateNodeCreationHandler implements NodeCreationHandlerInterface */ protected $templateConfigurationProcessor; - /** - * @var NodeCreationService - * @Flow\Inject - */ - protected $nodeCreationService; - /** * @var ExceptionHandler * @Flow\Inject @@ -56,7 +50,8 @@ public function handle(NodeInterface $node, array $data): void $template = $this->templateConfigurationProcessor->processTemplateConfiguration($templateConfiguration, $evaluationContext, $caughtExceptions); $this->exceptionHandler->handleAfterTemplateConfigurationProcessing($caughtExceptions, $node); - $this->nodeCreationService->apply($template, $node, $caughtExceptions); + $nodeMutators = (new NodeCreationService($node->getContext()))->apply($template, $node, $caughtExceptions); + $nodeMutators->apply($node); $this->exceptionHandler->handleAfterNodeCreation($caughtExceptions, $node); } catch (TemplateNotCreatedException|TemplatePartiallyCreatedException $templateCreationException) { } From 5bb98767da099fcf789b2137a36dbe78e13df159 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Tue, 13 Jun 2023 10:29:48 +0200 Subject: [PATCH 04/24] FEATURE: `flow nodeTemplate:validate` validate templates standalone --- .../Command/NodeTemplateCommandController.php | 86 ++++++++++++++++++- .../NodeCreation/NodeCreationService.php | 2 +- .../Domain/TemplateNodeCreationHandler.php | 3 +- 3 files changed, 85 insertions(+), 6 deletions(-) diff --git a/Classes/Application/Command/NodeTemplateCommandController.php b/Classes/Application/Command/NodeTemplateCommandController.php index 4cefd74..371986f 100644 --- a/Classes/Application/Command/NodeTemplateCommandController.php +++ b/Classes/Application/Command/NodeTemplateCommandController.php @@ -4,18 +4,23 @@ namespace Flowpack\NodeTemplates\Application\Command; +use Flowpack\NodeTemplates\Domain\ExceptionHandling\CaughtExceptions; +use Flowpack\NodeTemplates\Domain\NodeCreation\NodeCreationService; +use Flowpack\NodeTemplates\Domain\NodeCreation\ToBeCreatedNode; use Flowpack\NodeTemplates\Domain\NodeTemplateDumper\NodeTemplateDumper; +use Flowpack\NodeTemplates\Domain\TemplateConfiguration\TemplateConfigurationProcessor; +use Neos\ContentRepository\Domain\Service\ContextFactoryInterface; +use Neos\ContentRepository\Domain\Service\NodeTypeManager; 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 + * @var ContextFactoryInterface */ - protected $contentContextFactory; + protected $contextFactory; /** * @Flow\Inject @@ -23,6 +28,18 @@ class NodeTemplateCommandController extends CommandController */ protected $nodeTemplateDumper; + /** + * @Flow\Inject + * @var TemplateConfigurationProcessor + */ + protected $templateConfigurationProcessor; + + /** + * @Flow\Inject + * @var NodeTypeManager + */ + protected $nodeTypeManager; + /** * Dump the node tree structure into a NodeTemplate YAML structure. * References to Nodes and non-primitive property values are commented out in the YAML. @@ -33,7 +50,7 @@ class NodeTemplateCommandController extends CommandController */ public function createFromNodeSubtreeCommand(string $startingNodeId, string $workspaceName = 'live'): void { - $subgraph = $this->contentContextFactory->create([ + $subgraph = $this->contextFactory->create([ 'workspaceName' => $workspaceName ]); $node = $subgraph->getNodeByIdentifier($startingNodeId); @@ -42,4 +59,65 @@ public function createFromNodeSubtreeCommand(string $startingNodeId, string $wor } echo $this->nodeTemplateDumper->createNodeTemplateYamlDumpFromSubtree($node); } + + /** + * Checks if all configured NodeTemplates are valid. E.g no syntax errors in EEL expressions, + * that properties exist on the node type and their types match and other checks. + * + * We process and build all configured NodeType templates. No nodes will be created in the Content Repository. + * + */ + public function validateCommand(): void + { + $templatesChecked = 0; + /** @var array $nodeTypeNamesWithTheirTemplateExceptions */ + $nodeTypeNamesWithTheirTemplateExceptions = []; + + foreach ($this->nodeTypeManager->getNodeTypes(false) as $nodeType) { + $templateConfiguration = $nodeType->getOptions()['template'] ?? null; + if (!$templateConfiguration) { + continue; + } + $caughtExceptions = CaughtExceptions::create(); + + $subgraph = $this->contextFactory->create(); + + $template = $this->templateConfigurationProcessor->processTemplateConfiguration( + $templateConfiguration, + [ + 'data' => [], + 'triggeringNode' => $subgraph->getRootNode(), + ], + $caughtExceptions + ); + + $nodeCreation = new NodeCreationService($subgraph); + $nodeCreation->apply($template, new ToBeCreatedNode($nodeType), $caughtExceptions); + + + if ($caughtExceptions->hasExceptions()) { + $nodeTypeNamesWithTheirTemplateExceptions[$nodeType->getName()] = $caughtExceptions; + } + $templatesChecked++; + } + + if (empty($nodeTypeNamesWithTheirTemplateExceptions)) { + $this->outputFormatted(sprintf('%d NodeType templates validated.', $templatesChecked)); + return; + } + + $possiblyFaultyTemplates = count($nodeTypeNamesWithTheirTemplateExceptions); + $this->outputFormatted(sprintf('%d of %d NodeType template validated. %d could not be build standalone.', $templatesChecked - $possiblyFaultyTemplates, $templatesChecked, $possiblyFaultyTemplates)); + $this->outputFormatted('This might not be a problem, if they depend on certain data from the node-creation dialog.'); + + $this->outputLine(); + + foreach ($nodeTypeNamesWithTheirTemplateExceptions as $nodeTypeName => $caughtExceptions) { + $this->outputLine($nodeTypeName); + foreach ($caughtExceptions as $caughtException) { + $this->outputFormatted($caughtException->toMessage(), [], 4); + $this->outputLine(); + } + } + } } diff --git a/Classes/Domain/NodeCreation/NodeCreationService.php b/Classes/Domain/NodeCreation/NodeCreationService.php index 54379cd..e166727 100644 --- a/Classes/Domain/NodeCreation/NodeCreationService.php +++ b/Classes/Domain/NodeCreation/NodeCreationService.php @@ -38,7 +38,7 @@ public function __construct(Context $subgraph) * Applies the root template and its descending configured child node templates on the given node. * @throws \InvalidArgumentException */ - public function apply(RootTemplate $template, NodeInterface $node, CaughtExceptions $caughtExceptions): NodeMutators + public function apply(RootTemplate $template, ToBeCreatedNode $node, CaughtExceptions $caughtExceptions): NodeMutators { $nodeType = $node->getNodeType(); diff --git a/Classes/Domain/TemplateNodeCreationHandler.php b/Classes/Domain/TemplateNodeCreationHandler.php index 44bae02..ccd07ad 100644 --- a/Classes/Domain/TemplateNodeCreationHandler.php +++ b/Classes/Domain/TemplateNodeCreationHandler.php @@ -7,6 +7,7 @@ use Flowpack\NodeTemplates\Domain\ExceptionHandling\TemplateNotCreatedException; use Flowpack\NodeTemplates\Domain\ExceptionHandling\TemplatePartiallyCreatedException; use Flowpack\NodeTemplates\Domain\NodeCreation\NodeCreationService; +use Flowpack\NodeTemplates\Domain\NodeCreation\ToBeCreatedNode; use Flowpack\NodeTemplates\Domain\TemplateConfiguration\TemplateConfigurationProcessor; use Neos\ContentRepository\Domain\Model\NodeInterface; use Neos\Flow\Annotations as Flow; @@ -50,7 +51,7 @@ public function handle(NodeInterface $node, array $data): void $template = $this->templateConfigurationProcessor->processTemplateConfiguration($templateConfiguration, $evaluationContext, $caughtExceptions); $this->exceptionHandler->handleAfterTemplateConfigurationProcessing($caughtExceptions, $node); - $nodeMutators = (new NodeCreationService($node->getContext()))->apply($template, $node, $caughtExceptions); + $nodeMutators = (new NodeCreationService($node->getContext()))->apply($template, new ToBeCreatedNode($node->getNodeType()), $caughtExceptions); $nodeMutators->apply($node); $this->exceptionHandler->handleAfterNodeCreation($caughtExceptions, $node); } catch (TemplateNotCreatedException|TemplatePartiallyCreatedException $templateCreationException) { From f72ac0b3d976121b1e414ef736085e28b038148f Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Wed, 14 Jun 2023 12:32:27 +0200 Subject: [PATCH 05/24] BUGFIX: Use property mapping --- .../NodeCreation/NodeCreationService.php | 43 ++++++++++++++++++- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/Classes/Domain/NodeCreation/NodeCreationService.php b/Classes/Domain/NodeCreation/NodeCreationService.php index 80ce510..235dbea 100644 --- a/Classes/Domain/NodeCreation/NodeCreationService.php +++ b/Classes/Domain/NodeCreation/NodeCreationService.php @@ -8,11 +8,15 @@ use Flowpack\NodeTemplates\Domain\Template\Template; use Flowpack\NodeTemplates\Domain\Template\Templates; use Neos\ContentRepository\Domain\Model\NodeInterface; +use Neos\ContentRepository\Domain\Model\NodeType; use Neos\ContentRepository\Domain\Service\NodeTypeManager; use Neos\ContentRepository\Exception\NodeConstraintException; use Neos\Flow\Annotations as Flow; +use Neos\Flow\Property\PropertyMapper; +use Neos\Flow\Property\TypeConverter\PersistentObjectConverter; use Neos\Neos\Service\NodeOperations; use Neos\Neos\Utility\NodeUriPathSegmentGenerator; +use Neos\Utility\TypeHandling; class NodeCreationService { @@ -34,6 +38,12 @@ class NodeCreationService */ protected $nodeUriPathSegmentGenerator; + /** + * @Flow\Inject + * @var PropertyMapper + */ + protected $propertyMapper; + /** * Applies the root template and its descending configured child node templates on the given node. * @throws \InvalidArgumentException @@ -41,7 +51,7 @@ class NodeCreationService public function apply(RootTemplate $template, NodeInterface $node, CaughtExceptions $caughtExceptions): void { $nodeType = $node->getNodeType(); - $propertiesAndReferences = PropertiesAndReferences::createFromArrayAndTypeDeclarations($template->getProperties(), $nodeType); + $propertiesAndReferences = PropertiesAndReferences::createFromArrayAndTypeDeclarations($this->convertProperties($nodeType, $template->getProperties()), $nodeType); // set properties foreach ($propertiesAndReferences->requireValidProperties($nodeType, $caughtExceptions) as $key => $value) { @@ -101,7 +111,7 @@ private function applyTemplateRecursively(Templates $templates, NodeInterface $p } } $nodeType = $node->getNodeType(); - $propertiesAndReferences = PropertiesAndReferences::createFromArrayAndTypeDeclarations($template->getProperties(), $nodeType); + $propertiesAndReferences = PropertiesAndReferences::createFromArrayAndTypeDeclarations($this->convertProperties($nodeType, $template->getProperties()), $nodeType); // set properties foreach ($propertiesAndReferences->requireValidProperties($nodeType, $caughtExceptions) as $key => $value) { @@ -135,4 +145,33 @@ private function ensureNodeHasUriPathSegment(NodeInterface $node, $template) } $node->setProperty('uriPathSegment', $this->nodeUriPathSegmentGenerator->generateUriPathSegment($node, $properties['title'] ?? null)); } + + private function convertProperties(NodeType $nodeType, array $properties): array + { + $propertyMappingConfiguration = $this->propertyMapper->buildPropertyMappingConfiguration(); + $propertyMappingConfiguration->forProperty('*')->allowAllProperties(); + $propertyMappingConfiguration->setTypeConverterOption(PersistentObjectConverter::class, PersistentObjectConverter::CONFIGURATION_OVERRIDE_TARGET_TYPE_ALLOWED, true); + + foreach ($nodeType->getConfiguration('properties') as $propertyName => $propertyConfiguration) { + if ( + !isset($propertyConfiguration['ui']['showInCreationDialog']) + || $propertyConfiguration['ui']['showInCreationDialog'] !== true + ) { + continue; + } + if (!isset($properties[$propertyName])) { + continue; + } + $propertyType = TypeHandling::normalizeType($propertyConfiguration['type'] ?? 'string'); + $propertyValue = $properties[$propertyName]; + if ($propertyType === 'references' || $propertyType === 'reference') { + continue; + } + if ($propertyType === TypeHandling::getTypeForValue($propertyValue)) { + continue; + } + $properties[$propertyName] = $this->propertyMapper->convert($propertyValue, $propertyType, $propertyMappingConfiguration); + } + return $properties; + } } From 32fc04b912b2d705824c5ae848206a06c1befee9 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Thu, 15 Jun 2023 09:02:10 +0200 Subject: [PATCH 06/24] BUGFIX: Strict PropertyMapping for class types and array of class --- .../NodeCreation/NodeCreationService.php | 46 ++++++----- Classes/Domain/NodeCreation/PropertyType.php | 13 +++ .../NodeTypes.ResolvablePropertyValues.yaml | 23 ++++++ .../NodeTypes.UnresolvablePropertyValues.yaml | 27 +++++++ .../ResolvablePropertyValues.nodes.json | 26 ++++++ .../ResolvablePropertyValues.template.json | 15 ++++ .../Fixtures/ResolvablePropertyValues.yaml | 8 ++ .../UnresolvablePropertyValues.messages.json | 26 ++++++ .../UnresolvablePropertyValues.nodes.json | 1 + .../UnresolvablePropertyValues.template.json | 16 ++++ .../Fixtures/UnresolvablePropertyValues.yaml | 3 + .../Functional/JsonSerializeNodeTreeTrait.php | 4 +- Tests/Functional/NodeTemplateTest.php | 74 ++++++++++++++++-- Tests/Functional/image.png | Bin 0 -> 70534 bytes 14 files changed, 255 insertions(+), 27 deletions(-) create mode 100644 Configuration/Testing/NodeTypes.ResolvablePropertyValues.yaml create mode 100644 Configuration/Testing/NodeTypes.UnresolvablePropertyValues.yaml create mode 100644 Tests/Functional/Fixtures/ResolvablePropertyValues.nodes.json create mode 100644 Tests/Functional/Fixtures/ResolvablePropertyValues.template.json create mode 100644 Tests/Functional/Fixtures/ResolvablePropertyValues.yaml create mode 100644 Tests/Functional/Fixtures/UnresolvablePropertyValues.messages.json create mode 100644 Tests/Functional/Fixtures/UnresolvablePropertyValues.nodes.json create mode 100644 Tests/Functional/Fixtures/UnresolvablePropertyValues.template.json create mode 100644 Tests/Functional/Fixtures/UnresolvablePropertyValues.yaml create mode 100644 Tests/Functional/image.png diff --git a/Classes/Domain/NodeCreation/NodeCreationService.php b/Classes/Domain/NodeCreation/NodeCreationService.php index 235dbea..4af0d5d 100644 --- a/Classes/Domain/NodeCreation/NodeCreationService.php +++ b/Classes/Domain/NodeCreation/NodeCreationService.php @@ -13,10 +13,10 @@ use Neos\ContentRepository\Exception\NodeConstraintException; use Neos\Flow\Annotations as Flow; use Neos\Flow\Property\PropertyMapper; -use Neos\Flow\Property\TypeConverter\PersistentObjectConverter; +use Neos\Flow\Property\PropertyMappingConfiguration; use Neos\Neos\Service\NodeOperations; use Neos\Neos\Utility\NodeUriPathSegmentGenerator; -use Neos\Utility\TypeHandling; +use Neos\Flow\Property\Exception as PropertyWasNotMappedException; class NodeCreationService { @@ -51,7 +51,7 @@ class NodeCreationService public function apply(RootTemplate $template, NodeInterface $node, CaughtExceptions $caughtExceptions): void { $nodeType = $node->getNodeType(); - $propertiesAndReferences = PropertiesAndReferences::createFromArrayAndTypeDeclarations($this->convertProperties($nodeType, $template->getProperties()), $nodeType); + $propertiesAndReferences = PropertiesAndReferences::createFromArrayAndTypeDeclarations($this->convertProperties($nodeType, $template->getProperties(), $caughtExceptions), $nodeType); // set properties foreach ($propertiesAndReferences->requireValidProperties($nodeType, $caughtExceptions) as $key => $value) { @@ -111,7 +111,7 @@ private function applyTemplateRecursively(Templates $templates, NodeInterface $p } } $nodeType = $node->getNodeType(); - $propertiesAndReferences = PropertiesAndReferences::createFromArrayAndTypeDeclarations($this->convertProperties($nodeType, $template->getProperties()), $nodeType); + $propertiesAndReferences = PropertiesAndReferences::createFromArrayAndTypeDeclarations($this->convertProperties($nodeType, $template->getProperties(), $caughtExceptions), $nodeType); // set properties foreach ($propertiesAndReferences->requireValidProperties($nodeType, $caughtExceptions) as $key => $value) { @@ -146,31 +146,39 @@ private function ensureNodeHasUriPathSegment(NodeInterface $node, $template) $node->setProperty('uriPathSegment', $this->nodeUriPathSegmentGenerator->generateUriPathSegment($node, $properties['title'] ?? null)); } - private function convertProperties(NodeType $nodeType, array $properties): array + private function convertProperties(NodeType $nodeType, array $properties, CaughtExceptions $caughtExceptions): array { - $propertyMappingConfiguration = $this->propertyMapper->buildPropertyMappingConfiguration(); - $propertyMappingConfiguration->forProperty('*')->allowAllProperties(); - $propertyMappingConfiguration->setTypeConverterOption(PersistentObjectConverter::class, PersistentObjectConverter::CONFIGURATION_OVERRIDE_TARGET_TYPE_ALLOWED, true); - + // TODO combine with PropertiesAndReferences::requireValidProperties foreach ($nodeType->getConfiguration('properties') as $propertyName => $propertyConfiguration) { - if ( - !isset($propertyConfiguration['ui']['showInCreationDialog']) - || $propertyConfiguration['ui']['showInCreationDialog'] !== true - ) { - continue; - } if (!isset($properties[$propertyName])) { continue; } - $propertyType = TypeHandling::normalizeType($propertyConfiguration['type'] ?? 'string'); - $propertyValue = $properties[$propertyName]; + $propertyType = $nodeType->getPropertyType($propertyName); if ($propertyType === 'references' || $propertyType === 'reference') { continue; } - if ($propertyType === TypeHandling::getTypeForValue($propertyValue)) { + $propertyType = PropertyType::fromPropertyOfNodeType($propertyName, $nodeType); + $propertyValue = $properties[$propertyName]; + if (!$propertyType->isClass() + && !($propertyType->isArrayOf() && $propertyType->getArrayOfType()->isClass())) { + // property mapping only for class types or array of classes! continue; } - $properties[$propertyName] = $this->propertyMapper->convert($propertyValue, $propertyType, $propertyMappingConfiguration); + try { + $propertyMappingConfiguration = new PropertyMappingConfiguration(); + $propertyMappingConfiguration->allowAllProperties(); + + $properties[$propertyName] = $this->propertyMapper->convert($propertyValue, $propertyType->getValue(), $propertyMappingConfiguration); + $messages = $this->propertyMapper->getMessages(); + if ($messages->hasErrors()) { + throw new PropertyWasNotMappedException($this->propertyMapper->getMessages()->getFirstError()->getMessage(), 1686779371122); + } + } catch (PropertyWasNotMappedException $exception) { + $caughtExceptions->add(CaughtException::fromException( + $exception + )->withOrigin(sprintf('Property "%s" in NodeType "%s"', $propertyName, $nodeType->getName()))); + unset($properties[$propertyName]); + } } return $properties; } diff --git a/Classes/Domain/NodeCreation/PropertyType.php b/Classes/Domain/NodeCreation/PropertyType.php index fe0e4af..a65ddab 100644 --- a/Classes/Domain/NodeCreation/PropertyType.php +++ b/Classes/Domain/NodeCreation/PropertyType.php @@ -178,6 +178,19 @@ public function isArrayOf(): bool return (bool)preg_match(self::PATTERN_ARRAY_OF, $this->value); } + public function getArrayOfType(): self + { + return $this->arrayOfType; + } + + public function isClass(): bool + { + $className = $this->value[0] != '\\' + ? '\\' . $this->value + : $this->value; + return (class_exists($className) || interface_exists($className)); + } + public function isDate(): bool { return $this->value === self::TYPE_DATE; diff --git a/Configuration/Testing/NodeTypes.ResolvablePropertyValues.yaml b/Configuration/Testing/NodeTypes.ResolvablePropertyValues.yaml new file mode 100644 index 0000000..f9ff312 --- /dev/null +++ b/Configuration/Testing/NodeTypes.ResolvablePropertyValues.yaml @@ -0,0 +1,23 @@ +# Test, that asset ids are correctly resolved to asset objects (via the property mapper) +# Also reference node id's should be correctly resolved +--- + +'Flowpack.NodeTemplates:Content.ResolvablePropertyValues': + superTypes: + 'Neos.Neos:Content': true + properties: + asset: + type: Neos\Media\Domain\Model\Asset + images: + type: array + reference: + type: reference + references: + type: references + options: + template: + properties: + asset: 'c228200e-7472-4290-9936-4454a5b5692a' + reference: 'some-node-id' + references: "${['some-node-id', 'other-node-id', data.realNode]}" + images: "${['c8ae9f9f-dd11-4373-bf42-4bf31ec5bd19']}" diff --git a/Configuration/Testing/NodeTypes.UnresolvablePropertyValues.yaml b/Configuration/Testing/NodeTypes.UnresolvablePropertyValues.yaml new file mode 100644 index 0000000..424bccd --- /dev/null +++ b/Configuration/Testing/NodeTypes.UnresolvablePropertyValues.yaml @@ -0,0 +1,27 @@ +# We make sure that we dont trigger unwanted property mapping, so we wont allow an array in a string field. +--- + +'Flowpack.NodeTemplates:Content.UnresolvablePropertyValues': + superTypes: + 'Neos.Neos:Content': true + ui: + label: UnresolvablePropertyValues + properties: + someString: + type: string + asset: + type: Neos\Media\Domain\Model\Asset + images: + type: array + reference: + type: reference + references: + type: references + options: + template: + properties: + someString: "${['foo']}" + reference: true + references: "${['some-non-existing-node-id']}" + asset: "non-existing" + images: "${['non-existing']}" diff --git a/Tests/Functional/Fixtures/ResolvablePropertyValues.nodes.json b/Tests/Functional/Fixtures/ResolvablePropertyValues.nodes.json new file mode 100644 index 0000000..91dd85a --- /dev/null +++ b/Tests/Functional/Fixtures/ResolvablePropertyValues.nodes.json @@ -0,0 +1,26 @@ +{ + "properties": { + "asset": "object(Neos\\Media\\Domain\\Model\\Asset, c228200e-7472-4290-9936-4454a5b5692a)", + "images": [ + "object(Neos\\Media\\Domain\\Model\\Image, c8ae9f9f-dd11-4373-bf42-4bf31ec5bd19)" + ] + }, + "references": { + "reference": [ + { + "node": "Node(some-node-id, unstructured)" + } + ], + "references": [ + { + "node": "Node(some-node-id, unstructured)" + }, + { + "node": "Node(other-node-id, unstructured)" + }, + { + "node": "Node(real-node-id, unstructured)" + } + ] + } +} diff --git a/Tests/Functional/Fixtures/ResolvablePropertyValues.template.json b/Tests/Functional/Fixtures/ResolvablePropertyValues.template.json new file mode 100644 index 0000000..7b462df --- /dev/null +++ b/Tests/Functional/Fixtures/ResolvablePropertyValues.template.json @@ -0,0 +1,15 @@ +{ + "properties": { + "asset": "c228200e-7472-4290-9936-4454a5b5692a", + "reference": "some-node-id", + "references": [ + "some-node-id", + "other-node-id", + "Node(real-node-id, unstructured)" + ], + "images": [ + "c8ae9f9f-dd11-4373-bf42-4bf31ec5bd19" + ] + }, + "childNodes": [] +} diff --git a/Tests/Functional/Fixtures/ResolvablePropertyValues.yaml b/Tests/Functional/Fixtures/ResolvablePropertyValues.yaml new file mode 100644 index 0000000..63c8393 --- /dev/null +++ b/Tests/Functional/Fixtures/ResolvablePropertyValues.yaml @@ -0,0 +1,8 @@ +'{nodeTypeName}': + options: + template: + 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) diff --git a/Tests/Functional/Fixtures/UnresolvablePropertyValues.messages.json b/Tests/Functional/Fixtures/UnresolvablePropertyValues.messages.json new file mode 100644 index 0000000..7e514a2 --- /dev/null +++ b/Tests/Functional/Fixtures/UnresolvablePropertyValues.messages.json @@ -0,0 +1,26 @@ +[ + { + "message": "Template for \"UnresolvablePropertyValues\" only partially applied. Please check the newly created nodes beneath Node \/sites\/test-site\/homepage\/main\/new-node@live[Flowpack.NodeTemplates:Content.UnresolvablePropertyValues].", + "severity": "ERROR" + }, + { + "message": "Property \"asset\" in NodeType \"Flowpack.NodeTemplates:Content.UnresolvablePropertyValues\" | FlowException(Object of type \"Neos\\Media\\Domain\\Model\\Asset\" with identity \"non-existing\" not found., 1686779371122)", + "severity": "ERROR" + }, + { + "message": "Property \"images\" in NodeType \"Flowpack.NodeTemplates:Content.UnresolvablePropertyValues\" | FlowException(Could not convert target type \"array\", at property path \"0\": No converter found which can be used to convert from \"string\" to \"Neos\\Media\\Domain\\Model\\ImageInterface\"., 1297759968) | TypeConverterException(No converter found which can be used to convert from \"string\" to \"Neos\\Media\\Domain\\Model\\ImageInterface\"., 0)", + "severity": "ERROR" + }, + { + "message": "Property \"someString\" in NodeType \"Flowpack.NodeTemplates:Content.UnresolvablePropertyValues\" | PropertyIgnoredException(Because value `[\"foo\"]` is not assignable to property type \"string\"., 1685958105644)", + "severity": "ERROR" + }, + { + "message": "Reference \"reference\" in NodeType \"Flowpack.NodeTemplates:Content.UnresolvablePropertyValues\" | RuntimeException(Reference could not be set, because node reference(s) true cannot be resolved., 1685958176560)", + "severity": "ERROR" + }, + { + "message": "Reference \"references\" in NodeType \"Flowpack.NodeTemplates:Content.UnresolvablePropertyValues\" | RuntimeException(Reference could not be set, because node reference(s) [\"some-non-existing-node-id\"] cannot be resolved., 1685958176560)", + "severity": "ERROR" + } +] diff --git a/Tests/Functional/Fixtures/UnresolvablePropertyValues.nodes.json b/Tests/Functional/Fixtures/UnresolvablePropertyValues.nodes.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/Tests/Functional/Fixtures/UnresolvablePropertyValues.nodes.json @@ -0,0 +1 @@ +[] diff --git a/Tests/Functional/Fixtures/UnresolvablePropertyValues.template.json b/Tests/Functional/Fixtures/UnresolvablePropertyValues.template.json new file mode 100644 index 0000000..0921e0b --- /dev/null +++ b/Tests/Functional/Fixtures/UnresolvablePropertyValues.template.json @@ -0,0 +1,16 @@ +{ + "properties": { + "someString": [ + "foo" + ], + "reference": true, + "references": [ + "some-non-existing-node-id" + ], + "asset": "non-existing", + "images": [ + "non-existing" + ] + }, + "childNodes": [] +} diff --git a/Tests/Functional/Fixtures/UnresolvablePropertyValues.yaml b/Tests/Functional/Fixtures/UnresolvablePropertyValues.yaml new file mode 100644 index 0000000..f133bb4 --- /dev/null +++ b/Tests/Functional/Fixtures/UnresolvablePropertyValues.yaml @@ -0,0 +1,3 @@ +'{nodeTypeName}': + options: + template: [] diff --git a/Tests/Functional/JsonSerializeNodeTreeTrait.php b/Tests/Functional/JsonSerializeNodeTreeTrait.php index 9b8f023..6cfa74d 100644 --- a/Tests/Functional/JsonSerializeNodeTreeTrait.php +++ b/Tests/Functional/JsonSerializeNodeTreeTrait.php @@ -3,6 +3,7 @@ namespace Flowpack\NodeTemplates\Tests\Functional; use Neos\ContentRepository\Domain\Model\NodeInterface; +use Neos\Utility\ObjectAccess; trait JsonSerializeNodeTreeTrait { @@ -51,7 +52,8 @@ private function serializeValuesInArray(array $array): array $value = $this->serializeValuesInArray($value); } } elseif (is_object($value)) { - $value = sprintf('object(%s)', get_class($value)); + $id = ObjectAccess::getProperty($value, 'Persistence_Object_Identifier', true); + $value = sprintf('object(%s%s)', get_class($value), $id ? (sprintf(', %s', $id)) : ''); } else { continue; } diff --git a/Tests/Functional/NodeTemplateTest.php b/Tests/Functional/NodeTemplateTest.php index dcea19f..3d6fdea 100644 --- a/Tests/Functional/NodeTemplateTest.php +++ b/Tests/Functional/NodeTemplateTest.php @@ -4,10 +4,9 @@ namespace Flowpack\NodeTemplates\Tests\Functional; +use Flowpack\NodeTemplates\Domain\NodeTemplateDumper\NodeTemplateDumper; use Flowpack\NodeTemplates\Domain\Template\RootTemplate; use Flowpack\NodeTemplates\Domain\TemplateConfiguration\TemplateConfigurationProcessor; -use Flowpack\NodeTemplates\Domain\NodeTemplateDumper\NodeTemplateDumper; -use Neos\ContentRepository\Domain\Model\Node; use Neos\ContentRepository\Domain\Model\NodeInterface; use Neos\ContentRepository\Domain\Model\Workspace; use Neos\ContentRepository\Domain\NodeType\NodeTypeName; @@ -15,12 +14,18 @@ use Neos\ContentRepository\Domain\Repository\WorkspaceRepository; use Neos\ContentRepository\Domain\Service\ContextFactoryInterface; use Neos\ContentRepository\Domain\Service\NodeTypeManager; +use Neos\Flow\ResourceManagement\ResourceManager; use Neos\Flow\Tests\FunctionalTestCase; +use Neos\Media\Domain\Model\Asset; +use Neos\Media\Domain\Model\Image; +use Neos\Media\Domain\Repository\AssetRepository; +use Neos\Media\Domain\Repository\ImageRepository; use Neos\Neos\Domain\Model\Site; use Neos\Neos\Domain\Repository\SiteRepository; use Neos\Neos\Ui\Domain\Model\ChangeCollection; use Neos\Neos\Ui\Domain\Model\FeedbackCollection; use Neos\Neos\Ui\TypeConverter\ChangeCollectionConverter; +use Neos\Utility\ObjectAccess; class NodeTemplateTest extends FunctionalTestCase { @@ -33,7 +38,7 @@ class NodeTemplateTest extends FunctionalTestCase private ContextFactoryInterface $contextFactory; - private Node $homePageNode; + private NodeInterface $homePageNode; private NodeTemplateDumper $nodeTemplateDumper; @@ -96,10 +101,10 @@ private function setupContentRepository(): void } /** - * @param Node|NodeInterface $targetNode + * @param NodeInterface $targetNode * @param array $nodeCreationDialogValues */ - private function createNodeInto(Node $targetNode, NodeTypeName $nodeTypeName, array $nodeCreationDialogValues): void + private function createNodeInto(NodeInterface $targetNode, NodeTypeName $nodeTypeName, array $nodeCreationDialogValues): NodeInterface { $targetNodeContextPath = $targetNode->getContextPath(); @@ -121,6 +126,8 @@ private function createNodeInto(Node $targetNode, NodeTypeName $nodeTypeName, ar $changeCollection = (new ChangeCollectionConverter())->convertFrom($changeCollectionSerialized, null); assert($changeCollection instanceof ChangeCollection); $changeCollection->apply(); + + return $targetNode->getNode('new-node'); } @@ -216,6 +223,55 @@ public function testNodeCreationWithDifferentPropertyTypes(): void $this->assertNodeDumpAndTemplateDumpMatchSnapshot('DifferentPropertyTypes', $createdNode); } + /** @test */ + public function resolvablePropertyValues(): void + { + $this->homePageNode->createNode('some-node', null, 'some-node-id'); + $this->homePageNode->createNode('other-node', null, 'other-node-id'); + + $resource = $this->objectManager->get(ResourceManager::class)->importResource(__DIR__ . '/image.png'); + + $asset = new Asset($resource); + ObjectAccess::setProperty($asset, 'Persistence_Object_Identifier', 'c228200e-7472-4290-9936-4454a5b5692a', true); + $this->objectManager->get(AssetRepository::class)->add($asset); + + $resource = $this->objectManager->get(ResourceManager::class)->importResource(__DIR__ . '/image.png'); + + $image = new Image($resource); + ObjectAccess::setProperty($image, 'Persistence_Object_Identifier', 'c8ae9f9f-dd11-4373-bf42-4bf31ec5bd19', true); + $this->objectManager->get(ImageRepository::class)->add($image); + + $this->persistenceManager->persistAll(); + + $createdNode = $this->createNodeInto( + $this->homePageNode->getNode('main'), + NodeTypeName::fromString('Flowpack.NodeTemplates:Content.ResolvablePropertyValues'), + [ + 'realNode' => $this->homePageNode->createNode('real-node', null, 'real-node-id') + ] + ); + + $this->assertLastCreatedTemplateMatchesSnapshot('ResolvablePropertyValues'); + + self::assertSame([], $this->getMessagesOfFeedbackCollection()); + $this->assertNodeDumpAndTemplateDumpMatchSnapshot('ResolvablePropertyValues', $createdNode); + } + + /** @test */ + public function unresolvablePropertyValues(): void + { + $createdNode = $this->createNodeInto( + $this->homePageNode->getNode('main'), + NodeTypeName::fromString('Flowpack.NodeTemplates:Content.UnresolvablePropertyValues'), + [] + ); + + $this->assertLastCreatedTemplateMatchesSnapshot('UnresolvablePropertyValues'); + + $this->assertCaughtExceptionsMatchesSnapshot('UnresolvablePropertyValues'); + $this->assertNodeDumpAndTemplateDumpMatchSnapshot('UnresolvablePropertyValues', $createdNode); + } + /** @test */ public function exceptionsAreCaughtAndPartialTemplateIsBuild(): void { @@ -229,8 +285,7 @@ public function exceptionsAreCaughtAndPartialTemplateIsBuild(): void $createdNode = $targetNode->getChildNodes($toBeCreatedNodeTypeName->getValue())[0]; - $this->assertStringEqualsFileOrCreateSnapshot(__DIR__ . '/Fixtures/WithEvaluationExceptions.messages.json', json_encode($this->getMessagesOfFeedbackCollection(), JSON_PRETTY_PRINT)); - + $this->assertCaughtExceptionsMatchesSnapshot('WithEvaluationExceptions'); $this->assertNodeDumpAndTemplateDumpMatchSnapshot('WithEvaluationExceptions', $createdNode); } @@ -315,6 +370,11 @@ private function assertLastCreatedTemplateMatchesSnapshot(string $snapShotName): $this->assertStringEqualsFileOrCreateSnapshot(__DIR__ . '/Fixtures/' . $snapShotName . '.template.json', json_encode($lastCreatedTemplate, JSON_PRETTY_PRINT)); } + private function assertCaughtExceptionsMatchesSnapshot(string $snapShotName): void + { + $this->assertStringEqualsFileOrCreateSnapshot(__DIR__ . '/Fixtures/' . $snapShotName . '.messages.json', json_encode($this->getMessagesOfFeedbackCollection(), JSON_PRETTY_PRINT)); + } + private function assertNodeDumpAndTemplateDumpMatchSnapshot(string $snapShotName, NodeInterface $node): void { $serializedNodes = $this->jsonSerializeNodeAndDescendents($node); diff --git a/Tests/Functional/image.png b/Tests/Functional/image.png new file mode 100644 index 0000000000000000000000000000000000000000..490c987e39b8417d30f457931be2c50d783c626f GIT binary patch literal 70534 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⪚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�h29l3-N$rf>aIgvXbSKf64_xJ5|ZYcQD8-TqLO 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

+g*Cxr=~~q$ZDScR{zy3{CZ5!IKB9aEZItw={5s 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 Y0cO4JUAMK)aR2}S07*qoM6N<$f*OsFl>h($ literal 0 HcmV?d00001 From 719fa64666d0b4118195619cbc88347737c629d1 Mon Sep 17 00:00:00 2001 From: Marc Henry Schultz <85400359+mhsdesign@users.noreply.github.com> Date: Mon, 19 Jun 2023 11:16:34 +0200 Subject: [PATCH 07/24] BUGFIX: NodeType is not initialized in super rare edge cases (#59) * BUGFIX: NodeType is not initialized in super rare edge cases see https://github.com/neos/neos-development-collection/issues/4333 * Update Classes/Domain/NodeCreation/PropertiesAndReferences.php --------- Co-authored-by: Sebastian Helzle --- Classes/Domain/NodeCreation/PropertiesAndReferences.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Classes/Domain/NodeCreation/PropertiesAndReferences.php b/Classes/Domain/NodeCreation/PropertiesAndReferences.php index fd26a2b..de19bf4 100644 --- a/Classes/Domain/NodeCreation/PropertiesAndReferences.php +++ b/Classes/Domain/NodeCreation/PropertiesAndReferences.php @@ -28,6 +28,8 @@ public static function createFromArrayAndTypeDeclarations(array $propertiesAndRe $references = []; $properties = []; foreach ($propertiesAndReferences as $propertyName => $propertyValue) { + // TODO: remove the next line to initialise the nodeType, once https://github.com/neos/neos-development-collection/issues/4333 is fixed + $nodeType->getFullConfiguration(); $declaration = $nodeType->getPropertyType($propertyName); if ($declaration === 'reference' || $declaration === 'references') { $references[$propertyName] = $propertyValue; From d8cd0b488d68db4f1bfd0e48f37fde9a081cc9c0 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Mon, 19 Jun 2023 11:25:48 +0200 Subject: [PATCH 08/24] TASK: Automated Tests --- .github/workflows/tests.yml | 73 +++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..83ff90a --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,73 @@ +name: Tests + +on: + push: + branches: [ master, '[0-9]+.[0-9]' ] + pull_request: + branches: [ master, '[0-9]+.[0-9]' ] + +jobs: + build: + env: + NEOS_TARGET_VERSION: 7.3 + FLOW_CONTEXT: Testing + FLOW_PATH_ROOT: ../neos-base-distribution + + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + php-versions: ['7.4'] + + steps: + - uses: actions/checkout@v2 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + extensions: mbstring, xml, json, zlib, iconv, intl, pdo_sqlite, mysql + coverage: xdebug #optional + ini-values: opcache.fast_shutdown=0 + + - name: Update Composer + run: | + sudo composer self-update + composer --version + + # Directory permissions for .composer are wrong, so we remove the complete directory + # https://github.com/actions/virtual-environments/issues/824 + - name: Delete .composer directory + run: | + sudo rm -rf ~/.composer + + - name: Cache dependencies + uses: actions/cache@v1 + with: + path: ~/.composer/cache + key: dependencies-composer-${{ hashFiles('composer.json') }} + + - name: Prepare Neos distribution + run: | + git clone https://github.com/neos/neos-base-distribution.git -b ${NEOS_TARGET_VERSION} ${FLOW_PATH_ROOT} + cd ${FLOW_PATH_ROOT} + composer require --no-update --no-interaction flowpack/nodetemplates + + - name: Install distribution + run: | + cd ${FLOW_PATH_ROOT} + composer config --no-plugins allow-plugins.neos/composer-plugin true + composer install --no-interaction --no-progress + rm -rf Packages/Application/Flowpack.NodeTemplates + cp -r ../Flowpack.NodeTemplates Packages/Application/Flowpack.NodeTemplates + + - name: Run Unit tests + run: | + cd ${FLOW_PATH_ROOT} + bin/phpunit --colors -c Build/BuildEssentials/PhpUnit/UnitTests.xml Packages/Application/Flowpack.NodeTemplates/Tests/Unit + + - name: Run Functional tests + run: | + cd ${FLOW_PATH_ROOT} + bin/phpunit --colors -c Build/BuildEssentials/PhpUnit/FunctionalTests.xml Packages/Application/Flowpack.NodeTemplates/Tests/Functional From 847e738205ce14d97cf7673d340ca272ac9415d1 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Mon, 19 Jun 2023 11:57:51 +0200 Subject: [PATCH 09/24] TASK: Use `isArrayOfClass()` instead of exposing `getArrayOfType` --- Classes/Domain/NodeCreation/NodeCreationService.php | 3 +-- Classes/Domain/NodeCreation/PropertyType.php | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Classes/Domain/NodeCreation/NodeCreationService.php b/Classes/Domain/NodeCreation/NodeCreationService.php index bb8a16b..d69a369 100644 --- a/Classes/Domain/NodeCreation/NodeCreationService.php +++ b/Classes/Domain/NodeCreation/NodeCreationService.php @@ -177,8 +177,7 @@ private function convertProperties(NodeType $nodeType, array $properties, Caught } $propertyType = PropertyType::fromPropertyOfNodeType($propertyName, $nodeType); $propertyValue = $properties[$propertyName]; - if (!$propertyType->isClass() - && !($propertyType->isArrayOf() && $propertyType->getArrayOfType()->isClass())) { + if (!$propertyType->isClass() && !$propertyType->isArrayOfClass()) { // property mapping only for class types or array of classes! continue; } diff --git a/Classes/Domain/NodeCreation/PropertyType.php b/Classes/Domain/NodeCreation/PropertyType.php index a65ddab..7def05a 100644 --- a/Classes/Domain/NodeCreation/PropertyType.php +++ b/Classes/Domain/NodeCreation/PropertyType.php @@ -178,9 +178,9 @@ public function isArrayOf(): bool return (bool)preg_match(self::PATTERN_ARRAY_OF, $this->value); } - public function getArrayOfType(): self + public function isArrayOfClass(): bool { - return $this->arrayOfType; + return $this->isArrayOf() && $this->arrayOfType->isClass(); } public function isClass(): bool From 1e42b76453fe62911e55d5d9f9a9179090a9bc30 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Mon, 19 Jun 2023 12:11:32 +0200 Subject: [PATCH 10/24] TASK: Adjust namings --- .../Command/NodeTemplateCommandController.php | 2 +- .../NodeCreation/NodeCreationService.php | 37 +++++++++---------- Classes/Domain/NodeCreation/NodeMutator.php | 8 ++-- ...Mutators.php => NodeMutatorCollection.php} | 4 +- .../Domain/TemplateNodeCreationHandler.php | 2 +- 5 files changed, 26 insertions(+), 27 deletions(-) rename Classes/Domain/NodeCreation/{NodeMutators.php => NodeMutatorCollection.php} (90%) diff --git a/Classes/Application/Command/NodeTemplateCommandController.php b/Classes/Application/Command/NodeTemplateCommandController.php index 371986f..b1792d5 100644 --- a/Classes/Application/Command/NodeTemplateCommandController.php +++ b/Classes/Application/Command/NodeTemplateCommandController.php @@ -92,7 +92,7 @@ public function validateCommand(): void ); $nodeCreation = new NodeCreationService($subgraph); - $nodeCreation->apply($template, new ToBeCreatedNode($nodeType), $caughtExceptions); + $nodeCreation->createMutatorCollection($template, new ToBeCreatedNode($nodeType), $caughtExceptions); if ($caughtExceptions->hasExceptions()) { diff --git a/Classes/Domain/NodeCreation/NodeCreationService.php b/Classes/Domain/NodeCreation/NodeCreationService.php index e166727..84e35ac 100644 --- a/Classes/Domain/NodeCreation/NodeCreationService.php +++ b/Classes/Domain/NodeCreation/NodeCreationService.php @@ -38,7 +38,7 @@ public function __construct(Context $subgraph) * Applies the root template and its descending configured child node templates on the given node. * @throws \InvalidArgumentException */ - public function apply(RootTemplate $template, ToBeCreatedNode $node, CaughtExceptions $caughtExceptions): NodeMutators + public function createMutatorCollection(RootTemplate $template, ToBeCreatedNode $node, CaughtExceptions $caughtExceptions): NodeMutatorCollection { $nodeType = $node->getNodeType(); @@ -49,24 +49,23 @@ public function apply(RootTemplate $template, ToBeCreatedNode $node, CaughtExcep $propertiesAndReferences->requireValidReferences($nodeType, $this->subgraph, $caughtExceptions) ); - $nodeMutators = NodeMutators::create( + $nodeMutators = NodeMutatorCollection::from( NodeMutator::setProperties($properties), - $this->ensureNodeHasUriPathSegment($template), - NodeMutator::isolated( - $this->applyTemplateRecursively( - $template->getChildNodes(), - new ToBeCreatedNode($nodeType), - $caughtExceptions - ) + $this->createMutatorForUriPathSegment($template), + )->merge( + $this->createMutatorCollectionFromTemplate( + $template->getChildNodes(), + new ToBeCreatedNode($nodeType), + $caughtExceptions ) ); return $nodeMutators; } - private function applyTemplateRecursively(Templates $templates, ToBeCreatedNode $parentNode, CaughtExceptions $caughtExceptions): NodeMutators + private function createMutatorCollectionFromTemplate(Templates $templates, ToBeCreatedNode $parentNode, CaughtExceptions $caughtExceptions): NodeMutatorCollection { - $nodeMutators = NodeMutators::empty(); + $nodeMutators = NodeMutatorCollection::empty(); // `hasAutoCreatedChildNode` actually has a bug; it looks up the NodeName parameter against the raw configuration instead of the transliterated NodeName // https://github.com/neos/neos-ui/issues/3527 @@ -90,10 +89,10 @@ private function applyTemplateRecursively(Templates $templates, ToBeCreatedNode $nodeMutators = $nodeMutators->withNodeMutators( NodeMutator::isolated( - NodeMutators::create( + NodeMutatorCollection::from( NodeMutator::selectChildNode($template->getName()), NodeMutator::setProperties($properties) - )->merge($this->applyTemplateRecursively( + )->merge($this->createMutatorCollectionFromTemplate( $template->getChildNodes(), new ToBeCreatedNode($nodeType), $caughtExceptions @@ -144,11 +143,11 @@ private function applyTemplateRecursively(Templates $templates, ToBeCreatedNode $nodeMutators = $nodeMutators->withNodeMutators( NodeMutator::isolated( - NodeMutators::create( - NodeMutator::createIntoAndSelectNode($template->getType(), $template->getName()), + NodeMutatorCollection::from( + NodeMutator::createAndSelectNode($template->getType(), $template->getName()), NodeMutator::setProperties($properties), - $this->ensureNodeHasUriPathSegment($template) - )->merge($this->applyTemplateRecursively( + $this->createMutatorForUriPathSegment($template) + )->merge($this->createMutatorCollectionFromTemplate( $template->getChildNodes(), new ToBeCreatedNode($nodeType), $caughtExceptions @@ -167,10 +166,10 @@ private function applyTemplateRecursively(Templates $templates, ToBeCreatedNode * * @param Template|RootTemplate $template */ - private function ensureNodeHasUriPathSegment($template): NodeMutator + private function createMutatorForUriPathSegment($template): NodeMutator { $properties = $template->getProperties(); - return NodeMutator::bind(function (NodeInterface $previousNode) use ($properties) { + return NodeMutator::unsafeFromClosure(function (NodeInterface $previousNode) use ($properties) { if (!$previousNode->getNodeType()->isOfType('Neos.Neos:Document')) { return; } diff --git a/Classes/Domain/NodeCreation/NodeMutator.php b/Classes/Domain/NodeCreation/NodeMutator.php index f2acc0f..a91144e 100644 --- a/Classes/Domain/NodeCreation/NodeMutator.php +++ b/Classes/Domain/NodeCreation/NodeMutator.php @@ -38,15 +38,15 @@ private function __construct( * * @param \Closure(NodeInterface $currentNode): ?NodeInterface $mutator */ - public static function bind(\Closure $mutator): self + public static function unsafeFromClosure(\Closure $mutator): self { return new self($mutator); } /** - * Queues to execute the {@see NodeMutators} on the current node but the operations wont change the current node. + * Queues to execute the {@see NodeMutatorCollection} on the current node but the operations wont change the current node. */ - public static function isolated(NodeMutators $nodeMutators): self + public static function isolated(NodeMutatorCollection $nodeMutators): self { return new self(function (NodeInterface $currentNode) use($nodeMutators) { $nodeMutators->apply($currentNode); @@ -70,7 +70,7 @@ public static function selectChildNode(NodeName $nodeName): self /** * Queues to create a new node into the current node and select it */ - public static function createIntoAndSelectNode(NodeTypeName $nodeTypeName, ?NodeName $nodeName): self + public static function createAndSelectNode(NodeTypeName $nodeTypeName, ?NodeName $nodeName): self { return new static(function (NodeInterface $currentNode) use($nodeTypeName, $nodeName) { $nodeOperations = Bootstrap::$staticObjectManager->get(NodeOperations::class); // hack diff --git a/Classes/Domain/NodeCreation/NodeMutators.php b/Classes/Domain/NodeCreation/NodeMutatorCollection.php similarity index 90% rename from Classes/Domain/NodeCreation/NodeMutators.php rename to Classes/Domain/NodeCreation/NodeMutatorCollection.php index 9b301b9..bde1182 100644 --- a/Classes/Domain/NodeCreation/NodeMutators.php +++ b/Classes/Domain/NodeCreation/NodeMutatorCollection.php @@ -10,7 +10,7 @@ /** * @Flow\Proxy(false) */ -class NodeMutators +class NodeMutatorCollection { private array $items; @@ -20,7 +20,7 @@ private function __construct( $this->items = $items; } - public static function create(NodeMutator ...$items): self + public static function from(NodeMutator ...$items): self { return new self(...$items); } diff --git a/Classes/Domain/TemplateNodeCreationHandler.php b/Classes/Domain/TemplateNodeCreationHandler.php index ccd07ad..ec3d2d6 100644 --- a/Classes/Domain/TemplateNodeCreationHandler.php +++ b/Classes/Domain/TemplateNodeCreationHandler.php @@ -51,7 +51,7 @@ public function handle(NodeInterface $node, array $data): void $template = $this->templateConfigurationProcessor->processTemplateConfiguration($templateConfiguration, $evaluationContext, $caughtExceptions); $this->exceptionHandler->handleAfterTemplateConfigurationProcessing($caughtExceptions, $node); - $nodeMutators = (new NodeCreationService($node->getContext()))->apply($template, new ToBeCreatedNode($node->getNodeType()), $caughtExceptions); + $nodeMutators = (new NodeCreationService($node->getContext()))->createMutatorCollection($template, new ToBeCreatedNode($node->getNodeType()), $caughtExceptions); $nodeMutators->apply($node); $this->exceptionHandler->handleAfterNodeCreation($caughtExceptions, $node); } catch (TemplateNotCreatedException|TemplatePartiallyCreatedException $templateCreationException) { From 09e4586ec36414a06542128c75d2caf87b9f6785 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Wed, 21 Jun 2023 09:07:04 +0200 Subject: [PATCH 11/24] TASK: Cleanup test --- Configuration/Testing/NodeTypes.yaml | 162 -------- Tests/Functional/AbstractNodeTemplateTest.php | 213 ++++++++++ .../Features/ChildNodes/ChildNodesTest.php | 58 +++ .../NodeTypes.DynamicChildNodes1.yaml | 35 ++ .../NodeTypes.DynamicChildNodes2.yaml | 37 ++ .../NodeTypes.StaticChildNodes.yaml | 25 ++ .../Snapshots/ChildNodes.nodes.json} | 0 .../Snapshots/ChildNodes.template.json} | 0 .../ChildNodes/Snapshots/ChildNodes.yaml} | 0 .../Features/Exceptions/ExceptionsTest.php | 68 +++ .../Exceptions/NodeTypes.OnlyExceptions.yaml | 12 + .../Exceptions/NodeTypes.SomeExceptions.yaml | 19 +- .../Snapshots/OnlyExceptions.messages.json | 10 + .../Snapshots/OnlyExceptions.nodes.json} | 0 .../Snapshots/OnlyExceptions.template.json} | 0 .../Exceptions/Snapshots/OnlyExceptions.yaml} | 0 .../Snapshots/SomeExceptions.messages.json} | 20 +- .../Snapshots/SomeExceptions.nodes.json} | 0 .../Snapshots/SomeExceptions.template.json} | 4 +- .../Exceptions/Snapshots/SomeExceptions.yaml} | 0 .../Features/NodeNames/NodeNamesTest.php | 25 ++ .../NodeNames/NodeTypes.NodeNames.yaml | 2 +- .../NodeNames/Snapshots/NodeNames.nodes.json} | 0 .../Snapshots/NodeNames.template.json} | 0 .../NodeNames/Snapshots/NodeNames.yaml} | 0 Tests/Functional/Features/NodeTypes.yaml | 21 + .../Pages/NodeTypes.DynamicPages.yaml | 51 +++ .../Features/Pages/NodeTypes.StaticPages.yaml | 52 +-- Tests/Functional/Features/Pages/PagesTest.php | 43 ++ .../Pages/Snapshots/Pages.nodes.json} | 0 .../Pages/Snapshots/Pages.yaml} | 0 .../Pages/Snapshots/Pages1.template.json} | 0 .../Pages/Snapshots/Pages2.template.json | 93 +++++ .../Properties/NodeTypes.Properties.yaml | 53 +++ .../Features/Properties/PropertiesTest.php | 27 ++ .../Snapshots/Properties.nodes.json} | 0 .../Snapshots/Properties.template.json} | 0 .../Properties/Snapshots/Properties.yaml} | 2 +- .../NodeTypes.ResolvableProperties.yaml | 2 +- .../NodeTypes.UnresolvableProperties.yaml | 4 +- .../ResolvablePropertiesTest.php | 62 +++ .../ResolvableProperties.nodes.json} | 0 .../ResolvableProperties.template.json} | 0 .../Snapshots/ResolvableProperties.yaml} | 0 .../UnresolvableProperties.messages.json | 26 ++ .../UnresolvableProperties.nodes.json | 1 + .../UnresolvableProperties.template.json} | 0 .../Snapshots/UnresolvableProperties.yaml | 3 + .../ResolvableProperties}/image.png | Bin .../UnresolvablePropertyValues.messages.json | 26 -- Tests/Functional/NodeTemplateTest.php | 390 ------------------ 51 files changed, 885 insertions(+), 661 deletions(-) delete mode 100644 Configuration/Testing/NodeTypes.yaml create mode 100644 Tests/Functional/AbstractNodeTemplateTest.php create mode 100644 Tests/Functional/Features/ChildNodes/ChildNodesTest.php create mode 100644 Tests/Functional/Features/ChildNodes/NodeTypes.DynamicChildNodes1.yaml create mode 100644 Tests/Functional/Features/ChildNodes/NodeTypes.DynamicChildNodes2.yaml create mode 100644 Tests/Functional/Features/ChildNodes/NodeTypes.StaticChildNodes.yaml rename Tests/Functional/{Fixtures/TwoColumnPreset.nodes.json => Features/ChildNodes/Snapshots/ChildNodes.nodes.json} (100%) rename Tests/Functional/{Fixtures/TwoColumnPreset.template.json => Features/ChildNodes/Snapshots/ChildNodes.template.json} (100%) rename Tests/Functional/{Fixtures/TwoColumnPreset.yaml => Features/ChildNodes/Snapshots/ChildNodes.yaml} (100%) create mode 100644 Tests/Functional/Features/Exceptions/ExceptionsTest.php create mode 100644 Tests/Functional/Features/Exceptions/NodeTypes.OnlyExceptions.yaml rename Configuration/Testing/NodeTypes.Malformed.yaml => Tests/Functional/Features/Exceptions/NodeTypes.SomeExceptions.yaml (81%) create mode 100644 Tests/Functional/Features/Exceptions/Snapshots/OnlyExceptions.messages.json rename Tests/Functional/{Fixtures/UnresolvablePropertyValues.nodes.json => Features/Exceptions/Snapshots/OnlyExceptions.nodes.json} (100%) rename Tests/Functional/{Fixtures/WithOneEvaluationException.template.json => Features/Exceptions/Snapshots/OnlyExceptions.template.json} (100%) rename Tests/Functional/{Fixtures/UnresolvablePropertyValues.yaml => Features/Exceptions/Snapshots/OnlyExceptions.yaml} (100%) rename Tests/Functional/{Fixtures/WithEvaluationExceptions.messages.json => Features/Exceptions/Snapshots/SomeExceptions.messages.json} (72%) rename Tests/Functional/{Fixtures/WithEvaluationExceptions.nodes.json => Features/Exceptions/Snapshots/SomeExceptions.nodes.json} (100%) rename Tests/Functional/{Fixtures/WithEvaluationExceptions.template.json => Features/Exceptions/Snapshots/SomeExceptions.template.json} (92%) rename Tests/Functional/{Fixtures/WithEvaluationExceptions.yaml => Features/Exceptions/Snapshots/SomeExceptions.yaml} (100%) create mode 100644 Tests/Functional/Features/NodeNames/NodeNamesTest.php rename Configuration/Testing/NodeTypes.TransliterateNodeName.yaml => Tests/Functional/Features/NodeNames/NodeTypes.NodeNames.yaml (95%) rename Tests/Functional/{Fixtures/TransliterateNodeName.nodes.json => Features/NodeNames/Snapshots/NodeNames.nodes.json} (100%) rename Tests/Functional/{Fixtures/TransliterateNodeName.template.json => Features/NodeNames/Snapshots/NodeNames.template.json} (100%) rename Tests/Functional/{Fixtures/TransliterateNodeName.yaml => Features/NodeNames/Snapshots/NodeNames.yaml} (100%) create mode 100644 Tests/Functional/Features/NodeTypes.yaml create mode 100644 Tests/Functional/Features/Pages/NodeTypes.DynamicPages.yaml rename Configuration/Testing/NodeTypes.PageTemplates.yaml => Tests/Functional/Features/Pages/NodeTypes.StaticPages.yaml (51%) create mode 100644 Tests/Functional/Features/Pages/PagesTest.php rename Tests/Functional/{Fixtures/PagePreset.nodes.json => Features/Pages/Snapshots/Pages.nodes.json} (100%) rename Tests/Functional/{Fixtures/PagePreset.yaml => Features/Pages/Snapshots/Pages.yaml} (100%) rename Tests/Functional/{Fixtures/PagePreset.template.json => Features/Pages/Snapshots/Pages1.template.json} (100%) create mode 100644 Tests/Functional/Features/Pages/Snapshots/Pages2.template.json create mode 100644 Tests/Functional/Features/Properties/NodeTypes.Properties.yaml create mode 100644 Tests/Functional/Features/Properties/PropertiesTest.php rename Tests/Functional/{Fixtures/DifferentPropertyTypes.nodes.json => Features/Properties/Snapshots/Properties.nodes.json} (100%) rename Tests/Functional/{Fixtures/DifferentPropertyTypes.template.json => Features/Properties/Snapshots/Properties.template.json} (100%) rename Tests/Functional/{Fixtures/DifferentPropertyTypes.yaml => Features/Properties/Snapshots/Properties.yaml} (77%) rename Configuration/Testing/NodeTypes.ResolvablePropertyValues.yaml => Tests/Functional/Features/ResolvableProperties/NodeTypes.ResolvableProperties.yaml (91%) rename Configuration/Testing/NodeTypes.UnresolvablePropertyValues.yaml => Tests/Functional/Features/ResolvableProperties/NodeTypes.UnresolvableProperties.yaml (86%) create mode 100644 Tests/Functional/Features/ResolvableProperties/ResolvablePropertiesTest.php rename Tests/Functional/{Fixtures/ResolvablePropertyValues.nodes.json => Features/ResolvableProperties/Snapshots/ResolvableProperties.nodes.json} (100%) rename Tests/Functional/{Fixtures/ResolvablePropertyValues.template.json => Features/ResolvableProperties/Snapshots/ResolvableProperties.template.json} (100%) rename Tests/Functional/{Fixtures/ResolvablePropertyValues.yaml => Features/ResolvableProperties/Snapshots/ResolvableProperties.yaml} (100%) create mode 100644 Tests/Functional/Features/ResolvableProperties/Snapshots/UnresolvableProperties.messages.json create mode 100644 Tests/Functional/Features/ResolvableProperties/Snapshots/UnresolvableProperties.nodes.json rename Tests/Functional/{Fixtures/UnresolvablePropertyValues.template.json => Features/ResolvableProperties/Snapshots/UnresolvableProperties.template.json} (100%) create mode 100644 Tests/Functional/Features/ResolvableProperties/Snapshots/UnresolvableProperties.yaml rename Tests/Functional/{ => Features/ResolvableProperties}/image.png (100%) delete mode 100644 Tests/Functional/Fixtures/UnresolvablePropertyValues.messages.json delete mode 100644 Tests/Functional/NodeTemplateTest.php diff --git a/Configuration/Testing/NodeTypes.yaml b/Configuration/Testing/NodeTypes.yaml deleted file mode 100644 index cca63f4..0000000 --- a/Configuration/Testing/NodeTypes.yaml +++ /dev/null @@ -1,162 +0,0 @@ -'Flowpack.NodeTemplates:Document.Page': - superTypes: - 'Neos.Neos:Document': true - childNodes: - main: - type: 'Neos.Neos:ContentCollection' - -'Flowpack.NodeTemplates:Content.Text': - superTypes: - 'Neos.Neos:Content': true - properties: - text: - type: string - ui: - label: "Text" - -'Flowpack.NodeTemplates:Content.DifferentPropertyTypes': - superTypes: - 'Neos.Neos:Content': true - properties: - text: - type: string - ui: - label: "Text" - isEnchanted: - type: boolean - ui: - label: "Boolean" - selectBox: - type: string - ui: - label: "Select Box" - inspector: - editor: Neos.Neos/Inspector/Editors/SelectBoxEditor - editorOptions: - values: - karma: - label: "A" - longLive: - label: "B" - reference: - type: reference - ui: - label: "Reference" - inspector: - editorOptions: - nodeTypes: ['Flowpack.NodeTemplates:Content.DifferentPropertyTypes'] - nullValue: # Will be ignored from dumper but we nevertheless test, that we can set the value to "null" - type: string - ui: - label: "Null value" - unsetValueWithDefault: - defaultValue: true - type: boolean - someValueWithDefault: - defaultValue: true - type: boolean - options: - template: - properties: - text: "abc" - isEnchanted: false - selectBox: karma - reference: "${data.someNode}" - nullValue: null - unsetValueWithDefault: null -'Flowpack.NodeTemplates:Content.Columns.Two': - superTypes: - 'Neos.Neos:Content': true - childNodes: - column0: - type: 'Neos.Neos:ContentCollection' - column1: - type: 'Neos.Neos:ContentCollection' - options: - template: - childNodes: - column0Tethered: - name: column0 - childNodes: - content0: - type: 'Flowpack.NodeTemplates:Content.Text' - properties: - text: '

foo

' - column1Tethered: - name: column1 - childNodes: - content0: - type: 'Flowpack.NodeTemplates:Content.Text' - properties: - text: '

bar

' - -'Flowpack.NodeTemplates:Content.Columns.Two.CreationDialogAndWithItems': - superTypes: - 'Neos.Neos:Content': true - ui: - creationDialog: - elements: - text: - type: string - ui: - editor: Neos.Neos/Inspector/Editors/TextFieldEditor - childNodes: - column0: - type: 'Neos.Neos:ContentCollection' - column1: - type: 'Neos.Neos:ContentCollection' - options: - template: - childNodes: - tetheredColumns: - withItems: ["column0", "column1"] - name: "${item}" - childNodes: - content0: - type: 'Flowpack.NodeTemplates:Content.Text' - when: "${true}" - properties: - text: "${item == 'column0' ? '

foo

' : data.text}" - contentNever: - type: 'Flowpack.NodeTemplates:Content.Text' - when: "${false}" - properties: - text: "i'm never created" - -'Flowpack.NodeTemplates:Content.Columns.Two.WithContext': - superTypes: - 'Neos.Neos:Content': true - childNodes: - column0: - type: 'Neos.Neos:ContentCollection' - column1: - type: 'Neos.Neos:ContentCollection' - options: - template: - withContext: - tagName: 'p' - booleanType: true - arrayType: ["foo"] - childNodes: - column0Tethered: - name: column0 - childNodes: - content0: - type: 'Flowpack.NodeTemplates:Content.Text' - when: "${booleanType}" - withItems: "${arrayType}" - properties: - text: ${'<' + tagName + '>' + item + ''} - column1Tethered: - name: column1 - childNodes: - content0: - withContext: - otherBooleanType: true - oneItem: "${[false]}" - upperContext: "${''}" - when: "${otherBooleanType}" - withItems: "${oneItem}" - type: 'Flowpack.NodeTemplates:Content.Text' - properties: - text: "${'

bar' + upperContext}" diff --git a/Tests/Functional/AbstractNodeTemplateTest.php b/Tests/Functional/AbstractNodeTemplateTest.php new file mode 100644 index 0000000..f068df8 --- /dev/null +++ b/Tests/Functional/AbstractNodeTemplateTest.php @@ -0,0 +1,213 @@ +nodeTypeManager = $this->objectManager->get(NodeTypeManager::class); + + $this->loadFakeNodeTypes(); + + $this->setupContentRepository(); + $this->nodeTemplateDumper = $this->objectManager->get(NodeTemplateDumper::class); + + $templateFactory = $this->objectManager->get(TemplateConfigurationProcessor::class); + + $templateFactoryMock = $this->getMockBuilder(TemplateConfigurationProcessor::class)->disableOriginalConstructor()->getMock(); + $templateFactoryMock->expects(self::once())->method('processTemplateConfiguration')->willReturnCallback(function (...$args) use($templateFactory) { + $rootTemplate = $templateFactory->processTemplateConfiguration(...$args); + $this->lastCreatedRootTemplate = $rootTemplate; + return $rootTemplate; + }); + $this->objectManager->setInstance(TemplateConfigurationProcessor::class, $templateFactoryMock); + + $ref = new \ReflectionClass($this); + $this->fixturesDir = dirname($ref->getFileName()) . '/Snapshots'; + } + + private function loadFakeNodeTypes(): void + { + $configuration = []; + $yamlSource = new YamlSource(); + foreach (['Neos.Neos', 'Neos.ContentRepository', 'Flowpack.NodeTemplates'] as $packageKey) { + $package = $this->objectManager->get(PackageManager::class)->getPackage($packageKey); + $configuration = Arrays::arrayMergeRecursiveOverrule( + $configuration, + $yamlSource->load($package->getConfigurationPath() . 'NodeTypes', true) + ); + } + + $fileIterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator(__DIR__ . '/Features')); + + /** @var \SplFileInfo $fileInfo */ + foreach ($fileIterator as $fileInfo) { + if (!$fileInfo->isFile() || $fileInfo->getExtension() !== 'yaml' || strpos($fileInfo->getBasename(), 'NodeTypes.') !== 0) { + continue; + } + + $configuration = Arrays::arrayMergeRecursiveOverrule( + $configuration, + Yaml::parseFile($fileInfo->getRealPath()) ?? [] + ); + } + + $this->nodeTypeManager->overrideNodeTypes($configuration); + } + + public function tearDown(): void + { + parent::tearDown(); + $this->inject($this->contextFactory, 'contextInstances', []); + $this->objectManager->get(FeedbackCollection::class)->reset(); + $this->objectManager->forgetInstance(ContentDimensionRepository::class); + $this->objectManager->forgetInstance(TemplateConfigurationProcessor::class); + $this->objectManager->forgetInstance(NodeTypeManager::class); + } + + private function setupContentRepository(): void + { + // Create an environment to create nodes. + $this->objectManager->get(ContentDimensionRepository::class)->setDimensionsConfiguration([]); + + $liveWorkspace = new Workspace('live'); + $workspaceRepository = $this->objectManager->get(WorkspaceRepository::class); + $workspaceRepository->add($liveWorkspace); + + $testSite = new Site('test-site'); + $testSite->setSiteResourcesPackageKey('Test.Site'); + $siteRepository = $this->objectManager->get(SiteRepository::class); + $siteRepository->add($testSite); + + $this->persistenceManager->persistAll(); + $this->contextFactory = $this->objectManager->get(ContextFactoryInterface::class); + $this->subgraph = $this->contextFactory->create(['workspaceName' => 'live']); + + $rootNode = $this->subgraph->getRootNode(); + + + $sitesRootNode = $rootNode->createNode('sites'); + $testSiteNode = $sitesRootNode->createNode('test-site'); + $this->homePageNode = $testSiteNode->createNode( + 'homepage', + $this->nodeTypeManager->getNodeType('Flowpack.NodeTemplates:Document.Page') + ); + + $this->homePageMainContentCollectionNode = $this->homePageNode->getNode('main'); + } + + /** + * @param NodeInterface $targetNode + * @param array $nodeCreationDialogValues + */ + protected function createNodeInto(NodeInterface $targetNode, string $nodeTypeName, array $nodeCreationDialogValues): NodeInterface + { + self::assertTrue($this->nodeTypeManager->hasNodeType($nodeTypeName), sprintf('NodeType %s doesnt exits.', $nodeTypeName)); + + $targetNodeContextPath = $targetNode->getContextPath(); + + /** @see \Neos\Neos\Ui\Domain\Model\Changes\Create */ + $changeCollectionSerialized = [[ + 'type' => 'Neos.Neos.Ui:CreateInto', + 'subject' => $targetNodeContextPath, + 'payload' => [ + 'parentContextPath' => $targetNodeContextPath, + 'parentDomAddress' => [ + 'contextPath' => $targetNodeContextPath, + ], + 'nodeType' => $nodeTypeName, + 'name' => 'new-node', + 'data' => $nodeCreationDialogValues, + 'baseNodeType' => '', + ], + ]]; + + $changeCollection = (new ChangeCollectionConverter())->convertFrom($changeCollectionSerialized, null); + assert($changeCollection instanceof ChangeCollection); + $changeCollection->apply(); + + return $targetNode->getNode('new-node'); + } + + protected function createFakeNode(string $nodeAggregateId): NodeInterface + { + return $this->homePageNode->createNode(uniqid('node-'), $this->nodeTypeManager->getNodeType('unstructured'), $nodeAggregateId); + } + + protected function assertLastCreatedTemplateMatchesSnapshot(string $snapShotName): void + { + $lastCreatedTemplate = $this->serializeValuesInArray( + $this->lastCreatedRootTemplate->jsonSerialize() + ); + $this->assertJsonStringEqualsJsonFileOrCreateSnapshot($this->fixturesDir . '/' . $snapShotName . '.template.json', json_encode($lastCreatedTemplate, JSON_PRETTY_PRINT)); + } + + protected function assertCaughtExceptionsMatchesSnapshot(string $snapShotName): void + { + $this->assertJsonStringEqualsJsonFileOrCreateSnapshot($this->fixturesDir . '/' . $snapShotName . '.messages.json', json_encode($this->getMessagesOfFeedbackCollection(), JSON_PRETTY_PRINT)); + } + + protected function assertNoExceptionsWereCaught(): void + { + self::assertSame([], $this->getMessagesOfFeedbackCollection()); + } + + protected function assertNodeDumpAndTemplateDumpMatchSnapshot(string $snapShotName, NodeInterface $node): void + { + $serializedNodes = $this->jsonSerializeNodeAndDescendents($node); + unset($serializedNodes['nodeTypeName']); + $this->assertJsonStringEqualsJsonFileOrCreateSnapshot($this->fixturesDir . '/' . $snapShotName . '.nodes.json', json_encode($serializedNodes, JSON_PRETTY_PRINT)); + + $dumpedYamlTemplate = $this->nodeTemplateDumper->createNodeTemplateYamlDumpFromSubtree($node); + + $yamlTemplateWithoutOriginNodeTypeName = '\'{nodeTypeName}\'' . substr($dumpedYamlTemplate, strlen($node->getNodeType()->getName()) + 2); + + $this->assertStringEqualsFileOrCreateSnapshot($this->fixturesDir . '/' . $snapShotName . '.yaml', $yamlTemplateWithoutOriginNodeTypeName); + } +} diff --git a/Tests/Functional/Features/ChildNodes/ChildNodesTest.php b/Tests/Functional/Features/ChildNodes/ChildNodesTest.php new file mode 100644 index 0000000..15da9e1 --- /dev/null +++ b/Tests/Functional/Features/ChildNodes/ChildNodesTest.php @@ -0,0 +1,58 @@ +createNodeInto( + $this->homePageMainContentCollectionNode, + 'Flowpack.NodeTemplates:Content.StaticChildNodes', + [] + ); + + $this->assertLastCreatedTemplateMatchesSnapshot('ChildNodes'); + + $this->assertNoExceptionsWereCaught(); + $this->assertNodeDumpAndTemplateDumpMatchSnapshot('ChildNodes', $createdNode); + } + + + /** @test */ + public function testNodeCreationMatchesSnapshot2(): void + { + $createdNode = $this->createNodeInto( + $this->homePageMainContentCollectionNode, + 'Flowpack.NodeTemplates:Content.DynamicChildNodes1', + [ + 'text' => '

bar

' + ] + ); + + $this->assertLastCreatedTemplateMatchesSnapshot('ChildNodes'); + + $this->assertNoExceptionsWereCaught(); + $this->assertNodeDumpAndTemplateDumpMatchSnapshot('ChildNodes', $createdNode); + } + + /** @test */ + public function testNodeCreationMatchesSnapshot3(): void + { + $createdNode = $this->createNodeInto( + $this->homePageMainContentCollectionNode, + 'Flowpack.NodeTemplates:Content.DynamicChildNodes2', + [] + ); + + $this->assertLastCreatedTemplateMatchesSnapshot('ChildNodes'); + + $this->assertNoExceptionsWereCaught(); + $this->assertNodeDumpAndTemplateDumpMatchSnapshot('ChildNodes', $createdNode); + } +} diff --git a/Tests/Functional/Features/ChildNodes/NodeTypes.DynamicChildNodes1.yaml b/Tests/Functional/Features/ChildNodes/NodeTypes.DynamicChildNodes1.yaml new file mode 100644 index 0000000..47ef55a --- /dev/null +++ b/Tests/Functional/Features/ChildNodes/NodeTypes.DynamicChildNodes1.yaml @@ -0,0 +1,35 @@ +# We test that data from the node creation dialog is passed correctly +--- + +'Flowpack.NodeTemplates:Content.DynamicChildNodes1': + superTypes: + 'Neos.Neos:Content': true + ui: + creationDialog: + elements: + text: + type: string + ui: + editor: Neos.Neos/Inspector/Editors/TextFieldEditor + childNodes: + column0: + type: 'Neos.Neos:ContentCollection' + column1: + type: 'Neos.Neos:ContentCollection' + options: + template: + childNodes: + tetheredColumns: + withItems: ["column0", "column1"] + name: "${item}" + childNodes: + content0: + type: 'Flowpack.NodeTemplates:Content.Text' + when: "${true}" + properties: + text: "${item == 'column0' ? '

foo

' : data.text}" + contentNever: + type: 'Flowpack.NodeTemplates:Content.Text' + when: "${false}" + properties: + text: "i'm never created" diff --git a/Tests/Functional/Features/ChildNodes/NodeTypes.DynamicChildNodes2.yaml b/Tests/Functional/Features/ChildNodes/NodeTypes.DynamicChildNodes2.yaml new file mode 100644 index 0000000..9442a02 --- /dev/null +++ b/Tests/Functional/Features/ChildNodes/NodeTypes.DynamicChildNodes2.yaml @@ -0,0 +1,37 @@ +'Flowpack.NodeTemplates:Content.DynamicChildNodes2': + superTypes: + 'Neos.Neos:Content': true + childNodes: + column0: + type: 'Neos.Neos:ContentCollection' + column1: + type: 'Neos.Neos:ContentCollection' + options: + template: + withContext: + tagName: 'p' + booleanType: true + arrayType: ["foo"] + childNodes: + column0Tethered: + name: column0 + childNodes: + content0: + type: 'Flowpack.NodeTemplates:Content.Text' + when: "${booleanType}" + withItems: "${arrayType}" + properties: + text: ${'<' + tagName + '>' + item + ''} + column1Tethered: + name: column1 + childNodes: + content0: + withContext: + otherBooleanType: true + oneItem: "${[false]}" + upperContext: "${''}" + when: "${otherBooleanType}" + withItems: "${oneItem}" + type: 'Flowpack.NodeTemplates:Content.Text' + properties: + text: "${'

bar' + upperContext}" diff --git a/Tests/Functional/Features/ChildNodes/NodeTypes.StaticChildNodes.yaml b/Tests/Functional/Features/ChildNodes/NodeTypes.StaticChildNodes.yaml new file mode 100644 index 0000000..f19fc87 --- /dev/null +++ b/Tests/Functional/Features/ChildNodes/NodeTypes.StaticChildNodes.yaml @@ -0,0 +1,25 @@ +'Flowpack.NodeTemplates:Content.StaticChildNodes': + superTypes: + 'Neos.Neos:Content': true + childNodes: + column0: + type: 'Neos.Neos:ContentCollection' + column1: + type: 'Neos.Neos:ContentCollection' + options: + template: + childNodes: + column0Tethered: + name: column0 + childNodes: + content0: + type: 'Flowpack.NodeTemplates:Content.Text' + properties: + text: '

foo

' + column1Tethered: + name: column1 + childNodes: + content0: + type: 'Flowpack.NodeTemplates:Content.Text' + properties: + text: '

bar

' diff --git a/Tests/Functional/Fixtures/TwoColumnPreset.nodes.json b/Tests/Functional/Features/ChildNodes/Snapshots/ChildNodes.nodes.json similarity index 100% rename from Tests/Functional/Fixtures/TwoColumnPreset.nodes.json rename to Tests/Functional/Features/ChildNodes/Snapshots/ChildNodes.nodes.json diff --git a/Tests/Functional/Fixtures/TwoColumnPreset.template.json b/Tests/Functional/Features/ChildNodes/Snapshots/ChildNodes.template.json similarity index 100% rename from Tests/Functional/Fixtures/TwoColumnPreset.template.json rename to Tests/Functional/Features/ChildNodes/Snapshots/ChildNodes.template.json diff --git a/Tests/Functional/Fixtures/TwoColumnPreset.yaml b/Tests/Functional/Features/ChildNodes/Snapshots/ChildNodes.yaml similarity index 100% rename from Tests/Functional/Fixtures/TwoColumnPreset.yaml rename to Tests/Functional/Features/ChildNodes/Snapshots/ChildNodes.yaml diff --git a/Tests/Functional/Features/Exceptions/ExceptionsTest.php b/Tests/Functional/Features/Exceptions/ExceptionsTest.php new file mode 100644 index 0000000..bc4c97b --- /dev/null +++ b/Tests/Functional/Features/Exceptions/ExceptionsTest.php @@ -0,0 +1,68 @@ +createNodeInto( + $this->homePageMainContentCollectionNode, + 'Flowpack.NodeTemplates:Content.SomeExceptions', + [] + ); + + $this->assertLastCreatedTemplateMatchesSnapshot('SomeExceptions'); + + $this->assertCaughtExceptionsMatchesSnapshot('SomeExceptions'); + $this->assertNodeDumpAndTemplateDumpMatchSnapshot('SomeExceptions', $createdNode); + } + + /** @test */ + public function exceptionsAreCaughtAndPartialTemplateIsNotBuild(): void + { + $this->withMockedConfigurationSettings([ + 'Flowpack' => [ + 'NodeTemplates' => [ + 'exceptionHandling' => [ + 'templateConfigurationProcessing' => [ + 'stopOnException' => true + ] + ] + ] + ] + ], function () { + $createdNode = $this->createNodeInto( + $this->homePageMainContentCollectionNode, + 'Flowpack.NodeTemplates:Content.OnlyExceptions', + [] + ); + + $this->assertLastCreatedTemplateMatchesSnapshot('OnlyExceptions'); + + // self::assertSame([ + // [ + // 'message' => 'Template for "WithOneEvaluationException" was not applied. Only Node /sites/test-site/homepage/main/new-node@live[Flowpack.NodeTemplates:Content.WithOneEvaluationException] was created.', + // 'severity' => 'ERROR' + // ], + // [ + // 'message' => 'Expression "${\'left open" in "childNodes.abort.when" | EelException(The EEL expression "${\'left open" was not a valid EEL expression. Perhaps you forgot to wrap it in ${...}?, 1410441849)', + // 'severity' => 'ERROR' + // ] + // ], $this->getMessagesOfFeedbackCollection()); + + + $this->assertCaughtExceptionsMatchesSnapshot('OnlyExceptions'); + $this->assertNodeDumpAndTemplateDumpMatchSnapshot('OnlyExceptions', $createdNode); + }); + } + +} diff --git a/Tests/Functional/Features/Exceptions/NodeTypes.OnlyExceptions.yaml b/Tests/Functional/Features/Exceptions/NodeTypes.OnlyExceptions.yaml new file mode 100644 index 0000000..cda8e8d --- /dev/null +++ b/Tests/Functional/Features/Exceptions/NodeTypes.OnlyExceptions.yaml @@ -0,0 +1,12 @@ + +'Flowpack.NodeTemplates:Content.OnlyExceptions': + superTypes: + 'Neos.Neos:ContentCollection': true + ui: + label: "OnlyExceptions" + options: + template: + childNodes: + abort: + type: 'Flowpack.NodeTemplates:Content.Text' + when: "${'left open" diff --git a/Configuration/Testing/NodeTypes.Malformed.yaml b/Tests/Functional/Features/Exceptions/NodeTypes.SomeExceptions.yaml similarity index 81% rename from Configuration/Testing/NodeTypes.Malformed.yaml rename to Tests/Functional/Features/Exceptions/NodeTypes.SomeExceptions.yaml index 08c96bf..99182b0 100644 --- a/Configuration/Testing/NodeTypes.Malformed.yaml +++ b/Tests/Functional/Features/Exceptions/NodeTypes.SomeExceptions.yaml @@ -1,20 +1,9 @@ -'Flowpack.NodeTemplates:Content.WithOneEvaluationException': - superTypes: - 'Neos.Neos:ContentCollection': true - ui: - label: "WithOneEvaluationException" - options: - template: - childNodes: - abort: - type: 'Flowpack.NodeTemplates:Content.Text' - when: "${'left open" -'Flowpack.NodeTemplates:Content.WithEvaluationExceptions': +'Flowpack.NodeTemplates:Content.SomeExceptions': superTypes: 'Neos.Neos:ContentCollection': true ui: - label: "WithEvaluationExceptions" + label: "SomeExceptions" properties: boolValue: type: boolean @@ -57,7 +46,7 @@ abstractNodeAbort: type: 'Neos.Neos:Node' illegalNodeAbort: - type: 'Flowpack.NodeTemplates:Document.Page.Static' + type: 'Flowpack.NodeTemplates:Document.Page' name: 'illegal' properties: text: huhu @@ -92,6 +81,6 @@ type: "Flowpack.NodeTemplates:InvalidNodeType" typeCantMutate: name: "type-cant-mutate" - type: "Flowpack.NodeTemplates:Content.WithEvaluationExceptions" + type: "Flowpack.NodeTemplates:Content.SomeExceptions" invalidOption: crazy: me diff --git a/Tests/Functional/Features/Exceptions/Snapshots/OnlyExceptions.messages.json b/Tests/Functional/Features/Exceptions/Snapshots/OnlyExceptions.messages.json new file mode 100644 index 0000000..94ca9b1 --- /dev/null +++ b/Tests/Functional/Features/Exceptions/Snapshots/OnlyExceptions.messages.json @@ -0,0 +1,10 @@ +[ + { + "message": "Template for \"OnlyExceptions\" was not applied. Only Node \/sites\/test-site\/homepage\/main\/new-node@live[Flowpack.NodeTemplates:Content.OnlyExceptions] was created.", + "severity": "ERROR" + }, + { + "message": "Expression \"${'left open\" in \"childNodes.abort.when\" | EelException(The EEL expression \"${'left open\" was not a valid EEL expression. Perhaps you forgot to wrap it in ${...}?, 1410441849)", + "severity": "ERROR" + } +] diff --git a/Tests/Functional/Fixtures/UnresolvablePropertyValues.nodes.json b/Tests/Functional/Features/Exceptions/Snapshots/OnlyExceptions.nodes.json similarity index 100% rename from Tests/Functional/Fixtures/UnresolvablePropertyValues.nodes.json rename to Tests/Functional/Features/Exceptions/Snapshots/OnlyExceptions.nodes.json diff --git a/Tests/Functional/Fixtures/WithOneEvaluationException.template.json b/Tests/Functional/Features/Exceptions/Snapshots/OnlyExceptions.template.json similarity index 100% rename from Tests/Functional/Fixtures/WithOneEvaluationException.template.json rename to Tests/Functional/Features/Exceptions/Snapshots/OnlyExceptions.template.json diff --git a/Tests/Functional/Fixtures/UnresolvablePropertyValues.yaml b/Tests/Functional/Features/Exceptions/Snapshots/OnlyExceptions.yaml similarity index 100% rename from Tests/Functional/Fixtures/UnresolvablePropertyValues.yaml rename to Tests/Functional/Features/Exceptions/Snapshots/OnlyExceptions.yaml diff --git a/Tests/Functional/Fixtures/WithEvaluationExceptions.messages.json b/Tests/Functional/Features/Exceptions/Snapshots/SomeExceptions.messages.json similarity index 72% rename from Tests/Functional/Fixtures/WithEvaluationExceptions.messages.json rename to Tests/Functional/Features/Exceptions/Snapshots/SomeExceptions.messages.json index fdf9b33..e5a8d85 100644 --- a/Tests/Functional/Fixtures/WithEvaluationExceptions.messages.json +++ b/Tests/Functional/Features/Exceptions/Snapshots/SomeExceptions.messages.json @@ -1,6 +1,6 @@ [ { - "message": "Template for \"WithEvaluationExceptions\" only partially applied. Please check the newly created nodes beneath Node \/sites\/test-site\/homepage\/main\/new-node@live[Flowpack.NodeTemplates:Content.WithEvaluationExceptions].", + "message": "Template for \"SomeExceptions\" only partially applied. Please check the newly created nodes beneath Node \/sites\/test-site\/homepage\/main\/new-node@live[Flowpack.NodeTemplates:Content.SomeExceptions].", "severity": "ERROR" }, { @@ -52,31 +52,31 @@ "severity": "ERROR" }, { - "message": "Property \"_hidden\" in NodeType \"Flowpack.NodeTemplates:Content.WithEvaluationExceptions\" | PropertyIgnoredException(Because internal legacy property \"_hidden\" not implement., 1686149513158)", + "message": "Property \"_hidden\" in NodeType \"Flowpack.NodeTemplates:Content.SomeExceptions\" | PropertyIgnoredException(Because internal legacy property \"_hidden\" not implement., 1686149513158)", "severity": "ERROR" }, { - "message": "Property \"_hiddenAfterDateTime\" in NodeType \"Flowpack.NodeTemplates:Content.WithEvaluationExceptions\" | PropertyIgnoredException(Because internal legacy property \"_hiddenAfterDateTime\" not implement., 1686149513158)", + "message": "Property \"_hiddenAfterDateTime\" in NodeType \"Flowpack.NodeTemplates:Content.SomeExceptions\" | PropertyIgnoredException(Because internal legacy property \"_hiddenAfterDateTime\" not implement., 1686149513158)", "severity": "ERROR" }, { - "message": "Property \"boolValue\" in NodeType \"Flowpack.NodeTemplates:Content.WithEvaluationExceptions\" | PropertyIgnoredException(Because value `123` is not assignable to property type \"boolean\"., 1685958105644)", + "message": "Property \"boolValue\" in NodeType \"Flowpack.NodeTemplates:Content.SomeExceptions\" | PropertyIgnoredException(Because value `123` is not assignable to property type \"boolean\"., 1685958105644)", "severity": "ERROR" }, { - "message": "Property \"stringValue\" in NodeType \"Flowpack.NodeTemplates:Content.WithEvaluationExceptions\" | PropertyIgnoredException(Because value `false` is not assignable to property type \"string\"., 1685958105644)", + "message": "Property \"stringValue\" in NodeType \"Flowpack.NodeTemplates:Content.SomeExceptions\" | PropertyIgnoredException(Because value `false` is not assignable to property type \"string\"., 1685958105644)", "severity": "ERROR" }, { - "message": "Property \"nonDeclaredProperty\" in NodeType \"Flowpack.NodeTemplates:Content.WithEvaluationExceptions\" | PropertyIgnoredException(Because property is not declared in NodeType. Got value `\"hi\"`., 1685869035209)", + "message": "Property \"nonDeclaredProperty\" in NodeType \"Flowpack.NodeTemplates:Content.SomeExceptions\" | PropertyIgnoredException(Because property is not declared in NodeType. Got value `\"hi\"`., 1685869035209)", "severity": "ERROR" }, { - "message": "Reference \"reference\" in NodeType \"Flowpack.NodeTemplates:Content.WithEvaluationExceptions\" | RuntimeException(Reference could not be set, because node reference(s) \"non-existing-node-id\" cannot be resolved., 1685958176560)", + "message": "Reference \"reference\" in NodeType \"Flowpack.NodeTemplates:Content.SomeExceptions\" | RuntimeException(Reference could not be set, because node reference(s) \"non-existing-node-id\" cannot be resolved., 1685958176560)", "severity": "ERROR" }, { - "message": "Reference \"references\" in NodeType \"Flowpack.NodeTemplates:Content.WithEvaluationExceptions\" | RuntimeException(Reference could not be set, because node reference(s) [\"non-existing-node-id\"] cannot be resolved., 1685958176560)", + "message": "Reference \"references\" in NodeType \"Flowpack.NodeTemplates:Content.SomeExceptions\" | RuntimeException(Reference could not be set, because node reference(s) [\"non-existing-node-id\"] cannot be resolved., 1685958176560)", "severity": "ERROR" }, { @@ -84,7 +84,7 @@ "severity": "ERROR" }, { - "message": "RuntimeException(Node type \"Flowpack.NodeTemplates:Document.Page.Static\" is not allowed for child nodes of type Flowpack.NodeTemplates:Content.WithEvaluationExceptions, 1686417627173)", + "message": "RuntimeException(Node type \"Flowpack.NodeTemplates:Document.Page\" is not allowed for child nodes of type Flowpack.NodeTemplates:Content.SomeExceptions, 1686417627173)", "severity": "ERROR" }, { @@ -96,7 +96,7 @@ "severity": "ERROR" }, { - "message": "RuntimeException(Template cant mutate type of auto created child nodes. Got: \"Flowpack.NodeTemplates:Content.WithEvaluationExceptions\", 1685999829307)", + "message": "RuntimeException(Template cant mutate type of auto created child nodes. Got: \"Flowpack.NodeTemplates:Content.SomeExceptions\", 1685999829307)", "severity": "ERROR" } ] diff --git a/Tests/Functional/Fixtures/WithEvaluationExceptions.nodes.json b/Tests/Functional/Features/Exceptions/Snapshots/SomeExceptions.nodes.json similarity index 100% rename from Tests/Functional/Fixtures/WithEvaluationExceptions.nodes.json rename to Tests/Functional/Features/Exceptions/Snapshots/SomeExceptions.nodes.json diff --git a/Tests/Functional/Fixtures/WithEvaluationExceptions.template.json b/Tests/Functional/Features/Exceptions/Snapshots/SomeExceptions.template.json similarity index 92% rename from Tests/Functional/Fixtures/WithEvaluationExceptions.template.json rename to Tests/Functional/Features/Exceptions/Snapshots/SomeExceptions.template.json index 9ff7fd5..891cc60 100644 --- a/Tests/Functional/Fixtures/WithEvaluationExceptions.template.json +++ b/Tests/Functional/Features/Exceptions/Snapshots/SomeExceptions.template.json @@ -27,7 +27,7 @@ "childNodes": [] }, { - "type": "Flowpack.NodeTemplates:Document.Page.Static", + "type": "Flowpack.NodeTemplates:Document.Page", "name": "illegal", "properties": { "text": "huhu" @@ -63,7 +63,7 @@ "childNodes": [] }, { - "type": "Flowpack.NodeTemplates:Content.WithEvaluationExceptions", + "type": "Flowpack.NodeTemplates:Content.SomeExceptions", "name": "type-cant-mutate", "properties": [], "childNodes": [] diff --git a/Tests/Functional/Fixtures/WithEvaluationExceptions.yaml b/Tests/Functional/Features/Exceptions/Snapshots/SomeExceptions.yaml similarity index 100% rename from Tests/Functional/Fixtures/WithEvaluationExceptions.yaml rename to Tests/Functional/Features/Exceptions/Snapshots/SomeExceptions.yaml diff --git a/Tests/Functional/Features/NodeNames/NodeNamesTest.php b/Tests/Functional/Features/NodeNames/NodeNamesTest.php new file mode 100644 index 0000000..3d584fa --- /dev/null +++ b/Tests/Functional/Features/NodeNames/NodeNamesTest.php @@ -0,0 +1,25 @@ +createNodeInto( + $this->homePageMainContentCollectionNode, + 'Flowpack.NodeTemplates:Content.NodeNames', + [] + ); + + $this->assertLastCreatedTemplateMatchesSnapshot('NodeNames'); + + $this->assertNoExceptionsWereCaught(); + $this->assertNodeDumpAndTemplateDumpMatchSnapshot('NodeNames', $createdNode); + } +} diff --git a/Configuration/Testing/NodeTypes.TransliterateNodeName.yaml b/Tests/Functional/Features/NodeNames/NodeTypes.NodeNames.yaml similarity index 95% rename from Configuration/Testing/NodeTypes.TransliterateNodeName.yaml rename to Tests/Functional/Features/NodeNames/NodeTypes.NodeNames.yaml index 4231df2..c535e50 100644 --- a/Configuration/Testing/NodeTypes.TransliterateNodeName.yaml +++ b/Tests/Functional/Features/NodeNames/NodeTypes.NodeNames.yaml @@ -3,7 +3,7 @@ # We must make sure that the same transliteration is applied to our childNodes to match the original --- -'Flowpack.NodeTemplates:Content.TransliterateNodeName': +'Flowpack.NodeTemplates:Content.NodeNames': superTypes: 'Neos.Neos:ContentCollection': true childNodes: diff --git a/Tests/Functional/Fixtures/TransliterateNodeName.nodes.json b/Tests/Functional/Features/NodeNames/Snapshots/NodeNames.nodes.json similarity index 100% rename from Tests/Functional/Fixtures/TransliterateNodeName.nodes.json rename to Tests/Functional/Features/NodeNames/Snapshots/NodeNames.nodes.json diff --git a/Tests/Functional/Fixtures/TransliterateNodeName.template.json b/Tests/Functional/Features/NodeNames/Snapshots/NodeNames.template.json similarity index 100% rename from Tests/Functional/Fixtures/TransliterateNodeName.template.json rename to Tests/Functional/Features/NodeNames/Snapshots/NodeNames.template.json diff --git a/Tests/Functional/Fixtures/TransliterateNodeName.yaml b/Tests/Functional/Features/NodeNames/Snapshots/NodeNames.yaml similarity index 100% rename from Tests/Functional/Fixtures/TransliterateNodeName.yaml rename to Tests/Functional/Features/NodeNames/Snapshots/NodeNames.yaml diff --git a/Tests/Functional/Features/NodeTypes.yaml b/Tests/Functional/Features/NodeTypes.yaml new file mode 100644 index 0000000..c6e6773 --- /dev/null +++ b/Tests/Functional/Features/NodeTypes.yaml @@ -0,0 +1,21 @@ +# Basic NodeType definitions for all tests +--- + +'Flowpack.NodeTemplates:Document.Page': + superTypes: + 'Neos.Neos:Document': true + constraints: + nodeTypes: + unstructured: true + childNodes: + main: + type: 'Neos.Neos:ContentCollection' + +'Flowpack.NodeTemplates:Content.Text': + superTypes: + 'Neos.Neos:Content': true + properties: + text: + type: string + ui: + label: "Text" diff --git a/Tests/Functional/Features/Pages/NodeTypes.DynamicPages.yaml b/Tests/Functional/Features/Pages/NodeTypes.DynamicPages.yaml new file mode 100644 index 0000000..d2710d1 --- /dev/null +++ b/Tests/Functional/Features/Pages/NodeTypes.DynamicPages.yaml @@ -0,0 +1,51 @@ +# We test that nested more complex structures work correctly and that uriPathSegments are automatically generated +--- + +'Flowpack.NodeTemplates:Document.DynamicPages': + superTypes: + 'Neos.Neos:Document': true + childNodes: + main: + type: 'Neos.Neos:ContentCollection' + options: + template: + # title and uri path segment should be set via creation dialog data + childNodes: + 'Content Collection (main)': + name: main + childNodes: + content0: + type: 'Flowpack.NodeTemplates:Content.Text' + properties: + # Text + text: textOnPage1 + subPages: + withItems: + - Page2 + - Page4 + type: 'Flowpack.NodeTemplates:Document.Page' + properties: + title: "${item}" + # URL path segment will be set based on title + childNodes: + 'Content Collection (main)': + name: main + childNodes: + content0: + type: 'Flowpack.NodeTemplates:Content.Text' + properties: + text: "${item == 'Page2' ? 'textOnPage2' : 'textOnPage4'}" + page1: + when: "${item == 'Page2'}" + type: 'Flowpack.NodeTemplates:Document.Page' + properties: + title: Page3 + uriPathSegment: page3 + childNodes: + 'Content Collection (main)': + name: main + childNodes: + content0: + type: 'Flowpack.NodeTemplates:Content.Text' + properties: + text: textOnPage3 diff --git a/Configuration/Testing/NodeTypes.PageTemplates.yaml b/Tests/Functional/Features/Pages/NodeTypes.StaticPages.yaml similarity index 51% rename from Configuration/Testing/NodeTypes.PageTemplates.yaml rename to Tests/Functional/Features/Pages/NodeTypes.StaticPages.yaml index 35a1b1b..643e26d 100644 --- a/Configuration/Testing/NodeTypes.PageTemplates.yaml +++ b/Tests/Functional/Features/Pages/NodeTypes.StaticPages.yaml @@ -1,5 +1,5 @@ -'Flowpack.NodeTemplates:Document.Page.Static': +'Flowpack.NodeTemplates:Document.StaticPages': superTypes: 'Neos.Neos:Document': true childNodes: @@ -57,53 +57,3 @@ type: 'Flowpack.NodeTemplates:Content.Text' properties: text: textOnPage4 - - -'Flowpack.NodeTemplates:Document.Page.Dynamic': - superTypes: - 'Neos.Neos:Document': true - childNodes: - main: - type: 'Neos.Neos:ContentCollection' - options: - template: - # title and uri path segment should be set via creation dialog data - childNodes: - 'Content Collection (main)': - name: main - childNodes: - content0: - type: 'Flowpack.NodeTemplates:Content.Text' - properties: - # Text - text: textOnPage1 - subPages: - withItems: - - Page2 - - Page4 - type: 'Flowpack.NodeTemplates:Document.Page' - properties: - title: "${item}" - # URL path segment will be set based on title - childNodes: - 'Content Collection (main)': - name: main - childNodes: - content0: - type: 'Flowpack.NodeTemplates:Content.Text' - properties: - text: "${item == 'Page2' ? 'textOnPage2' : 'textOnPage4'}" - page1: - when: "${item == 'Page2'}" - type: 'Flowpack.NodeTemplates:Document.Page' - properties: - title: Page3 - uriPathSegment: page3 - childNodes: - 'Content Collection (main)': - name: main - childNodes: - content0: - type: 'Flowpack.NodeTemplates:Content.Text' - properties: - text: textOnPage3 diff --git a/Tests/Functional/Features/Pages/PagesTest.php b/Tests/Functional/Features/Pages/PagesTest.php new file mode 100644 index 0000000..37986f2 --- /dev/null +++ b/Tests/Functional/Features/Pages/PagesTest.php @@ -0,0 +1,43 @@ +createNodeInto( + $this->homePageNode, + 'Flowpack.NodeTemplates:Document.StaticPages', + [] + ); + + $this->assertLastCreatedTemplateMatchesSnapshot('Pages1'); + + $this->assertNoExceptionsWereCaught(); + $this->assertNodeDumpAndTemplateDumpMatchSnapshot('Pages', $createdNode); + } + + /** @test */ + public function itMatchesSnapsho2(): void + { + $createdNode = $this->createNodeInto( + $this->homePageNode, + 'Flowpack.NodeTemplates:Document.DynamicPages', + [ + 'title' => 'Page1' + ] + ); + + // we use a different snapshot here, because the uriPathSegments are not included at this time + $this->assertLastCreatedTemplateMatchesSnapshot('Pages2'); + + $this->assertNoExceptionsWereCaught(); + $this->assertNodeDumpAndTemplateDumpMatchSnapshot('Pages', $createdNode); + } +} diff --git a/Tests/Functional/Fixtures/PagePreset.nodes.json b/Tests/Functional/Features/Pages/Snapshots/Pages.nodes.json similarity index 100% rename from Tests/Functional/Fixtures/PagePreset.nodes.json rename to Tests/Functional/Features/Pages/Snapshots/Pages.nodes.json diff --git a/Tests/Functional/Fixtures/PagePreset.yaml b/Tests/Functional/Features/Pages/Snapshots/Pages.yaml similarity index 100% rename from Tests/Functional/Fixtures/PagePreset.yaml rename to Tests/Functional/Features/Pages/Snapshots/Pages.yaml diff --git a/Tests/Functional/Fixtures/PagePreset.template.json b/Tests/Functional/Features/Pages/Snapshots/Pages1.template.json similarity index 100% rename from Tests/Functional/Fixtures/PagePreset.template.json rename to Tests/Functional/Features/Pages/Snapshots/Pages1.template.json diff --git a/Tests/Functional/Features/Pages/Snapshots/Pages2.template.json b/Tests/Functional/Features/Pages/Snapshots/Pages2.template.json new file mode 100644 index 0000000..a2517ab --- /dev/null +++ b/Tests/Functional/Features/Pages/Snapshots/Pages2.template.json @@ -0,0 +1,93 @@ +{ + "properties": [], + "childNodes": [ + { + "type": null, + "name": "main", + "properties": [], + "childNodes": [ + { + "type": "Flowpack.NodeTemplates:Content.Text", + "name": null, + "properties": { + "text": "textOnPage1" + }, + "childNodes": [] + } + ] + }, + { + "type": "Flowpack.NodeTemplates:Document.Page", + "name": null, + "properties": { + "title": "Page2" + }, + "childNodes": [ + { + "type": null, + "name": "main", + "properties": [], + "childNodes": [ + { + "type": "Flowpack.NodeTemplates:Content.Text", + "name": null, + "properties": { + "text": "textOnPage2" + }, + "childNodes": [] + } + ] + }, + { + "type": "Flowpack.NodeTemplates:Document.Page", + "name": null, + "properties": { + "title": "Page3", + "uriPathSegment": "page3" + }, + "childNodes": [ + { + "type": null, + "name": "main", + "properties": [], + "childNodes": [ + { + "type": "Flowpack.NodeTemplates:Content.Text", + "name": null, + "properties": { + "text": "textOnPage3" + }, + "childNodes": [] + } + ] + } + ] + } + ] + }, + { + "type": "Flowpack.NodeTemplates:Document.Page", + "name": null, + "properties": { + "title": "Page4" + }, + "childNodes": [ + { + "type": null, + "name": "main", + "properties": [], + "childNodes": [ + { + "type": "Flowpack.NodeTemplates:Content.Text", + "name": null, + "properties": { + "text": "textOnPage4" + }, + "childNodes": [] + } + ] + } + ] + } + ] +} diff --git a/Tests/Functional/Features/Properties/NodeTypes.Properties.yaml b/Tests/Functional/Features/Properties/NodeTypes.Properties.yaml new file mode 100644 index 0000000..fdb851c --- /dev/null +++ b/Tests/Functional/Features/Properties/NodeTypes.Properties.yaml @@ -0,0 +1,53 @@ +# We test that properties can be set to their declared type or null +--- + +'Flowpack.NodeTemplates:Content.Properties': + superTypes: + 'Neos.Neos:Content': true + properties: + text: + type: string + ui: + label: "Text" + isEnchanted: + type: boolean + ui: + label: "Boolean" + selectBox: + type: string + ui: + label: "Select Box" + inspector: + editor: Neos.Neos/Inspector/Editors/SelectBoxEditor + editorOptions: + values: + karma: + label: "A" + longLive: + label: "B" + reference: + type: reference + ui: + label: "Reference" + inspector: + editorOptions: + nodeTypes: ['Flowpack.NodeTemplates:Content.Properties'] + nullValue: # Will be ignored from dumper but we nevertheless test, that we can set the value to "null" + type: string + ui: + label: "Null value" + unsetValueWithDefault: + defaultValue: true + type: boolean + someValueWithDefault: + defaultValue: true + type: boolean + options: + template: + properties: + text: "abc" + isEnchanted: false + selectBox: karma + reference: "${data.someNode}" + nullValue: null + unsetValueWithDefault: null diff --git a/Tests/Functional/Features/Properties/PropertiesTest.php b/Tests/Functional/Features/Properties/PropertiesTest.php new file mode 100644 index 0000000..9e44cc5 --- /dev/null +++ b/Tests/Functional/Features/Properties/PropertiesTest.php @@ -0,0 +1,27 @@ +createNodeInto( + $this->homePageMainContentCollectionNode, + 'Flowpack.NodeTemplates:Content.Properties', + [ + 'someNode' => $this->createFakeNode('7f7bac1c-9400-4db5-bbaa-2b8251d127c5') + ] + ); + + $this->assertLastCreatedTemplateMatchesSnapshot('Properties'); + + $this->assertNoExceptionsWereCaught(); + $this->assertNodeDumpAndTemplateDumpMatchSnapshot('Properties', $createdNode); + } +} diff --git a/Tests/Functional/Fixtures/DifferentPropertyTypes.nodes.json b/Tests/Functional/Features/Properties/Snapshots/Properties.nodes.json similarity index 100% rename from Tests/Functional/Fixtures/DifferentPropertyTypes.nodes.json rename to Tests/Functional/Features/Properties/Snapshots/Properties.nodes.json diff --git a/Tests/Functional/Fixtures/DifferentPropertyTypes.template.json b/Tests/Functional/Features/Properties/Snapshots/Properties.template.json similarity index 100% rename from Tests/Functional/Fixtures/DifferentPropertyTypes.template.json rename to Tests/Functional/Features/Properties/Snapshots/Properties.template.json diff --git a/Tests/Functional/Fixtures/DifferentPropertyTypes.yaml b/Tests/Functional/Features/Properties/Snapshots/Properties.yaml similarity index 77% rename from Tests/Functional/Fixtures/DifferentPropertyTypes.yaml rename to Tests/Functional/Features/Properties/Snapshots/Properties.yaml index 141c5ba..fe79027 100644 --- a/Tests/Functional/Fixtures/DifferentPropertyTypes.yaml +++ b/Tests/Functional/Features/Properties/Snapshots/Properties.yaml @@ -9,4 +9,4 @@ # Select Box # selectBox -> SelectBox of ["karma","longLive"] with value "karma" # Reference - # reference -> Reference of NodeTypes (Flowpack.NodeTemplates:Content.DifferentPropertyTypes) with value Node(7f7bac1c-9400-4db5-bbaa-2b8251d127c5) + # reference -> Reference of NodeTypes (Flowpack.NodeTemplates:Content.Properties) with value Node(7f7bac1c-9400-4db5-bbaa-2b8251d127c5) diff --git a/Configuration/Testing/NodeTypes.ResolvablePropertyValues.yaml b/Tests/Functional/Features/ResolvableProperties/NodeTypes.ResolvableProperties.yaml similarity index 91% rename from Configuration/Testing/NodeTypes.ResolvablePropertyValues.yaml rename to Tests/Functional/Features/ResolvableProperties/NodeTypes.ResolvableProperties.yaml index f9ff312..2d497c8 100644 --- a/Configuration/Testing/NodeTypes.ResolvablePropertyValues.yaml +++ b/Tests/Functional/Features/ResolvableProperties/NodeTypes.ResolvableProperties.yaml @@ -2,7 +2,7 @@ # Also reference node id's should be correctly resolved --- -'Flowpack.NodeTemplates:Content.ResolvablePropertyValues': +'Flowpack.NodeTemplates:Content.ResolvableProperties': superTypes: 'Neos.Neos:Content': true properties: diff --git a/Configuration/Testing/NodeTypes.UnresolvablePropertyValues.yaml b/Tests/Functional/Features/ResolvableProperties/NodeTypes.UnresolvableProperties.yaml similarity index 86% rename from Configuration/Testing/NodeTypes.UnresolvablePropertyValues.yaml rename to Tests/Functional/Features/ResolvableProperties/NodeTypes.UnresolvableProperties.yaml index 424bccd..a81cee3 100644 --- a/Configuration/Testing/NodeTypes.UnresolvablePropertyValues.yaml +++ b/Tests/Functional/Features/ResolvableProperties/NodeTypes.UnresolvableProperties.yaml @@ -1,11 +1,11 @@ # We make sure that we dont trigger unwanted property mapping, so we wont allow an array in a string field. --- -'Flowpack.NodeTemplates:Content.UnresolvablePropertyValues': +'Flowpack.NodeTemplates:Content.UnresolvableProperties': superTypes: 'Neos.Neos:Content': true ui: - label: UnresolvablePropertyValues + label: UnresolvableProperties properties: someString: type: string diff --git a/Tests/Functional/Features/ResolvableProperties/ResolvablePropertiesTest.php b/Tests/Functional/Features/ResolvableProperties/ResolvablePropertiesTest.php new file mode 100644 index 0000000..9860185 --- /dev/null +++ b/Tests/Functional/Features/ResolvableProperties/ResolvablePropertiesTest.php @@ -0,0 +1,62 @@ +createFakeNode('some-node-id'); + $this->createFakeNode('other-node-id'); + + $resource = $this->objectManager->get(ResourceManager::class)->importResource(__DIR__ . '/image.png'); + + $asset = new Asset($resource); + ObjectAccess::setProperty($asset, 'Persistence_Object_Identifier', 'c228200e-7472-4290-9936-4454a5b5692a', true); + $this->objectManager->get(AssetRepository::class)->add($asset); + + $resource2 = $this->objectManager->get(ResourceManager::class)->importResource(__DIR__ . '/image.png'); + + $image = new Image($resource2); + ObjectAccess::setProperty($image, 'Persistence_Object_Identifier', 'c8ae9f9f-dd11-4373-bf42-4bf31ec5bd19', true); + $this->objectManager->get(ImageRepository::class)->add($image); + + $createdNode = $this->createNodeInto( + $this->homePageMainContentCollectionNode, + 'Flowpack.NodeTemplates:Content.ResolvableProperties', + [ + 'realNode' => $this->createFakeNode('real-node-id') + ] + ); + + $this->assertLastCreatedTemplateMatchesSnapshot('ResolvableProperties'); + $this->assertNoExceptionsWereCaught(); + $this->assertNodeDumpAndTemplateDumpMatchSnapshot('ResolvableProperties', $createdNode); + } + + /** @test */ + public function itMatchesSnapshot2(): void + { + $createdNode = $this->createNodeInto( + $this->homePageMainContentCollectionNode, + 'Flowpack.NodeTemplates:Content.UnresolvableProperties', + [] + ); + + $this->assertLastCreatedTemplateMatchesSnapshot('UnresolvableProperties'); + + $this->assertCaughtExceptionsMatchesSnapshot('UnresolvableProperties'); + $this->assertNodeDumpAndTemplateDumpMatchSnapshot('UnresolvableProperties', $createdNode); + } +} diff --git a/Tests/Functional/Fixtures/ResolvablePropertyValues.nodes.json b/Tests/Functional/Features/ResolvableProperties/Snapshots/ResolvableProperties.nodes.json similarity index 100% rename from Tests/Functional/Fixtures/ResolvablePropertyValues.nodes.json rename to Tests/Functional/Features/ResolvableProperties/Snapshots/ResolvableProperties.nodes.json diff --git a/Tests/Functional/Fixtures/ResolvablePropertyValues.template.json b/Tests/Functional/Features/ResolvableProperties/Snapshots/ResolvableProperties.template.json similarity index 100% rename from Tests/Functional/Fixtures/ResolvablePropertyValues.template.json rename to Tests/Functional/Features/ResolvableProperties/Snapshots/ResolvableProperties.template.json diff --git a/Tests/Functional/Fixtures/ResolvablePropertyValues.yaml b/Tests/Functional/Features/ResolvableProperties/Snapshots/ResolvableProperties.yaml similarity index 100% rename from Tests/Functional/Fixtures/ResolvablePropertyValues.yaml rename to Tests/Functional/Features/ResolvableProperties/Snapshots/ResolvableProperties.yaml diff --git a/Tests/Functional/Features/ResolvableProperties/Snapshots/UnresolvableProperties.messages.json b/Tests/Functional/Features/ResolvableProperties/Snapshots/UnresolvableProperties.messages.json new file mode 100644 index 0000000..aa1b97a --- /dev/null +++ b/Tests/Functional/Features/ResolvableProperties/Snapshots/UnresolvableProperties.messages.json @@ -0,0 +1,26 @@ +[ + { + "message": "Template for \"UnresolvableProperties\" only partially applied. Please check the newly created nodes beneath Node \/sites\/test-site\/homepage\/main\/new-node@live[Flowpack.NodeTemplates:Content.UnresolvableProperties].", + "severity": "ERROR" + }, + { + "message": "Property \"asset\" in NodeType \"Flowpack.NodeTemplates:Content.UnresolvableProperties\" | FlowException(Object of type \"Neos\\Media\\Domain\\Model\\Asset\" with identity \"non-existing\" not found., 1686779371122)", + "severity": "ERROR" + }, + { + "message": "Property \"images\" in NodeType \"Flowpack.NodeTemplates:Content.UnresolvableProperties\" | FlowException(Could not convert target type \"array\", at property path \"0\": No converter found which can be used to convert from \"string\" to \"Neos\\Media\\Domain\\Model\\ImageInterface\"., 1297759968) | TypeConverterException(No converter found which can be used to convert from \"string\" to \"Neos\\Media\\Domain\\Model\\ImageInterface\"., 0)", + "severity": "ERROR" + }, + { + "message": "Property \"someString\" in NodeType \"Flowpack.NodeTemplates:Content.UnresolvableProperties\" | PropertyIgnoredException(Because value `[\"foo\"]` is not assignable to property type \"string\"., 1685958105644)", + "severity": "ERROR" + }, + { + "message": "Reference \"reference\" in NodeType \"Flowpack.NodeTemplates:Content.UnresolvableProperties\" | RuntimeException(Reference could not be set, because node reference(s) true cannot be resolved., 1685958176560)", + "severity": "ERROR" + }, + { + "message": "Reference \"references\" in NodeType \"Flowpack.NodeTemplates:Content.UnresolvableProperties\" | RuntimeException(Reference could not be set, because node reference(s) [\"some-non-existing-node-id\"] cannot be resolved., 1685958176560)", + "severity": "ERROR" + } +] diff --git a/Tests/Functional/Features/ResolvableProperties/Snapshots/UnresolvableProperties.nodes.json b/Tests/Functional/Features/ResolvableProperties/Snapshots/UnresolvableProperties.nodes.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/Tests/Functional/Features/ResolvableProperties/Snapshots/UnresolvableProperties.nodes.json @@ -0,0 +1 @@ +[] diff --git a/Tests/Functional/Fixtures/UnresolvablePropertyValues.template.json b/Tests/Functional/Features/ResolvableProperties/Snapshots/UnresolvableProperties.template.json similarity index 100% rename from Tests/Functional/Fixtures/UnresolvablePropertyValues.template.json rename to Tests/Functional/Features/ResolvableProperties/Snapshots/UnresolvableProperties.template.json diff --git a/Tests/Functional/Features/ResolvableProperties/Snapshots/UnresolvableProperties.yaml b/Tests/Functional/Features/ResolvableProperties/Snapshots/UnresolvableProperties.yaml new file mode 100644 index 0000000..f133bb4 --- /dev/null +++ b/Tests/Functional/Features/ResolvableProperties/Snapshots/UnresolvableProperties.yaml @@ -0,0 +1,3 @@ +'{nodeTypeName}': + options: + template: [] diff --git a/Tests/Functional/image.png b/Tests/Functional/Features/ResolvableProperties/image.png similarity index 100% rename from Tests/Functional/image.png rename to Tests/Functional/Features/ResolvableProperties/image.png diff --git a/Tests/Functional/Fixtures/UnresolvablePropertyValues.messages.json b/Tests/Functional/Fixtures/UnresolvablePropertyValues.messages.json deleted file mode 100644 index 7e514a2..0000000 --- a/Tests/Functional/Fixtures/UnresolvablePropertyValues.messages.json +++ /dev/null @@ -1,26 +0,0 @@ -[ - { - "message": "Template for \"UnresolvablePropertyValues\" only partially applied. Please check the newly created nodes beneath Node \/sites\/test-site\/homepage\/main\/new-node@live[Flowpack.NodeTemplates:Content.UnresolvablePropertyValues].", - "severity": "ERROR" - }, - { - "message": "Property \"asset\" in NodeType \"Flowpack.NodeTemplates:Content.UnresolvablePropertyValues\" | FlowException(Object of type \"Neos\\Media\\Domain\\Model\\Asset\" with identity \"non-existing\" not found., 1686779371122)", - "severity": "ERROR" - }, - { - "message": "Property \"images\" in NodeType \"Flowpack.NodeTemplates:Content.UnresolvablePropertyValues\" | FlowException(Could not convert target type \"array\", at property path \"0\": No converter found which can be used to convert from \"string\" to \"Neos\\Media\\Domain\\Model\\ImageInterface\"., 1297759968) | TypeConverterException(No converter found which can be used to convert from \"string\" to \"Neos\\Media\\Domain\\Model\\ImageInterface\"., 0)", - "severity": "ERROR" - }, - { - "message": "Property \"someString\" in NodeType \"Flowpack.NodeTemplates:Content.UnresolvablePropertyValues\" | PropertyIgnoredException(Because value `[\"foo\"]` is not assignable to property type \"string\"., 1685958105644)", - "severity": "ERROR" - }, - { - "message": "Reference \"reference\" in NodeType \"Flowpack.NodeTemplates:Content.UnresolvablePropertyValues\" | RuntimeException(Reference could not be set, because node reference(s) true cannot be resolved., 1685958176560)", - "severity": "ERROR" - }, - { - "message": "Reference \"references\" in NodeType \"Flowpack.NodeTemplates:Content.UnresolvablePropertyValues\" | RuntimeException(Reference could not be set, because node reference(s) [\"some-non-existing-node-id\"] cannot be resolved., 1685958176560)", - "severity": "ERROR" - } -] diff --git a/Tests/Functional/NodeTemplateTest.php b/Tests/Functional/NodeTemplateTest.php deleted file mode 100644 index 6780ee9..0000000 --- a/Tests/Functional/NodeTemplateTest.php +++ /dev/null @@ -1,390 +0,0 @@ -setupContentRepository(); - $this->nodeTemplateDumper = $this->objectManager->get(NodeTemplateDumper::class); - - $templateFactory = $this->objectManager->get(TemplateConfigurationProcessor::class); - - $templateFactoryMock = $this->getMockBuilder(TemplateConfigurationProcessor::class)->disableOriginalConstructor()->getMock(); - $templateFactoryMock->expects(self::once())->method('processTemplateConfiguration')->willReturnCallback(function (...$args) use($templateFactory) { - $rootTemplate = $templateFactory->processTemplateConfiguration(...$args); - $this->lastCreatedRootTemplate = $rootTemplate; - return $rootTemplate; - }); - $this->objectManager->setInstance(TemplateConfigurationProcessor::class, $templateFactoryMock); - } - - public function tearDown(): void - { - parent::tearDown(); - $this->inject($this->contextFactory, 'contextInstances', []); - $this->objectManager->get(FeedbackCollection::class)->reset(); - $this->objectManager->forgetInstance(ContentDimensionRepository::class); - $this->objectManager->forgetInstance(TemplateConfigurationProcessor::class); - } - - private function setupContentRepository(): void - { - // Create an environment to create nodes. - $this->objectManager->get(ContentDimensionRepository::class)->setDimensionsConfiguration([]); - - $liveWorkspace = new Workspace('live'); - $workspaceRepository = $this->objectManager->get(WorkspaceRepository::class); - $workspaceRepository->add($liveWorkspace); - - $testSite = new Site('test-site'); - $testSite->setSiteResourcesPackageKey('Test.Site'); - $siteRepository = $this->objectManager->get(SiteRepository::class); - $siteRepository->add($testSite); - - $this->persistenceManager->persistAll(); - $this->contextFactory = $this->objectManager->get(ContextFactoryInterface::class); - $this->subgraph = $this->contextFactory->create(['workspaceName' => 'live']); - - $rootNode = $this->subgraph->getRootNode(); - - $nodeTypeManager = $this->objectManager->get(NodeTypeManager::class); - - $sitesRootNode = $rootNode->createNode('sites'); - $testSiteNode = $sitesRootNode->createNode('test-site'); - $this->homePageNode = $testSiteNode->createNode( - 'homepage', - $nodeTypeManager->getNodeType('Flowpack.NodeTemplates:Document.Page') - ); - } - - /** - * @param NodeInterface $targetNode - * @param array $nodeCreationDialogValues - */ - private function createNodeInto(NodeInterface $targetNode, NodeTypeName $nodeTypeName, array $nodeCreationDialogValues): NodeInterface - { - $targetNodeContextPath = $targetNode->getContextPath(); - - $changeCollectionSerialized = [[ - 'type' => 'Neos.Neos.Ui:CreateInto', - 'subject' => $targetNodeContextPath, - 'payload' => [ - 'parentContextPath' => $targetNodeContextPath, - 'parentDomAddress' => [ - 'contextPath' => $targetNodeContextPath, - ], - 'nodeType' => $nodeTypeName->getValue(), - 'name' => 'new-node', - 'data' => $nodeCreationDialogValues, - 'baseNodeType' => '', - ], - ]]; - - $changeCollection = (new ChangeCollectionConverter())->convertFrom($changeCollectionSerialized, null); - assert($changeCollection instanceof ChangeCollection); - $changeCollection->apply(); - - return $targetNode->getNode('new-node'); - } - - - /** @test */ - public function testNodeCreationMatchesSnapshot1(): void - { - $this->createNodeInto( - $targetNode = $this->homePageNode->getNode('main'), - $toBeCreatedNodeTypeName = NodeTypeName::fromString('Flowpack.NodeTemplates:Content.Columns.Two'), - [] - ); - - $this->assertLastCreatedTemplateMatchesSnapshot('TwoColumnPreset'); - - $createdNode = $targetNode->getChildNodes($toBeCreatedNodeTypeName->getValue())[0]; - - self::assertSame([], $this->getMessagesOfFeedbackCollection()); - - $this->assertNodeDumpAndTemplateDumpMatchSnapshot('TwoColumnPreset', $createdNode); - } - - /** @test */ - public function testNodeCreationMatchesSnapshot2(): void - { - $this->createNodeInto( - $targetNode = $this->homePageNode->getNode('main'), - $toBeCreatedNodeTypeName = NodeTypeName::fromString('Flowpack.NodeTemplates:Content.Columns.Two.CreationDialogAndWithItems'), - [ - 'text' => '

bar

' - ] - ); - - $this->assertLastCreatedTemplateMatchesSnapshot('TwoColumnPreset'); - - $createdNode = $targetNode->getChildNodes($toBeCreatedNodeTypeName->getValue())[0]; - - self::assertSame([], $this->getMessagesOfFeedbackCollection()); - $this->assertNodeDumpAndTemplateDumpMatchSnapshot('TwoColumnPreset', $createdNode); - } - - /** @test */ - public function testNodeCreationMatchesSnapshot3(): void - { - $this->createNodeInto( - $targetNode = $this->homePageNode->getNode('main'), - $toBeCreatedNodeTypeName = NodeTypeName::fromString('Flowpack.NodeTemplates:Content.Columns.Two.WithContext'), - [] - ); - - $this->assertLastCreatedTemplateMatchesSnapshot('TwoColumnPreset'); - - $createdNode = $targetNode->getChildNodes($toBeCreatedNodeTypeName->getValue())[0]; - - self::assertSame([], $this->getMessagesOfFeedbackCollection()); - $this->assertNodeDumpAndTemplateDumpMatchSnapshot('TwoColumnPreset', $createdNode); - } - - - /** @test */ - public function transliterateNodeName(): void - { - $this->createNodeInto( - $targetNode = $this->homePageNode->getNode('main'), - $toBeCreatedNodeTypeName = NodeTypeName::fromString('Flowpack.NodeTemplates:Content.TransliterateNodeName'), - [] - ); - - $this->assertLastCreatedTemplateMatchesSnapshot('TransliterateNodeName'); - - $createdNode = $targetNode->getChildNodes($toBeCreatedNodeTypeName->getValue())[0]; - - self::assertSame([], $this->getMessagesOfFeedbackCollection()); - $this->assertNodeDumpAndTemplateDumpMatchSnapshot('TransliterateNodeName', $createdNode); - } - - - /** @test */ - public function testNodeCreationWithDifferentPropertyTypes(): void - { - $this->createNodeInto( - $targetNode = $this->homePageNode->getNode('main'), - $toBeCreatedNodeTypeName = NodeTypeName::fromString('Flowpack.NodeTemplates:Content.DifferentPropertyTypes'), - [ - 'someNode' => $this->homePageNode->createNode('some-node', null, '7f7bac1c-9400-4db5-bbaa-2b8251d127c5') - ] - ); - - $this->assertLastCreatedTemplateMatchesSnapshot('DifferentPropertyTypes'); - - $createdNode = $targetNode->getChildNodes($toBeCreatedNodeTypeName->getValue())[0]; - - self::assertSame([], $this->getMessagesOfFeedbackCollection()); - $this->assertNodeDumpAndTemplateDumpMatchSnapshot('DifferentPropertyTypes', $createdNode); - } - - /** @test */ - public function resolvablePropertyValues(): void - { - $this->homePageNode->createNode('some-node', null, 'some-node-id'); - $this->homePageNode->createNode('other-node', null, 'other-node-id'); - - $resource = $this->objectManager->get(ResourceManager::class)->importResource(__DIR__ . '/image.png'); - - $asset = new Asset($resource); - ObjectAccess::setProperty($asset, 'Persistence_Object_Identifier', 'c228200e-7472-4290-9936-4454a5b5692a', true); - $this->objectManager->get(AssetRepository::class)->add($asset); - - $resource = $this->objectManager->get(ResourceManager::class)->importResource(__DIR__ . '/image.png'); - - $image = new Image($resource); - ObjectAccess::setProperty($image, 'Persistence_Object_Identifier', 'c8ae9f9f-dd11-4373-bf42-4bf31ec5bd19', true); - $this->objectManager->get(ImageRepository::class)->add($image); - - $this->persistenceManager->persistAll(); - - $createdNode = $this->createNodeInto( - $this->homePageNode->getNode('main'), - NodeTypeName::fromString('Flowpack.NodeTemplates:Content.ResolvablePropertyValues'), - [ - 'realNode' => $this->homePageNode->createNode('real-node', null, 'real-node-id') - ] - ); - - $this->assertLastCreatedTemplateMatchesSnapshot('ResolvablePropertyValues'); - - self::assertSame([], $this->getMessagesOfFeedbackCollection()); - $this->assertNodeDumpAndTemplateDumpMatchSnapshot('ResolvablePropertyValues', $createdNode); - } - - /** @test */ - public function unresolvablePropertyValues(): void - { - $createdNode = $this->createNodeInto( - $this->homePageNode->getNode('main'), - NodeTypeName::fromString('Flowpack.NodeTemplates:Content.UnresolvablePropertyValues'), - [] - ); - - $this->assertLastCreatedTemplateMatchesSnapshot('UnresolvablePropertyValues'); - - $this->assertCaughtExceptionsMatchesSnapshot('UnresolvablePropertyValues'); - $this->assertNodeDumpAndTemplateDumpMatchSnapshot('UnresolvablePropertyValues', $createdNode); - } - - /** @test */ - public function exceptionsAreCaughtAndPartialTemplateIsBuild(): void - { - $this->createNodeInto( - $targetNode = $this->homePageNode->getNode('main'), - $toBeCreatedNodeTypeName = NodeTypeName::fromString('Flowpack.NodeTemplates:Content.WithEvaluationExceptions'), - [] - ); - - $this->assertLastCreatedTemplateMatchesSnapshot('WithEvaluationExceptions'); - - $createdNode = $targetNode->getChildNodes($toBeCreatedNodeTypeName->getValue())[0]; - - $this->assertCaughtExceptionsMatchesSnapshot('WithEvaluationExceptions'); - $this->assertNodeDumpAndTemplateDumpMatchSnapshot('WithEvaluationExceptions', $createdNode); - } - - /** @test */ - public function exceptionsAreCaughtAndPartialTemplateNotBuild(): void - { - $this->withMockedConfigurationSettings([ - 'Flowpack' => [ - 'NodeTemplates' => [ - 'exceptionHandling' => [ - 'templateConfigurationProcessing' => [ - 'stopOnException' => true - ] - ] - ] - ] - ], function () { - $this->createNodeInto( - $targetNode = $this->homePageNode->getNode('main'), - $toBeCreatedNodeTypeName = NodeTypeName::fromString('Flowpack.NodeTemplates:Content.WithOneEvaluationException'), - [] - ); - - $this->assertLastCreatedTemplateMatchesSnapshot('WithOneEvaluationException'); - - self::assertSame([ - [ - 'message' => 'Template for "WithOneEvaluationException" was not applied. Only Node /sites/test-site/homepage/main/new-node@live[Flowpack.NodeTemplates:Content.WithOneEvaluationException] was created.', - 'severity' => 'ERROR' - ], - [ - 'message' => 'Expression "${\'left open" in "childNodes.abort.when" | EelException(The EEL expression "${\'left open" was not a valid EEL expression. Perhaps you forgot to wrap it in ${...}?, 1410441849)', - 'severity' => 'ERROR' - ] - ], $this->getMessagesOfFeedbackCollection()); - - $createdNode = $targetNode->getChildNodes($toBeCreatedNodeTypeName->getValue())[0]; - - self::assertEmpty($createdNode->getChildNodes()); - }); - } - - /** @test */ - public function testPageNodeCreationMatchesSnapshot1(): void - { - $this->createNodeInto( - $targetNode = $this->homePageNode, - $toBeCreatedNodeTypeName = NodeTypeName::fromString('Flowpack.NodeTemplates:Document.Page.Static'), - [] - ); - - $this->assertLastCreatedTemplateMatchesSnapshot('PagePreset'); - - $createdNode = $targetNode->getChildNodes($toBeCreatedNodeTypeName->getValue())[0]; - - self::assertSame([], $this->getMessagesOfFeedbackCollection()); - $this->assertNodeDumpAndTemplateDumpMatchSnapshot('PagePreset', $createdNode); - } - - /** @test */ - public function testPageNodeCreationMatchesSnapshot2(): void - { - $this->createNodeInto( - $targetNode = $this->homePageNode, - $toBeCreatedNodeTypeName = NodeTypeName::fromString('Flowpack.NodeTemplates:Document.Page.Dynamic'), - [ - 'title' => 'Page1' - ] - ); - - $createdNode = $targetNode->getChildNodes($toBeCreatedNodeTypeName->getValue())[0]; - - self::assertSame([], $this->getMessagesOfFeedbackCollection()); - $this->assertNodeDumpAndTemplateDumpMatchSnapshot('PagePreset', $createdNode); - } - - private function assertLastCreatedTemplateMatchesSnapshot(string $snapShotName): void - { - $lastCreatedTemplate = $this->serializeValuesInArray( - $this->lastCreatedRootTemplate->jsonSerialize() - ); - $this->assertJsonStringEqualsJsonFileOrCreateSnapshot(__DIR__ . '/Fixtures/' . $snapShotName . '.template.json', json_encode($lastCreatedTemplate, JSON_PRETTY_PRINT)); - } - - private function assertCaughtExceptionsMatchesSnapshot(string $snapShotName): void - { - $this->assertJsonStringEqualsJsonFileOrCreateSnapshot(__DIR__ . '/Fixtures/' . $snapShotName . '.messages.json', json_encode($this->getMessagesOfFeedbackCollection(), JSON_PRETTY_PRINT)); - } - - private function assertNodeDumpAndTemplateDumpMatchSnapshot(string $snapShotName, NodeInterface $node): void - { - $serializedNodes = $this->jsonSerializeNodeAndDescendents($node); - unset($serializedNodes['nodeTypeName']); - $this->assertJsonStringEqualsJsonFileOrCreateSnapshot(__DIR__ . '/Fixtures/' . $snapShotName . '.nodes.json', json_encode($serializedNodes, JSON_PRETTY_PRINT)); - - $dumpedYamlTemplate = $this->nodeTemplateDumper->createNodeTemplateYamlDumpFromSubtree($node); - - $yamlTemplateWithoutOriginNodeTypeName = '\'{nodeTypeName}\'' . substr($dumpedYamlTemplate, strlen($node->getNodeType()->getName()) + 2); - - $this->assertStringEqualsFileOrCreateSnapshot(__DIR__ . '/Fixtures/' . $snapShotName . '.yaml', $yamlTemplateWithoutOriginNodeTypeName); - } -} From ad45c98e0ddc921a936fe4022b029722553dd9ff Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Fri, 23 Jun 2023 20:21:03 +0200 Subject: [PATCH 12/24] BUGFIX: 66 correct constraint checks --- .../NodeCreation/NodeConstraintException.php | 7 ++ .../NodeCreation/NodeCreationService.php | 14 ++-- .../Domain/NodeCreation/ToBeCreatedNode.php | 81 ++++++++++++++++++- .../Domain/TemplateNodeCreationHandler.php | 2 +- .../Features/ChildNodes/ChildNodesTest.php | 34 +++++++- .../NodeTypes.AllowedChildNodes.yaml | 20 +++++ .../NodeTypes.DisallowedChildNodes.yaml | 22 +++++ .../Snapshots/AllowedChildNodes.nodes.json | 16 ++++ .../Snapshots/AllowedChildNodes.template.json | 20 +++++ .../Snapshots/AllowedChildNodes.yaml | 12 +++ .../DisallowedChildNodes.messages.json | 14 ++++ .../Snapshots/DisallowedChildNodes.nodes.json | 8 ++ .../DisallowedChildNodes.template.json | 26 ++++++ .../Snapshots/DisallowedChildNodes.yaml | 3 + .../Snapshots/SomeExceptions.messages.json | 2 +- Tests/Functional/Features/NodeTypes.yaml | 7 ++ 16 files changed, 274 insertions(+), 14 deletions(-) create mode 100644 Classes/Domain/NodeCreation/NodeConstraintException.php create mode 100644 Tests/Functional/Features/ChildNodes/NodeTypes.AllowedChildNodes.yaml create mode 100644 Tests/Functional/Features/ChildNodes/NodeTypes.DisallowedChildNodes.yaml create mode 100644 Tests/Functional/Features/ChildNodes/Snapshots/AllowedChildNodes.nodes.json create mode 100644 Tests/Functional/Features/ChildNodes/Snapshots/AllowedChildNodes.template.json create mode 100644 Tests/Functional/Features/ChildNodes/Snapshots/AllowedChildNodes.yaml create mode 100644 Tests/Functional/Features/ChildNodes/Snapshots/DisallowedChildNodes.messages.json create mode 100644 Tests/Functional/Features/ChildNodes/Snapshots/DisallowedChildNodes.nodes.json create mode 100644 Tests/Functional/Features/ChildNodes/Snapshots/DisallowedChildNodes.template.json create mode 100644 Tests/Functional/Features/ChildNodes/Snapshots/DisallowedChildNodes.yaml diff --git a/Classes/Domain/NodeCreation/NodeConstraintException.php b/Classes/Domain/NodeCreation/NodeConstraintException.php new file mode 100644 index 0000000..c483d92 --- /dev/null +++ b/Classes/Domain/NodeCreation/NodeConstraintException.php @@ -0,0 +1,7 @@ +merge( $this->createMutatorCollectionFromTemplate( $template->getChildNodes(), - new ToBeCreatedNode($nodeType), + $node, $caughtExceptions ) ); @@ -104,7 +104,7 @@ private function createMutatorCollectionFromTemplate(Templates $templates, ToBeC NodeMutator::setProperties($properties) )->merge($this->createMutatorCollectionFromTemplate( $template->getChildNodes(), - new ToBeCreatedNode($nodeType), + $parentNode->forTetheredChildNode($template->getName()), $caughtExceptions )) ) @@ -135,15 +135,15 @@ private function createMutatorCollectionFromTemplate(Templates $templates, ToBeC continue; } - if (!$parentNode->getNodeType()->allowsChildNodeType($nodeType)) { + try { + $parentNode->requireConstraintsImposedByAncestorsAreMet($nodeType); + } catch (NodeConstraintException $nodeConstraintException) { $caughtExceptions->add( - CaughtException::fromException(new \RuntimeException(sprintf('Node type "%s" is not allowed for child nodes of type %s', $template->getType()->getValue(), $parentNode->getNodeType()->getName()), 1686417627173)) + CaughtException::fromException($nodeConstraintException) ); continue; } - // todo maybe check also explicitly for allowsGrandchildNodeType (we do this currently like below) - $propertiesAndReferences = PropertiesAndReferences::createFromArrayAndTypeDeclarations($this->convertProperties($nodeType, $template->getProperties(), $caughtExceptions), $nodeType); $properties = array_merge( @@ -159,7 +159,7 @@ private function createMutatorCollectionFromTemplate(Templates $templates, ToBeC $this->createMutatorForUriPathSegment($template) )->merge($this->createMutatorCollectionFromTemplate( $template->getChildNodes(), - new ToBeCreatedNode($nodeType), + $parentNode->forRegularChildNode($nodeType), $caughtExceptions )) ) diff --git a/Classes/Domain/NodeCreation/ToBeCreatedNode.php b/Classes/Domain/NodeCreation/ToBeCreatedNode.php index dfc9cda..c023a15 100644 --- a/Classes/Domain/NodeCreation/ToBeCreatedNode.php +++ b/Classes/Domain/NodeCreation/ToBeCreatedNode.php @@ -3,6 +3,7 @@ namespace Flowpack\NodeTemplates\Domain\NodeCreation; use Neos\ContentRepository\Domain\Model\NodeType; +use Neos\ContentRepository\Domain\NodeAggregate\NodeName; use Neos\Flow\Annotations as Flow; /** @@ -12,14 +13,88 @@ class ToBeCreatedNode { private NodeType $nodeType; - public function __construct( - NodeType $nodeType - ) { + /** @var \Closure(NodeType $nodeType): void */ + private \Closure $requireConstraintsImposedByAncestorsAreMet; + + private function __construct(NodeType $nodeType, \Closure $requireConstraintsImposedByAncestorsAreMet) + { $this->nodeType = $nodeType; + $this->requireConstraintsImposedByAncestorsAreMet = $requireConstraintsImposedByAncestorsAreMet; + } + + public static function fromRegular(NodeType $nodeType): self + { + $parentNodeType = $nodeType; + $requireConstraintsImposedByAncestorsAreMet = function (NodeType $nodeType) use ($parentNodeType) : void { + self::requireNodeTypeConstraintsImposedByParentToBeMet($parentNodeType, $nodeType); + }; + return new self($nodeType, $requireConstraintsImposedByAncestorsAreMet); + } + + public function forTetheredChildNode(NodeName $nodeName): self + { + $parentNodeType = $this->nodeType; + // `getTypeOfAutoCreatedChildNode` actually has a bug; it looks up the NodeName parameter against the raw configuration instead of the transliterated NodeName + // https://github.com/neos/neos-ui/issues/3527 + $parentNodesAutoCreatedChildNodes = $parentNodeType->getAutoCreatedChildNodes(); + $childNodeType = $parentNodesAutoCreatedChildNodes[$nodeName->__toString()] ?? null; + if (!$childNodeType instanceof NodeType) { + throw new \InvalidArgumentException('forTetheredChildNode only works for tethered nodes.'); + } + $requireConstraintsImposedByAncestorsAreMet = function (NodeType $nodeType) use ($parentNodeType, $nodeName) : void { + self::requireNodeTypeConstraintsImposedByGrandparentToBeMet($parentNodeType, $nodeName, $nodeType); + }; + return new self($childNodeType, $requireConstraintsImposedByAncestorsAreMet); + } + + public function forRegularChildNode(NodeType $nodeType): self + { + $parentNodeType = $this->nodeType; + $requireConstraintsImposedByAncestorsAreMet = function (NodeType $nodeType) use ($parentNodeType) : void { + self::requireNodeTypeConstraintsImposedByParentToBeMet($parentNodeType, $nodeType); + }; + return new self($nodeType, $requireConstraintsImposedByAncestorsAreMet); + } + + /** + * @throws NodeConstraintException + */ + public function requireConstraintsImposedByAncestorsAreMet(NodeType $nodeType): void + { + ($this->requireConstraintsImposedByAncestorsAreMet)($nodeType); } public function getNodeType(): NodeType { return $this->nodeType; } + + private static function requireNodeTypeConstraintsImposedByParentToBeMet(NodeType $parentNodeType, NodeType $nodeType): void + { + if (!$parentNodeType->allowsChildNodeType($nodeType)) { + throw new NodeConstraintException( + sprintf( + 'Node type "%s" is not allowed for child nodes of type %s', + $nodeType->getName(), + $parentNodeType->getName() + ), + 1686417627173 + ); + } + } + + private static function requireNodeTypeConstraintsImposedByGrandparentToBeMet(NodeType $grandParentNodeType, NodeName $nodeName, NodeType $nodeType): void + { + if (!$grandParentNodeType->allowsGrandchildNodeType($nodeName->__toString(), $nodeType)) { + throw new NodeConstraintException( + sprintf( + 'Node type "%s" is not allowed below tethered child nodes "%s" of nodes of type "%s"', + $nodeType->getName(), + $nodeName->__toString(), + $grandParentNodeType->getName() + ), + 1687541480146 + ); + } + } } diff --git a/Classes/Domain/TemplateNodeCreationHandler.php b/Classes/Domain/TemplateNodeCreationHandler.php index ec3d2d6..25ab890 100644 --- a/Classes/Domain/TemplateNodeCreationHandler.php +++ b/Classes/Domain/TemplateNodeCreationHandler.php @@ -51,7 +51,7 @@ public function handle(NodeInterface $node, array $data): void $template = $this->templateConfigurationProcessor->processTemplateConfiguration($templateConfiguration, $evaluationContext, $caughtExceptions); $this->exceptionHandler->handleAfterTemplateConfigurationProcessing($caughtExceptions, $node); - $nodeMutators = (new NodeCreationService($node->getContext()))->createMutatorCollection($template, new ToBeCreatedNode($node->getNodeType()), $caughtExceptions); + $nodeMutators = (new NodeCreationService($node->getContext()))->createMutatorCollection($template, ToBeCreatedNode::fromRegular($node->getNodeType()), $caughtExceptions); $nodeMutators->apply($node); $this->exceptionHandler->handleAfterNodeCreation($caughtExceptions, $node); } catch (TemplateNotCreatedException|TemplatePartiallyCreatedException $templateCreationException) { diff --git a/Tests/Functional/Features/ChildNodes/ChildNodesTest.php b/Tests/Functional/Features/ChildNodes/ChildNodesTest.php index 15da9e1..ab1a372 100644 --- a/Tests/Functional/Features/ChildNodes/ChildNodesTest.php +++ b/Tests/Functional/Features/ChildNodes/ChildNodesTest.php @@ -25,7 +25,7 @@ public function itMatchesSnapshot1(): void /** @test */ - public function testNodeCreationMatchesSnapshot2(): void + public function itMatchesSnapshot2(): void { $createdNode = $this->createNodeInto( $this->homePageMainContentCollectionNode, @@ -42,7 +42,7 @@ public function testNodeCreationMatchesSnapshot2(): void } /** @test */ - public function testNodeCreationMatchesSnapshot3(): void + public function itMatchesSnapshot3(): void { $createdNode = $this->createNodeInto( $this->homePageMainContentCollectionNode, @@ -55,4 +55,34 @@ public function testNodeCreationMatchesSnapshot3(): void $this->assertNoExceptionsWereCaught(); $this->assertNodeDumpAndTemplateDumpMatchSnapshot('ChildNodes', $createdNode); } + + /** @test */ + public function itMatchesSnapshot4(): void + { + $createdNode = $this->createNodeInto( + $this->homePageMainContentCollectionNode, + 'Flowpack.NodeTemplates:Content.AllowedChildNodes', + [] + ); + + $this->assertLastCreatedTemplateMatchesSnapshot('AllowedChildNodes'); + + $this->assertNoExceptionsWereCaught(); + $this->assertNodeDumpAndTemplateDumpMatchSnapshot('AllowedChildNodes', $createdNode); + } + + /** @test */ + public function itMatchesSnapshot5(): void + { + $createdNode = $this->createNodeInto( + $this->homePageMainContentCollectionNode, + 'Flowpack.NodeTemplates:Content.DisallowedChildNodes', + [] + ); + + $this->assertLastCreatedTemplateMatchesSnapshot('DisallowedChildNodes'); + + $this->assertCaughtExceptionsMatchesSnapshot('DisallowedChildNodes'); + $this->assertNodeDumpAndTemplateDumpMatchSnapshot('DisallowedChildNodes', $createdNode); + } } diff --git a/Tests/Functional/Features/ChildNodes/NodeTypes.AllowedChildNodes.yaml b/Tests/Functional/Features/ChildNodes/NodeTypes.AllowedChildNodes.yaml new file mode 100644 index 0000000..be035ac --- /dev/null +++ b/Tests/Functional/Features/ChildNodes/NodeTypes.AllowedChildNodes.yaml @@ -0,0 +1,20 @@ + +'Flowpack.NodeTemplates:Content.AllowedChildNodes': + superTypes: + 'Neos.Neos:Content': true + childNodes: + content: + type: 'Flowpack.NodeTemplates:Collection.Disallowed' + constraints: + nodeTypes: + 'Flowpack.NodeTemplates:Content.Text': true + options: + template: + childNodes: + content: + name: 'content' + childNodes: + text: + type: 'Flowpack.NodeTemplates:Content.Text' + properties: + text: 'Text' diff --git a/Tests/Functional/Features/ChildNodes/NodeTypes.DisallowedChildNodes.yaml b/Tests/Functional/Features/ChildNodes/NodeTypes.DisallowedChildNodes.yaml new file mode 100644 index 0000000..89ce8c5 --- /dev/null +++ b/Tests/Functional/Features/ChildNodes/NodeTypes.DisallowedChildNodes.yaml @@ -0,0 +1,22 @@ + +'Flowpack.NodeTemplates:Content.DisallowedChildNodes': + superTypes: + 'Neos.Neos:Content': true + ui: + label: DisallowedChildNodes + childNodes: + content: + type: 'Flowpack.NodeTemplates:Collection.Disallowed' + options: + template: + childNodes: + content: + name: 'content' + childNodes: + text: + type: 'Flowpack.NodeTemplates:Content.Text' + properties: + text: 'Text' + restrictedNode: + type: 'Flowpack.NodeTemplates:Document.Page' + name: 'illegal-node-1' diff --git a/Tests/Functional/Features/ChildNodes/Snapshots/AllowedChildNodes.nodes.json b/Tests/Functional/Features/ChildNodes/Snapshots/AllowedChildNodes.nodes.json new file mode 100644 index 0000000..44eeeb2 --- /dev/null +++ b/Tests/Functional/Features/ChildNodes/Snapshots/AllowedChildNodes.nodes.json @@ -0,0 +1,16 @@ +{ + "childNodes": [ + { + "nodeTypeName": "Flowpack.NodeTemplates:Collection.Disallowed", + "nodeName": "content", + "childNodes": [ + { + "nodeTypeName": "Flowpack.NodeTemplates:Content.Text", + "properties": { + "text": "Text" + } + } + ] + } + ] +} diff --git a/Tests/Functional/Features/ChildNodes/Snapshots/AllowedChildNodes.template.json b/Tests/Functional/Features/ChildNodes/Snapshots/AllowedChildNodes.template.json new file mode 100644 index 0000000..5b4f019 --- /dev/null +++ b/Tests/Functional/Features/ChildNodes/Snapshots/AllowedChildNodes.template.json @@ -0,0 +1,20 @@ +{ + "properties": [], + "childNodes": [ + { + "type": null, + "name": "content", + "properties": [], + "childNodes": [ + { + "type": "Flowpack.NodeTemplates:Content.Text", + "name": null, + "properties": { + "text": "Text" + }, + "childNodes": [] + } + ] + } + ] +} diff --git a/Tests/Functional/Features/ChildNodes/Snapshots/AllowedChildNodes.yaml b/Tests/Functional/Features/ChildNodes/Snapshots/AllowedChildNodes.yaml new file mode 100644 index 0000000..d5cbe7e --- /dev/null +++ b/Tests/Functional/Features/ChildNodes/Snapshots/AllowedChildNodes.yaml @@ -0,0 +1,12 @@ +'{nodeTypeName}': + options: + template: + childNodes: + 'Content Collection (content)': + name: content + childNodes: + content0: + type: 'Flowpack.NodeTemplates:Content.Text' + properties: + # Text + text: Text diff --git a/Tests/Functional/Features/ChildNodes/Snapshots/DisallowedChildNodes.messages.json b/Tests/Functional/Features/ChildNodes/Snapshots/DisallowedChildNodes.messages.json new file mode 100644 index 0000000..b687c11 --- /dev/null +++ b/Tests/Functional/Features/ChildNodes/Snapshots/DisallowedChildNodes.messages.json @@ -0,0 +1,14 @@ +[ + { + "message": "Template for \"DisallowedChildNodes\" only partially applied. Please check the newly created nodes beneath Node \/sites\/test-site\/homepage\/main\/new-node@live[Flowpack.NodeTemplates:Content.DisallowedChildNodes].", + "severity": "ERROR" + }, + { + "message": "NodeConstraintException(Node type \"Flowpack.NodeTemplates:Content.Text\" is not allowed below tethered child nodes \"content\" of nodes of type \"Flowpack.NodeTemplates:Content.DisallowedChildNodes\", 1687541480146)", + "severity": "ERROR" + }, + { + "message": "NodeConstraintException(Node type \"Flowpack.NodeTemplates:Document.Page\" is not allowed for child nodes of type Flowpack.NodeTemplates:Content.DisallowedChildNodes, 1686417627173)", + "severity": "ERROR" + } +] diff --git a/Tests/Functional/Features/ChildNodes/Snapshots/DisallowedChildNodes.nodes.json b/Tests/Functional/Features/ChildNodes/Snapshots/DisallowedChildNodes.nodes.json new file mode 100644 index 0000000..3eb49a8 --- /dev/null +++ b/Tests/Functional/Features/ChildNodes/Snapshots/DisallowedChildNodes.nodes.json @@ -0,0 +1,8 @@ +{ + "childNodes": [ + { + "nodeTypeName": "Flowpack.NodeTemplates:Collection.Disallowed", + "nodeName": "content" + } + ] +} diff --git a/Tests/Functional/Features/ChildNodes/Snapshots/DisallowedChildNodes.template.json b/Tests/Functional/Features/ChildNodes/Snapshots/DisallowedChildNodes.template.json new file mode 100644 index 0000000..14a1692 --- /dev/null +++ b/Tests/Functional/Features/ChildNodes/Snapshots/DisallowedChildNodes.template.json @@ -0,0 +1,26 @@ +{ + "properties": [], + "childNodes": [ + { + "type": null, + "name": "content", + "properties": [], + "childNodes": [ + { + "type": "Flowpack.NodeTemplates:Content.Text", + "name": null, + "properties": { + "text": "Text" + }, + "childNodes": [] + } + ] + }, + { + "type": "Flowpack.NodeTemplates:Document.Page", + "name": "illegal-node-1", + "properties": [], + "childNodes": [] + } + ] +} diff --git a/Tests/Functional/Features/ChildNodes/Snapshots/DisallowedChildNodes.yaml b/Tests/Functional/Features/ChildNodes/Snapshots/DisallowedChildNodes.yaml new file mode 100644 index 0000000..f133bb4 --- /dev/null +++ b/Tests/Functional/Features/ChildNodes/Snapshots/DisallowedChildNodes.yaml @@ -0,0 +1,3 @@ +'{nodeTypeName}': + options: + template: [] diff --git a/Tests/Functional/Features/Exceptions/Snapshots/SomeExceptions.messages.json b/Tests/Functional/Features/Exceptions/Snapshots/SomeExceptions.messages.json index e5a8d85..0c9f48f 100644 --- a/Tests/Functional/Features/Exceptions/Snapshots/SomeExceptions.messages.json +++ b/Tests/Functional/Features/Exceptions/Snapshots/SomeExceptions.messages.json @@ -84,7 +84,7 @@ "severity": "ERROR" }, { - "message": "RuntimeException(Node type \"Flowpack.NodeTemplates:Document.Page\" is not allowed for child nodes of type Flowpack.NodeTemplates:Content.SomeExceptions, 1686417627173)", + "message": "NodeConstraintException(Node type \"Flowpack.NodeTemplates:Document.Page\" is not allowed for child nodes of type Flowpack.NodeTemplates:Content.SomeExceptions, 1686417627173)", "severity": "ERROR" }, { diff --git a/Tests/Functional/Features/NodeTypes.yaml b/Tests/Functional/Features/NodeTypes.yaml index c6e6773..9d2d69c 100644 --- a/Tests/Functional/Features/NodeTypes.yaml +++ b/Tests/Functional/Features/NodeTypes.yaml @@ -19,3 +19,10 @@ type: string ui: label: "Text" + +'Flowpack.NodeTemplates:Collection.Disallowed': + superTypes: + 'Neos.Neos:ContentCollection': true + constraints: + nodeTypes: + '*': false From a8f640893df3c6c8c3bd86f91915ed27ea4ccb48 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Fri, 23 Jun 2023 21:16:45 +0200 Subject: [PATCH 13/24] TASK: Unit test ToBeCreatedNode --- .../Command/NodeTemplateCommandController.php | 2 +- .../Domain/NodeCreation/ToBeCreatedNode.php | 2 +- .../NodeCreation/ToBeCreatedNodeTest.php | 183 ++++++++++++++++++ 3 files changed, 185 insertions(+), 2 deletions(-) create mode 100644 Tests/Unit/Domain/NodeCreation/ToBeCreatedNodeTest.php diff --git a/Classes/Application/Command/NodeTemplateCommandController.php b/Classes/Application/Command/NodeTemplateCommandController.php index b1792d5..18b9a08 100644 --- a/Classes/Application/Command/NodeTemplateCommandController.php +++ b/Classes/Application/Command/NodeTemplateCommandController.php @@ -92,7 +92,7 @@ public function validateCommand(): void ); $nodeCreation = new NodeCreationService($subgraph); - $nodeCreation->createMutatorCollection($template, new ToBeCreatedNode($nodeType), $caughtExceptions); + $nodeCreation->createMutatorCollection($template, ToBeCreatedNode::fromRegular($nodeType), $caughtExceptions); if ($caughtExceptions->hasExceptions()) { diff --git a/Classes/Domain/NodeCreation/ToBeCreatedNode.php b/Classes/Domain/NodeCreation/ToBeCreatedNode.php index c023a15..0b4644c 100644 --- a/Classes/Domain/NodeCreation/ToBeCreatedNode.php +++ b/Classes/Domain/NodeCreation/ToBeCreatedNode.php @@ -49,7 +49,7 @@ public function forTetheredChildNode(NodeName $nodeName): self public function forRegularChildNode(NodeType $nodeType): self { - $parentNodeType = $this->nodeType; + $parentNodeType = $nodeType; $requireConstraintsImposedByAncestorsAreMet = function (NodeType $nodeType) use ($parentNodeType) : void { self::requireNodeTypeConstraintsImposedByParentToBeMet($parentNodeType, $nodeType); }; diff --git a/Tests/Unit/Domain/NodeCreation/ToBeCreatedNodeTest.php b/Tests/Unit/Domain/NodeCreation/ToBeCreatedNodeTest.php new file mode 100644 index 0000000..7b1889e --- /dev/null +++ b/Tests/Unit/Domain/NodeCreation/ToBeCreatedNodeTest.php @@ -0,0 +1,183 @@ +> */ + private array $nodeTypesFixture; + + /** @var array */ + private array $nodeTypes; + + public function setUp(): void + { + parent::setUp(); + $this->nodeTypesFixture = Yaml::parse(self::NODE_TYPE_FIXTURES); + } + + /** @test */ + public function fromRegularAllowedChildNode(): void + { + $parentNode = ToBeCreatedNode::fromRegular($this->getNodeType('A:Content1')); + self::assertSame($this->getNodeType('A:Content1'), $parentNode->getNodeType()); + $parentNode->requireConstraintsImposedByAncestorsAreMet($this->getNodeType('A:Content2')); + } + + /** @test */ + public function forTetheredChildNodeAllowedChildNode(): void + { + $grandParentNode = ToBeCreatedNode::fromRegular($this->getNodeType('A:WithContent1AllowedCollectionAsChildNode')); + + $parentNode = $grandParentNode->forTetheredChildNode(NodeName::fromString('collection')); + self::assertSame($this->getNodeType('A:Collection.Allowed'), $parentNode->getNodeType()); + + $parentNode->requireConstraintsImposedByAncestorsAreMet($this->getNodeType('A:Content1')); + } + + /** @test */ + public function forTetheredChildNodeAllowedChildNodeBecauseConstraintOverride(): void + { + $grandParentNode = ToBeCreatedNode::fromRegular($this->getNodeType('A:WithContent1AllowedCollectionAsChildNodeViaOverride')); + + $parentNode = $grandParentNode->forTetheredChildNode(NodeName::fromString('collection')); + self::assertSame($this->getNodeType('A:Collection.Disallowed'), $parentNode->getNodeType()); + + $parentNode->requireConstraintsImposedByAncestorsAreMet($this->getNodeType('A:Content1')); + } + + /** @test */ + public function forRegularChildNodeAllowedChildNode(): void + { + $grandParentNode = ToBeCreatedNode::fromRegular($this->getNodeType('A:Content1')); + + $parentNode = $grandParentNode->forRegularChildNode($this->getNodeType('A:Content2')); + self::assertSame($this->getNodeType('A:Content2'), $parentNode->getNodeType()); + + $parentNode->requireConstraintsImposedByAncestorsAreMet($this->getNodeType('A:Content3')); + } + + /** @test */ + public function fromRegularDisallowedChildNode(): void + { + $this->expectException(NodeConstraintException::class); + $this->expectExceptionMessage('Node type "A:Content1" is not allowed for child nodes of type A:Collection.Disallowed'); + + $parentNode = ToBeCreatedNode::fromRegular($this->getNodeType('A:Collection.Disallowed')); + self::assertSame($this->getNodeType('A:Collection.Disallowed'), $parentNode->getNodeType()); + + $parentNode->requireConstraintsImposedByAncestorsAreMet($this->getNodeType('A:Content1')); + } + + /** @test */ + public function forTetheredChildNodeDisallowedChildNode(): void + { + $this->expectException(NodeConstraintException::class); + $this->expectExceptionMessage('Node type "A:Content1" is not allowed below tethered child nodes "collection" of nodes of type "A:WithDisallowedCollectionAsChildNode"'); + + $grandParentNode = ToBeCreatedNode::fromRegular($this->getNodeType('A:WithDisallowedCollectionAsChildNode')); + + $parentNode = $grandParentNode->forTetheredChildNode(NodeName::fromString('collection')); + self::assertSame($this->getNodeType('A:Collection.Disallowed'), $parentNode->getNodeType()); + + $parentNode->requireConstraintsImposedByAncestorsAreMet($this->getNodeType('A:Content1')); + } + + /** @test */ + public function forRegularChildNodeDisallowedChildNode(): void + { + $this->expectException(NodeConstraintException::class); + $this->expectExceptionMessage('Node type "A:Content1" is not allowed for child nodes of type A:Collection.Disallowed'); + + $grandParentNode = ToBeCreatedNode::fromRegular($this->getNodeType('A:Content2')); + + $parentNode = $grandParentNode->forRegularChildNode($this->getNodeType('A:Collection.Disallowed')); + self::assertSame($this->getNodeType('A:Collection.Disallowed'), $parentNode->getNodeType()); + + $parentNode->requireConstraintsImposedByAncestorsAreMet($this->getNodeType('A:Content1')); + } + + /** + * Return a nodetype built from the nodeTypesFixture + */ + protected function getNodeType(string $nodeTypeName): ?NodeType + { + if (!isset($this->nodeTypesFixture[$nodeTypeName])) { + return null; + } + + if (isset($this->nodeTypes[$nodeTypeName])) { + return $this->nodeTypes[$nodeTypeName]; + } + + $configuration = $this->nodeTypesFixture[$nodeTypeName]; + $declaredSuperTypes = []; + if (isset($configuration['superTypes']) && is_array($configuration['superTypes'])) { + foreach ($configuration['superTypes'] as $superTypeName => $enabled) { + $declaredSuperTypes[$superTypeName] = $enabled === true ? $this->getNodeType($superTypeName) : null; + } + } + + $nodeType = new NodeType( + $nodeTypeName, + $declaredSuperTypes, + $configuration + ); + + $fakeNodeTypeManager = $this->getMockBuilder(NodeTypeManager::class)->onlyMethods(['getNodeType'])->getMock(); + + $fakeNodeTypeManager->expects(self::any())->method('getNodeType')->willReturnCallback(fn ($nodeType) => $this->getNodeType($nodeType)); + + ObjectAccess::setProperty($nodeType, 'nodeTypeManager', $fakeNodeTypeManager, true); + + return $this->nodeTypes[$nodeTypeName] = $nodeType; + } +} From 1b55745d665a70021e97c876e4746f9db08b4976 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Fri, 23 Jun 2023 22:20:13 +0200 Subject: [PATCH 14/24] TASK: `flow nodetemplate:validate` show when data was accessed Fooo.Bar:Bla (depends on "data" context) Expression ... --- .../Command/NodeTemplateCommandController.php | 42 +++++++++++++------ 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/Classes/Application/Command/NodeTemplateCommandController.php b/Classes/Application/Command/NodeTemplateCommandController.php index b1792d5..8fec75b 100644 --- a/Classes/Application/Command/NodeTemplateCommandController.php +++ b/Classes/Application/Command/NodeTemplateCommandController.php @@ -70,8 +70,11 @@ public function createFromNodeSubtreeCommand(string $startingNodeId, string $wor public function validateCommand(): void { $templatesChecked = 0; - /** @var array $nodeTypeNamesWithTheirTemplateExceptions */ - $nodeTypeNamesWithTheirTemplateExceptions = []; + /** + * nodeTypeNames as index + * @var array $faultyNodeTypeTemplates + */ + $faultyNodeTypeTemplates = []; foreach ($this->nodeTypeManager->getNodeTypes(false) as $nodeType) { $templateConfiguration = $nodeType->getOptions()['template'] ?? null; @@ -82,10 +85,20 @@ public function validateCommand(): void $subgraph = $this->contextFactory->create(); + $observableEmptyData = new class ([]) extends \ArrayObject + { + public bool $dataWasAccessed = false; + public function offsetExists($key) + { + $this->dataWasAccessed = true; + return false; + } + }; + $template = $this->templateConfigurationProcessor->processTemplateConfiguration( $templateConfiguration, [ - 'data' => [], + 'data' => $observableEmptyData, 'triggeringNode' => $subgraph->getRootNode(), ], $caughtExceptions @@ -94,28 +107,31 @@ public function validateCommand(): void $nodeCreation = new NodeCreationService($subgraph); $nodeCreation->createMutatorCollection($template, new ToBeCreatedNode($nodeType), $caughtExceptions); - if ($caughtExceptions->hasExceptions()) { - $nodeTypeNamesWithTheirTemplateExceptions[$nodeType->getName()] = $caughtExceptions; + $faultyNodeTypeTemplates[$nodeType->getName()] = ['caughtExceptions' => $caughtExceptions, 'dataWasAccessed' => $observableEmptyData->dataWasAccessed]; } $templatesChecked++; } - if (empty($nodeTypeNamesWithTheirTemplateExceptions)) { - $this->outputFormatted(sprintf('%d NodeType templates validated.', $templatesChecked)); + if (empty($faultyNodeTypeTemplates)) { + $this->outputLine(sprintf('%d NodeType templates validated.', $templatesChecked)); return; } - $possiblyFaultyTemplates = count($nodeTypeNamesWithTheirTemplateExceptions); - $this->outputFormatted(sprintf('%d of %d NodeType template validated. %d could not be build standalone.', $templatesChecked - $possiblyFaultyTemplates, $templatesChecked, $possiblyFaultyTemplates)); - $this->outputFormatted('This might not be a problem, if they depend on certain data from the node-creation dialog.'); + $possiblyFaultyTemplates = count($faultyNodeTypeTemplates); + $this->outputLine(sprintf('%d of %d NodeType template validated. %d could not be build standalone.', $templatesChecked - $possiblyFaultyTemplates, $templatesChecked, $possiblyFaultyTemplates)); $this->outputLine(); - foreach ($nodeTypeNamesWithTheirTemplateExceptions as $nodeTypeName => $caughtExceptions) { - $this->outputLine($nodeTypeName); + foreach ($faultyNodeTypeTemplates as $nodeTypeName => ['caughtExceptions' => $caughtExceptions, 'dataWasAccessed' => $dataWasAccessed]) { + if ($dataWasAccessed) { + $this->outputLine(sprintf('%s (depends on "data" context)', $nodeTypeName)); + } else { + $this->outputLine(sprintf('%s', $nodeTypeName)); + } + foreach ($caughtExceptions as $caughtException) { - $this->outputFormatted($caughtException->toMessage(), [], 4); + $this->outputLine(' ' . $caughtException->toMessage()); $this->outputLine(); } } From 4bb59ba1749bbb5ceb20335462d738be84083ded Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sat, 24 Jun 2023 10:17:21 +0200 Subject: [PATCH 15/24] TASK: Fix tests on Neos 8.3 --- ...eTest.php => AbstractNodeTemplateTestCase.php} | 15 +++------------ .../Features/ChildNodes/ChildNodesTest.php | 4 ++-- .../Features/Exceptions/ExceptionsTest.php | 4 ++-- .../Features/NodeNames/NodeNamesTest.php | 4 ++-- Tests/Functional/Features/Pages/PagesTest.php | 4 ++-- .../Features/Properties/PropertiesTest.php | 4 ++-- .../ResolvablePropertiesTest.php | 4 ++-- 7 files changed, 15 insertions(+), 24 deletions(-) rename Tests/Functional/{AbstractNodeTemplateTest.php => AbstractNodeTemplateTestCase.php} (92%) diff --git a/Tests/Functional/AbstractNodeTemplateTest.php b/Tests/Functional/AbstractNodeTemplateTestCase.php similarity index 92% rename from Tests/Functional/AbstractNodeTemplateTest.php rename to Tests/Functional/AbstractNodeTemplateTestCase.php index f068df8..df482df 100644 --- a/Tests/Functional/AbstractNodeTemplateTest.php +++ b/Tests/Functional/AbstractNodeTemplateTestCase.php @@ -13,8 +13,7 @@ use Neos\ContentRepository\Domain\Repository\WorkspaceRepository; use Neos\ContentRepository\Domain\Service\ContextFactoryInterface; use Neos\ContentRepository\Domain\Service\NodeTypeManager; -use Neos\Flow\Configuration\Source\YamlSource; -use Neos\Flow\Package\PackageManager; +use Neos\Flow\Configuration\ConfigurationManager; use Neos\Flow\Tests\FunctionalTestCase; use Neos\Neos\Domain\Model\Site; use Neos\Neos\Domain\Repository\SiteRepository; @@ -24,7 +23,7 @@ use Neos\Utility\Arrays; use Symfony\Component\Yaml\Yaml; -abstract class AbstractNodeTemplateTest extends FunctionalTestCase +abstract class AbstractNodeTemplateTestCase extends FunctionalTestCase { use SnapshotTrait; use FeedbackCollectionMessagesTrait; @@ -74,15 +73,7 @@ public function setUp(): void private function loadFakeNodeTypes(): void { - $configuration = []; - $yamlSource = new YamlSource(); - foreach (['Neos.Neos', 'Neos.ContentRepository', 'Flowpack.NodeTemplates'] as $packageKey) { - $package = $this->objectManager->get(PackageManager::class)->getPackage($packageKey); - $configuration = Arrays::arrayMergeRecursiveOverrule( - $configuration, - $yamlSource->load($package->getConfigurationPath() . 'NodeTypes', true) - ); - } + $configuration = $this->objectManager->get(ConfigurationManager::class)->getConfiguration('NodeTypes'); $fileIterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator(__DIR__ . '/Features')); diff --git a/Tests/Functional/Features/ChildNodes/ChildNodesTest.php b/Tests/Functional/Features/ChildNodes/ChildNodesTest.php index 15da9e1..869bd99 100644 --- a/Tests/Functional/Features/ChildNodes/ChildNodesTest.php +++ b/Tests/Functional/Features/ChildNodes/ChildNodesTest.php @@ -4,9 +4,9 @@ namespace Flowpack\NodeTemplates\Tests\Functional\Features\ChildNodes; -use Flowpack\NodeTemplates\Tests\Functional\AbstractNodeTemplateTest; +use Flowpack\NodeTemplates\Tests\Functional\AbstractNodeTemplateTestCase; -class ChildNodesTest extends AbstractNodeTemplateTest +class ChildNodesTest extends AbstractNodeTemplateTestCase { /** @test */ public function itMatchesSnapshot1(): void diff --git a/Tests/Functional/Features/Exceptions/ExceptionsTest.php b/Tests/Functional/Features/Exceptions/ExceptionsTest.php index bc4c97b..71b67bf 100644 --- a/Tests/Functional/Features/Exceptions/ExceptionsTest.php +++ b/Tests/Functional/Features/Exceptions/ExceptionsTest.php @@ -4,10 +4,10 @@ namespace Flowpack\NodeTemplates\Tests\Functional\Features\Exceptions; -use Flowpack\NodeTemplates\Tests\Functional\AbstractNodeTemplateTest; +use Flowpack\NodeTemplates\Tests\Functional\AbstractNodeTemplateTestCase; use Flowpack\NodeTemplates\Tests\Functional\WithConfigurationTrait; -class ExceptionsTest extends AbstractNodeTemplateTest +class ExceptionsTest extends AbstractNodeTemplateTestCase { use WithConfigurationTrait; diff --git a/Tests/Functional/Features/NodeNames/NodeNamesTest.php b/Tests/Functional/Features/NodeNames/NodeNamesTest.php index 3d584fa..87c6d5d 100644 --- a/Tests/Functional/Features/NodeNames/NodeNamesTest.php +++ b/Tests/Functional/Features/NodeNames/NodeNamesTest.php @@ -4,9 +4,9 @@ namespace Flowpack\NodeTemplates\Tests\Functional\Features\NodeNames; -use Flowpack\NodeTemplates\Tests\Functional\AbstractNodeTemplateTest; +use Flowpack\NodeTemplates\Tests\Functional\AbstractNodeTemplateTestCase; -class NodeNamesTest extends AbstractNodeTemplateTest +class NodeNamesTest extends AbstractNodeTemplateTestCase { /** @test */ public function itMatchesSnapshot(): void diff --git a/Tests/Functional/Features/Pages/PagesTest.php b/Tests/Functional/Features/Pages/PagesTest.php index 37986f2..3c31dd5 100644 --- a/Tests/Functional/Features/Pages/PagesTest.php +++ b/Tests/Functional/Features/Pages/PagesTest.php @@ -4,9 +4,9 @@ namespace Flowpack\NodeTemplates\Tests\Functional\Features\Pages; -use Flowpack\NodeTemplates\Tests\Functional\AbstractNodeTemplateTest; +use Flowpack\NodeTemplates\Tests\Functional\AbstractNodeTemplateTestCase; -class PagesTest extends AbstractNodeTemplateTest +class PagesTest extends AbstractNodeTemplateTestCase { /** @test */ public function itMatchesSnapshot1(): void diff --git a/Tests/Functional/Features/Properties/PropertiesTest.php b/Tests/Functional/Features/Properties/PropertiesTest.php index 9e44cc5..2bcc78c 100644 --- a/Tests/Functional/Features/Properties/PropertiesTest.php +++ b/Tests/Functional/Features/Properties/PropertiesTest.php @@ -4,9 +4,9 @@ namespace Flowpack\NodeTemplates\Tests\Functional\Features\Properties; -use Flowpack\NodeTemplates\Tests\Functional\AbstractNodeTemplateTest; +use Flowpack\NodeTemplates\Tests\Functional\AbstractNodeTemplateTestCase; -class PropertiesTest extends AbstractNodeTemplateTest +class PropertiesTest extends AbstractNodeTemplateTestCase { /** @test */ public function itMatchesSnapshot(): void diff --git a/Tests/Functional/Features/ResolvableProperties/ResolvablePropertiesTest.php b/Tests/Functional/Features/ResolvableProperties/ResolvablePropertiesTest.php index 9860185..d09a56a 100644 --- a/Tests/Functional/Features/ResolvableProperties/ResolvablePropertiesTest.php +++ b/Tests/Functional/Features/ResolvableProperties/ResolvablePropertiesTest.php @@ -4,7 +4,7 @@ namespace Flowpack\NodeTemplates\Tests\Functional\Features\ResolvableProperties; -use Flowpack\NodeTemplates\Tests\Functional\AbstractNodeTemplateTest; +use Flowpack\NodeTemplates\Tests\Functional\AbstractNodeTemplateTestCase; use Neos\Flow\ResourceManagement\ResourceManager; use Neos\Media\Domain\Model\Asset; use Neos\Media\Domain\Model\Image; @@ -12,7 +12,7 @@ use Neos\Media\Domain\Repository\ImageRepository; use Neos\Utility\ObjectAccess; -class ResolvablePropertiesTest extends AbstractNodeTemplateTest +class ResolvablePropertiesTest extends AbstractNodeTemplateTestCase { /** @test */ public function itMatchesSnapshot1(): void From 82c79b4a164a4db604f114928367198fa9957881 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sat, 24 Jun 2023 10:18:25 +0200 Subject: [PATCH 16/24] TASK: Cleanup internals of ToBeCreatedNode --- .../Domain/NodeCreation/ToBeCreatedNode.php | 41 +++++++++---------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/Classes/Domain/NodeCreation/ToBeCreatedNode.php b/Classes/Domain/NodeCreation/ToBeCreatedNode.php index 0b4644c..caea3a4 100644 --- a/Classes/Domain/NodeCreation/ToBeCreatedNode.php +++ b/Classes/Domain/NodeCreation/ToBeCreatedNode.php @@ -13,55 +13,52 @@ class ToBeCreatedNode { private NodeType $nodeType; - /** @var \Closure(NodeType $nodeType): void */ - private \Closure $requireConstraintsImposedByAncestorsAreMet; + private ?NodeName $tetheredNodeName; - private function __construct(NodeType $nodeType, \Closure $requireConstraintsImposedByAncestorsAreMet) + private ?NodeType $tetheredParentNodeType; + + public function __construct(NodeType $nodeType, ?NodeName $tetheredNodeName, ?NodeType $tetheredParentNodeType) { $this->nodeType = $nodeType; - $this->requireConstraintsImposedByAncestorsAreMet = $requireConstraintsImposedByAncestorsAreMet; + $this->tetheredNodeName = $tetheredNodeName; + $this->tetheredParentNodeType = $tetheredParentNodeType; + if ($tetheredNodeName !== null) { + assert($tetheredParentNodeType !== null); + } } public static function fromRegular(NodeType $nodeType): self { - $parentNodeType = $nodeType; - $requireConstraintsImposedByAncestorsAreMet = function (NodeType $nodeType) use ($parentNodeType) : void { - self::requireNodeTypeConstraintsImposedByParentToBeMet($parentNodeType, $nodeType); - }; - return new self($nodeType, $requireConstraintsImposedByAncestorsAreMet); + return new self($nodeType, null, null); } public function forTetheredChildNode(NodeName $nodeName): self { - $parentNodeType = $this->nodeType; // `getTypeOfAutoCreatedChildNode` actually has a bug; it looks up the NodeName parameter against the raw configuration instead of the transliterated NodeName // https://github.com/neos/neos-ui/issues/3527 - $parentNodesAutoCreatedChildNodes = $parentNodeType->getAutoCreatedChildNodes(); + $parentNodesAutoCreatedChildNodes = $this->nodeType->getAutoCreatedChildNodes(); $childNodeType = $parentNodesAutoCreatedChildNodes[$nodeName->__toString()] ?? null; if (!$childNodeType instanceof NodeType) { throw new \InvalidArgumentException('forTetheredChildNode only works for tethered nodes.'); } - $requireConstraintsImposedByAncestorsAreMet = function (NodeType $nodeType) use ($parentNodeType, $nodeName) : void { - self::requireNodeTypeConstraintsImposedByGrandparentToBeMet($parentNodeType, $nodeName, $nodeType); - }; - return new self($childNodeType, $requireConstraintsImposedByAncestorsAreMet); + return new self($childNodeType, $nodeName, $this->nodeType); } public function forRegularChildNode(NodeType $nodeType): self { - $parentNodeType = $nodeType; - $requireConstraintsImposedByAncestorsAreMet = function (NodeType $nodeType) use ($parentNodeType) : void { - self::requireNodeTypeConstraintsImposedByParentToBeMet($parentNodeType, $nodeType); - }; - return new self($nodeType, $requireConstraintsImposedByAncestorsAreMet); + return new self($nodeType, null, null); } /** * @throws NodeConstraintException */ - public function requireConstraintsImposedByAncestorsAreMet(NodeType $nodeType): void + public function requireConstraintsImposedByAncestorsAreMet(NodeType $childNodeType): void { - ($this->requireConstraintsImposedByAncestorsAreMet)($nodeType); + if ($this->tetheredNodeName) { + self::requireNodeTypeConstraintsImposedByGrandparentToBeMet($this->tetheredParentNodeType, $this->tetheredNodeName, $childNodeType); + } else { + self::requireNodeTypeConstraintsImposedByParentToBeMet($this->nodeType, $childNodeType); + } } public function getNodeType(): NodeType From 308607a30a573fdaabecd362e1f01764d8a301db Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sat, 24 Jun 2023 10:25:15 +0200 Subject: [PATCH 17/24] TASK: Run CI also on neos 8.3 + php 8.1 --- .github/workflows/tests.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 83ff90a..febd39a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -9,7 +9,6 @@ on: jobs: build: env: - NEOS_TARGET_VERSION: 7.3 FLOW_CONTEXT: Testing FLOW_PATH_ROOT: ../neos-base-distribution @@ -18,7 +17,11 @@ jobs: strategy: fail-fast: false matrix: - php-versions: ['7.4'] + include: + - php-version: 7.4 + neos-version: 7.3 + - php-version: 8.1 + neos-version: 8.3 steps: - uses: actions/checkout@v2 @@ -26,7 +29,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: ${{ matrix.php-versions }} + php-version: ${{ matrix.php-version }} extensions: mbstring, xml, json, zlib, iconv, intl, pdo_sqlite, mysql coverage: xdebug #optional ini-values: opcache.fast_shutdown=0 @@ -50,7 +53,7 @@ jobs: - name: Prepare Neos distribution run: | - git clone https://github.com/neos/neos-base-distribution.git -b ${NEOS_TARGET_VERSION} ${FLOW_PATH_ROOT} + git clone https://github.com/neos/neos-base-distribution.git -b ${{ matrix.neos-version }} ${FLOW_PATH_ROOT} cd ${FLOW_PATH_ROOT} composer require --no-update --no-interaction flowpack/nodetemplates From c708b1169257156918f67226efe300a9c3a75167 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sat, 24 Jun 2023 12:01:11 +0200 Subject: [PATCH 18/24] TASK: Improve CI composer package installation --- .github/workflows/tests.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index febd39a..62c6394 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -55,15 +55,14 @@ jobs: run: | git clone https://github.com/neos/neos-base-distribution.git -b ${{ matrix.neos-version }} ${FLOW_PATH_ROOT} cd ${FLOW_PATH_ROOT} - composer require --no-update --no-interaction flowpack/nodetemplates + composer config --no-plugins allow-plugins.neos/composer-plugin true + composer config repositories.tested-package path ../Flowpack.NodeTemplates + composer require --no-update --no-interaction flowpack/nodetemplates:@dev - name: Install distribution run: | cd ${FLOW_PATH_ROOT} - composer config --no-plugins allow-plugins.neos/composer-plugin true composer install --no-interaction --no-progress - rm -rf Packages/Application/Flowpack.NodeTemplates - cp -r ../Flowpack.NodeTemplates Packages/Application/Flowpack.NodeTemplates - name: Run Unit tests run: | From ab441da07fe4121b33536c7a742fa5f0baa02621 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sat, 24 Jun 2023 12:16:22 +0200 Subject: [PATCH 19/24] TASK: Cleanup CI script The caching dindt really work and doesnt make a difference --- .github/workflows/tests.yml | 25 ++++++------------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 62c6394..5f343e4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -24,45 +24,32 @@ jobs: neos-version: 8.3 steps: - - uses: actions/checkout@v2 + - name: Checkout code + uses: actions/checkout@v3 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-version }} extensions: mbstring, xml, json, zlib, iconv, intl, pdo_sqlite, mysql - coverage: xdebug #optional - ini-values: opcache.fast_shutdown=0 - name: Update Composer run: | - sudo composer self-update + composer self-update composer --version - # Directory permissions for .composer are wrong, so we remove the complete directory - # https://github.com/actions/virtual-environments/issues/824 - - name: Delete .composer directory - run: | - sudo rm -rf ~/.composer - - - name: Cache dependencies - uses: actions/cache@v1 - with: - path: ~/.composer/cache - key: dependencies-composer-${{ hashFiles('composer.json') }} - - name: Prepare Neos distribution run: | - git clone https://github.com/neos/neos-base-distribution.git -b ${{ matrix.neos-version }} ${FLOW_PATH_ROOT} + git clone --depth 1 --branch ${{ matrix.neos-version }} https://github.com/neos/neos-base-distribution.git ${FLOW_PATH_ROOT} cd ${FLOW_PATH_ROOT} composer config --no-plugins allow-plugins.neos/composer-plugin true composer config repositories.tested-package path ../Flowpack.NodeTemplates composer require --no-update --no-interaction flowpack/nodetemplates:@dev - - name: Install distribution + - name: Install dependencies run: | cd ${FLOW_PATH_ROOT} - composer install --no-interaction --no-progress + composer install --no-interaction --no-progress --prefer-dist - name: Run Unit tests run: | From f789bedb5a74872fe8fa00c3daa978c259b6fa43 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sat, 24 Jun 2023 13:12:18 +0200 Subject: [PATCH 20/24] TASK: Composer cache --- .github/workflows/tests.yml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5f343e4..cbd3f46 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -33,10 +33,15 @@ jobs: php-version: ${{ matrix.php-version }} extensions: mbstring, xml, json, zlib, iconv, intl, pdo_sqlite, mysql - - name: Update Composer - run: | - composer self-update - composer --version + - id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + shell: bash + + - uses: actions/cache@v2 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: ${{ runner.os }}-composer- - name: Prepare Neos distribution run: | From df5277f189b82ec292284f1d77b298c10963eb4b Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sat, 24 Jun 2023 16:05:23 +0200 Subject: [PATCH 21/24] TASK: Combine `convertProperties` into `requireValidProperties` --- .../NodeCreation/NodeCreationService.php | 51 +++---------------- .../NodeCreation/PropertiesAndReferences.php | 30 +++++++---- .../UnresolvableProperties.messages.json | 6 +-- 3 files changed, 30 insertions(+), 57 deletions(-) diff --git a/Classes/Domain/NodeCreation/NodeCreationService.php b/Classes/Domain/NodeCreation/NodeCreationService.php index ce7a26a..2625d32 100644 --- a/Classes/Domain/NodeCreation/NodeCreationService.php +++ b/Classes/Domain/NodeCreation/NodeCreationService.php @@ -8,13 +8,10 @@ use Flowpack\NodeTemplates\Domain\Template\Template; use Flowpack\NodeTemplates\Domain\Template\Templates; use Neos\ContentRepository\Domain\Model\NodeInterface; -use Neos\ContentRepository\Domain\Model\NodeType; use Neos\ContentRepository\Domain\Service\Context; use Neos\ContentRepository\Domain\Service\NodeTypeManager; use Neos\Flow\Annotations as Flow; -use Neos\Flow\Property\Exception as PropertyWasNotMappedException; use Neos\Flow\Property\PropertyMapper; -use Neos\Flow\Property\PropertyMappingConfiguration; use Neos\Neos\Utility\NodeUriPathSegmentGenerator; class NodeCreationService @@ -52,10 +49,10 @@ public function createMutatorCollection(RootTemplate $template, ToBeCreatedNode { $nodeType = $node->getNodeType(); - $propertiesAndReferences = PropertiesAndReferences::createFromArrayAndTypeDeclarations($this->convertProperties($nodeType, $template->getProperties(), $caughtExceptions), $nodeType); + $propertiesAndReferences = PropertiesAndReferences::createFromArrayAndTypeDeclarations($template->getProperties(), $nodeType); $properties = array_merge( - $propertiesAndReferences->requireValidProperties($nodeType, $caughtExceptions), + $propertiesAndReferences->requireValidProperties($nodeType, $this->propertyMapper, $caughtExceptions), $propertiesAndReferences->requireValidReferences($nodeType, $this->subgraph, $caughtExceptions) ); @@ -90,10 +87,10 @@ private function createMutatorCollectionFromTemplate(Templates $templates, ToBeC } $nodeType = $parentNodesAutoCreatedChildNodes[$template->getName()->__toString()]; - $propertiesAndReferences = PropertiesAndReferences::createFromArrayAndTypeDeclarations($this->convertProperties($nodeType, $template->getProperties(), $caughtExceptions), $nodeType); + $propertiesAndReferences = PropertiesAndReferences::createFromArrayAndTypeDeclarations($template->getProperties(), $nodeType); $properties = array_merge( - $propertiesAndReferences->requireValidProperties($nodeType, $caughtExceptions), + $propertiesAndReferences->requireValidProperties($nodeType, $this->propertyMapper, $caughtExceptions), $propertiesAndReferences->requireValidReferences($nodeType, $this->subgraph, $caughtExceptions) ); @@ -144,10 +141,10 @@ private function createMutatorCollectionFromTemplate(Templates $templates, ToBeC // todo maybe check also explicitly for allowsGrandchildNodeType (we do this currently like below) - $propertiesAndReferences = PropertiesAndReferences::createFromArrayAndTypeDeclarations($this->convertProperties($nodeType, $template->getProperties(), $caughtExceptions), $nodeType); + $propertiesAndReferences = PropertiesAndReferences::createFromArrayAndTypeDeclarations($template->getProperties(), $nodeType); $properties = array_merge( - $propertiesAndReferences->requireValidProperties($nodeType, $caughtExceptions), + $propertiesAndReferences->requireValidProperties($nodeType, $this->propertyMapper, $caughtExceptions), $propertiesAndReferences->requireValidReferences($nodeType, $this->subgraph, $caughtExceptions) ); @@ -189,40 +186,4 @@ private function createMutatorForUriPathSegment($template): NodeMutator $previousNode->setProperty('uriPathSegment', $this->nodeUriPathSegmentGenerator->generateUriPathSegment($previousNode, $properties['title'] ?? null)); }); } - - private function convertProperties(NodeType $nodeType, array $properties, CaughtExceptions $caughtExceptions): array - { - // TODO combine with PropertiesAndReferences::requireValidProperties - foreach ($nodeType->getConfiguration('properties') as $propertyName => $propertyConfiguration) { - if (!isset($properties[$propertyName])) { - continue; - } - $propertyType = $nodeType->getPropertyType($propertyName); - if ($propertyType === 'references' || $propertyType === 'reference') { - continue; - } - $propertyType = PropertyType::fromPropertyOfNodeType($propertyName, $nodeType); - $propertyValue = $properties[$propertyName]; - if (!$propertyType->isClass() && !$propertyType->isArrayOfClass()) { - // property mapping only for class types or array of classes! - continue; - } - try { - $propertyMappingConfiguration = new PropertyMappingConfiguration(); - $propertyMappingConfiguration->allowAllProperties(); - - $properties[$propertyName] = $this->propertyMapper->convert($propertyValue, $propertyType->getValue(), $propertyMappingConfiguration); - $messages = $this->propertyMapper->getMessages(); - if ($messages->hasErrors()) { - throw new PropertyWasNotMappedException($this->propertyMapper->getMessages()->getFirstError()->getMessage(), 1686779371122); - } - } catch (PropertyWasNotMappedException $exception) { - $caughtExceptions->add(CaughtException::fromException( - $exception - )->withOrigin(sprintf('Property "%s" in NodeType "%s"', $propertyName, $nodeType->getName()))); - unset($properties[$propertyName]); - } - } - return $properties; - } } diff --git a/Classes/Domain/NodeCreation/PropertiesAndReferences.php b/Classes/Domain/NodeCreation/PropertiesAndReferences.php index de19bf4..ca61f82 100644 --- a/Classes/Domain/NodeCreation/PropertiesAndReferences.php +++ b/Classes/Domain/NodeCreation/PropertiesAndReferences.php @@ -7,6 +7,9 @@ use Neos\ContentRepository\Domain\Model\NodeType; use Neos\ContentRepository\Domain\Service\Context; use Neos\Flow\Annotations as Flow; +use Neos\Flow\Property\Exception as PropertyMappingException; +use Neos\Flow\Property\PropertyMapper; +use Neos\Flow\Property\PropertyMappingConfiguration; /** * @Flow\Proxy(false) @@ -45,14 +48,10 @@ public static function createFromArrayAndTypeDeclarations(array $propertiesAndRe * * 1. It is checked, that only properties will be set, that were declared in the NodeType * - * 2. In case the property is a select-box, it is checked, that the current value is a valid option of the select-box - * - * 3. It is made sure is that a property value is not null when there is a default value: - * In case that due to a condition in the nodeTemplate `null` is assigned to a node property, it will override the defaultValue. - * This is a problem, as setting `null` might not be possible via the Neos UI and the Fusion rendering is most likely not going to handle this edge case. - * Related discussion {@link https://github.com/Flowpack/Flowpack.NodeTemplates/issues/41} + * 2. It is checked, that the property value is assignable to the property type. + * In case the type is class or an array of classes, the property mapper will be used map the given type to it. If it doesn't succeed, we will log an error. */ - public function requireValidProperties(NodeType $nodeType, CaughtExceptions $caughtExceptions): array + public function requireValidProperties(NodeType $nodeType, PropertyMapper $propertyMapper, CaughtExceptions $caughtExceptions): array { $validProperties = []; foreach ($this->properties as $propertyName => $propertyValue) { @@ -68,6 +67,19 @@ public function requireValidProperties(NodeType $nodeType, CaughtExceptions $cau ); } $propertyType = PropertyType::fromPropertyOfNodeType($propertyName, $nodeType); + + if (!$propertyType->isMatchedBy($propertyValue) + && ($propertyType->isClass() || $propertyType->isArrayOfClass())) { + // we try property mapping only for class types or array of classes + $propertyMappingConfiguration = new PropertyMappingConfiguration(); + $propertyMappingConfiguration->allowAllProperties(); + $propertyValue = $propertyMapper->convert($propertyValue, $propertyType->getValue(), $propertyMappingConfiguration); + $messages = $propertyMapper->getMessages(); + if ($messages->hasErrors()) { + throw new PropertyIgnoredException($propertyMapper->getMessages()->getFirstError()->getMessage(), 1686779371122); + } + } + if (!$propertyType->isMatchedBy($propertyValue)) { throw new PropertyIgnoredException( sprintf( @@ -79,9 +91,9 @@ public function requireValidProperties(NodeType $nodeType, CaughtExceptions $cau ); } $validProperties[$propertyName] = $propertyValue; - } catch (PropertyIgnoredException $propertyNotSetException) { + } catch (PropertyIgnoredException|PropertyMappingException $exception) { $caughtExceptions->add( - CaughtException::fromException($propertyNotSetException)->withOrigin(sprintf('Property "%s" in NodeType "%s"', $propertyName, $nodeType->getName())) + CaughtException::fromException($exception)->withOrigin(sprintf('Property "%s" in NodeType "%s"', $propertyName, $nodeType->getName())) ); } } diff --git a/Tests/Functional/Features/ResolvableProperties/Snapshots/UnresolvableProperties.messages.json b/Tests/Functional/Features/ResolvableProperties/Snapshots/UnresolvableProperties.messages.json index aa1b97a..875fc82 100644 --- a/Tests/Functional/Features/ResolvableProperties/Snapshots/UnresolvableProperties.messages.json +++ b/Tests/Functional/Features/ResolvableProperties/Snapshots/UnresolvableProperties.messages.json @@ -4,15 +4,15 @@ "severity": "ERROR" }, { - "message": "Property \"asset\" in NodeType \"Flowpack.NodeTemplates:Content.UnresolvableProperties\" | FlowException(Object of type \"Neos\\Media\\Domain\\Model\\Asset\" with identity \"non-existing\" not found., 1686779371122)", + "message": "Property \"someString\" in NodeType \"Flowpack.NodeTemplates:Content.UnresolvableProperties\" | PropertyIgnoredException(Because value `[\"foo\"]` is not assignable to property type \"string\"., 1685958105644)", "severity": "ERROR" }, { - "message": "Property \"images\" in NodeType \"Flowpack.NodeTemplates:Content.UnresolvableProperties\" | FlowException(Could not convert target type \"array\", at property path \"0\": No converter found which can be used to convert from \"string\" to \"Neos\\Media\\Domain\\Model\\ImageInterface\"., 1297759968) | TypeConverterException(No converter found which can be used to convert from \"string\" to \"Neos\\Media\\Domain\\Model\\ImageInterface\"., 0)", + "message": "Property \"asset\" in NodeType \"Flowpack.NodeTemplates:Content.UnresolvableProperties\" | PropertyIgnoredException(Object of type \"Neos\\Media\\Domain\\Model\\Asset\" with identity \"non-existing\" not found., 1686779371122)", "severity": "ERROR" }, { - "message": "Property \"someString\" in NodeType \"Flowpack.NodeTemplates:Content.UnresolvableProperties\" | PropertyIgnoredException(Because value `[\"foo\"]` is not assignable to property type \"string\"., 1685958105644)", + "message": "Property \"images\" in NodeType \"Flowpack.NodeTemplates:Content.UnresolvableProperties\" | FlowException(Could not convert target type \"array\", at property path \"0\": No converter found which can be used to convert from \"string\" to \"Neos\\Media\\Domain\\Model\\ImageInterface\"., 1297759968) | TypeConverterException(No converter found which can be used to convert from \"string\" to \"Neos\\Media\\Domain\\Model\\ImageInterface\"., 0)", "severity": "ERROR" }, { From 98cfdc8de84628cd6d468303aa78389b3ad4e224 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sat, 24 Jun 2023 20:24:31 +0200 Subject: [PATCH 22/24] TASK: Refactor `PropertiesAndReferences` into service and dto --- .../NodeCreation/NodeCreationService.php | 30 ++++++-------- Classes/Domain/NodeCreation/Properties.php | 40 +++++++++++++++++++ ...ndReferences.php => PropertiesHandler.php} | 34 ++++++++-------- 3 files changed, 70 insertions(+), 34 deletions(-) create mode 100644 Classes/Domain/NodeCreation/Properties.php rename Classes/Domain/NodeCreation/{PropertiesAndReferences.php => PropertiesHandler.php} (80%) diff --git a/Classes/Domain/NodeCreation/NodeCreationService.php b/Classes/Domain/NodeCreation/NodeCreationService.php index 2625d32..071812e 100644 --- a/Classes/Domain/NodeCreation/NodeCreationService.php +++ b/Classes/Domain/NodeCreation/NodeCreationService.php @@ -28,17 +28,11 @@ class NodeCreationService */ protected $nodeUriPathSegmentGenerator; - /** - * @Flow\Inject - * @var PropertyMapper - */ - protected $propertyMapper; - - private Context $subgraph; + private PropertiesHandler $propertiesHandler; - public function __construct(Context $subgraph) + public function __construct(Context $subgraph, PropertyMapper $propertyMapper) { - $this->subgraph = $subgraph; + $this->propertiesHandler = new PropertiesHandler($subgraph, $propertyMapper); } /** @@ -49,11 +43,11 @@ public function createMutatorCollection(RootTemplate $template, ToBeCreatedNode { $nodeType = $node->getNodeType(); - $propertiesAndReferences = PropertiesAndReferences::createFromArrayAndTypeDeclarations($template->getProperties(), $nodeType); + $properties = $this->propertiesHandler->createdFromArrayByTypeDeclaration($template->getProperties(), $nodeType); $properties = array_merge( - $propertiesAndReferences->requireValidProperties($nodeType, $this->propertyMapper, $caughtExceptions), - $propertiesAndReferences->requireValidReferences($nodeType, $this->subgraph, $caughtExceptions) + $this->propertiesHandler->requireValidProperties($properties, $caughtExceptions), + $this->propertiesHandler->requireValidReferences($properties, $caughtExceptions) ); $nodeMutators = NodeMutatorCollection::from( @@ -87,11 +81,11 @@ private function createMutatorCollectionFromTemplate(Templates $templates, ToBeC } $nodeType = $parentNodesAutoCreatedChildNodes[$template->getName()->__toString()]; - $propertiesAndReferences = PropertiesAndReferences::createFromArrayAndTypeDeclarations($template->getProperties(), $nodeType); + $properties = $this->propertiesHandler->createdFromArrayByTypeDeclaration($template->getProperties(), $nodeType); $properties = array_merge( - $propertiesAndReferences->requireValidProperties($nodeType, $this->propertyMapper, $caughtExceptions), - $propertiesAndReferences->requireValidReferences($nodeType, $this->subgraph, $caughtExceptions) + $this->propertiesHandler->requireValidProperties($properties, $caughtExceptions), + $this->propertiesHandler->requireValidReferences($properties, $caughtExceptions) ); $nodeMutators = $nodeMutators->withNodeMutators( @@ -141,11 +135,11 @@ private function createMutatorCollectionFromTemplate(Templates $templates, ToBeC // todo maybe check also explicitly for allowsGrandchildNodeType (we do this currently like below) - $propertiesAndReferences = PropertiesAndReferences::createFromArrayAndTypeDeclarations($template->getProperties(), $nodeType); + $properties = $this->propertiesHandler->createdFromArrayByTypeDeclaration($template->getProperties(), $nodeType); $properties = array_merge( - $propertiesAndReferences->requireValidProperties($nodeType, $this->propertyMapper, $caughtExceptions), - $propertiesAndReferences->requireValidReferences($nodeType, $this->subgraph, $caughtExceptions) + $this->propertiesHandler->requireValidProperties($properties, $caughtExceptions), + $this->propertiesHandler->requireValidReferences($properties, $caughtExceptions) ); $nodeMutators = $nodeMutators->withNodeMutators( diff --git a/Classes/Domain/NodeCreation/Properties.php b/Classes/Domain/NodeCreation/Properties.php new file mode 100644 index 0000000..c669c6b --- /dev/null +++ b/Classes/Domain/NodeCreation/Properties.php @@ -0,0 +1,40 @@ +properties = $properties; + $this->references = $references; + $this->nodeType = $nodeType; + } + + public function getProperties(): array + { + return $this->properties; + } + + public function getReferences(): array + { + return $this->references; + } + + public function getNodeType(): NodeType + { + return $this->nodeType; + } +} diff --git a/Classes/Domain/NodeCreation/PropertiesAndReferences.php b/Classes/Domain/NodeCreation/PropertiesHandler.php similarity index 80% rename from Classes/Domain/NodeCreation/PropertiesAndReferences.php rename to Classes/Domain/NodeCreation/PropertiesHandler.php index ca61f82..296937d 100644 --- a/Classes/Domain/NodeCreation/PropertiesAndReferences.php +++ b/Classes/Domain/NodeCreation/PropertiesHandler.php @@ -14,19 +14,19 @@ /** * @Flow\Proxy(false) */ -class PropertiesAndReferences +class PropertiesHandler { - private array $properties; + private Context $subgraph; - private array $references; + private PropertyMapper $propertyMapper; - private function __construct(array $properties, array $references) + public function __construct(Context $subgraph, PropertyMapper $propertyMapper) { - $this->properties = $properties; - $this->references = $references; + $this->subgraph = $subgraph; + $this->propertyMapper = $propertyMapper; } - public static function createFromArrayAndTypeDeclarations(array $propertiesAndReferences, NodeType $nodeType): self + public function createdFromArrayByTypeDeclaration(array $propertiesAndReferences, NodeType $nodeType): Properties { $references = []; $properties = []; @@ -40,7 +40,7 @@ public static function createFromArrayAndTypeDeclarations(array $propertiesAndRe } $properties[$propertyName] = $propertyValue; } - return new self($properties, $references); + return new Properties($properties, $references, $nodeType); } /** @@ -51,10 +51,11 @@ public static function createFromArrayAndTypeDeclarations(array $propertiesAndRe * 2. It is checked, that the property value is assignable to the property type. * In case the type is class or an array of classes, the property mapper will be used map the given type to it. If it doesn't succeed, we will log an error. */ - public function requireValidProperties(NodeType $nodeType, PropertyMapper $propertyMapper, CaughtExceptions $caughtExceptions): array + public function requireValidProperties(Properties $properties, CaughtExceptions $caughtExceptions): array { + $nodeType = $properties->getNodeType(); $validProperties = []; - foreach ($this->properties as $propertyName => $propertyValue) { + foreach ($properties->getProperties() as $propertyName => $propertyValue) { try { $this->assertValidPropertyName($propertyName); if (!isset($nodeType->getProperties()[$propertyName])) { @@ -73,10 +74,10 @@ public function requireValidProperties(NodeType $nodeType, PropertyMapper $prope // we try property mapping only for class types or array of classes $propertyMappingConfiguration = new PropertyMappingConfiguration(); $propertyMappingConfiguration->allowAllProperties(); - $propertyValue = $propertyMapper->convert($propertyValue, $propertyType->getValue(), $propertyMappingConfiguration); - $messages = $propertyMapper->getMessages(); + $propertyValue = $this->propertyMapper->convert($propertyValue, $propertyType->getValue(), $propertyMappingConfiguration); + $messages = $this->propertyMapper->getMessages(); if ($messages->hasErrors()) { - throw new PropertyIgnoredException($propertyMapper->getMessages()->getFirstError()->getMessage(), 1686779371122); + throw new PropertyIgnoredException($this->propertyMapper->getMessages()->getFirstError()->getMessage(), 1686779371122); } } @@ -100,12 +101,13 @@ public function requireValidProperties(NodeType $nodeType, PropertyMapper $prope return $validProperties; } - public function requireValidReferences(NodeType $nodeType, Context $subgraph, CaughtExceptions $caughtExceptions): array + public function requireValidReferences(Properties $properties, CaughtExceptions $caughtExceptions): array { + $nodeType = $properties->getNodeType(); $validReferences = []; - foreach ($this->references as $referenceName => $referenceValue) { + foreach ($properties->getReferences() as $referenceName => $referenceValue) { $referenceType = ReferenceType::fromPropertyOfNodeType($referenceName, $nodeType); - if (!$referenceType->isMatchedBy($referenceValue, $subgraph)) { + if (!$referenceType->isMatchedBy($referenceValue, $this->subgraph)) { $caughtExceptions->add(CaughtException::fromException(new \RuntimeException( sprintf( 'Reference could not be set, because node reference(s) %s cannot be resolved.', From 4bb9d69b542a2d04c1f5f17f89d1350b85c65e4c Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sat, 24 Jun 2023 21:14:50 +0200 Subject: [PATCH 23/24] TASK: Cleanup Reference handling --- .../InvalidReferenceException.php | 9 +++ .../NodeCreation/NodeCreationService.php | 21 +++--- .../Domain/NodeCreation/PropertiesHandler.php | 49 +++++++++++--- Classes/Domain/NodeCreation/ReferenceType.php | 65 +++++++++++++++---- .../Snapshots/SomeExceptions.messages.json | 4 +- .../UnresolvableProperties.messages.json | 4 +- .../Domain/NodeCreation/ReferenceTypeTest.php | 39 +++++------ 7 files changed, 135 insertions(+), 56 deletions(-) create mode 100644 Classes/Domain/NodeCreation/InvalidReferenceException.php diff --git a/Classes/Domain/NodeCreation/InvalidReferenceException.php b/Classes/Domain/NodeCreation/InvalidReferenceException.php new file mode 100644 index 0000000..062707e --- /dev/null +++ b/Classes/Domain/NodeCreation/InvalidReferenceException.php @@ -0,0 +1,9 @@ +propertiesHandler->createdFromArrayByTypeDeclaration($template->getProperties(), $nodeType); - $properties = array_merge( + $validProperties = array_merge( $this->propertiesHandler->requireValidProperties($properties, $caughtExceptions), $this->propertiesHandler->requireValidReferences($properties, $caughtExceptions) ); $nodeMutators = NodeMutatorCollection::from( - NodeMutator::setProperties($properties), - $this->createMutatorForUriPathSegment($template), + NodeMutator::setProperties($validProperties), + $this->createMutatorForUriPathSegment($template->getProperties()), )->merge( $this->createMutatorCollectionFromTemplate( $template->getChildNodes(), @@ -83,7 +83,7 @@ private function createMutatorCollectionFromTemplate(Templates $templates, ToBeC $nodeType = $parentNodesAutoCreatedChildNodes[$template->getName()->__toString()]; $properties = $this->propertiesHandler->createdFromArrayByTypeDeclaration($template->getProperties(), $nodeType); - $properties = array_merge( + $validProperties = array_merge( $this->propertiesHandler->requireValidProperties($properties, $caughtExceptions), $this->propertiesHandler->requireValidReferences($properties, $caughtExceptions) ); @@ -92,7 +92,7 @@ private function createMutatorCollectionFromTemplate(Templates $templates, ToBeC NodeMutator::isolated( NodeMutatorCollection::from( NodeMutator::selectChildNode($template->getName()), - NodeMutator::setProperties($properties) + NodeMutator::setProperties($validProperties) )->merge($this->createMutatorCollectionFromTemplate( $template->getChildNodes(), new ToBeCreatedNode($nodeType), @@ -137,7 +137,7 @@ private function createMutatorCollectionFromTemplate(Templates $templates, ToBeC $properties = $this->propertiesHandler->createdFromArrayByTypeDeclaration($template->getProperties(), $nodeType); - $properties = array_merge( + $validProperties = array_merge( $this->propertiesHandler->requireValidProperties($properties, $caughtExceptions), $this->propertiesHandler->requireValidReferences($properties, $caughtExceptions) ); @@ -146,8 +146,8 @@ private function createMutatorCollectionFromTemplate(Templates $templates, ToBeC NodeMutator::isolated( NodeMutatorCollection::from( NodeMutator::createAndSelectNode($template->getType(), $template->getName()), - NodeMutator::setProperties($properties), - $this->createMutatorForUriPathSegment($template) + NodeMutator::setProperties($validProperties), + $this->createMutatorForUriPathSegment($template->getProperties()) )->merge($this->createMutatorCollectionFromTemplate( $template->getChildNodes(), new ToBeCreatedNode($nodeType), @@ -164,12 +164,9 @@ private function createMutatorCollectionFromTemplate(Templates $templates, ToBeC /** * All document node types get a uri path segment; if it is not explicitly set in the properties, * it should be built based on the title property - * - * @param Template|RootTemplate $template */ - private function createMutatorForUriPathSegment($template): NodeMutator + private function createMutatorForUriPathSegment(array $properties): NodeMutator { - $properties = $template->getProperties(); return NodeMutator::unsafeFromClosure(function (NodeInterface $previousNode) use ($properties) { if (!$previousNode->getNodeType()->isOfType('Neos.Neos:Document')) { return; diff --git a/Classes/Domain/NodeCreation/PropertiesHandler.php b/Classes/Domain/NodeCreation/PropertiesHandler.php index 296937d..e421052 100644 --- a/Classes/Domain/NodeCreation/PropertiesHandler.php +++ b/Classes/Domain/NodeCreation/PropertiesHandler.php @@ -4,6 +4,7 @@ use Flowpack\NodeTemplates\Domain\ExceptionHandling\CaughtException; use Flowpack\NodeTemplates\Domain\ExceptionHandling\CaughtExceptions; +use Neos\ContentRepository\Domain\Model\NodeInterface; use Neos\ContentRepository\Domain\Model\NodeType; use Neos\ContentRepository\Domain\Service\Context; use Neos\Flow\Annotations as Flow; @@ -107,17 +108,47 @@ public function requireValidReferences(Properties $properties, CaughtExceptions $validReferences = []; foreach ($properties->getReferences() as $referenceName => $referenceValue) { $referenceType = ReferenceType::fromPropertyOfNodeType($referenceName, $nodeType); - if (!$referenceType->isMatchedBy($referenceValue, $this->subgraph)) { - $caughtExceptions->add(CaughtException::fromException(new \RuntimeException( - sprintf( - 'Reference could not be set, because node reference(s) %s cannot be resolved.', - json_encode($referenceValue) - ), - 1685958176560 - ))->withOrigin(sprintf('Reference "%s" in NodeType "%s"', $referenceName, $nodeType->getName()))); + + try { + if ($referenceType->isReference()) { + $nodeAggregateIdentifier = $referenceType->toNodeAggregateId($referenceValue); + if ($nodeAggregateIdentifier === null) { + continue; + } + if (!($node = $this->subgraph->getNodeByIdentifier($nodeAggregateIdentifier->__toString())) instanceof NodeInterface) { + throw new InvalidReferenceException(sprintf( + 'Node with identifier "%s" does not exist.', + $nodeAggregateIdentifier->__toString() + ), 1687632330292); + } + $validReferences[$referenceName] = $node; + continue; + } + + if ($referenceType->isReferences()) { + $nodeAggregateIdentifiers = $referenceType->toNodeAggregateIds($referenceValue); + if (count($nodeAggregateIdentifiers) === 0) { + continue; + } + $nodes = []; + foreach ($nodeAggregateIdentifiers as $nodeAggregateIdentifier) { + if (!($nodes[] = $this->subgraph->getNodeByIdentifier($nodeAggregateIdentifier->__toString())) instanceof NodeInterface) { + throw new InvalidReferenceException(sprintf( + 'Node with identifier "%s" does not exist.', + $nodeAggregateIdentifier->__toString() + ), 1687632330292); + } + } + $validReferences[$referenceName] = $nodes; + continue; + } + } catch (InvalidReferenceException $runtimeException) { + $caughtExceptions->add( + CaughtException::fromException($runtimeException) + ->withOrigin(sprintf('Reference "%s" in NodeType "%s"', $referenceName, $nodeType->getName())) + ); continue; } - $validReferences[$referenceName] = $referenceValue; } return $validReferences; } diff --git a/Classes/Domain/NodeCreation/ReferenceType.php b/Classes/Domain/NodeCreation/ReferenceType.php index 85d9d8a..ce6cc98 100644 --- a/Classes/Domain/NodeCreation/ReferenceType.php +++ b/Classes/Domain/NodeCreation/ReferenceType.php @@ -6,7 +6,7 @@ use Neos\ContentRepository\Domain\Model\NodeInterface; use Neos\ContentRepository\Domain\Model\NodeType; -use Neos\ContentRepository\Domain\Service\Context; +use Neos\ContentRepository\Domain\NodeAggregate\NodeAggregateIdentifier; use Neos\Flow\Annotations as Flow; /** @@ -73,24 +73,65 @@ public function getValue(): string return $this->value; } - public function isMatchedBy($propertyValue, Context $subgraphForResolving): bool + public function toNodeAggregateId($referenceValue): ?NodeAggregateIdentifier { - if ($propertyValue === null) { - return true; + if ($referenceValue === null) { + return null; } - $nodeAggregatesOrIds = $this->isReference() ? [$propertyValue] : $propertyValue; - if (is_array($nodeAggregatesOrIds) === false) { - return false; + if ($referenceValue instanceof NodeInterface) { + return NodeAggregateIdentifier::fromString($referenceValue->getIdentifier()); } - foreach ($nodeAggregatesOrIds as $singleNodeAggregateOrId) { + try { + return NodeAggregateIdentifier::fromString($referenceValue); + } catch (\Throwable $exception) { + throw new InvalidReferenceException( + sprintf( + 'Invalid reference value. Value `%s` is not a valid node or node identifier.', + json_encode($referenceValue) + ), + 1687632177555 + ); + } + } + + /** + * @param mixed $referenceValue + * @return array + */ + public function toNodeAggregateIds($referenceValue): array + { + if ($referenceValue === null) { + return []; + } + + if (is_array($referenceValue) === false) { + throw new InvalidReferenceException( + sprintf( + 'Invalid reference value. Value `%s` is not a valid list of nodes or node identifiers.', + json_encode($referenceValue) + ), + 1685958176560 + ); + } + + $nodeAggregateIds = []; + foreach ($referenceValue as $singleNodeAggregateOrId) { if ($singleNodeAggregateOrId instanceof NodeInterface) { + $nodeAggregateIds[] = NodeAggregateIdentifier::fromString($singleNodeAggregateOrId->getIdentifier()); continue; } - if (is_string($singleNodeAggregateOrId) && $subgraphForResolving->getNodeByIdentifier($singleNodeAggregateOrId) instanceof NodeInterface) { - continue; + try { + $nodeAggregateIds[] = NodeAggregateIdentifier::fromString($singleNodeAggregateOrId); + } catch (\Throwable $exception) { + throw new InvalidReferenceException( + sprintf( + 'Invalid reference value. Value `%s` is not a valid list of nodes or node identifiers.', + json_encode($referenceValue) + ), + 1685958176560 + ); } - return false; } - return true; + return $nodeAggregateIds; } } diff --git a/Tests/Functional/Features/Exceptions/Snapshots/SomeExceptions.messages.json b/Tests/Functional/Features/Exceptions/Snapshots/SomeExceptions.messages.json index e5a8d85..3ba09fc 100644 --- a/Tests/Functional/Features/Exceptions/Snapshots/SomeExceptions.messages.json +++ b/Tests/Functional/Features/Exceptions/Snapshots/SomeExceptions.messages.json @@ -72,11 +72,11 @@ "severity": "ERROR" }, { - "message": "Reference \"reference\" in NodeType \"Flowpack.NodeTemplates:Content.SomeExceptions\" | RuntimeException(Reference could not be set, because node reference(s) \"non-existing-node-id\" cannot be resolved., 1685958176560)", + "message": "Reference \"reference\" in NodeType \"Flowpack.NodeTemplates:Content.SomeExceptions\" | InvalidReferenceException(Node with identifier \"non-existing-node-id\" does not exist., 1687632330292)", "severity": "ERROR" }, { - "message": "Reference \"references\" in NodeType \"Flowpack.NodeTemplates:Content.SomeExceptions\" | RuntimeException(Reference could not be set, because node reference(s) [\"non-existing-node-id\"] cannot be resolved., 1685958176560)", + "message": "Reference \"references\" in NodeType \"Flowpack.NodeTemplates:Content.SomeExceptions\" | InvalidReferenceException(Node with identifier \"non-existing-node-id\" does not exist., 1687632330292)", "severity": "ERROR" }, { diff --git a/Tests/Functional/Features/ResolvableProperties/Snapshots/UnresolvableProperties.messages.json b/Tests/Functional/Features/ResolvableProperties/Snapshots/UnresolvableProperties.messages.json index 875fc82..7d90e52 100644 --- a/Tests/Functional/Features/ResolvableProperties/Snapshots/UnresolvableProperties.messages.json +++ b/Tests/Functional/Features/ResolvableProperties/Snapshots/UnresolvableProperties.messages.json @@ -16,11 +16,11 @@ "severity": "ERROR" }, { - "message": "Reference \"reference\" in NodeType \"Flowpack.NodeTemplates:Content.UnresolvableProperties\" | RuntimeException(Reference could not be set, because node reference(s) true cannot be resolved., 1685958176560)", + "message": "Reference \"reference\" in NodeType \"Flowpack.NodeTemplates:Content.UnresolvableProperties\" | InvalidReferenceException(Invalid reference value. Value `true` is not a valid node or node identifier., 1687632177555)", "severity": "ERROR" }, { - "message": "Reference \"references\" in NodeType \"Flowpack.NodeTemplates:Content.UnresolvableProperties\" | RuntimeException(Reference could not be set, because node reference(s) [\"some-non-existing-node-id\"] cannot be resolved., 1685958176560)", + "message": "Reference \"references\" in NodeType \"Flowpack.NodeTemplates:Content.UnresolvableProperties\" | InvalidReferenceException(Node with identifier \"some-non-existing-node-id\" does not exist., 1687632330292)", "severity": "ERROR" } ] diff --git a/Tests/Unit/Domain/NodeCreation/ReferenceTypeTest.php b/Tests/Unit/Domain/NodeCreation/ReferenceTypeTest.php index 29680b0..a1007f3 100644 --- a/Tests/Unit/Domain/NodeCreation/ReferenceTypeTest.php +++ b/Tests/Unit/Domain/NodeCreation/ReferenceTypeTest.php @@ -1,21 +1,21 @@ getMockBuilder(Context::class)->disableOriginalConstructor()->getMock(); - $subgraphMock->expects(self::any())->method('getNodeByIdentifier')->willReturnCallback(function ($nodeId) { - if ($nodeId === self::VALID_NODE_ID_1 || $nodeId === self::VALID_NODE_ID_2) { - return $this->createStub(NodeInterface::class); - } - return null; - }); $nodeTypeMock = $this->getMockBuilder(NodeType::class)->disableOriginalConstructor()->getMock(); $nodeTypeMock->expects(self::once())->method('getPropertyType')->with('test')->willReturn($declarationType); $subject = ReferenceType::fromPropertyOfNodeType( @@ -39,10 +31,16 @@ public function testIsMatchedBy(string $declarationType, array $validValues, arr $nodeTypeMock, ); foreach ($validValues as $validValue) { - Assert::assertTrue($subject->isMatchedBy($validValue, $subgraphMock), sprintf('Value %s should match.', get_debug_type($validValue))); + $subject->isReference() ? $subject->toNodeAggregateId($validValue) : $subject->toNodeAggregateIds($validValue); + self::assertTrue(true); } foreach ($invalidValues as $invalidValue) { - Assert::assertFalse($subject->isMatchedBy($invalidValue, $subgraphMock), sprintf('Value %s should not match.', get_debug_type($validValue))); + try { + $subject->isReference() ? $subject->toNodeAggregateId($invalidValue) : $subject->toNodeAggregateIds($invalidValue); + self::fail(sprintf('Value %s should not match.', var_export($invalidValue, true))); + } catch (InvalidReferenceException $exception) { + self::assertTrue(true); + } } } @@ -50,26 +48,29 @@ public function declarationAndValueProvider(): array { $int = 13; $float = 4.2; - $string = 'It\'s a graph!'; - $stringArray = [$string]; + $stringWithSpecialChars = 'Special äüö chars'; + $stringWithSpecialCharsArray = [$stringWithSpecialChars]; $image = new Image(new PersistentResource()); $asset = new Asset(new PersistentResource()); $date = \DateTimeImmutable::createFromFormat(\DateTimeInterface::W3C, '2020-08-20T18:56:15+00:00'); $uri = new Uri('https://www.neos.io'); - $nodeMock1 = $this->createStub(NodeInterface::class); - $nodeMock2 = $this->createStub(NodeInterface::class); + $nodeMock1 = $this->getMockBuilder(NodeInterface::class)->getMock(); + $nodeMock1->method('getIdentifier')->willReturn(self::VALID_NODE_ID_1); + + $nodeMock2 = $this->getMockBuilder(NodeInterface::class)->getMock(); + $nodeMock2->method('getIdentifier')->willReturn(self::VALID_NODE_ID_2); return [ [ 'reference', [null, $nodeMock1, $nodeMock2, self::VALID_NODE_ID_1, self::VALID_NODE_ID_2], - [0, $int, 0.0, $float, '', $string, [], $stringArray, $date, $uri, $image, $asset, [$asset]] + [0, $int, 0.0, $float, '', $stringWithSpecialChars, [], $stringWithSpecialCharsArray, $date, $uri, $image, $asset, [$asset]] ], [ 'references', [[], null, [self::VALID_NODE_ID_1], [$nodeMock1], [self::VALID_NODE_ID_2, $nodeMock2]], - [true, $float, $string, $stringArray, $date, $uri, $image, $asset, [$asset]] + [true, $float, $stringWithSpecialChars, $stringWithSpecialCharsArray, $date, $uri, $image, $asset, [$asset]] ], ]; } From 330fa20e47f9bcbb6e47b87967cdddec843f8ff2 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sat, 24 Jun 2023 21:39:54 +0200 Subject: [PATCH 24/24] TASK: Allow manual triggering of CI --- .github/workflows/tests.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index cbd3f46..74dba43 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -2,9 +2,10 @@ name: Tests on: push: - branches: [ master, '[0-9]+.[0-9]' ] + branches: [ main, '[0-9]+.[0-9]' ] pull_request: - branches: [ master, '[0-9]+.[0-9]' ] + branches: [ main, '[0-9]+.[0-9]' ] + workflow_dispatch: # Allow manual triggering on any branch via `gh workflow run tests.yml -r branch-name` jobs: build: